公眾號(hào)后臺(tái)有豐富的數(shù)據(jù)統(tǒng)計(jì),但是可能依然沒(méi)有我想要的統(tǒng)計(jì)指標(biāo)华嘹。比如吧趣,我公眾號(hào)粉絲量雖然不高,但是閱讀率卻普遍很高耙厚,那我能不能根據(jù)我公眾號(hào)每篇文章的閱讀率的變化情況强挫,畫一張散點(diǎn)圖,來(lái)展現(xiàn)我的公眾號(hào)運(yùn)營(yíng)成果呢薛躬?
登陸后臺(tái)發(fā)現(xiàn)俯渤,公眾號(hào)每篇文章發(fā)送情況的左側(cè),點(diǎn)擊發(fā)送完畢按鈕型宝,可以看到送達(dá)人數(shù)八匠,這是公眾號(hào)發(fā)某篇文章前的粉絲數(shù)絮爷,而標(biāo)題下方有閱讀數(shù)。通過(guò)爬蟲梨树,依次提取每篇文章的送達(dá)人數(shù)和閱讀數(shù)坑夯,根據(jù)公式:閱讀率=閱讀數(shù)/送達(dá)人數(shù),就可以計(jì)算出每篇文章的閱讀率了劝萤。
思路一:在進(jìn)行數(shù)據(jù)可視化的時(shí)候渊涝,用該篇文章的當(dāng)前粉絲數(shù)作為橫軸慎璧,用該篇文章的閱讀率作為縱軸床嫌,就可以畫出每篇文章的閱讀率分布。然后加上一條普通公眾號(hào)的平均閱讀率輔助線胸私,就可以展現(xiàn)出本公眾號(hào)的閱讀率和一般公眾號(hào)相比是什么水平厌处。
思路二:還有一種思路,對(duì)文章閱讀率從小到大依次進(jìn)行排序岁疼,橫軸為文章編號(hào)阔涉,縱軸為閱讀率,這樣可以畫一張帕累托累進(jìn)圖捷绒,加上一條普通公眾號(hào)平均閱讀率的輔助線瑰排,就可以直觀看出有多大比例的文章高于平均閱讀率,并且可以讓讀者忽略粉絲數(shù)這條信息暖侨。
在散點(diǎn)圖的基礎(chǔ)上椭住,還可以再加上文章閱讀量大小,用散點(diǎn)的大小來(lái)表示字逗,但是考慮到我有一百篇文章代表一百個(gè)點(diǎn)京郑,有些文章的閱讀率非常高,用散點(diǎn)大小表示的話葫掉,不便閱讀些举,于是放棄這個(gè)思路。
預(yù)計(jì)的編程邏輯:
(1)登陸到公眾號(hào)后臺(tái)主頁(yè)俭厚。
這一步我在第一個(gè)爬取公眾號(hào)文章url鏈接生成pdf文檔的項(xiàng)目中已經(jīng)實(shí)現(xiàn)過(guò)户魏,直接套用過(guò)來(lái)就可以。
(2)定義一個(gè)抓取送達(dá)人數(shù)和閱讀數(shù)的動(dòng)作挪挤。
這是個(gè)難點(diǎn)绪抛。
(3)進(jìn)行循環(huán),依次抓取每一頁(yè)的7條文章數(shù)據(jù)电禀,寫入一個(gè)字典數(shù)據(jù)里幢码。
公眾號(hào)翻頁(yè)的for循環(huán)在第一個(gè)爬取公眾號(hào)文章的項(xiàng)目中也已經(jīng)實(shí)現(xiàn)過(guò)了,本次稍作改編套用即可尖飞。
(4)將數(shù)據(jù)存入csv文件症副。
這個(gè)動(dòng)作之前也實(shí)現(xiàn)過(guò)店雅。
(5)通過(guò)pandas導(dǎo)入csv文件里的數(shù)據(jù),并進(jìn)行數(shù)據(jù)清洗贞铣,如計(jì)算閱讀率闹啦。
(6)通過(guò)matplotlib等庫(kù),根據(jù)清洗好的數(shù)據(jù)辕坝,繪圖窍奋。
實(shí)際實(shí)現(xiàn)起來(lái),遇到了諸多問(wèn)題酱畅,我們一個(gè)個(gè)解決琳袄,一步步推進(jìn)。
具體步驟
導(dǎo)入模塊
我后來(lái)導(dǎo)入了以下這些模塊纺酸,并不是每個(gè)都用上了窖逗,并不是開(kāi)始就想到要導(dǎo)入這些,而是在實(shí)現(xiàn)程序的過(guò)程中餐蔬,慢慢發(fā)現(xiàn)需要導(dǎo)入某個(gè)模塊碎紊。
<pre>from selenium import webdriver
import re
import time
import pickle
import csv
from selenium.common.exceptions import TimeoutException</pre>
登陸公眾號(hào)后臺(tái)
Python從放棄到入門那一篇,已經(jīng)講過(guò)了樊诺。構(gòu)造了一個(gè)登陸的函數(shù)仗考,之后需要調(diào)用登陸函數(shù),傳入?yún)?shù)為公眾號(hào)的用戶名和密碼词爬。
<pre>def login(username, password):
#打開(kāi)微信公眾號(hào)登錄頁(yè)面
driver.get('https://mp.weixin.qq.com/')
driver.maximize_window()
time.sleep(3)
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[2]/a").click()
# 自動(dòng)填充帳號(hào)密碼
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[1]/div/span/input").clear()
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[1]/div/span/input").send_keys(username)
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[2]/div/span/input").clear()
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[1]/div[2]/div/span/input").send_keys(password)
time.sleep(1)
#自動(dòng)點(diǎn)擊登錄按鈕進(jìn)行登錄
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[4]/a").click()
# 手動(dòng)拿手機(jī)掃二維碼秃嗜!
time.sleep(15)</pre>
定義抓取送達(dá)人數(shù)和閱讀數(shù)的函數(shù)
使用Chrome瀏覽器登陸公眾號(hào)后臺(tái),按F12查看網(wǎng)頁(yè)代碼缸夹,按ctrl+shift+C組合鍵來(lái)查看網(wǎng)頁(yè)上某個(gè)具體的元素痪寻。包含“送達(dá)人數(shù)”文本的那個(gè)元素的xpath為
“//*[@id="list"]/li[1]/div[1]/div[1]/span/div/div/div[2]/p[1]/span”。查看xpath的方式為源代碼中點(diǎn)擊這個(gè)元素所在行虽惭,右鍵選擇-copy-copy xpath橡类。
閱讀數(shù)這個(gè)元素的xpath為“//*[@id="list"]/li[1]/div[2]/span/div/div[2]/div/div[1]/div/span”。由于xpath是精確定位芽唇,在一個(gè)網(wǎng)頁(yè)里某個(gè)元素只有唯一的xpath顾画,但是我要在這個(gè)網(wǎng)頁(yè)里提取7個(gè)同樣的元素,如果我選擇xpath定位匆笤,我就要查看這7個(gè)元素的構(gòu)造規(guī)律研侣。或者我可以用class等元素定位炮捧,這樣我往往能找到同樣的class元素出現(xiàn)7次庶诡,然后用for循環(huán)遍歷。
幾種元素定位方式我都嘗試過(guò)了咆课,在本項(xiàng)目中我最終決定用xpath定位的方式末誓。讀者不信邪的話可以嘗試下其他定位元素的方式扯俱。
查找七個(gè)元素xpath的規(guī)律,發(fā)它們只是在li[i]中的i依次增加而已喇澡,可以用format函數(shù)進(jìn)行格式化迅栅。
搜到菜鳥教程里對(duì)format函數(shù)的講解。
格式化字符串的函數(shù) str.format()晴玖,它增強(qiáng)了字符串格式化的功能读存。
基本語(yǔ)法是通過(guò) {} 和 : 來(lái)代替以前的 % 。
format 函數(shù)可以接受不限個(gè)參數(shù)呕屎,位置可以不按順序让簿。
<pre>>>>"{} {}".format("hello", "world") # 不設(shè)置指定位置,按默認(rèn)順序
'hello world'
"{0} {1}".format("hello", "world") # 設(shè)置指定位置
'hello world'
"{1} {0} {1}".format("hello", "world") # 設(shè)置指定位置
'world hello world'</pre>
于是我用format函數(shù)來(lái)構(gòu)造xpath路徑榨惰。
<pre>'readnum': driver.find_element_by_xpath('//*[@id="list"]/li[{0}]/div[2]/span/div/div[2]/div/div[1]/div/span'.format(i)).text,</pre>
for循環(huán)構(gòu)造好后拜英,運(yùn)行程序静汤,發(fā)現(xiàn)提取到的數(shù)據(jù)沒(méi)有送達(dá)人數(shù)琅催,有閱讀數(shù)。猜想是送達(dá)人數(shù)的數(shù)據(jù)被隱藏了虫给,需要點(diǎn)擊送達(dá)人數(shù)按鈕藤抡,才能調(diào)用數(shù)據(jù)。
于是在每次循環(huán)的開(kāi)始抹估,都設(shè)置點(diǎn)擊送達(dá)人數(shù)處缠黍。結(jié)果是第一行數(shù)據(jù)的送達(dá)人數(shù)有數(shù)據(jù)了,但是之后的六行都沒(méi)有數(shù)據(jù)药蜻。
于是發(fā)現(xiàn)點(diǎn)擊送達(dá)人數(shù)按鈕后瓷式,生成的新數(shù)據(jù)框正好擋住了第二行數(shù)據(jù),導(dǎo)致提取不到之后的數(shù)據(jù)语泽。
于是設(shè)置在每一次提取完數(shù)據(jù)后贸典,鼠標(biāo)點(diǎn)擊頁(yè)面的某個(gè)位置,并且這個(gè)位置點(diǎn)擊后可以無(wú)反應(yīng)踱卵。
運(yùn)行程序后廊驼,發(fā)現(xiàn)可以爬取數(shù)據(jù)了,但有些數(shù)據(jù)爬取不到惋砂,查看數(shù)據(jù)發(fā)現(xiàn)妒挎,每當(dāng)有刪文章的時(shí)候,刪文后的下一篇文章的數(shù)據(jù)就提取不到西饵。于是設(shè)置當(dāng)程序執(zhí)行失敗時(shí)酝掩,也讓鼠標(biāo)點(diǎn)擊頁(yè)面某個(gè)無(wú)反應(yīng)的位置,然后continue繼續(xù)程序的循環(huán)眷柔。
運(yùn)行后期虾,發(fā)現(xiàn)100條數(shù)據(jù)里积蜻,有兩條數(shù)據(jù)沒(méi)有提取到,再次運(yùn)行程序彻消,發(fā)現(xiàn)又是有兩條數(shù)據(jù)沒(méi)有提取到竿拆,并且和上一次的兩條數(shù)據(jù)不完全一樣。猜想是因?yàn)槌绦驁?zhí)行過(guò)快宾尚,服務(wù)器沒(méi)來(lái)得及返回?cái)?shù)據(jù)丙笋。于是設(shè)置了每次循環(huán)睡眠1秒鐘。
最終獲取每一頁(yè)的送達(dá)人數(shù)和閱讀數(shù)的代碼如下:
<pre>def get_postnum_readnum(html):
lst = []
for i in range(1, 8):
try:
driver.find_element_by_xpath("http://[@id="list"]/li[{0}]/div[1]/div[1]".format(i)).click()
time.sleep(1)
temp_dict = {
'postnum': driver.find_element_by_xpath("http://[@id="list"]/li[{0}]/div[1]/div[1]/span/div/div/div[2]/p[1]/span".format(i)).text,
'readnum': driver.find_element_by_xpath('//[@id="list"]/li[{0}]/div[2]/span/div/div[2]/div/div[1]/div/span'.format(i)).text,
'title': driver.find_element_by_xpath(
'//[@id="list"]/li[{0}]/div[2]/span/div/div[2]/a/span'.format(i)).get_attribute(
'textContent'),
'date': driver.find_element_by_xpath("http://[@id="list"]/li[{0}]/div[1]/em".format(i)).text,
}
driver.find_element_by_xpath("http://[@id="list_container"]/div[1]/div[2]/div/span/input").click()
lst.append(temp_dict)
except:
driver.find_element_by_xpath("http://*[@id="list_container"]/div[1]/div[2]/div/span/input").click()
continue
return lst</pre>
進(jìn)行循環(huán)煌贴,依次抓取每頁(yè)的7條數(shù)據(jù)
代碼和Python從放棄到入門那一篇差不多御板。
<pre>#用webdriver啟動(dòng)谷歌瀏覽器
chrome_driver = r"C:\Users\jiansi\PycharmProjects\jiansidata\venv\Lib\site-packages\selenium\webdriver\chrome\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_driver)
"""需要手動(dòng)輸入個(gè)人微信公眾號(hào)的賬號(hào),密碼牛郑,要導(dǎo)出的公眾號(hào)名稱"""
username = '' # 賬號(hào)
password = '' # 密碼
login(username, password)
page_num = int(driver.find_elements_by_class_name('weui-desktop-pagination__num__wrp')[-1].text.split('/')[-1])
點(diǎn)擊下一頁(yè)
num_lst = get_postnum_readnum(driver.page_source)
print(num_lst)
for _ in range(1, page_num):
try:
pagination = driver.find_elements_by_class_name('weui-desktop-pagination__nav')[-1]
pagination.find_elements_by_tag_name('a')[-1].click()
time.sleep(5)
num_lst += get_postnum_readnum(driver.page_source)
except:
continue</pre>
將數(shù)據(jù)存入csv文件
代碼和Python從放棄到入門那一篇差不多怠肋。
<pre>with open('2.csv', 'w', encoding="utf-8", newline='') as f:
writer = csv.DictWriter(f, fieldnames=['postnum', 'readnum', 'title', 'date'])
writer.writeheader()
writer.writerows(num_lst)</pre>
通過(guò)pandas導(dǎo)入csv數(shù)據(jù),并進(jìn)行數(shù)據(jù)清洗
從這一步開(kāi)始淹朋,我新建了一個(gè)文件寫入笙各。
導(dǎo)入模塊,不一定全用上了础芍。
<pre>import sys
import pandas as pd
import csv
import matplotlib.pyplot as plt
from matplotlib.pyplot import savefig
import matplotlib as mpl
import numpy as np
import seaborn as sns
from datetime import datetime
from pandas import to_datetime</pre>
我先讀取csv表格里的數(shù)據(jù)杈抢,看看讀取效果。
<pre>"""用pandas讀取csv文件里的數(shù)據(jù)仑性,生成二維表惶楼,并合并兩張表"""
df1 = pd.read_csv('1.csv', delimiter=',', sep='\t', encoding='utf-8')
df2 = pd.read_csv('2.csv', delimiter=',', sep='\t', encoding='utf-8')
print(df1)
print(df2)
df1.info()
df2.info()</pre>
可能會(huì)報(bào)錯(cuò),原因和encoding的編碼格式有關(guān)诊杆,可是嘗試改變編碼格式歼捐,從gbk換為gbk18030,或者再換位utf-8晨汹,unicode等豹储。
df.info()是查看數(shù)據(jù)的基本情況,方便觀察數(shù)據(jù)有沒(méi)有空值等錯(cuò)誤宰缤。這次數(shù)據(jù)沒(méi)有空值颂翼,所以處理空值等錯(cuò)誤的操作這里就沒(méi)有采用。
因?yàn)槲姨崛×藘蓚€(gè)公眾號(hào)的數(shù)據(jù)慨灭,要將兩個(gè)公眾號(hào)的數(shù)據(jù)合并朦乏,并且我只需要csv數(shù)據(jù)里的某幾列。
<pre>cols1 = df1[['postnum', 'readnum', 'title', 'date']]
cols2 = df2[['postnum', 'readnum', 'title', 'date']]
df3 = cols1.append(cols2, ignore_index=True)
print(df3)</pre>
df3就是我合并兩張表之后的數(shù)據(jù)氧骤。
由于我的送達(dá)人數(shù)這列的數(shù)據(jù)不是純數(shù)字呻疹,而是**人的字符串,我需要去掉這個(gè)人字筹陵,并且變?yōu)檎麛?shù)型數(shù)據(jù)刽锤。
我找了一些pandas教程或者公式集錦镊尺,發(fā)現(xiàn)都沒(méi)有較如何對(duì)某一列的數(shù)據(jù)進(jìn)行處理。
后來(lái)才知道用pandas里的apply()函數(shù)可以實(shí)現(xiàn)并思。并且庐氮,apply函數(shù)還可以實(shí)現(xiàn)對(duì)某些列進(jìn)行運(yùn)算生成新的列,所以計(jì)算閱讀率的任務(wù)也可以通過(guò)apply()函數(shù)完成了宋彼。實(shí)際上Excel里面使用函數(shù)的各種操作弄砍,在pandas里面基本就可以用apply()函數(shù)完成了。
鏈接這篇文章對(duì)apply()输涕,map()音婶,applymap()函數(shù)的講解就很不錯(cuò)。https://zhuanlan.zhihu.com/p/100064394?utm_source=wechat_session
我實(shí)現(xiàn)去掉數(shù)據(jù)里“人”字的代碼
<pre>"""實(shí)現(xiàn)更改postnum列的149人這類數(shù)據(jù)為149莱坎,更改刷新到dataframe中衣式。"""
def postnum_int(series):
postnum = series['postnum']
postnum_int = int(postnum[0:-1])
return postnum_int
df3['postnum'] = df3.apply(postnum_int, axis=1)
print(df3)</pre>
在這串代碼中,我通過(guò)定義一個(gè)變換方法檐什,然后用apply函數(shù)引用這種變換方法碴卧,按列刷新,把生成的數(shù)據(jù)改到原來(lái)那一列厢汹。
類似地螟深,我生成了閱讀率數(shù)據(jù)谐宙。
<pre>"""增加閱讀率數(shù)據(jù)"""
def read_rate(series):
postnum = series['postnum']
readnum = series['readnum']
read_rate = readnum / postnum
return read_rate
df3['read_rate'] = df3.apply(read_rate, axis=1)
print(df3)</pre>
排序方式可以用sort_index()函數(shù)按序號(hào)排序烫葬,也可以用sort_values()函數(shù)按值排序。
我用兩種排序方式生成了兩個(gè)數(shù)據(jù)凡蜻。
<pre>"""對(duì)dataframe按照postnum從小到大進(jìn)行排序"""
df4 = df3.sort_values(axis=0, ascending=True, by='postnum')
print(df4)
"""對(duì)dataframe按照read_rate從小到大進(jìn)行排序"""
df5 = df3.sort_values(axis=0, ascending=True, by='read_rate')
print(df5)</pre>
將數(shù)據(jù)傳入matplotlib的繪圖函數(shù)后發(fā)現(xiàn)搭综,有的閱讀率太高了,影響圖的效果划栓,于是決定刪掉幾個(gè)閱讀率太高的數(shù)據(jù)兑巾,剔除掉三個(gè)閱讀率高于1500%的數(shù)據(jù)。
用drop()函數(shù)進(jìn)行刪除某一行數(shù)據(jù)的操作忠荞。
<pre>"""刪除某一行數(shù)據(jù)"""
df6 = df4.drop(df4[df4.read_rate > 15].index, inplace=False)
print(df6)
df7 = df5.drop(df5[df5.read_rate > 15].index, inplace=False)
print(df7)</pre>
matplotlib繪圖
保存了一張png圖片到文件夾蒋歌。我在圖中加了兩條輔助線,一條紅線代表閱讀率8%委煤,一條綠線代表閱讀率50%堂油。
<pre>"""使用matplotlib生成氣泡圖,按照postnum排序"""
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(df6['postnum'], df6['read_rate'], )
ax.set_xlabel('postnum')
ax.set_ylabel('read_rate')
plt.axhline(y=0.08, ls=":", c="red")
plt.axhline(y=0.5, ls=":", c="green")
plt.savefig('readrate1.png', dpi=750, bboxinches='tight')
plt.show()</pre>
從圖中可見(jiàn),閱讀率普遍高于8%碧绞,也普遍高于50%府框。
按照思路二,將文章按閱讀率從小到大排序讥邻,橫軸為文章序號(hào)迫靖,縱軸為閱讀率院峡,更直觀展現(xiàn)高于某一閱讀率的文章比例。
從圖中可見(jiàn)系宜,我有約80%的文章閱讀率超過(guò)50%照激,有超過(guò)95%的文章閱讀率超過(guò)8%,有約20%的文章閱讀率超過(guò)400%盹牧。以下為這張圖的代碼實(shí)現(xiàn)实抡。
<pre>df7['index'] = np.arange(len(df7))</pre>
df7為按閱讀率排序后的數(shù)組,上面這一句的目的是生成一列index欢策,按照每一條數(shù)據(jù)的行號(hào)輸出編號(hào)吆寨。
<pre>"""用matplotlib生成散點(diǎn)圖,橫軸為文章序號(hào)"""
df7['index'] = np.arange(len(df7))
print(df7)
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(df7['index'], df7['read_rate'], )
ax.set_xlabel('index')
ax.set_ylabel('read_rate')
plt.axhline(y=0.08, ls=":", c="red")
plt.axhline(y=0.5, ls=":", c="green")
plt.savefig('readrate1.png', dpi=750, bboxinches='tight')
plt.show()</pre>
以上這個(gè)項(xiàng)目就完成了踩寇。
畫圖的幾個(gè)包有matplotlib啄清、seaborn、plotnine俺孙,還有pyecharts辣卒,有興趣的可以體驗(yàn)下其他幾個(gè)繪圖包。