文章目錄
1. 前言
2月15日,CSDN 聯合PyCon中國、wuhan2020、xinguan2020 等力量,舉辦以「抗擊疫情,開發者在行動」爲主題的2020 Python開發者日·線上技術峯會,圍繞Python在疫情中的具體落地應用與項目,爲廣大Python開發者、愛好者揭祕代碼的力量。
當接到主辦方邀請的時候,我的第一感覺是壓力和責任。因爲這個活動的背景是當前疫情,各行各業都在援助湖北,所有的目光都聚焦在武漢,武漢牽動着億民衆的心。那一刻,我情不自禁,在ppt上寫出了下面的文字:
這是一場純粹的公益活動,沒有牽扯到任何的利益關係。參與者可以掃碼後選擇免費加入,或者支付19元加入,完全自願。如果有收入的話,所有的收入將會由主辦方捐獻給急需援助的地區。
本文所有的代碼,已上傳至我的Github:https://github.com/xufive/2020Pyday,如有需要,請自行下載。
2. 關於爬蟲,我們必須瞭解的一些概念
爬蟲大概是Pythoneer最先接觸、使用最多的技術之一,看似簡單,卻涉及到網絡通訊、應用協議、html/CSS/js、數據解析、服務框架等多個技術領域,初學者往往不容易駕馭,甚至很多人把爬蟲等同爲某個流行的爬蟲庫,比如scrapy等。我認爲,概念是理論的基石,思路是代碼的先驅。弄清楚基本概念、原理,再去編寫使用爬蟲,會有事半功倍的效果。
2.1 爬蟲的定義
- 定義1:爬蟲(crawler)是指一段自動抓取互聯網信息的程序,從互聯網上抓取對於我們有價值的信息。
- 定義2:爬蟲也叫網絡蜘蛛(spider),是一種用來自動瀏覽萬維網的網絡機器人。
仔細體會,兩者有細微的差異:前者傾向於爬取特定對象,後者傾向於全站或全網搜索。
2.2 爬蟲的法律風險
作爲一種計算機技術,爬蟲本身在法律上並不被禁止,但利用爬蟲技術獲取數據這一行爲具有違法甚至是犯罪風險:
- 超負荷爬取,從而導致網站癱瘓或不能訪問
- 非法侵入計算機信息系統
- 爬取個人信息
- 侵犯隱私
- 不正當競爭
2.3 從爬蟲應用場景理解爬蟲類型
- 聚焦網絡爬蟲:針對特定對象或目標,通常是一過性的
- 增量式網絡爬蟲:僅爬取增量部分,這意味着爬取是頻繁的或週期性的活動
- 深層網絡爬蟲:針對那些內容不能通過靜態鏈接獲取的、隱藏在登錄、搜索等表單後的,只有用戶提交必要信息才能獲得的數據
- 通用網絡爬蟲:又稱全網爬蟲,主要爲門戶站點搜索引擎和大型 Web 服務提供商採集數據
2.4 爬蟲的基本技術和爬蟲框架
一個基本的爬蟲框架,至少要包含三個部件:調度服務器、數據下載器、數據處理器,分別對應着爬蟲的三個基本技術:調度服務框架、數據抓取技術、數據預處理技術。
上圖是我們近年來一直使用的一個框架,比基本框架多了一個管理平臺,用於配置下載任務、監視系統個部件工作狀況、監視數據到達情況、均衡各節點負載、分析下載數據的連續性、補齊或重新下載數據等。
3. 數據抓取技術
通常情況下,我們使用標準模塊urllib,或者第三方模塊requests/pycurl等模塊抓取數據,有時候也使用自動化測試工具selenium模塊。當然,也有很多封裝好的框架可用,比如,pyspider/scrapy等等。抓取數據的技術要點包括:
- 構造併發送請求:方法、報頭、參數、cookie文件
- 接收並解讀應答:應答代碼、應答類型、應答內容、編碼格式
- 一次數據抓取,往往由多次請求-應答組成
3.1 騰訊NPC疫情數據下載
稍加分析,我們就能很容易地從騰訊的疫情實時追蹤站點上,得到它的數據服務url:
https://view.inews.qq.com/g2/getOnsInfo
以及3個QueryString參數:
- name: disease_h5
- callback: 回調函數
- _: 精確到毫秒的時間戳
接下來,就是水到渠成了:
>>> import time, json, requests
>>> url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
>>> data = json.loads(requests.get(url=url).json()['data'])
>>> data.keys()
dict_keys(['lastUpdateTime', 'chinaTotal', 'chinaAdd', 'isShowAdd', 'chinaDayList', 'chinaDayAddList', 'dailyNewAddHistory', 'dailyDeadRateHistory', 'confirmAddRank', 'areaTree', 'articleList'])
>>> d = data['areaTree'][0]['children'] >>> [item['name'] for item in d]
['湖北', '廣東', '河南', '浙江', '湖南', '安徽', '江西', '江蘇', '重慶', '山東’, …, '香港', '臺灣', '青海', '澳門', '西藏']
>>> d[0]['children'][0]
{'name': '武漢', 'today': {'confirm': 1104, 'suspect': 0, 'dead': 0, 'heal': 0, 'isUpdated': True}, 'total': {'confirm': 19558, 'suspect': 0, 'dead': 820, 'heal': 1377, 'showRate': True, 'showHeal': False, 'deadRate': 4.19, 'healRate': 7.04}}
更詳細的說明,請參考《Python實戰:抓肺炎疫情實時數據,畫2019-nCoV疫情地圖》。
3.2 Modis數據下載
Modis是搭載在TERRA和AQUA遙感衛星上的一個重要的傳感器,是衛星上唯一將實時觀測數據通過x波段向全世界直接廣播,並可以免費接收數據並無償使用的星載儀器。光譜範圍廣:共有36個波段,光譜範圍從0.4μm-14.4μm。下載Modis數據的步驟如下:
- 以GET方式請求https://urs.earthdata.nasa.gov/home
- 從應答中解析出解析token
- 構造表單,填寫用戶名密碼以及token
- 以POST方式請求https://urs.earthdata.nasa.gov/login
- 記錄cookie
- 以GET方式請求文件下載頁面
- 從應答中解析出文件下載的url
- 下載文件
下面,我們在Python IDLE中以交互方式使用requests模塊完成整個流程。當然,同樣的功能,也可以使用pycurl模塊實現。我在Github上同時提供了requests和pycurl兩種實現代碼。
>>> import re
>>> from requests import request
>>> from requests.cookies import RequestsCookieJar
>>> resp = request('GET', 'https://urs.earthdata.nasa.gov/home')
>>> pt = re.compile(r'.*<input type="hidden" name="authenticity_token" value="(.*)" />.*')
>>> token = pt.findall(resp.text)[0]
>>> jar = RequestsCookieJar()
>>> jar.update(resp.cookies)
>>> url = 'https://urs.earthdata.nasa.gov/login'
>>> forms = {“username”: “linhl”, “redirect_uri”: “”, “commit”: “Log+in”, “client_id”: “”, “authenticity_token”: token, “password”: “*********"}
>>> resp = request('POST', url, data=forms, cookies=jar)
>>> resp.cookies.items()
[('urs_user_already_logged', 'yes'), ('_urs-gui_session', '4f87b3fd825b06ad825a666133481861')]
>>> jar.update(resp.cookies)
>>> url = 'https://ladsweb.modaps.eosdis.nasa.gov/archive/allData/6/MOD13Q1/2019/321/MOD13Q1.A2019321.h00v08.006.2019337235356.hdf'
>>> resp = request('GET', url, cookies=jar)
>>> pu = re.compile(r'href="(https://ladsweb.modaps.eosdis.nasa.gov.*hdf)"')
>>> furl = pu.findall(resp.text)[0]
>>> furl= furl.replace('&', '&')
>>> resp = request('GET', furl, cookies=jar)
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\fb\modis_demo.hdf', 'wb') as fp:
fp.write(resp.content)
我們下載的Modis數據,是HDF格式的。HDF(Hierarchical Data File),意爲多層數據文件,是美國國家高級計算應用中心(National Center for Supercomputing Application, NCSA)爲了滿足各種領域研究需求而研製的一種能高效存儲和分發科學數據的新型數據格式 。HDF可以表示出科學數據存儲和分佈的許多必要條件。HDF ,以及另一種數據格式文件netCDF, 不僅僅美國人在用,我們中國也在用,尤其是空間科學、大氣科學、地球物理等領域,幾乎所有的數據分發,都依賴這兩種格式的文件。
這是剛纔下載的hdf數據文件的廬山真面貌:
3.3 夸克AI搜索NPC疫情數據
夸克AI搜索NPC疫情數據,這個站點無法使用常規的手段抓取數據,網頁源代碼和頁面顯示的內容也完全不搭調(未渲染)。面對這樣的網站,我們還有什麼技術手段嗎?別擔心,我給大家介紹一個有趣的數據抓取技術:只要通過瀏覽器地址欄可以訪問的數據,都可以抓到,真正做到“可見即可抓”。
可見即可抓的實現,依賴於selenium模塊。實際上,selenium並不是專門用於數據抓取的工具,而是一個用於測試網站的自動化測試工具,支持各種瀏覽器包括Chrome、Firefox、Safari等主流界面瀏覽器。用selenium抓取數據,並不是一個通用的方法,因爲它僅支持GET方法(當然,也有一些擴展技術可以幫助selenium實現POST,比如安裝seleniumrequests模塊)。
>>> from selenium import webdriver
>>> from selenium.webdriver.chrome.options import Options
>>> opt = Options()
>>> opt.add_argument('--headless')
>>> opt.add_argument('--disable-gpu')
>>> opt.add_argument('--window-size=1366,768')
>>> driver = webdriver.Chrome(options=opt)
>>> url = 'https://broccoli.uc.cn/apps/pneumonia/routes/index?uc_param_str=dsdnfrpfbivesscpgimibtbmnijblauputogpintnwktprchmt&fromsource=doodle'
>>> driver.get(url)
>>> with open(r'd:\broccoli.html', 'w') as fp:
fp.write(driver.page_source)
247532
>>> driver.quit()
關於selenium模塊的安裝和使用,更多詳細信息,請參考介紹一種有趣的數據抓取技術:可見即可抓。
4. 數據預處理技術
4.1 常見的預處理技術
數據預處理需要考慮的問題:
- 數據格式是否規範?
- 數據是否完整?
- 不符合規範、不完整的數據如何處理?
- 如何保存?如何分發?
基於上述考慮,產生了如下預處理技術:
- xml/html數據解析
- 文本數據解析
- 數據清洗、校驗、去重、補缺、插值、標準化
- 數據存儲和分發
4.2 解析示例:地磁指數(dst)
地磁指數,是描述一時間段內地磁擾動強度的一種分級指標。在中低緯度的觀測站使用的地磁指數稱之爲Dst 指數,這個指數每小時量測一次,主要是量測地磁水平分量的強度變化。這個站點提供了Dst 指數下載,頁面提供上個月每一天每個小時的Dst 指數。全部流程如下:
- 抓取html頁面,使用requests
- 從html中解析出文本數據,保存成數據文件,使用bs4
- 解析文本數據,保存成二維數據表,使用正則表達式
我們還是在Python IDLE中以交互方式實現這個流程:
>>> import requests
>>> html = requests.get('http://wdc.kugi.kyoto-u.ac.jp/dst_realtime/lastmonth/index.html')
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\dst.html', 'w') as fp:
fp.write(html.text)
>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html.text, "lxml")
>>> data_text = soup.pre.text
>>> with open(r'C:\Users\xufive\Documents\215PyCSDN\dst.txt', 'w') as fp:
fp.write(data_text)
>>> import re
>>> r = re.compile('^\s?(\d{1,2})\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$', flags=re.M)
>>> data_re = r.findall(data_text)
>>> data = dict()
>>> for day in data_re:
data.update({int(day[0]):[int(day[hour+1]) for hour in range(24)]})
5. 數據深加工技術
5.1 數據可視化
數據可視化,是數據的視覺表現,旨在藉助於圖形化手段,清晰有效地傳達與溝通信息。它是一個處於不斷演變之中的概念,其邊界在不斷地擴大。事實上,數據可視化也被認爲是數據挖掘的手段之一。
Matplotlib是Python旗下最有影響力的2D繪圖庫,它提供了一整套和Matlab相似的命令API,十分適合交互式地進行製圖。而且也可以方便地將它作爲繪圖控件,嵌入GUI應用程序中。matplotlib 可以繪製多種形式的圖形包括普通的線圖,直方圖,餅圖,散點圖以及誤差線圖等;可以比較方便的定製圖形的各種屬性比如圖線的類型,顏色,粗細,字體的大小等;它能夠很好地支持一部分TeX排版命令,可以比較美觀地顯示圖形中的數學公式。
雖然matplotlib主要專注於繪圖,並且主要是二維的圖形,但是它也有一些不同的擴展,能讓我們在地理圖上繪圖,讓我們把Excel和3D圖表結合起來。在matplotlib的世界裏,這些擴展叫做工具包(toolkits)。工具包是一些關注在某個話題(如3D繪圖)的特定函數的集合。比較流行的工具包有Basemap、GTK 工具、Excel工具、Natgrid、AxesGrid和mplot3d等。
Pyecharts也是一個很棒的繪圖庫,特別是它的Geo地理座標系功能強大,使用方便。我是從它的js版本echarts開始認識它的。不過,Pyecharts的缺點也很突出:一是版本更迭沒有延續性,二是不支持TeX排版命令。尤其是第2個問題,嚴重製約了Pyecharts的發展空間。
數據3D的可視化方面,推薦選擇PyOpenGL,此外還有VTK / Mayavi / Vispy等可供選擇。我自己也有一個3D庫,已經在https://github.com/xufive/wxgl開源。詳情請參考開源我的3D庫WxGL:40行代碼將疫情地圖變成三維地球模型。
關於Matplotlib,請參考我的一篇博文:數學建模三劍客MSN。關於Basemap,我最近在Python實戰:抓肺炎疫情實時數據,畫2019-nCoV疫情地圖一文中有較詳細的應用實例。
5.2 數據挖掘
所謂數據挖掘,是指從大量的數據中通過算法揭示出隱含的、先前未知的並有潛在價值的信息的過程。數據挖掘是一種決策支持過程,它主要基於統計學、數據庫、可視化技術、人工智能、機器學習、模式識別等技術,高度自動化地分析數據。
下面,我們以全國每天確診NCP人數變化曲線爲例,簡單演示曲線擬合技術。曲線擬合多用於趨勢預測,常用的擬合方法有最小二乘曲線擬合、目標函數擬合等。
# -*- coding: utf-8 -*-
import time, json, requests
import numpy as np
import matplotlib.pyplot as plt
from scipy import optimize
plt.rcParams['font.sans-serif'] = ['FangSong'] # 設置默認字體
plt.rcParams['axes.unicode_minus'] = False # 解決保存圖像時'-'顯示爲方塊的問題
def get_day_list():
"""獲取每日數據"""
url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
data = json.loads(requests.get(url=url).json()['data'])['chinaDayList']
return [(item['date'], item['confirm']) for item in data]
def fit_exp():
"""擬合"""
def func(x, a, b):
return np.power(a, (x+b)) # 指數函數y = a^(x+b)
_date, _y = zip(*get_day_list())
_x = np.arange(len(_y))
x = np.arange(len(_y)+1)
fita, fitb = optimize.curve_fit(func, _x, _y, (2,0))
y = func(x, fita[0], fita[1]) # fita即爲最優擬合參數
plt.plot(_date, _y, label='原始數據')
plt.plot(x, y, label='$%0.3f^{x+%0.3f}$'%(fita[0], fita[1]))
plt.legend(loc='upper left')
plt.gcf().autofmt_xdate() # 優化標註(自動傾斜)
plt.grid(linestyle=':') # 顯示網格
plt.show()
if __name__ == '__main__':
fit_exp()
擬合效果如下:
當前全國一心,抗擊病毒,疫情發展已經逐漸趨於穩定,我們使用指數函數作爲擬合目標,在後期的偏差會越來越大,但在初期,該擬合方式對於趨勢預估有一定的參考價值。
5.3 數據服務
所謂數據服務,就是提供數據供爬蟲抓取。Python有很多成熟的web框架,比如,Django,Tornado,Falsk等,都可以很輕鬆地實現數據服務。當然,除了服務框架,數據服務也離不開數據庫。因爲時間限制,這裏就簡單演示一下最經濟的數據服務器吧:
PS D:\XufiveGit\2020Pyday\fb> python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
試試看,瀏覽器變成了文件瀏覽器。
6. 調度服務框架
前面講過,一個基本的爬蟲框架,至少要包含三個部件:調度服務器、數據下載器、數據處理器。我們就用這三個部件,演示一個最小的爬蟲框架。
6.1 調度服務模塊
APScheduler 是我最喜歡的一個用於調度服務的模塊,其全稱是 Advanced Python Scheduler。這是一個輕量級的 Python 定時任務調度框架,功能非常強大。APScheduler 有很多種觸發器,下面的代碼中,使用了cron觸發器,這是APScheduler 中最爲複雜的觸發器,支持cron語法,可以設定非常複雜的觸發方式。
6.2 迷你爬蟲框架
全部代碼,包括註釋,只有五十幾行,卻能實現每10分鐘從騰訊疫情數據服務站點抓取一次數據,並解析保存爲csv格式的數據文件。
# -*- coding: utf-8 -*-
import os, time, json, requests
import multiprocessing as mp
from apscheduler.schedulers.blocking import BlockingScheduler
def data_obtain():
"""獲取數據"""
url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
with open('fb/ncp.txt', 'w') as fp:
fp.write(requests.get(url=url).json()['data'])
print('Obtain OK')
def data_process():
"""處理數據"""
while True:
if os.path.isfile('fb/ncp.txt'):
with open('fb/ncp.txt', 'r') as fp:
data = json.loads(fp.read())
with open('fb/ncp.csv', 'w') as fp:
for p in data['areaTree'][0]['children']:
fp.write('%s,%d,%d,%d,%d\n'%(p['name'], p['total']['confirm'], p['total']['suspect'], p['total']['dead'], p['total']['heal']))
os.remove('fb/ncp.txt')
print('Process OK')
else:
print('No data file')
time.sleep(10)
if __name__ == '__main__':
# 創建並啓動數據處理子進程
p_process = mp.Process(target=data_process) # 創建數據處理子進程
p_process.daemon = True # 設置子進程爲守護進程
p_process.start() # 啓動數據處理子進程
# 創建調度器
scheduler = BlockingScheduler()
# 添加任務
scheduler.add_job(
data_obtain, # 獲取數據的任務
trigger = 'cron', # 設置觸發器爲cron
minute = '*/1', # 設置每分鐘執行一次
misfire_grace_time = 30 # 30秒內沒有執行此job,則放棄執行
)
# 啓動調度服務
scheduler.start()