爬一个AJAX加载的网页
目前多数网站都不会将数据直接放在HTML里,而是采用异步加载的方式,原始页面不包含数据,只是一些样式,在页面加载完后,向服务器发送AJAX请求,从其他接口获取数据,处理后在页面上展示。
一些官方的网站数据,有不少是采用这种方式,比较典型的就是外汇交易中心里的一些数据,下面拿每日活跃债券统计这个功能做个例子。
如何看是不是AJAX请求
这种网站大家一般都见过,在左上角选择需要查询的日期,然后会显示出所需的数据,但是浏览器上地址栏中网址并没有变化。那在选择日期的时候到底发生了什么?可以通过开发者工具来进行分析。Chorme
自带开发者工具,Chorme
内核的Edge也包含此工具,感觉两个应该是一样的。在打开上面的页面后,按F12
,打开开发者工具,就会看到图2的界面。
如果是Chorme
可能是全英文的,Edge会有中文的显示。开发者工具里,最上面选择网络
(官方的Chorme
是Network
),然后在下面筛选器的位置选择XHR
,因为AJAX
的请求类型就是XHR
。目前开发者工具中是空的,因为在打开此工具后有任何操作,没有产生请求。
现在回到CFETS的网页上,换一个日期,就会发现开发者工具中多了一项AtbDlyBltn
,类型为xhr
,这就是点击查询后产生的AJAX
请求。
单击这个请求后,查看详细信息,可以看到右边的请求URL
为http://www.chinamoney.com.cn/dqs/rest/dqs-u-bond/AtbDlyBltn
,请求方式为POST
。在请求标头部分,X-Requested-With: XMLHttpRequest
标记了这个请求就是AJAX
。最下面的表单数据里有POST的具体数据:lang: cn
和searchDate: 2020-09-29
。再看右边的预览,这里是从请求获取的数据,其中records
部分就是网页中显示的活跃债券的信息。(官方的CHORME
是英文的,但顺序都是一样的,英文看一下也都看得明白。)
请求结果的提取
对这部分数据的请求是向http://www.chinamoney.com.cn/dqs/rest/dqs-u-bond/AtbDlyBltn
POST日期和语言,python
里的requests
就可以完成模拟请求。代码如下。
1import json
2import requests
3
4
5BASE_URL = 'http://www.chinamoney.com.cn/dqs/rest/dqs-u-bond/AtbDlyBltn'
6date = '2020-09-28'
7post_data = {
8 'lang': 'cn',
9 'searchDate': date,
10}
11
12r = requests.post(BASE_URL, data=post_data)
13records_list = json.loads(r.text)['records']
14
15for record in records_list:
16 for k, v in record.items():
17 print(k, v)
前面就是一些基本的变量,r = requests.post(BASE_URL, data=post_data)
发送请求的部分,取回的结果即在r.text
中,是标准的json
格式,我们所需要的成交数据就在records
的部分,所以使用python
中的json
包,将其转为字典,字典中records
的值为一个包含当日前十成交量债券数据的列表,列表中的每一个元素都是一个字典,对应网页中表格的一行。
如果要批量爬取数据,可以用pandas
先生成一个包含所有日期的列表,然后逐日重复上面的爬取过程,而数据的存储比较简便的方式就是利用pandas
存储到Excel
里。具体代码如下。
1import requests
2import pandas as pd
3import time
4import datetime
5import json
6
7
8BASE_URL = 'http://www.chinamoney.com.cn/dqs/rest/dqs-u-bond/AtbDlyBltn'
9
10
11def genr_date_range(start: datetime.date, end: datetime.date):
12
13 date_range = pd.date_range(start, end).strftime("%Y-%m-%d").to_list()
14 return date_range
15
16
17def session_fetch(start: datetime.date, end: datetime.date):
18
19 date_range = genr_date_range(start, end) # 生成日期列表,格式为YYYY-MM-DD
20
21 s = requests.Session()
22 records_list_to_df = list()
23
24 for date in date_range:
25 print(date)
26 post_data = {
27 'lang': 'cn',
28 'searchDate': date,
29 }
30
31 r = s.post(BASE_URL, data=post_data)
32 records_list = json.loads(r.text)['records']
33
34 if len(records_list) == 0:
35 continue
36
37 for record in records_list:
38 record['date'] = date
39 records_list_to_df += records_list
40 time.sleep(3)
41
42 result_df.to_excel('活跃券成交.xlsx')
43
44
45if __name__ == '__main__':
46 start_date = datetime.date(2018, 1, 1)
47 end_date = datetime.date(2020, 9, 30)
48 session_fetch(start_date, end_date)
加入time.sleep(3)
的目的是为了防止访问过快被封IP,具体怎么个屏蔽规则我也不敢试,反正在用另一种方法爬这个页面的时候把公司的IP弄屏蔽了,同事也都看不了这个页面了,好在一天之后就恢复了。另一种方式最后会介绍。
另一个例子:爬中金所国债期货持仓数据
打开中金所的国债期货成交排名,一看这个页面就和CFETS的很像,打开网页源代码,搜索一下永安,果然也是没有,按照上面的操作看一下请求,果然也是AJAX
的情况。
但是和CFETS不同的是,中金所的网站是请求方式是GET
,而CFETS的是POST
,还有就是在具体请求的URL里最后还包含了一个id=
,后面会有一个数字,换了不同日期后,这个数字会改变,再换回原来的日期,还是会产生一个不同的数字,因此是一个与日期没有对应关系的随机数。但是不要紧,经过测试,随便写个数字应该就行。。。
具体的代码如下。
1import requests
2import pandas as pd
3from xml.dom.minidom import parse
4import xml.dom.minidom
5
6
7BASE_URL = 'http://www.cffex.com.cn/sj/ccpm/{}/{}/T.xml?id=50'
8date = '20200819'
9
10header = {
11 'Host': 'www.cffex.com.cn',
12 'Referer': 'http://www.cffex.com.cn/ccpm/',
13 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 Edg/85.0.564.63',
14 'X-Requested-With': 'XMLHttpRequest',
15}
16
17r = requests.get(BASE_URL.format(date[:6], date[6:]), headers=header)
18
19DOMTree = xml.dom.minidom.parseString(r.text)
20collection = DOMTree.documentElement
21records = collection.getElementsByTagName('data')
22
23tags = ['instrumentid', 'tradingday', 'datatypeid', 'rank', 'shortname', 'volume', 'varvolume', 'partyid', 'productid']
24
25result_df = pd.DataFrame()
26result_list = list()
27for record in records:
28 record_dict = dict()
29 for tag in tags:
30 record_dict[tag] = record.getElementsByTagName(tag)[0].childNodes[0].data
31 result_list.append(record_dict)
32
33result_df = result_df.append(result_list, ignore_index=True)
34result_df.to_excel('cffex.xlsx')
首先,和之前不一样的是BASE_URL
,里面多了两个{}
。我们再看两个请求的URL,下面两个图一个是请求2020年8月18日的T合约,一个是请求2020年9月10日的T合约,可以看到两个括号位置分别对应年份+月份和日期。将BASE_URL
写成这种形式方便之后在批量请求时利用format
来补充相关日期信息。
接下来就是header
字典,这部分是请求头,与开发者工具中下图部分对应。
再下面就是请求,这里的用requests.get
替代了之前的requests.post
,因为中金所的网站的请求方式是GET
。BASE_URL.format(date[:6], date[6:])
就是把日期分成两个部分,分别填到{}
的位置,形成一个与日期对应的URL,而第二个参数headers=header
,是将请求头信息加入到请求中,其实在这个例子里并没有实际的作用,加不加都能获取数据,其他的网站可能会验证请求头,如果确实一些必要的信息,例如User-Agent
、Referer
、Cookies
等,则会限制访问。在之后就是数据处理的过程,与请求数据无关,因为中金所返回的数据不是json
而是xml
,需要利用xml
模块进行解析,然后转换成字典,写入DataFrame
后保存。具体批量爬取的代码就不放了,思路和上面是一样的。这是爬取T合约的方法,要是爬取TF和TS,需要调整URL。
另一个被限制了的方法
这个方法是最开始不会上面方法时候用到的,简单粗暴,中金所似乎没有限制,但是外汇交易中心应该是限制了的,就是利用页面自带的导出功能。
中金所持仓数据下面有个Excel
附件,链接地址是http://www.cffex.com.cn/sj/ccpm/202009/30/T_1.csv
,链接也是一样的规律,日期分成了两个部分,用requests.get
是可以下载下来的,然后将这些下载下来的csv
文件用pandas
等再处理一下,也可以得到数据。在get
中是否需要headers
,具体需要包含什么,我也没有具体试。而CFETS导出到excel后链接地址是http://www.chinamoney.com.cn/dqs/rest/dqs-u-bond/AtbDlyBltnExcel?lang=cn&searchDate=2020-09-30
,地址的规律也都可以看出来,也可以下载下来,但是下了几个后,我就被屏蔽了,不知道是不是因为太快了,之前没有加入sleep
。