[关闭]
@wade123 2019-04-07T08:56:08.000000Z 字数 12031 阅读 2577

实战项目 6:调用 API 接口快速提取财务报表

Python入门爬虫与数据分析


摘要: 上一篇文章,我们用 Selenium 爬取了东方财富网的报表数据,但受限于 Selenium 速度,爬取一份报表用时数十分钟,效率很低,本文尝试另外一种方法,分析网页 JavaScript 请求,调用网站 API 数据接口,快速获取报表数据。

1. API 介绍

API(Application Programming Interface)即应用程序接口。常见的是数据 API 接口,简单说就是网站把干净整齐的数据放在后台,我们发送请求之后就可以快速调用得到这些数据,不用再去爬虫了,省时省事,也能降低网站服务器压力。

API 接口的网址形式通常含有 api 字样,比如知乎专栏的 API 接口:

  1. https://zhuanlan.zhihu.com/api/columns/python-programming/posts?limit=20

东方财富网就有公开的数据 API 接口,下面来尝试如何利用这些接口获取上一篇文章中爬取下来的财务报表数据。

2. 调用 API

上一篇文章,我们简单分了网页后台的 js 请求,下面接着分析下去。点击报表底部下一页,观察左侧 Name 列新生成的请求:

不断点击下一页会随之弹出很多以 get?type 开头的请求,点击右边 Headers 选项卡,可以看到请求的 URL网址,很长:

  1. http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?type=CWBB_LRB&token=70f12f2f4f091e459a279469fe49eca5&st=noticedate&sr=-1&p=2&ps=50&js=var%20spmVUpAF={pages:(tp),data:%20(x)}&filter=(reportdate=^2018-06-30^)&rt=51312886

可以看到 URL 中带有 api字样,接着查看右侧的 Preview 和 Response,可以看到里面有很多整齐的数据,尝试猜测这可能就是 API 接口提供的数据。

将 URL 复制到浏览器中,可以看到这些数据正是报表中的数据:

到这里,爬取思路就很清晰了。用 Request 请求 URL,对获取到的数据做正则匹配,把数据转变为 json 格式,再加一个分页循环爬取所有数据,然后写入保存到本地文件就可以了。

这比之前使用 Selenium 的方法要简单很多,而且速度也会快很多倍,下面就来实际操练一下,先爬单页数据。

3. 爬取单页

3.1. 抓取分析

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

获取到的 api 接口 URL如下:

  1. http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?type=CWBB_LRB&token=70f12f2f4f091e459a279469fe49eca5&st=noticedate&sr=-1&p=2&ps=50&js=var%20spmVUpAF={pages:(tp),data:%20(x)}&filter=(reportdate=^2018-06-30^)&rt=51312886

该 URL 由很多参数构成,将这些参数分别写到 params 参数中:

  1. import requests
  2. def get_table():
  3. params = {
  4. 'type': 'CWBB_LRB', # 表格类型,LRB 为利润表缩写,必须
  5. 'token': '70f12f2f4f091e459a279469fe49eca5', # 访问令牌,必须
  6. 'st': 'noticedate', # 公告日期
  7. 'sr': -1, # 保持-1 不用改动即可
  8. 'p': 1, # 表格页数
  9. 'ps': 50, # 每页显示多少条信息
  10. 'js': 'var LFtlXDqn={pages:(tp),data: (x)}', # js 函数,必须
  11. 'filter': '(reportdate=^2018-06-30^)', # 筛选条件
  12. # 'rt': 51294261 可不用
  13. }
  14. url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?'
  15. response = requests.get(url, params=params).text
  16. print(response)
  17. get_table()

解释一下各项参数:type为 7 个表格的类型说明,将 type 拆成两部分:'CWBB_' 和'LRB',资产负债表等后 3 个表是以'CWBB_' 开头,业绩报表至预约披露时间表等前 4 个表是以'YJBB20_'开头的;'LRB'为利润表的首字母缩写,同理业绩报表则为'YJBB'。所以,如果要爬取不同的表格,就需要更改 type 参数。'filter'为表格筛选参数,这里筛选出年中报的数据。不同的表格筛选条件会不一样,所以当 type 类型更改的时候,也要相应修改 filter 类型。

params 参数设置好之后,将 url 和 params 参数一起传进 requests.get()方法中并请求。然后就可以获取网页第一页的表格数据:

可以看到,表格信息存储在 LFtlXDqn 变量中,pages 表示表格有 72 页。data 为表格数据,是一个由多个字典构成的列表,每个字典表示表格的一行数据可以用之前学过的正则表达式分别提取出 pages 和 data 数据。

3.2. 正则表达式提取表格

  1. # 确定页数
  2. import re
  3. pat = re.compile('var.*?{pages:(\d+),data:.*?')
  4. page_all = re.search(pat, response)
  5. print(page_all.group(1))
  6. 结果:
  7. 72

这里用\d+匹配页数中的数值,然后用 re.search()方法提取出来。group(1)表示输出第一个结果,这里就是()中的页数。

  1. # 提取出 list,可以使用 json.dumps 和 json.loads
  2. import json
  3. pattern = re.compile('var.*?data: (.*)}', re.S)
  4. items = re.search(pattern, response)
  5. data = items.group(1)
  6. print(data)
  7. print(type(data))
  8. 结果如下:
  9. [{'scode': '600478', 'hycode': '016040', 'companycode': '10001305', 'sname': '科力远', 'publishname': '材料行业'...
  10. 'sjltz': 10.466665, 'kcfjcxsyjlr': 46691230.93, 'sjlktz': 10.4666649042, 'eutime': '2018/9/6 20:18:42', 'yyzc': 14238766.31}]
  11. <class 'str'>

这里在匹配表格数据用了(.*)表示贪婪匹配,因为 data 中有很多个字典,每个字典都是以'}'结尾,所以我们利用贪婪匹配到最后一个'}',这样才能获取 data 所有数据。多数情况下,我们可能会用到(.*?),这表示非贪婪匹配,意味着之多匹配一个'}',这样的话,我们只能匹配到第一行数据,显然是不对的。

3.3. json.loads()输出表格

这里提取出来的 list 是 str 字符型的,我们需要转换为 list 列表类型。为什么要转换为 list 类型呢,因为无法用操作 list 的方法去操作 str,比如 list 切片。转换为 list 后,我们可以对 list 进行切片,比如 data[0]可以获取第一个{}中的数据,也就是表格第一行,这样方便后续构造循环从而逐行输出表格数据。这里采用 json.loads()方法将 str 转换为 list。

  1. data = json.loads(data)
  2. # print(data) 和上面的一样
  3. print(type(data))
  4. print(data[0])
  5. 结果如下:
  6. <class 'list'>
  7. {'scode': '600478', 'hycode': '016040', 'companycode': '10001305', 'sname': '科力远', 'publishname': '材料行业', 'reporttimetypecode': '002', 'combinetypecode': '001', 'dataajusttype': '2', 'mkt': 'shzb', 'noticedate': '2018-10-13T00:00:00', 'reportdate': '2018-06-30T00:00:00', 'parentnetprofit': -46515200.15, 'totaloperatereve': 683459458.22, 'totaloperateexp': 824933386.17, 'totaloperateexp_tb': -0.0597570689015973, 'operateexp': 601335611.67, 'operateexp_tb': -0.105421872593886, 'saleexp': 27004422.05, 'manageexp': 141680603.83, 'financeexp': 33258589.95, 'operateprofit': -94535963.65, 'sumprofit': -92632216.61, 'incometax': -8809471.54, 'operatereve': '-', 'intnreve': '-', 'intnreve_tb': '-', 'commnreve': '-', 'commnreve_tb': '-', 'operatetax': 7777267.21, 'operatemanageexp': '-', 'commreve_commexp': '-', 'intreve_intexp': '-', 'premiumearned': '-', 'premiumearned_tb': '-', 'investincome': '-', 'surrenderpremium': '-', 'indemnityexp': '-', 'tystz': -0.092852, 'yltz': 0.178351, 'sjltz': 0.399524, 'kcfjcxsyjlr': -58082725.17, 'sjlktz': 0.2475682609, 'eutime': '2018/10/12 21:01:36', 'yyzc': 601335611.67}

接下来我们就将表格内容输入到 csv 文件中。

  1. # 写入 csv 文件
  2. import csv
  3. for d in data:
  4. with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
  5. w = csv.writer(f)
  6. w.writerow(d.values())

通过 for 循环,依次取出表格中的每一行字典数据{},然后用 with...open 的方法写入'eastmoney.csv'文件中。

tips:'a'表示可重复写入;encoding='utf_8_sig' 能保持 csv 文件的汉字不会乱码;newline 为空能避免每行数据中产生空行。

这样,第一页 50 行的表格数据就成功输出到 csv 文件中去了:

这里,我们还可以在输出表格之前添加上表头:

  1. # 添加列标题
  2. def write_header(data):
  3. with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
  4. headers = list(data[0].keys())
  5. print(headers)
  6. print(len(headers)) # 输出 list 长度,也就是有多少列
  7. writer = csv.writer(f)
  8. writer.writerow(headers)

这里,data[0]表示 list 的一个字典中的数据,data[0].keys()表示获取字典中的 key 键值,也就是列标题。外面再加一个 list 序列化(结果如下),然后将该 list 输出到'eastmoney.csv'中作为表格的列标题即可。

  1. ['scode', 'hycode', 'companycode', 'sname', 'publishname', 'reporttimetypecode', 'combinetypecode', 'dataajusttype', 'mkt', 'noticedate', 'reportdate', 'parentnetprofit', 'totaloperatereve', 'totaloperateexp', 'totaloperateexp_tb', 'operateexp', 'operateexp_tb', 'saleexp', 'manageexp', 'financeexp', 'operateprofit', 'sumprofit', 'incometax', 'operatereve', 'intnreve', 'intnreve_tb', 'commnreve', 'commnreve_tb', 'operatetax', 'operatemanageexp', 'commreve_commexp', 'intreve_intexp', 'premiumearned', 'premiumearned_tb', 'investincome', 'surrenderpremium', 'indemnityexp', 'tystz', 'yltz', 'sjltz', 'kcfjcxsyjlr', 'sjlktz', 'eutime', 'yyzc']
  2. 44 # 一共有 44 个字段,也就是说表格有 44 列。

以上,就完成了单页表格的爬取和下载到本地的过程。

4. 多页表格爬取

将上述代码整理为相应的函数,再添加 for 循环,仅 50 行代码就可以爬取 72 页的利润报表数据:

  1. import requests
  2. import re
  3. import json
  4. import csv
  5. import time
  6. def get_table(page):
  7. params = {
  8. 'type': 'CWBB_LRB', # 表格类型,LRB 为利润表缩写,必须
  9. 'token': '70f12f2f4f091e459a279469fe49eca5', # 访问令牌,必须
  10. 'st': 'noticedate', # 公告日期
  11. 'sr': -1, # 保持-1 不用改动即可
  12. 'p': page, # 表格页数
  13. 'ps': 50, # 每页显示多少条信息
  14. 'js': 'var LFtlXDqn={pages:(tp),data: (x)}', # js 函数,必须
  15. 'filter': '(reportdate=^2018-06-30^)', # 筛选条件,如果不选则默认下载全部时期的数据
  16. # 'rt': 51294261 可不用
  17. }
  18. url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?'
  19. response = requests.get(url, params=params).text
  20. # 确定页数
  21. pat = re.compile('var.*?{pages:(\d+),data:.*?')
  22. page_all = re.search(pat, response) # 总页数
  23. pattern = re.compile('var.*?data: (.*)}', re.S)
  24. items = re.search(pattern, response)
  25. data = items.group(1)
  26. data = json.loads(data)
  27. print('\n 正在下载第 %s 页表格' % page)
  28. return page_all,data
  29. def write_header(data):
  30. with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
  31. headers = list(data[0].keys())
  32. writer = csv.writer(f)
  33. writer.writerow(headers)
  34. def write_table(data):
  35. for d in data:
  36. with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
  37. w = csv.writer(f)
  38. w.writerow(d.values())
  39. def main(page):
  40. data = get_table(page)
  41. write_table(data)
  42. if __name__ == '__main__':
  43. start_time = time.time() # 下载开始时间
  44. # 写入表头
  45. write_header(get_table(1))
  46. page_all = get_table(1)[0]
  47. page_all = int(page_all.group(1))
  48. for page in range(1, page_all):
  49. main(page)
  50. end_time = time.time() - start_time # 结束时间
  51. print('下载用时: {:.1f} s' .format(end_time))

整个下载只用了 20 多秒,而之前用 selenium 花了几十分钟,这效率提升了足有 100 倍!

这里,如果我们想下载全部时期(从 2007 年-2018 年)利润报表数据,也很简单。只要将type中的filter参数注释掉,意味着也就是不筛选日期,那么就可以下载全部时期的数据。这里当我们取消注释 filter 列,将会发现总页数 page_all 会从 2018 年中报的 72 页增加到 2528 页,全部下载完成后,表格有超过 12 万行的数据。基于这些数据,可以尝试从中进行一些有价值的数据分析。

5. 通用代码构造

以上代码实现了 2018 年中报利润报表的爬取,但如果不想局限于该报表,还想爬取其他报表或者其他任意时期的数据,那么就需要手动地去修改代码中相应的字段,很不方便。所以上面的代码可以说是简短但不够强大。

为了能够灵活实现爬取任意类别和任意时期的报表数据,需要对代码再进行一些加工,就可以构造出通用强大的爬虫程序了。

  1. """
  2. e.g: http://data.eastmoney.com/bbsj/201806/lrb.html
  3. """
  4. import requests
  5. import re
  6. from multiprocessing import Pool
  7. import json
  8. import csv
  9. import pandas as pd
  10. import os
  11. import time
  12. # 设置文件保存在 D 盘 eastmoney 文件夹下
  13. file_path = 'D:\\eastmoney'
  14. if not os.path.exists(file_path):
  15. os.mkdir(file_path)
  16. os.chdir(file_path)
  17. # 1 设置表格爬取时期、类别
  18. def set_table():
  19. print('*' * 80)
  20. print('\t\t\t\t 东方财富网报表下载')
  21. print('作者:高级农民工 2018.10.10')
  22. print('--------------')
  23. year = int(float(input('请输入要查询的年份(四位数 2007-2018):\n')))
  24. # int 表示取整,里面加 float 是因为输入的是 str,直接 int 会报错,float 则不会
  25. # https://stackoverflow.com/questions/1841565/valueerror-invalid-literal-for-int-with-base-10
  26. while (year < 2007 or year > 2018):
  27. year = int(float(input('年份数值输入错误,请重新输入:\n')))
  28. quarter = int(float(input('请输入小写数字季度(1:1 季报,2-年中报,3:3 季报,4-年报):\n')))
  29. while (quarter < 1 or quarter > 4):
  30. quarter = int(float(input('季度数值输入错误,请重新输入:\n')))
  31. # 转换为所需的 quarter 两种方法,2 表示两位数,0 表示不满 2 位用 0 补充,
  32. # http://www.runoob.com/python/att-string-format.html
  33. quarter = '{:02d}'.format(quarter * 3)
  34. # quarter = '%02d' %(int(month)*3)
  35. # 确定季度所对应的最后一天是 30 还是 31 号
  36. if (quarter == '06') or (quarter == '09'):
  37. day = 30
  38. else:
  39. day = 31
  40. date = '{}-{}-{}' .format(year, quarter, day)
  41. # print('date:', date) # 测试日期 ok
  42. # 2 设置财务报表种类
  43. tables = int(
  44. input('请输入查询的报表种类对应的数字(1-业绩报表;2-业绩快报表:3-业绩预告表;4-预约披露时间表;5-资产负债表;6-利润表;7-现金流量表): \n'))
  45. dict_tables = {1: '业绩报表', 2: '业绩快报表', 3: '业绩预告表',
  46. 4: '预约披露时间表', 5: '资产负债表', 6: '利润表', 7: '现金流量表'}
  47. dict = {1: 'YJBB', 2: 'YJKB', 3: 'YJYG',
  48. 4: 'YYPL', 5: 'ZCFZB', 6: 'LRB', 7: 'XJLLB'}
  49. category = dict[tables]
  50. # js 请求参数里的 type,第 1-4 个表的前缀是'YJBB20_',后 3 个表是'CWBB_'
  51. # 设置 set_table()中的 type、st、sr、filter 参数
  52. if tables == 1:
  53. category_type = 'YJBB20_'
  54. st = 'latestnoticedate'
  55. sr = -1
  56. filter = "(securitytypecode in ('058001001','058001002'))(reportdate=^%s^)" %(date)
  57. elif tables == 2:
  58. category_type = 'YJBB20_'
  59. st = 'ldate'
  60. sr = -1
  61. filter = "(securitytypecode in ('058001001','058001002'))(rdate=^%s^)" %(date)
  62. elif tables == 3:
  63. category_type = 'YJBB20_'
  64. st = 'ndate'
  65. sr = -1
  66. filter=" (IsLatest='T')(enddate=^2018-06-30^)"
  67. elif tables == 4:
  68. category_type = 'YJBB20_'
  69. st = 'frdate'
  70. sr = 1
  71. filter = "(securitytypecode ='058001001')(reportdate=^%s^)" %(date)
  72. else:
  73. category_type = 'CWBB_'
  74. st = 'noticedate'
  75. sr = -1
  76. filter = '(reportdate=^%s^)' % (date)
  77. category_type = category_type + category
  78. # print(category_type)
  79. # 设置 set_table()中的 filter 参数
  80. yield{
  81. 'date':date,
  82. 'category':dict_tables[tables],
  83. 'category_type':category_type,
  84. 'st':st,
  85. 'sr':sr,
  86. 'filter':filter
  87. }
  88. # 2 设置表格爬取起始页数
  89. def page_choose(page_all):
  90. # 选择爬取页数范围
  91. start_page = int(input('请输入下载起始页数:\n'))
  92. nums = input('请输入要下载的页数,(若需下载全部则按回车):\n')
  93. print('*' * 80)
  94. # 判断输入的是数值还是回车空格
  95. if nums.isdigit():
  96. end_page = start_page + int(nums)
  97. elif nums == '':
  98. end_page = int(page_all.group(1))
  99. else:
  100. print('页数输入错误')
  101. # 返回所需的起始页数,供后续程序调用
  102. yield{
  103. 'start_page': start_page,
  104. 'end_page': end_page
  105. }
  106. # 3 表格正式爬取
  107. def get_table(date, category_type,st,sr,filter,page):
  108. # 参数设置
  109. params = {
  110. # 'type': 'CWBB_LRB',
  111. 'type': category_type, # 表格类型
  112. 'token': '70f12f2f4f091e459a279469fe49eca5',
  113. 'st': st,
  114. 'sr': sr,
  115. 'p': page,
  116. 'ps': 50, # 每页显示多少条信息
  117. 'js': 'var LFtlXDqn={pages:(tp),data: (x)}',
  118. 'filter': filter,
  119. # 'rt': 51294261 可不用
  120. }
  121. url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?'
  122. response = requests.get(url, params=params).text
  123. # 确定页数
  124. pat = re.compile('var.*?{pages:(\d+),data:.*?')
  125. page_all = re.search(pat, response)
  126. # print(page_all.group(1)) # ok
  127. # 提取出 list,可以使用 json.dumps 和 json.loads
  128. pattern = re.compile('var.*?data: (.*)}', re.S)
  129. items = re.search(pattern, response)
  130. # 等价于
  131. # items = re.findall(pattern,response)
  132. # print(items[0])
  133. data = items.group(1)
  134. data = json.loads(data)
  135. return page_all, data,page
  136. # 4 写入表头
  137. # 方法 1 借助 csv 包,最常用
  138. def write_header(data,category):
  139. with open('{}.csv' .format(category), 'a', encoding='utf_8_sig', newline='') as f:
  140. headers = list(data[0].keys())
  141. # print(headers) # 测试 ok
  142. writer = csv.writer(f)
  143. writer.writerow(headers)
  144. # 5 写入表格
  145. def write_table(data,page,category):
  146. print('\n 正在下载第 %s 页表格' % page)
  147. # 写入文件方法 1
  148. for d in data:
  149. with open('{}.csv' .format(category), 'a', encoding='utf_8_sig', newline='') as f:
  150. w = csv.writer(f)
  151. w.writerow(d.values())
  152. def main(date, category_type,st,sr,filter,page):
  153. func = get_table(date, category_type,st,sr,filter,page)
  154. data = func[1]
  155. page = func[2]
  156. write_table(data,page,category)
  157. if __name__ == '__main__':
  158. # 获取总页数,确定起始爬取页数
  159. for i in set_table():
  160. date = i.get('date')
  161. category = i.get('category')
  162. category_type = i.get('category_type')
  163. st = i.get('st')
  164. sr = i.get('sr')
  165. filter = i.get('filter')
  166. constant = get_table(date,category_type,st,sr,filter, 1)
  167. page_all = constant[0]
  168. for i in page_choose(page_all):
  169. start_page = i.get('start_page')
  170. end_page = i.get('end_page')
  171. # 先写入表头
  172. write_header(constant[1],category)
  173. start_time = time.time() # 下载开始时间
  174. # 爬取表格主程序
  175. for page in range(start_page, end_page):
  176. main(date,category_type,st,sr,filter, page)
  177. end_time = time.time() - start_time # 结束时间
  178. print('下载完成')
  179. print('下载用时: {:.1f} s' .format(end_time))

以爬取 2018 年中业绩报表为例,感受一下比 selenium 快得多的爬取效果(视频链接):

https://v.qq.com/x/page/a0519bfxajc.html

利用上面的程序,我们可以下载任意时期和任意报表的数据。这里,我下载完成了 2018 年中报所有 7 个报表的数据。

文中代码和素材资源可以在下面的链接中得到:

https://github.com/makcyun/eastmoney_spider

本文完。

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