一净嘀、預(yù)期結(jié)果:
在空氣質(zhì)量歷史數(shù)據(jù)查詢網(wǎng)站上可以查詢到每個(gè)城市的空氣質(zhì)量歷史數(shù)據(jù)报咳。我們的計(jì)劃是用Python
獲取并打印某個(gè)城市某個(gè)月當(dāng)中每一天的空氣質(zhì)量數(shù)據(jù),包括pm2.5
挖藏,pm10
暑刃,SO2
等數(shù)據(jù)。如下圖效果:
二膜眠、爬取數(shù)據(jù)過程:
1岩臣、需要爬取頁面
很顯然由網(wǎng)頁的url看到,只需要兩個(gè)參數(shù)宵膨,一個(gè)
city
一個(gè)month
架谎,就可以獲取這個(gè)網(wǎng)頁內(nèi)容。但是由于這些數(shù)據(jù)是動態(tài)加載的辟躏,而不是直接出現(xiàn)在原始的html源文件中狐树,所以直接用requests
庫的get方法是拿不到數(shù)據(jù)內(nèi)容的。
2鸿脓、查找js代碼
既然直接get看不到實(shí)際顯示的數(shù)據(jù)抑钟,那么就很容易想到數(shù)據(jù)加載是通過js
完成的。我們在該頁面包含的js
代碼中去找野哭。通過用chrome
瀏覽器的調(diào)試在塔,我發(fā)現(xiàn)這些空氣質(zhì)量數(shù)據(jù)實(shí)際是用js
代碼向下面這個(gè) url 發(fā)送post
請求獲得的。
所以我們的任務(wù)就變成用python
模擬執(zhí)行這個(gè)post
請求拨黔。
https://www.aqistudy.cn/historydata/api/historyapi.php
3蛔溃、解析js代碼
在js
代碼里面找到下面的部分,根據(jù)getServerData
函數(shù)名很容易判斷數(shù)據(jù)就是通過這里獲取的篱蝇。
我們用
Chrome
瀏覽器調(diào)試贺待,在這里設(shè)置斷點(diǎn),刷新頁面零截,中斷后逐步調(diào)試麸塞,就找到了實(shí)際獲取數(shù)據(jù)使用的代碼,居然隱藏在jquery.min.js
文件里面一個(gè)很隱蔽的地方涧衙,并且明顯經(jīng)過了混淆哪工,代碼顯示成了一大堆看不懂的數(shù)字,在網(wǎng)上找一個(gè)js的反混淆工具弧哎,解析之后部分代碼如下:這里已經(jīng)可以很明顯看出post請求的參數(shù):
url
和data
了,我們只要按照這個(gè)格式發(fā)送一個(gè)post
請求就解決問題了撤嫩。
4偎捎、用python模擬執(zhí)行js
第3步已經(jīng)很接近實(shí)現(xiàn)了,但是這里post
請求的參數(shù)是經(jīng)過加密的。如果我們要用python
來直接獲取數(shù)據(jù)茴她,就需要研究清楚它加密和解密的方法寻拂,再用python
把這個(gè)js
代碼加密解密的過程重寫一下,但是這樣很花時(shí)間還容易錯败京,完全沒有必要兜喻。我們用Python的PyExecJs
庫,只要在Python中直接調(diào)用這部分js
代碼就行了赡麦。還是使用原封不動的js
代碼朴皆,使用getParam()
將參數(shù)加密并上傳,獲取到服務(wù)器的返回?cái)?shù)據(jù)后泛粹,再使用decodeData()
將數(shù)據(jù)解析出來遂铡。
三、代碼
代碼:因?yàn)閖s文件太大就不貼了晶姊。
import requests
import execjs
import json
def createParams(city, month, ctx):
'''由城市名扒接、年月得出經(jīng)js加密后的post參數(shù),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):
'''由城市名、年月向服務(wù)器發(fā)送post請求并解密返回?cái)?shù)據(jù),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
# 解析數(shù)據(jù)
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環(huán)境们衙,這里用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質(zhì)量等級')
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('數(shù)據(jù)獲取失敗!')