一、预期结果:
在空气质量历史数据查询网站上可以查询到每个城市的空气质量历史数据。我们的计划是用Python
获取并打印某个城市某个月当中每一天的空气质量数据,包括pm2.5
,pm10
,SO2
等数据。如下图效果:

二、爬取数据过程:
1、需要爬取页面

很显然由网页的url看到,只需要两个参数,一个
city
一个month
,就可以获取这个网页内容。但是由于这些数据是动态加载的,而不是直接出现在原始的html源文件中,所以直接用requests
库的get方法是拿不到数据内容的。
2、查找js代码
既然直接get看不到实际显示的数据,那么就很容易想到数据加载是通过js
完成的。我们在该页面包含的js
代码中去找。通过用chrome
浏览器的调试,我发现这些空气质量数据实际是用js
代码向下面这个 url 发送post
请求获得的。
所以我们的任务就变成用python
模拟执行这个post
请求。
https://www.aqistudy.cn/historydata/api/historyapi.php
3、解析js代码
在js
代码里面找到下面的部分,根据getServerData
函数名很容易判断数据就是通过这里获取的。

我们用
Chrome
浏览器调试,在这里设置断点,刷新页面,中断后逐步调试,就找到了实际获取数据使用的代码,居然隐藏在jquery.min.js
文件里面一个很隐蔽的地方,并且明显经过了混淆,代码显示成了一大堆看不懂的数字,在网上找一个js的反混淆工具,解析之后部分代码如下:

这里已经可以很明显看出post请求的参数:
url
和data
了,我们只要按照这个格式发送一个post
请求就解决问题了。
4、用python模拟执行js
第3步已经很接近实现了,但是这里post
请求的参数是经过加密的。如果我们要用python
来直接获取数据,就需要研究清楚它加密和解密的方法,再用python
把这个js
代码加密解密的过程重写一下,但是这样很花时间还容易错,完全没有必要。我们用Python的PyExecJs
库,只要在Python中直接调用这部分js
代码就行了。还是使用原封不动的js
代码,使用getParam()
将参数加密并上传,获取到服务器的返回数据后,再使用decodeData()
将数据解析出来。
三、代码
代码:因为js文件太大就不贴了。
import requests
import execjs
import json
def createParams(city, month, ctx):
'''由城市名、年月得出经js加密后的post参数,ctx由js代码解析得到'''
method = 'GETDAYDATA'
js = 'getEncryptedData("{0}", "{1}", "{2}")'.format(method, city, month)
params = ctx.eval(js)
return {'hd': params}
def getResponseData(city, month, ctx):
'''由城市名、年月向服务器发送post请求并解密返回数据,ctx由js代码解析得到'''
apiUrl = 'https://www.aqistudy.cn/historydata/api/historyapi.php'
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36'
}
response = requests.post(apiUrl, data=createParams(
city, month, ctx), headers=headers, timeout=10)
if response.status_code != 200:
return None
# 解析数据
js = 'decodeData("{0}")'.format(response.text)
decrypted_data = ctx.eval(js)
data = json.loads(decrypted_data)
return data['result']['data']['items']
if __name__ == '__main__':
# js环境,这里用nodeJS
node = execjs.get()
# compile javascript
ctx = node.compile(open('encryption.js', encoding='utf-8').read())
city = input('请输入城市名(如: 西安):')
year = input('请输入年份(如: 2018):')
month = input('请输入月份(如: 5):').zfill(2)
items = getResponseData(city, year + month, ctx)
if items is not None:
print('\n')
print('日期\tAQI\tPM2.5\tPM10\tSO2\tNO2\tCO\tO3\t质量等级')
for item in items:
print(item['time_point'], end='\t')
print(item['aqi'], end='\t')
print(item['pm2_5'], end='\t')
print(item['pm10'], end='\t')
print(item['so2'], end='\t')
print(item['no2'], end='\t')
print(item['co'], end='\t')
print(item['o3'], end='\t')
print(item['quality'])
else:
print('数据获取失败!')
网友评论