[关闭]
@wade123 2019-04-02T09:27:50.000000Z 字数 5714 阅读 5666

实战项目 5:Selenium 自动爬取东方财富网财务报表

Python入门爬虫与数据分析


摘要: 利用 Selenium 爬取东方财富网各上市公司历年的财务报表数据。

上一篇文章爬取了使用 Ajax 的网页图片,该网页的 Ajax 接口参数能够直接得到。不过有些 Ajax 网页的接口参数是加密的无法直接获取,比如淘宝。有的动态网页也采用 JavaScript,但不是 Ajax 技术,比如 Echarts 官网。所以当遇到这两类网页时,上一篇文章中介绍的方法就不能用了,需要采取新方法,其中最简单粗暴的是 Selenium 大法,接下面就介绍一下这种方法,然后实战爬取方财富网的财务报表数据。

1. Selenium 介绍

Selenium 是什么,一句话概括就是「自动化测试工具」,起初主要用于测试,近几年爬虫火了之后,它摇身一变成了爬虫利器,它能控制浏览器,模仿人的动作去上网,实现自动翻页、登录网站、发送邮件、下载图片/音乐/视频等等。它能做到「可见即可爬」,也就是说网页上你能看到的东西,基本上都能爬取下来,而且不需要再去查看后台用了什么 JavaScript 技术或者 Ajax 参数,简单直接。

举个例子,几行代码就可以实现登陆网站然后跳转的功能,很强大。

这仅是 Selenium 最简单的功能,我们也只需利用这些基础功能就可以爬虫,

下面就来实战操练一下。

2. 目标网站

我们要爬取东方财富网数据中心的年报数据。

URL:http://data.eastmoney.com/center/

点开该分类查看 2018 年中报,数据包括:业绩报表、业绩快报、利润表等 7 个报表。以业绩报表为例,报表包含全部 3000 多只股票数据,一共有 70 多页。

以 2018 年中报的业绩报表为例,查看 URL 规律:

URL:http://data.eastmoney.com/bbsj/201806/lrb.html

点击下一页表格更新后发现 URL 没有变化,判断采用了 Javscript 动态渲染,接着看看是不是 Ajax 加载,右键点击检查,切换到 network 下面的 XHR,再按 F5 刷新。点击下一页并没有生成新的 Ajax 请求,可以判断该网页结构不是常见的那种点击下一页或者下拉会源源不断出现的 Ajax 请求类型,便无法使用构造 url 循环来爬取多页内容了。

3. 爬取实战

3.1. 单页爬取

我们先以2018 年中报的利润表为例,抓取该网页的第一页表格数据,网页 url:http://data.eastmoney.com/bbsj/201806/lrb.html

快速定位到表格所在的节点:id = dt_1,用 Selenium 抓取,代码实现如下:

  1. from selenium import webdriver
  2. browser = webdriver.Chrome()
  3. browser.get('http://data.eastmoney.com/bbsj/201806/lrb.html')
  4. element = browser.find_element_by_css_selector('#dt_1') # 定位表格,element 是 WebElement 类型
  5. # 提取表格内容 td
  6. td_content = element.find_elements_by_tag_name("td") # 进一步定位到表格内容所在的 td 节点
  7. lst = [] # 存储为 list
  8. for td in td_content:
  9. lst.append(td.text)
  10. print(lst) # 输出表格内容

这里使用 Chrome 浏览器构造一个 Webdriver 对象,赋值给变量 browser,browser 调用 get()方法请求想要抓取的网页。接着使用find_element_by_css_selector方法查找表格所在的节点:'#dt_1'。向下定位到 td 节点,因为网页中有很多个 td 节点,所以要用 find_elements 方法。然后遍历数据节点存储到 list 中,几行代码就能抓取下来这页表格,查看一下结果:

  1. # list 形式:
  2. ['1', '002161', '远望谷', ...'-7960 万', '09-29',
  3. '2','002316', '亚联发展', ...'1.79 亿', '09-29',
  4. '3',...
  5. '50', '002683', '宏大爆破',...'1.37 亿', '09-01']

为便于后续存储,将 list 转换为 DataFrame。需要先把整个 list 分割为多个子 list ,可以手动数出表格一共有 16 列,但这里不能使用这个数字,因为除了利润表其他报表的列数并不是 16,爬取其他表格就会报错。更好的方法是选择 find_elements_by_css_selector 方法自动确定列数,只需要定位首行 td 节点的数量就能确定,代码实现如下:

  1. import pandas as pd
  2. # 确定表格列数
  3. col = len(element.find_elements_by_css_selector('tr:nth-child(1) td'))
  4. # 通过定位一行 td 的数量,可获得表格的列数,然后将 list 拆分为对应列数的子 list
  5. lst = [lst[i:i + col] for i in range(0, len(lst), col)]
  6. # 原网页中打开"详细"链接可以查看更详细的数据,这里我们把 url 提取出来,方便后期查看
  7. lst_link = []
  8. links = element.find_elements_by_css_selector('#dt_1 a.red')
  9. for link in links:
  10. url = link.get_attribute('href')
  11. lst_link.append(url)
  12. lst_link = pd.Series(lst_link)
  13. # list 转为 dataframe
  14. df_table = pd.DataFrame(lst)
  15. # 添加 url 列
  16. df_table['url'] = lst_link
  17. print(df_table.head()) # 查看 DataFrame

输出的 DataFrame 结果如下:

3.2. 多页爬取

单页表格爬取完成后,接下来爬取多页。

先实现 Selenium 模拟翻页跳转操作,成功后再爬取每一页的表格数据,代码实现如下:

  1. from selenium import webdriver
  2. from selenium.common.exceptions import TimeoutException
  3. from selenium.webdriver.common.by import By
  4. from selenium.webdriver.support import expected_conditions as EC
  5. from selenium.webdriver.support.wait import WebDriverWait
  6. import time
  7. browser = webdriver.Chrome()
  8. browser.maximize_window() # 最大化窗口,可以选择设置
  9. wait = WebDriverWait(browser, 10)
  10. def index_page(page):
  11. try:
  12. browser.get('http://data.eastmoney.com/bbsj/201806/lrb.html')
  13. print('正在爬取第: %s 页' % page)
  14. wait.until(
  15. EC.presence_of_element_located((By.ID, "dt_1")))
  16. # 判断是否是第 1 页,如果大于 1 就输入跳转,否则等待加载完成。
  17. if page > 1:
  18. # 确定页数输入框
  19. input = wait.until(EC.presence_of_element_located(
  20. (By.XPATH, '//*[@id="PageContgopage"]')))
  21. input.click()
  22. input.clear()
  23. input.send_keys(page)
  24. submit = wait.until(EC.element_to_be_clickable(
  25. (By.CSS_SELECTOR, '#PageCont > a.btn_link')))
  26. submit.click()
  27. time.sleep(2)
  28. # 确认成功跳转到输入框中的指定页
  29. wait.until(EC.text_to_be_present_in_element(
  30. (By.CSS_SELECTOR, '#PageCont > span.at'), str(page)))
  31. except Exception:
  32. return None
  33. def main():
  34. for page in range(1,5): # 测试翻 4 页
  35. index_page(page)
  36. if __name__ == '__main__':
  37. main()

上面先加载了需要用到的包,设置了最长 10s 的显式等待时间,以便网页加载出表格。用EC.presence_of_element_located 条件判断表格是否加载出来,加载出来后,设置一个页面判断,如果在第 1 页就等待页面加载完成,如果大于第 1 页就开始跳转。获取输入框 input 节点后,clear() 方法清空输入框, send_keys() 方法自动填写页码,最后 submit.click()方法点击下一页完成翻页跳转。

测试一下前 4 页跳转效果,可以看到网页跳转成功。

接下来,就可以像第一页那样爬取后续每一页的表格数据,转为 DataFrame 存储到 csv 文件中。

3.3. 通用爬虫

上面爬取了 2018 年中报利润表单个网页表格,还可以增加代码灵活性,来爬取任意时期、任意报表类型的数据,比如 2017 年 3 季度的利润表、2016 年全年的业绩报表等。

下图中可以看到年报季报有 7 张表格,财务报表最早从 2007 年开始每季度一次。从这两个维度入手,重新构造 url 就可以爬取任意时期和任意一份报表了,代码进行实现:

  1. # 重构 url
  2. # 1 设置财务报表获取时期
  3. year = int(float(input('请输入要查询的年份(四位数 2007-2018): ')))
  4. # int 表示取整,里面加 float 是因为输入的是 str,直接 int 会报错,float 则不会
  5. while (year < 2007 or year > 2018):
  6. year = int(float(input('年份数值输入错误,请重新输入:')))
  7. quarter = int(float(input('请输入小写数字季度(1:1 季报,2-年中报,3:3 季报,4-年报): ')))
  8. while (quarter < 1 or quarter > 4):
  9. quarter = int(float(input('季度数值输入错误,请重新输入: ')))
  10. # 转换为所需的 quarter 两种方法,2 表示两位数,0 表示不满 2 位用 0 补充
  11. quarter = '{:02d}'.format(quarter * 3)
  12. # quarter = '%02d' %(int(month)*3)
  13. date = '{}{}' .format(year, quarter)
  14. # 2 设置财务报表种类
  15. tables = int(
  16. input('请输入查询的报表种类对应的数字(1-业绩报表;2-业绩快报表:3-业绩预告表;4-预约披露时间表;5-资产负债表;6-利润表;7-现金流量表): '))
  17. dict_tables = {1: '业绩报表', 2: '业绩快报表', 3: '业绩预告表',
  18. 4: '预约披露时间表', 5: '资产负债表', 6: '利润表', 7: '现金流量表'}
  19. dict = {1: 'yjbb', 2: 'yjkb/13', 3: 'yjyg',
  20. 4: 'yysj', 5: 'zcfz', 6: 'lrb', 7: 'xjll'}
  21. category = dict[tables]
  22. # 3 设置 url
  23. url = 'http://data.eastmoney.com/{}/{}/{}.html' .format('bbsj', date, category)
  24. print(url) # 测试输出的 url

上面的代码可以返回任意时期、任意报表类型的 url 链接。有了链接就可以像前面那样爬取报表数据了。

另外,还可以自定义爬取的起始页数,比如从第 1 页开始,爬到第 10 页结束,代码实现如下:

  1. # 4 选择爬取页数范围
  2. start_page = int(input('请输入下载起始页数:\n'))
  3. nums = input('请输入要下载的页数,(若需下载全部则按回车):\n')
  4. # 确定网页中的最后一页
  5. browser.get(url)
  6. # 确定最后一页页数不直接用数字而是采用定位,因为不同时间段的页码会不一样
  7. try:
  8. page = browser.find_element_by_css_selector('.next+ a') # next 节点后面的 a 节点
  9. except:
  10. page = browser.find_element_by_css_selector('.at+ a')
  11. else:
  12. print('没有找到该节点')
  13. # 上面用 try.except 是因为绝大多数页码定位可用'.next+ a',但是业绩快报表有的只有 2 页,无'.next+ a'节点
  14. end_page = int(page.text)
  15. if nums.isdigit():
  16. end_page = start_page + int(nums)
  17. elif nums == '':
  18. end_page = end_page
  19. else:
  20. print('页数输入错误')
  21. # 输入准备下载表格类型
  22. print('准备下载:{}-{}' .format(date, dict_tables[tables]))

经过上面的设置,我们就可以实现自定义时期和财务报表类型的表格爬取了,将代码再稍微整理一下,可实现下面的爬虫效果:

2018 年中报业绩报表:

2017 年报的利润表:

你可以看到用 Selenium 爬虫,速度慢且占内存,建议爬虫时首选 Requests ,行不通的时候再考虑这个方法。这个爬虫还有另外更快的方法可以做到,下一篇文章,我们换一个思路再尝试一次。

完整代码和素材,可以在下面的链接中得到:

https://github.com/makcyun/eastmoney_spider

本文完。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注