
2020,努力做一個(gè)無可替代的人!
寫在前面的話
先說明一下,這是一篇爬蟲+分析+自動(dòng)化的文章,并不是上節(jié)說到的 NumPy 系列文章,NumPy 系列請(qǐng)期待下節(jié)內(nèi)容。
這篇實(shí)戰(zhàn)文章也屬于心血來潮吧,簡(jiǎn)單說一下:
小一我自從疫情發(fā)生了之后,每天早上第一件事就是關(guān)注微博熱搜里面關(guān)于各地確診人數(shù)的新聞,不得不說,確實(shí)很牽動(dòng)人心,前幾天的突增1w+,有點(diǎn)害怕,還好這幾天降下來了。
最近幾天和往常一樣去看熱搜的時(shí)候,卻發(fā)現(xiàn)好像確診人數(shù)的新聞并不在熱搜里面,有時(shí)候還需要折騰一會(huì)才能搜到相關(guān)數(shù)據(jù)。
好吧,既然這樣,那咱們就自己寫一個(gè)程序,自己更新數(shù)據(jù)。
大概這篇文章的起源就是這樣,就一個(gè)心血來潮的沖動(dòng),就有了。
ok,該介紹的背景都說完了,再來說下這篇文章:
技術(shù)方面:會(huì)用到 爬蟲+數(shù)據(jù)庫+數(shù)據(jù)處理+繪圖+郵件 相關(guān)技術(shù)
咋一看,發(fā)現(xiàn)技術(shù)點(diǎn)還挺多,如果你經(jīng)常讀公眾號(hào)的文章,會(huì)發(fā)現(xiàn)大部分知識(shí)點(diǎn)都有專門寫過。
我都一一列出來,文章哪一塊看不明白了回來查一下再繼續(xù)
爬蟲:動(dòng)態(tài)獲取數(shù)據(jù)、BeautifulSoup詳解
數(shù)據(jù)庫:數(shù)據(jù)庫存儲(chǔ)
郵件:郵件發(fā)送
正文
我們要做一個(gè)自動(dòng)化的程序,當(dāng)然就不只是爬蟲那么簡(jiǎn)單了。
先明確一下需求:
- 爬蟲獲取最新疫情數(shù)據(jù)
- 數(shù)據(jù)簡(jiǎn)單清洗,保存數(shù)據(jù)庫
- 繪制熱力地圖,與前一日數(shù)據(jù)進(jìn)行比較
- 將結(jié)果以郵件形式發(fā)送
- 每日定時(shí)執(zhí)行程序
大概就上面五個(gè)步驟,也不是很難嘛。畫熱力地圖是個(gè)新知識(shí),可能需要花一些時(shí)間
準(zhǔn)備好了,我們就開始吧!
爬取數(shù)據(jù)
首先我們需要確定數(shù)據(jù)源,這個(gè)簡(jiǎn)單。
說個(gè)題外話,這次疫情期間,我感覺官方媒體還是很給力的,數(shù)據(jù)都能在第一時(shí)間公開公布,讓大眾知道,還是很給力的
其中包括衛(wèi)健委、人民日?qǐng)?bào)、丁香園、百度地圖等,都有最新數(shù)據(jù)。
就不一一列舉了,網(wǎng)上都能搜得到。
本次爬蟲我用的是丁香園的數(shù)據(jù)。
再說個(gè)題外話,別整那些惡意爬蟲去搞這些網(wǎng)站,特別是最近一段時(shí)間。慎之慎之
看一下丁香園的疫情官網(wǎng),可以看到有這樣一些(國內(nèi))數(shù)據(jù)


一個(gè)是地區(qū)累積確診人數(shù)的熱力分布圖,一個(gè)是當(dāng)前的最新數(shù)據(jù),當(dāng)然還有很多折線圖,我沒有截
我們需要的是每日的各個(gè)省、地市的相關(guān)數(shù)據(jù)。
檢查源代碼,可以看到:

其中有三個(gè) div 需要注意:
- class=’fold___xVOZX‘ 的 div:每個(gè)省的所有數(shù)據(jù)(總)
- class=’areaBlock1___3V3UU‘ 的 div:每個(gè)省的匯總數(shù)據(jù)(分)
- class=’areaBlock2___27vn7‘ 的 div:每個(gè)省下的所有地市數(shù)據(jù)(分)
我們需要的數(shù)據(jù)就在這三個(gè) div 里面,再看看 div 里面有什么:

紅色的是省份匯總數(shù)據(jù),黃色的是地市的數(shù)據(jù),黑色的是具體數(shù)據(jù)標(biāo)簽。
省份匯總數(shù)據(jù)的 div 和地市的數(shù)據(jù)的 div 下面都有5個(gè) p 標(biāo)簽存放數(shù)據(jù),基本一致
5個(gè) p 標(biāo)簽分別是:
- class=’subBlock1___j0DGa‘ 的 p 標(biāo)簽:表示省份/城市名稱
- class=’subBlock2___E7-fW‘ 的 p 標(biāo)簽:表示現(xiàn)存確診人數(shù)
- class=’subBlock4___ANk6l‘ 的 p 標(biāo)簽:表示累計(jì)確診人數(shù)
- class=’subBlock3___3mcDz‘ 的 p 標(biāo)簽:表示死亡人數(shù)
- class=’subBlock5___2EkOU‘ 的 p 標(biāo)簽:表示治愈人數(shù)
數(shù)據(jù)就這些了,選擇一種爬蟲方式爬下來吧
打開頁面,我第一感覺就是動(dòng)態(tài)數(shù)據(jù),不信你也可以試試
選用 selenium 進(jìn)行數(shù)據(jù)爬取,我盡量貼一下核心代碼,文末也有源碼獲取方式
# 初始化 seleniumexecutable_path = "你本機(jī)的chromedriver.exe路徑"# 設(shè)置不彈窗顯示chrome_options = Options()chrome_options.add_argument('--headless')chrome_options.add_argument('--disable-gpu')browser = webdriver.Chrome(chrome_options=chrome_options,executable_path=executable_path)
你也可以選擇 selenium 的彈窗顯示,源碼里面也有寫。
browser.get(url)# 輸出網(wǎng)頁源碼content = browser.page_sourcesoup = BeautifulSoup(content, 'html.parser')# 獲取中國城市疫情人數(shù)soup_city_class = soup.find('div', class_='areaBox___3jZkr').find_all('div',class_='areaBlock2___27vn7')# 獲取每一個(gè)地市的數(shù)據(jù)# 循環(huán)省略resolve_info(per_city, 'city')# 獲取中國省份疫情人數(shù)soup_province_class = soup.find('div', class_='areaBox___3jZkr').find_all('div',class_='areaBlock1___3V3UU')# 獲取每一個(gè)省的數(shù)據(jù)# 循環(huán)省略resolve_info(per_province, 'province')
循環(huán)拿到每一個(gè)省份和每一個(gè)城市的代碼我沒寫,你知道這里面的 per_city 和 per_province 代表每一個(gè)城市和省份就行了。
解析函數(shù)里面,直接獲取我們需要的幾個(gè)數(shù)據(jù)
# 解析省份和地市詳細(xì)數(shù)據(jù)if tag == 'city': # 城市 data_name = data.find('p', class_='subBlock1___j0DGa').find('span').stringelse: # 省份 data_name = [string for string in data.find('p', class_='subBlock1___j0DGa').strings][0]# 現(xiàn)存確診人數(shù)data_curr_diagnose = data.find('p', class_='subBlock2___E7-fW').string# 累計(jì)確診人數(shù)data_sum_diagnose = data.find('p', class_='subBlock4___ANk6l').string# 死亡人數(shù)data_death = data.find('p', class_='subBlock3___3mcDz').string# 治愈人數(shù)data_cure = data.find('p', class_='subBlock5___2EkOU').string
當(dāng)然會(huì)存在一些特殊情況
比如:有的省份最下面有特殊注釋,有的數(shù)據(jù)是空缺的等等,合理處理就行了
[圖片上傳失敗...(image-958b84-1581743457799)]

好了,數(shù)據(jù)已經(jīng)全部拿到了,爬蟲就算結(jié)束了。
數(shù)據(jù)清洗
拿到數(shù)據(jù)以后,大致看了一眼,還算比較規(guī)整的。
在數(shù)據(jù)中,我發(fā)現(xiàn)了兩處需要處理的地方
- 數(shù)據(jù)存在空值
- 部分地市名稱其實(shí)并不是地市名稱
就拿北京來說,看一下數(shù)據(jù):

黃顏色標(biāo)出的是缺失數(shù)據(jù),紅顏色的是非正常名稱
我是這樣處理的:
第一處地方:官網(wǎng)的數(shù)據(jù)并沒有0,所有這個(gè)空值就是0,直接填充就可
第二處地方:部分?jǐn)?shù)據(jù)名稱不對(duì),根據(jù)需求剔除或者合并到省會(huì)城市都可
看一下源代碼:
# 刪除地市的不明確數(shù)據(jù)if tag == 'city': df_data.drop(index=df_data[df_data['city'] == '待明確地區(qū)'].index, axis=1,inplace=True) # df_data.drop(df_data['city'] == '外地來京人員', axis=1, inplace=True) # df_data.drop(df_data['city'] == '外地來滬人員', axis=1, inplace=True) # df_data.drop(df_data['city'] == '外地來津人員', axis=1, inplace=True)# 填充空記錄為0df_data.fillna(0, inplace=True)# 增加日期字段df_data['date'] = time_str
代碼應(yīng)該都能看懂,就不解釋了,日期字段是為了方便取出近兩天的數(shù)據(jù)進(jìn)行比較
接下來就是導(dǎo)數(shù)據(jù)到數(shù)據(jù)庫了,一共兩種表,省份數(shù)據(jù)表和地市數(shù)據(jù)表。
看一下數(shù)據(jù)庫表結(jié)構(gòu):

省份表類似,只是把城市名換成了省份名。
當(dāng)然,你要覺得兩張表麻煩,一張表也可以存這些數(shù)據(jù),看你自己。
對(duì)于我們的 DataFrame 類型的數(shù)據(jù),是可以直接導(dǎo)入數(shù)據(jù)庫的
一行代碼就行,看好了
# 連接數(shù)據(jù)庫connect = create_engine('mysql+pymysql://username:passwd@localhost:3306/db_name?charset=utf8')# 保存數(shù)據(jù)到數(shù)據(jù)庫中df_data.to_sql(name=table_name, con=connect, index=False, if_exists='append')
你不會(huì)覺得連接數(shù)據(jù)庫也算一行吧?那就兩行,給大哥跪下
數(shù)據(jù)搞定了,下面開始繪圖
數(shù)據(jù)繪圖
我們要畫的是熱力地圖,直接用 pyecharts,上手簡(jiǎn)單
用 echarts 的原因是我曾經(jīng)寫過一段時(shí)間前端代碼,echarts研究過一段時(shí)間,比較容易上手
這里需要安裝兩個(gè)模塊 pyecharts 和 ,用來畫圖和輸出成圖片保存
安裝也很簡(jiǎn)單, cmd 下直接輸入 pip install 模塊名稱


模塊包安裝沒有問題的話就可以畫圖了
# 導(dǎo)入相應(yīng)模塊from pyecharts import options as optsfrom pyecharts.charts import Mapfrom pyecharts.render import make_snapshotfrom snapshot_selenium import snapshot"""繪制熱力地圖"""# 獲取數(shù)據(jù)list_data = df_data.iloc[:, [1, 3]].values.tolist()# 繪制地圖ncp_map = ( Map(init_opts=opts.InitOpts('1000px', '600px')) .add('', list_data, 'china') .set_global_opts( title_opts=opts.TitleOpts( title=title, pos_left='center' ), visualmap_opts=opts.VisualMapOpts( # 設(shè)置為分段形數(shù)據(jù)顯示 is_piecewise=True, # 設(shè)置拖拽用的手柄 is_calculable=True, # 設(shè)置數(shù)據(jù)最大值 max_=df_data['sum_diagnose'].max(), # 自定義的每一段的范圍,以及每一段的文字,以及每一段的特別的樣式。 pieces=[ {'min': 10001, 'label': '>10000', 'color': '#4F040A'}, {'min': 1000, 'max': 10000, 'label': '1000 - 10000', 'color': '#811C24'}, {'min': 500, 'max': 999, 'label': '500 - 999', 'color': '#CB2A2F'}, {'min': 100, 'max': 499, 'label': '100 - 499', 'color': '#E55A4E'}, {'min': 10, 'max': 99, 'label': '10 - 99', 'color': '#F59E83'}, {'min': 1, 'max': 9, 'label': '1 - 9', 'color': '#FDEBCF'}, {'min': 0, 'max': 0, 'label': '0', 'color': '#F7F7F7'} ] ), ))# 保存圖片到本地make_snapshot(snapshot, ncp_map.render(), filepath_save)
看著效果還不錯(cuò)。
需要提到的是,我們需要的是省份/地市名稱+累積確診人數(shù)兩列數(shù)據(jù)
它們對(duì)應(yīng)的是第二列和第四列,所以上面代碼是這樣寫的
df_data.iloc[:, [1, 3]]
還有一些地圖的控件設(shè)置,看懂是什么意思就行了,不會(huì)了再去查API文檔
我有挨個(gè)行寫注釋,你可別說你看不懂
圖片生成了,看看張什么樣子

根據(jù)每日的數(shù)據(jù)更新,我們比較最近兩天的增長(zhǎng)情況,做一個(gè)表格出來
獲取到最近兩天的數(shù)據(jù)庫數(shù)據(jù)
# 設(shè)置日期data_time = datetime.now() + timedelta(-2)data_time_str = data_time.strftime('%Y-%m-%d')# 獲取數(shù)據(jù)庫近兩天的數(shù)據(jù)sql_province = 'select * from t_ncp_province_info where date>={0}'.format(data_time_str)df_province_data = pd.read_sql_query(sql_province, connect)
將數(shù)據(jù)按天分成兩部分,做差即可,直接貼代碼
# 獲取數(shù)據(jù)日期date_list = df_data['date'].drop_duplicates().values.tolist()# 根據(jù)日期拆分dataframedf_data_1 = df_data[df_data['date'] == date_list[0]]df_data_2 = df_data[df_data['date'] == date_list[1]]# 昨天-前天 比較新增數(shù)據(jù)df_data_result = df_data_2[['curr_diagnose', 'sum_diagnose', 'death', 'cure']] - df_data_1[['curr_diagnose', 'sum_diagnose', 'death', 'cure']]
更進(jìn)一步的,計(jì)算數(shù)據(jù)的環(huán)比增長(zhǎng)率
# 新增較上一日環(huán)比列df_data_result['curr_diagnose_ratio'] = (df_data_result['curr_diagnose']/df_data_1['curr_diagnose']).apply(lambda x: format(x, '.2%'))df_data_result['sum_diagnose_ratio'] = (df_data_result['sum_diagnose']/df_data_1['sum_diagnose']).apply(lambda x: format(x, '.2%'))df_data_result['death_ratio'] = (df_data_result['death']/df_data_1['death']).apply(lambda x: format(x, '.2%'))df_data_result['cure_ratio'] = (df_data_result['cure']/df_data_1['cure']).apply(lambda x: format(x, '.2%'))
如果要在郵件中顯示表格內(nèi)容,我們還需要對(duì)列名進(jìn)行排序和更改
并且根據(jù)相應(yīng)的數(shù)據(jù)進(jìn)行降序排序,這樣增長(zhǎng)變化看起來更明顯
if tag == 'city': name = '城市'else: name = '省份'df_data = df_data[[tag, 'sum_diagnose', 'sum_diagnose_ratio', 'curr_diagnose','curr_diagnose_ratio', 'death', 'death_ratio', 'cure', 'cure_ratio']]df_data.rename( columns={ tag: name, 'sum_diagnose': '累計(jì)確診人數(shù)', 'sum_diagnose_ratio': '累計(jì)確診環(huán)比增長(zhǎng)率', 'curr_diagnose': '現(xiàn)存確診人數(shù)', 'curr_diagnose_ratio': '現(xiàn)存確診環(huán)比增長(zhǎng)率', 'death': '死亡人數(shù)', 'death_ratio': '死亡環(huán)比增長(zhǎng)率', 'cure': '治愈人數(shù)', 'cure_ratio': '治愈環(huán)比增長(zhǎng)率' }, inplace=True)# 數(shù)據(jù)排序df_data.sort_values(['累計(jì)確診人數(shù)', '累計(jì)確診環(huán)比增長(zhǎng)率'], inplace=True, ascending=False)df_data.reset_index(inplace=True)
ok,以上的數(shù)據(jù),包括生成的圖片都是我們需要在郵件中顯示的。
郵件發(fā)送
郵件中,需要加入上一步的圖片和表格數(shù)據(jù),添加到正文中發(fā)送
因此,郵件正文需要設(shè)置成 html 格式發(fā)送。
并且我們?cè)谡闹行枰迦虢鼉商斓臄?shù)據(jù),所以 html 中需要這樣設(shè)置
# 部分 html 內(nèi)容'<p><img src="cid:image1" alt="最新數(shù)據(jù)地圖" width="1200" height="600"></a></p>''<p><img src="cid:image2" alt="最新數(shù)據(jù)地圖" width="1200" height="600"></a></p>'
根據(jù) cid 區(qū)分不同的照片,同樣的,需要在郵件中這樣設(shè)置
# 讀取圖片并創(chuàng)建MIMEImagefor i, imag_filepath in enumerate(img_path_list): with open(imag_filepath, 'rb') as fp: msg_image = fp.read() msg_image = MIMEImage(msg_image) # 定義圖片 ID,在 HTML 文本中引用 msg_image.add_header('Content-ID', '<image{0}>'.format(i + 1)) message.attach(msg_image)
另外,郵件中設(shè)置 html 格式正文也需要設(shè)置
# 設(shè)置主題subject = '截止 ' + date_str + ' 疫情最新數(shù)據(jù)(自動(dòng)推送)'# 設(shè)置發(fā)送內(nèi)容:1:發(fā)送html表格數(shù)據(jù)message = MIMEMultipart()# 生成郵件正文內(nèi)容emain_content = get_email_content(df_data_1, df_data_2)send_text = MIMEText(emain_content, 'html', 'utf-8')message.attach(send_text)
具體的郵件發(fā)送教程可以看最前面提到的,之前寫的很詳細(xì)
如果沒有什么異常,你會(huì)收到這樣的一封郵件

打開之后,你需要點(diǎn)擊【顯示圖片】
郵件正文部分內(nèi)容是這樣的:


搞定!還有最后一部分
定時(shí)任務(wù)
程序基本上已經(jīng)算是完成了,自動(dòng)化這一步提供一個(gè)方法,大家參考即可:
- Linux下:
可以使用 crontab 設(shè)置定時(shí)任務(wù) - Win下:
可以使用(控制面板搜)任務(wù)計(jì)劃程序設(shè)置定時(shí)任務(wù)
另外,我已經(jīng)部署好了自己的定時(shí)任務(wù),如果有需要的同學(xué)可以在評(píng)論區(qū)留言自己的郵箱 ,每天早上定時(shí)更新
總結(jié)一下:
先列好需求,再把需求一個(gè)個(gè)實(shí)現(xiàn)了,其實(shí)今天的項(xiàng)目就比較清晰明了了。
一個(gè)五個(gè)需求,我們?cè)倩仡櫼幌拢?/p>
- 爬蟲獲取最新疫情數(shù)據(jù)
- 數(shù)據(jù)簡(jiǎn)單清洗,保存數(shù)據(jù)庫
- 繪制熱力地圖,與前一日數(shù)據(jù)進(jìn)行比較
- 將結(jié)果以郵件形式發(fā)送
- 每日定時(shí)執(zhí)行程序
最后一步大家可以先百度,以后我會(huì)專門拎出來寫一節(jié),可以自動(dòng)化的任務(wù)它不香嗎?
源碼獲取
在公眾號(hào)后臺(tái)回復(fù) 武漢加油 獲取文章源碼
有需要交流學(xué)習(xí)的同學(xué)可以加我們的交流群。(后臺(tái)回復(fù)加群)
寫在后面的話
疫情還沒過去,下周大家伙應(yīng)該都要上班了
我已經(jīng)窩了兩星期,雖然特別想出來,但是一想到上下班的人,我就有點(diǎn)慫。
不多說了,下周上班,我們都要保護(hù)好自己。
碎碎念一下
對(duì)了,需要每天定時(shí)郵件更新疫情數(shù)據(jù)的同學(xué)評(píng)論區(qū)留自己的郵箱
我們?cè)u(píng)論區(qū)見
原創(chuàng)不易,歡迎點(diǎn)贊噢
文章首發(fā):公眾號(hào)【知秋小一】
文章同步:掘金,簡(jiǎn)書