爬取表格類網站數據並保存為excel文件


本文轉載自以下網站:50 行代碼爬取東方財富網上市公司 10 年近百萬行財務報表數據 https://www.makcyun.top/web_scraping_withpython6.html

 

主要學習的地方:

1.分析網站的ajax請求信息

2.構造參數

3.發起請求后處理獲得的數據

4.保存表格

 

重點:分析表格類網站的ajax請求,以及如何保存這類信息(關於表格方面的)

 

通過分析網址 JavaScript 請求,以比 Selenium 快 100 倍的方法,快速爬取東方財富網各上市公司歷年的財務報表數據。

摘要: 上一篇文章,我們用Selenium成功爬取了東方財富網的財務報表數據,但是速度非常慢,爬取 70 頁需要好幾十分鍾。為了加快速度,本文分析網頁JavaScript請求,找到數據接口然后快速爬取財務報表數據。

1. JavaScript請求分析

上一篇文章,我們簡單分了東方財富網財務報表網頁后台的js請求,文章回顧:(https://www.makcyun.top/web_scraping_withpython5.html

接下來,我們深入分析。首先,點擊報表底部的下一頁,然后觀察左側Name列,看會彈出什么新的請求來:

可以看到,當不斷點擊下一頁時,會相應彈出以get?type開頭的請求。點擊右邊Headers選項卡,可以看到請求的URL,網址非常長,先不管它,后續我們會分析各項參數。接着,點擊右側的Preview和Response,可以看到里面有很多整齊的數據,嘗試猜測這可能是財務報表中的數據,經過和表格進行對比,發現這正是我們所需的數據,太好了。

然后將URL復制到新鏈接中打開看看,可以看到表格中的數據完美地顯示出來了。竟然不用添加Headers、UA去請求就能獲取到,看來東方財富網很大方啊。

到這里,爬取思路已經很清晰了。首先,用Request請求該URL,將獲取到的數據進行正則匹配,將數據轉變為json格式,然后寫入本地文件,最后再加一個分頁循環爬取就OK了。這比之前的Selenium要簡單很多,而且速度應該會快很多倍。下面我們就先來嘗試爬一頁數據看看。

2. 爬取單頁

2.1. 抓取分析

這里仍然以2018年中報的利潤表為例,抓取該網頁的第一頁表格數據,網頁url為:http://data.eastmoney.com/bbsj/201806/lrb.html

表格第一頁的js請求的url為: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,data:%20(x)}&filter=(reportdate=^2018-06-30^)&rt=51312886)

下面,我們通過分析該url,來抓取表格內容。

import requests
def get_table():
params = {
'type': 'CWBB_LRB', # 表格類型,LRB為利潤表縮寫,必須
'token': '70f12f2f4f091e459a279469fe49eca5', # 訪問令牌,必須
'st': 'noticedate', # 公告日期
'sr': -1, # 保持-1不用改動即可
'p': 1, # 表格頁數
'ps': 50, # 每頁顯示多少條信息
'js': 'var LFtlXDqn={pages:(tp),data: (x)}', # js函數,必須
'filter': '(reportdate=^2018-06-30^)', # 篩選條件
# 'rt': 51294261 可不用
}
url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?'
response = requests.get(url, params=params).text
print(response)
get_table()

這里我們定義了一個get_table()方法,來輸出抓取的第一頁表格內容。params為url請求中所包含的參數。

這里對重要參數進行簡單說明: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數據。

2.2. 正則表達式提取表格

# 確定頁數
import re
pat = re.compile('var.*?{pages:(\d+),data:.*?')
page_all = re.search(pat, response)
print(page_all.group(1))
結果:
72

這里用\d+匹配頁數中的數值,然后用re.search()方法提取出來。group(1)表示輸出第一個結果,這里就是()中的頁數。

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

這里在匹配表格數據用了(.*)表示貪婪匹配,因為data中有很多個字典,每個字典都是以’}’結尾,所以我們利用貪婪匹配到最后一個’}’,這樣才能獲取data所有數據。多數情況下,我們可能會用到(.*?),這表示非貪婪匹配,意味着之多匹配一個’}’,這樣的話,我們只能匹配到第一行數據,顯然是不對的。

2.3. json.loads()輸出表格

這里提取出來的list是str字符型的,我們需要轉換為list列表類型。為什么要轉換為list類型呢,因為無法用操作list的方法去操作str,比如list切片。轉換為list后,我們可以對list進行切片,比如data[0]可以獲取第一個{}中的數據,也就是表格第一行,這樣方便后續構造循環從而逐行輸出表格數據。這里采用json.loads()方法將str轉換為list。

data = json.loads(data)
# print(data) 和上面的一樣
print(type(data))
print(data[0])
結果如下:
<class 'list'>
{'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文件中。

# 寫入csv文件
import csv
for d in data:
with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
w = csv.writer(f)
w.writerow(d.values())

通過for循環,依次取出表格中的每一行字典數據{},然后用with…open的方法寫入’eastmoney.csv’文件中。

tips:’a’表示可重復寫入;encoding=’utf_8_sig’ 能保持csv文件的漢字不會亂碼;newline為空能避免每行數據中產生空行。

這樣,第一頁50行的表格數據就成功輸出到csv文件中去了:

這里,我們還可以在輸出表格之前添加上表頭:

# 添加列標題
def write_header(data):
with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
headers = list(data[0].keys())
print(headers)
print(len(headers)) # 輸出list長度,也就是有多少列
writer = csv.writer(f)
writer.writerow(headers)

這里,data[0]表示list的一個字典中的數據,data[0].keys()表示獲取字典中的key鍵值,也就是列標題。外面再加一個list序列化(結果如下),然后將該list輸出到’eastmoney.csv’中作為表格的列標題即可。

['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']
44 # 一共有44個字段,也就是說表格有44列。

以上,就完成了單頁表格的爬取和下載到本地的過程。

3. 多頁表格爬取

將上述代碼整理為相應的函數,再添加for循環,僅50行代碼就可以爬取72頁的利潤報表數據:

import requests
import re
import json
import csv
import time
def get_table(page):
params = {
'type': 'CWBB_LRB', # 表格類型,LRB為利潤表縮寫,必須
'token': '70f12f2f4f091e459a279469fe49eca5', # 訪問令牌,必須
'st': 'noticedate', # 公告日期
'sr': -1, # 保持-1不用改動即可
'p': page, # 表格頁數
'ps': 50, # 每頁顯示多少條信息
'js': 'var LFtlXDqn={pages:(tp),data: (x)}', # js函數,必須
'filter': '(reportdate=^2018-06-30^)', # 篩選條件,如果不選則默認下載全部時期的數據
# 'rt': 51294261 可不用
}
url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?'
response = requests.get(url, params=params).text
# 確定頁數
pat = re.compile('var.*?{pages:(\d+),data:.*?')
page_all = re.search(pat, response) # 總頁數
pattern = re.compile('var.*?data: (.*)}', re.S)
items = re.search(pattern, response)
data = items.group(1)
data = json.loads(data)
print('\n正在下載第 %s 頁表格' % page)
return page_all,data
def write_header(data):
with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
headers = list(data[0].keys())
writer = csv.writer(f)
writer.writerow(headers)
def write_table(data):
for d in data:
with open('eastmoney.csv', 'a', encoding='utf_8_sig', newline='') as f:
w = csv.writer(f)
w.writerow(d.values())

def main(page):
data = get_table(page)
write_table(data)

if __name__ == '__main__':
start_time = time.time() # 下載開始時間
# 寫入表頭
write_header(get_table(1))
page_all = get_table(1)[0]
page_all = int(page_all.group(1))
for page in range(1, page_all):
main(page)
end_time = time.time() - start_time # 結束時間
print('下載用時: {:.1f} s' .format(end_time))

整個下載只用了20多秒,而之前用selenium花了幾十分鍾,這效率提升了足有100倍!

這里,如果我們想下載全部時期(從2007年-2018年)利潤報表數據,也很簡單。只要將type中的filter參數注釋掉,意味着也就是不篩選日期,那么就可以下載全部時期的數據。這里當我們取消注釋filter列,將會發現總頁數page_all會從2018年中報的72頁增加到2528頁,全部下載完成后,表格有超過12萬行的數據。基於這些數據,可以嘗試從中進行一些有價值的數據分析。

4. 通用代碼構造

以上代碼實現了2018年中報利潤報表的爬取,但如果不想局限於該報表,還想爬取其他報表或者其他任意時期的數據,那么就需要手動地去修改代碼中相應的字段,很不方便。所以上面的代碼可以說是簡短但不夠強大。

為了能夠靈活實現爬取任意類別和任意時期的報表數據,需要對代碼再進行一些加工,就可以構造出通用強大的爬蟲程序了。

"""
e.g: http://data.eastmoney.com/bbsj/201806/lrb.html
"""
import requests
import re
from multiprocessing import Pool
import json
import csv
import pandas as pd
import os
import time

# 設置文件保存在D盤eastmoney文件夾下
file_path = 'D:\\eastmoney'
if not os.path.exists(file_path):
os.mkdir(file_path)
os.chdir(file_path)

# 1 設置表格爬取時期、類別
def set_table():
print('*' * 80)
print('\t\t\t\t東方財富網報表下載')
print('作者:高級農民工 2018.10.10')
print('--------------')
year = int(float(input('請輸入要查詢的年份(四位數2007-2018):\n')))
# int表示取整,里面加float是因為輸入的是str,直接int會報錯,float則不會
# https://stackoverflow.com/questions/1841565/valueerror-invalid-literal-for-int-with-base-10
while (year < 2007 or year > 2018):
year = int(float(input('年份數值輸入錯誤,請重新輸入:\n')))

quarter = int(float(input('請輸入小寫數字季度(1:1季報,2-年中報,3:3季報,4-年報):\n')))
while (quarter < 1 or quarter > 4):
quarter = int(float(input('季度數值輸入錯誤,請重新輸入:\n')))

# 轉換為所需的quarter 兩種方法,2表示兩位數,0表示不滿2位用0補充,
# http://www.runoob.com/python/att-string-format.html
quarter = '{:02d}'.format(quarter * 3)
# quarter = '%02d' %(int(month)*3)

# 確定季度所對應的最后一天是30還是31號
if (quarter == '06') or (quarter == '09'):
day = 30
else:
day = 31
date = '{}-{}-{}' .format(year, quarter, day)
# print('date:', date) # 測試日期 ok

# 2 設置財務報表種類
tables = int(
input('請輸入查詢的報表種類對應的數字(1-業績報表;2-業績快報表:3-業績預告表;4-預約披露時間表;5-資產負債表;6-利潤表;7-現金流量表): \n'))

dict_tables = {1: '業績報表', 2: '業績快報表', 3: '業績預告表',
4: '預約披露時間表', 5: '資產負債表', 6: '利潤表', 7: '現金流量表'}

dict = {1: 'YJBB', 2: 'YJKB', 3: 'YJYG',
4: 'YYPL', 5: 'ZCFZB', 6: 'LRB', 7: 'XJLLB'}
category = dict[tables]

# js請求參數里的type,第1-4個表的前綴是'YJBB20_',后3個表是'CWBB_'
# 設置set_table()中的type、st、sr、filter參數
if tables == 1:
category_type = 'YJBB20_'
st = 'latestnoticedate'
sr = -1
filter = "(securitytypecode in ('058001001','058001002'))(reportdate=^%s^)" %(date)
elif tables == 2:
category_type = 'YJBB20_'
st = 'ldate'
sr = -1
filter = "(securitytypecode in ('058001001','058001002'))(rdate=^%s^)" %(date)
elif tables == 3:
category_type = 'YJBB20_'
st = 'ndate'
sr = -1
filter=" (IsLatest='T')(enddate=^2018-06-30^)"
elif tables == 4:
category_type = 'YJBB20_'
st = 'frdate'
sr = 1
filter = "(securitytypecode ='058001001')(reportdate=^%s^)" %(date)
else: category_type = 'CWBB_' st = 'noticedate' sr = -1 filter = '(reportdate=^%s^)' % (date) category_type = category_type + category # print(category_type) # 設置set_table()中的filter參數 yield{ 'date':date, 'category':dict_tables[tables], 'category_type':category_type, 'st':st, 'sr':sr, 'filter':filter }# 2 設置表格爬取起始頁數def page_choose(page_all): # 選擇爬取頁數范圍 start_page = int(input('請輸入下載起始頁數:\n')) nums = input('請輸入要下載的頁數,(若需下載全部則按回車):\n') print('*' * 80) # 判斷輸入的是數值還是回車空格 if nums.isdigit(): end_page = start_page + int(nums) elif nums == '': end_page = int(page_all.group(1)) else: print('頁數輸入錯誤') # 返回所需的起始頁數,供后續程序調用 yield{ 'start_page': start_page, 'end_page': end_page }# 3 表格正式爬取def get_table(date, category_type,st,sr,filter,page): # 參數設置 params = { # 'type': 'CWBB_LRB', 'type': category_type, # 表格類型 'token': '70f12f2f4f091e459a279469fe49eca5', 'st': st, 'sr': sr, 'p': page, 'ps': 50, # 每頁顯示多少條信息 'js': 'var LFtlXDqn={pages:(tp),data: (x)}', 'filter': filter, # 'rt': 51294261 可不用 } url = 'http://dcfm.eastmoney.com/em_mutisvcexpandinterface/api/js/get?' response = requests.get(url, params=params).text # 確定頁數 pat = re.compile('var.*?{pages:(\d+),data:.*?') page_all = re.search(pat, response) # print(page_all.group(1)) # ok # 提取出list,可以使用json.dumps和json.loads pattern = re.compile('var.*?data: (.*)}', re.S) items = re.search(pattern, response) # 等價於 # items = re.findall(pattern,response) # print(items[0]) data = items.group(1) data = json.loads(data) return page_all, data,page# 4 寫入表頭# 方法1 借助csv包,最常用def write_header(data,category): with open('{}.csv' .format(category), 'a', encoding='utf_8_sig', newline='') as f: headers = list(data[0].keys()) # print(headers) # 測試 ok writer = csv.writer(f) writer.writerow(headers)# 5 寫入表格def write_table(data,page,category): print('\n正在下載第 %s 頁表格' % page) # 寫入文件方法1 for d in data: with open('{}.csv' .format(category), 'a', encoding='utf_8_sig', newline='') as f: w = csv.writer(f) w.writerow(d.values())def main(date, category_type,st,sr,filter,page): func = get_table(date, category_type,st,sr,filter,page) data = func[1] page = func[2] write_table(data,page,category)if __name__ == '__main__': # 獲取總頁數,確定起始爬取頁數 for i in set_table(): date = i.get('date') category = i.get('category') category_type = i.get('category_type') st = i.get('st') sr = i.get('sr') filter = i.get('filter') constant = get_table(date,category_type,st,sr,filter, 1) page_all = constant[0] for i in page_choose(page_all): start_page = i.get('start_page') end_page = i.get('end_page') # 先寫入表頭 write_header(constant[1],category) start_time = time.time() # 下載開始時間 # 爬取表格主程序 for page in range(start_page, end_page): main(date,category_type,st,sr,filter, page) end_time = time.time() - start_time # 結束時間 print('下載完成') print('下載用時: {:.1f} s' .format(end_time))

以爬取2018年中業績報表為例,感受一下比selenium快得多的爬取效果(視頻鏈接):

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

利用上面的程序,我們可以下載任意時期和任意報表的數據。這里,我下載完成了2018年中報所有7個報表的數據。

文中代碼和素材資源可以在下面的鏈接中獲取:

https://github.com/makcyun/eastmoney_spider

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM