前言
本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理。
PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取
python免费学习资料以及群交流解答点击即可加入
1. 实战背景
很多网站都提供上市公司的公告、财务报表等金融投资信息和数据,比如:腾讯财经、网易财经、新浪财经、东方财富网等。这之中,发现东方财富网的数据非常齐全。
东方财富网有一个数据中心:http://data.eastmoney.com/center/,该数据中心提供包括特色数据、研究报告、年报季报等在内的大量数据(见下图)。
以年报季报类别为例,我们点开该分类查看一下2018年中报(见下图),可以看到该分类下又包括:业绩报表、业绩快报、利润表等7个报表的数据。以业绩报表为例,报表包含全部3000多只股票的业绩报表数据,一共有70多页。
假如,我们想获取所有股票2018年中的业绩报表数据,然后对该数据进行一些分析。采取手动复制的方法,70多页可以勉强完成。但如果想获取任意一年、任意季度、任意报表的数据,要再通过手动复制的方法,工作量会非常地大。举个例子,假设要获取10年间(40个季度)、所有7个报表的数据,那么手动复制的工作量大约将是:40×7×70(每个报表大约70页),差不多要重复性地复制2万次!!!可以说是人工不可能完成的任务。所以,本文的目标就是利用Selenium自动化技术,爬取年报季报类别下,任意一年(网站有数据至今)、任意财务报表数据。我们所需要做的,仅是简单输入几个字符,其他就全部交给电脑,然后过一会儿打开excel,就可以看到所需数据"静静地躺在那里",是不是挺酷的?
好,下面我们就开始实操一下。首先,需要分析要爬取的网页对象。
2. 网页分析
之前,我们已经爬过表格型的数据,所以对表格数据的结构应该不会太陌生。
我们这里以上面的2018年中报的业绩报表为例,查看一下表格的形式。
网址url:http://data.eastmoney.com/bbsj/201806/lrb.html,bbsj代表年报季报,201803代表2018年一季报,类似地,201806表示年中报;lrb是利润表的首字母缩写,同理,yjbb表示业绩报表。可以看出,该网址格式很简单,便于构造url。
接着,我们点击下一页按钮,可以看到表格更新后url没有发生改变,可以判定是采用了Javscript。那么,我们首先判断是不是采用了Ajax加载的。方法也很简单,右键检查或按F12,切换到network并选择下面的XHR,再按F5刷新。可以看到只有一个Ajax请求,点击下一页也并没有生成新的Ajax请求,可以判断该网页结构不是常见的那种点击下一页或者下拉会源源不断出现的Ajax请求类型,那么便无法构造url来实现分页爬取。
XHR选项里没有找到我们需要的请求,接下来试试看能不能再JS里找到表格的数据请求。将选项选为JS,再次F5刷新,可以看到出现了很多JS请求,然后我们点击几次下一页,会发现弹出新的请求来,然后右边为响应的请求信息。url链接非常长,看上去很复杂。好,这里我们先在这里打住不往下了。
可以看到,通过分析后台元素来爬取该动态网页的方法,相对比较复杂。那么有没有干脆、直截了当地就能够抓取表格内容的方法呢?有的,就是本文接下来要介绍的Selenium大法。
3. Selenium知识
Selenium 是什么?一句话,自动化测试工具。它是为了测试而出生的,但在近几年火热的爬虫领域中,它摇身一变,变成了爬虫的利器。直白点说, Seleninm能控制浏览器, 像人一样"上网"。比如,可以实现网页自动翻页、登录网站、发送邮件、下载图片/音乐/视频等等。举个例子,写几行python代码就可以用Selenium实现登录IT桔子,然后浏览网页的功能。
只需要记住重要的一点就是:Selenium能做到"可见即可爬"。也就是说网页上你能看到的东西,Selenium基本上都能爬取下来。包括上面我们提到的东方财富网的财务报表数据,它也能够做到,而且非常简单直接,不用去后台查看用了什么JavaScript技术或者Ajax参数。下面我们就实际来操练下吧
编码实现
思路
- 安装配置好Selenium运行的相关环境,浏览器可以用Chrome、Firefox、PhantomJS等,我用的是Chrome;
- 东方财富网的财务报表数据不用登录可直接获得,Selenium更加方便爬取;
- 先以单个网页中的财务报表为例,表格数据结构简单,可先直接定位到整个表格,然后一次性获取所有td节点对应的表格单元内容;
- 接着循环分页爬取所有上市公司的数据,并保存为csv文件。
- 重新构造灵活的url,实现可以爬取任意时期、任意一张财务报表的数据。
根据上述思路,下面就用代码一步步来实现。
爬取单页表格
我们先以2018年中报的利润表为例,抓取该网页的第一页表格数据,网页url:http://data.eastmoney.com/bbsj/201806/lrb.html
快速定位到表格所在的节点:id = dt_1,然后可以用Selenium进行抓取了,方法如下:
1from selenium import webdriver
2browser = webdriver.Chrome()
3# 当测试好能够顺利爬取后,为加快爬取速度可设置无头模式,即不弹出浏览器
4# 添加无头headlesss 1使用chrome headless,2使用PhantomJS
5# 使用 PhantomJS 会警告高不建议使用phantomjs,建议chrome headless
6# chrome_options = webdriver.ChromeOptions()
7# chrome_options.add_argument('--headless')
8# browser = webdriver.Chrome(chrome_options=chrome_options)
9# browser = webdriver.PhantomJS()
10# browser.maximize_window() # 最大化窗口,可以选择设置
11
12browser.get('http://data.eastmoney.com/bbsj/201806/lrb.html')
13element = browser.find_element_by_css_selector('#dt_1') # 定位表格,element是WebElement类型
14# 提取表格内容td
15td_content = element.find_elements_by_tag_name("td") # 进一步定位到表格内容所在的td节点
16lst = [] # 存储为list
17for td in td_content:
18 lst.append(td.text)
19print(lst) # 输出表格内容
完整代码
1from selenium import webdriver
2from selenium.common.exceptions import TimeoutException
3from selenium.webdriver.common.by import By
4from selenium.webdriver.support import expected_conditions as EC
5from selenium.webdriver.support.wait import WebDriverWait
6import time
7import pandas as pd
8import os
9
10# 先chrome,后phantomjs
11# browser = webdriver.Chrome()
12# 添加无头headlesss
13chrome_options = webdriver.ChromeOptions()
14chrome_options.add_argument('--headless')
15browser = webdriver.Chrome(chrome_options=chrome_options)
16
17# browser = webdriver.PhantomJS() # 会报警高提示不建议使用phantomjs,建议chrome添加无头
18browser.maximize_window() # 最大化窗口
19wait = WebDriverWait(browser, 10)
20
21def index_page(page):
22 try:
23 print('正在爬取第: %s 页' % page)
24 wait.until(
25 EC.presence_of_element_located((By.ID, "dt_1")))
26 # 判断是否是第1页,如果大于1就输入跳转,否则等待加载完成。
27 if page > 1:
28 # 确定页数输入框
29 input = wait.until(EC.presence_of_element_located(
30 (By.XPATH, '//*[@id="PageContgopage"]')))
31 input.click()
32 input.clear()
33 input.send_keys(page)
34 submit = wait.until(EC.element_to_be_clickable(
35 (By.CSS_SELECTOR, '#PageCont > a.btn_link')))
36 submit.click()
37 time.sleep(2)
38 # 确认成功跳转到输入框中的指定页
39 wait.until(EC.text_to_be_present_in_element(
40 (By.CSS_SELECTOR, '#PageCont > span.at'), str(page)))
41 except Exception:
42 return None
43
44def parse_table():
45 # 提取表格第一种方法
46 # element = wait.until(EC.presence_of_element_located((By.ID, "dt_1")))
47 # 第二种方法
48 element = browser.find_element_by_css_selector('#dt_1')
49
50 # 提取表格内容td
51 td_content = element.find_elements_by_tag_name("td")
52 lst = []
53 for td in td_content:
54 # print(type(td.text)) # str
55 lst.append(td.text)
56
57 # 确定表格列数
58 col = len(element.find_elements_by_css_selector('tr:nth-child(1) td'))
59 # 通过定位一行td的数量,可获得表格的列数,然后将list拆分为对应列数的子list
60 lst = [lst[i:i + col] for i in range(0, len(lst), col)]
61
62 # 原网页中打开"详细"链接,可以查看更详细的数据,这里我们把url提取出来,方便后期查看
63 lst_link = []
64 links = element.find_elements_by_css_selector('#dt_1 a.red')
65 for link in links:
66 url = link.get_attribute('href')
67 lst_link.append(url)
68
69 lst_link = pd.Series(lst_link)
70 # list转为dataframe
71 df_table = pd.DataFrame(lst)
72 # 添加url列
73 df_table['url'] = lst_link
74
75 # print(df_table.head())
76 return df_table
77
78# 写入文件
79def write_to_file(df_table, category):
80 # 设置文件保存在D盘eastmoney文件夹下
81 file_path = 'D:\\eastmoney'
82 if not os.path.exists(file_path):
83 os.mkdir(file_path)
84 os.chdir(file_path)
85 df_table.to_csv('{}.csv' .format(category), mode='a',
86 encoding='utf_8_sig', index=0, header=0)
87
88# 设置表格获取时间、类型
89def set_table():
90 print('*' * 80)
91 print('\t\t\t\t东方财富网报表下载')
92 print('作者:高级农民工 2018.10.6')
93 print('--------------')
94
95 # 1 设置财务报表获取时期
96 year = int(float(input('请输入要查询的年份(四位数2007-2018):\n')))
97 # int表示取整,里面加float是因为输入的是str,直接int会报错,float则不会
98 # https://stackoverflow.com/questions/1841565/valueerror-invalid-literal-for-int-with-base-10
99 while (year < 2007 or year > 2018):
100 year = int(float(input('年份数值输入错误,请重新输入:\n')))
101
102 quarter = int(float(input('请输入小写数字季度(1:1季报,2-年中报,3:3季报,4-年报):\n')))
103 while (quarter < 1 or quarter > 4):
104 quarter = int(float(input('季度数值输入错误,请重新输入:\n')))
105
106 # 转换为所需的quarter 两种方法,2表示两位数,0表示不满2位用0补充,
107 # http://www.runoob.com/python/att-string-format.html
108 quarter = '{:02d}'.format(quarter * 3)
109 # quarter = '%02d' %(int(month)*3)
110 date = '{}{}' .format(year, quarter)
111 # print(date) 测试日期 ok
112
113 # 2 设置财务报表种类
114 tables = int(
115 input('请输入查询的报表种类对应的数字(1-业绩报表;2-业绩快报表:3-业绩预告表;4-预约披露时间表;5-资产负债表;6-利润表;7-现金流量表): \n'))
116
117 dict_tables = {1: '业绩报表', 2: '业绩快报表', 3: '业绩预告表',
118 4: '预约披露时间表', 5: '资产负债表', 6: '利润表', 7: '现金流量表'}
119 dict = {1: 'yjbb', 2: 'yjkb/13', 3: 'yjyg',
120 4: 'yysj', 5: 'zcfz', 6: 'lrb', 7: 'xjll'}
121 category = dict[tables]
122
123 # 3 设置url
124 # url = 'http://data.eastmoney.com/bbsj/201803/lrb.html' eg.
125 url = 'http://data.eastmoney.com/{}/{}/{}.html' .format(
126 'bbsj', date, category)
127
128 # # 4 选择爬取页数范围
129 start_page = int(input('请输入下载起始页数:\n'))
130 nums = input('请输入要下载的页数,(若需下载全部则按回车):\n')
131 print('*' * 80)
132
133 # 确定网页中的最后一页
134 browser.get(url)
135 # 确定最后一页页数不直接用数字而是采用定位,因为不同时间段的页码会不一样
136 try:
137 page = browser.find_element_by_css_selector('.next+ a') # next节点后面的a节点
138 except:
139 page = browser.find_element_by_css_selector('.at+ a')
140 # else:
141 # print('没有找到该节点')
142 # 上面用try.except是因为绝大多数页码定位可用'.next+ a',但是业绩快报表有的只有2页,无'.next+ a'节点
143 end_page = int(page.text)
144
145 if nums.isdigit():
146 end_page = start_page + int(nums)
147 elif nums == '':
148 end_page = end_page
149 else:
150 print('页数输入错误')
151 # 输入准备下载表格类型
152 print('准备下载:{}-{}' .format(date, dict_tables[tables]))
153 print(url)
154 yield{
155 'url': url,
156 'category': dict_tables[tables],
157 'start_page': start_page,
158 'end_page': end_page
159 }
160
161def main(category, page):
162 try:
163 index_page(page)
164 # parse_table() #测试print
165 df_table = parse_table()
166 write_to_file(df_table, category)
167 print('第 %s 页抓取完成' % page)
168 print('--------------')
169 except Exception:
170 print('网页爬取失败,请检查网页中表格内容是否存在')
171# 单进程
172if __name__ == '__main__':
173
174 for i in set_table():
175 # url = i.get('url')
176 category = i.get('category')
177 start_page = i.get('start_page')
178 end_page = i.get('end_page')
179
180 for page in range(start_page, end_page):
181 # for page in range(44,pageall+1): # 如果下载中断,可以尝试手动更改网页继续下载
182 main(category, page)
183 print('全部抓取完成')