公眾號后臺有豐富的數(shù)據(jù)統(tǒng)計(jì),但是可能依然沒有我想要的統(tǒng)計(jì)指標(biāo)。比如,我公眾號粉絲量雖然不高,但是閱讀率卻普遍很高,那我能不能根據(jù)我公眾號每篇文章的閱讀率的變化情況,畫一張散點(diǎn)圖,來展現(xiàn)我的公眾號運(yùn)營成果呢?
登陸后臺發(fā)現(xiàn),公眾號每篇文章發(fā)送情況的左側(cè),點(diǎn)擊發(fā)送完畢按鈕,可以看到送達(dá)人數(shù),這是公眾號發(fā)某篇文章前的粉絲數(shù),而標(biāo)題下方有閱讀數(shù)。通過爬蟲,依次提取每篇文章的送達(dá)人數(shù)和閱讀數(shù),根據(jù)公式:閱讀率=閱讀數(shù)/送達(dá)人數(shù),就可以計(jì)算出每篇文章的閱讀率了。
思路一:在進(jìn)行數(shù)據(jù)可視化的時(shí)候,用該篇文章的當(dāng)前粉絲數(shù)作為橫軸,用該篇文章的閱讀率作為縱軸,就可以畫出每篇文章的閱讀率分布。然后加上一條普通公眾號的平均閱讀率輔助線,就可以展現(xiàn)出本公眾號的閱讀率和一般公眾號相比是什么水平。
思路二:還有一種思路,對文章閱讀率從小到大依次進(jìn)行排序,橫軸為文章編號,縱軸為閱讀率,這樣可以畫一張帕累托累進(jìn)圖,加上一條普通公眾號平均閱讀率的輔助線,就可以直觀看出有多大比例的文章高于平均閱讀率,并且可以讓讀者忽略粉絲數(shù)這條信息。
在散點(diǎn)圖的基礎(chǔ)上,還可以再加上文章閱讀量大小,用散點(diǎn)的大小來表示,但是考慮到我有一百篇文章代表一百個(gè)點(diǎn),有些文章的閱讀率非常高,用散點(diǎn)大小表示的話,不便閱讀,于是放棄這個(gè)思路。
預(yù)計(jì)的編程邏輯:
(1)登陸到公眾號后臺主頁。
這一步我在第一個(gè)爬取公眾號文章url鏈接生成pdf文檔的項(xiàng)目中已經(jīng)實(shí)現(xiàn)過,直接套用過來就可以。
(2)定義一個(gè)抓取送達(dá)人數(shù)和閱讀數(shù)的動作。
這是個(gè)難點(diǎn)。
(3)進(jìn)行循環(huán),依次抓取每一頁的7條文章數(shù)據(jù),寫入一個(gè)字典數(shù)據(jù)里。
公眾號翻頁的for循環(huán)在第一個(gè)爬取公眾號文章的項(xiàng)目中也已經(jīng)實(shí)現(xiàn)過了,本次稍作改編套用即可。
(4)將數(shù)據(jù)存入csv文件。
這個(gè)動作之前也實(shí)現(xiàn)過。
(5)通過pandas導(dǎo)入csv文件里的數(shù)據(jù),并進(jìn)行數(shù)據(jù)清洗,如計(jì)算閱讀率。
(6)通過matplotlib等庫,根據(jù)清洗好的數(shù)據(jù),繪圖。
實(shí)際實(shí)現(xiàn)起來,遇到了諸多問題,我們一個(gè)個(gè)解決,一步步推進(jìn)。
具體步驟
導(dǎo)入模塊
我后來導(dǎo)入了以下這些模塊,并不是每個(gè)都用上了,并不是開始就想到要導(dǎo)入這些,而是在實(shí)現(xiàn)程序的過程中,慢慢發(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>
登陸公眾號后臺
Python從放棄到入門那一篇,已經(jīng)講過了。構(gòu)造了一個(gè)登陸的函數(shù),之后需要調(diào)用登陸函數(shù),傳入?yún)?shù)為公眾號的用戶名和密碼。
<pre>def login(username, password):
#打開微信公眾號登錄頁面
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()
# 自動填充帳號密碼
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)
#自動點(diǎn)擊登錄按鈕進(jìn)行登錄
driver.find_element_by_xpath("http://[@id="header"]/div[2]/div/div/div[1]/form/div[4]/a").click()
# 手動拿手機(jī)掃二維碼!
time.sleep(15)</pre>
定義抓取送達(dá)人數(shù)和閱讀數(shù)的函數(shù)
使用Chrome瀏覽器登陸公眾號后臺,按F12查看網(wǎng)頁代碼,按ctrl+shift+C組合鍵來查看網(wǎng)頁上某個(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)頁里某個(gè)元素只有唯一的xpath,但是我要在這個(gè)網(wǎng)頁里提取7個(gè)同樣的元素,如果我選擇xpath定位,我就要查看這7個(gè)元素的構(gòu)造規(guī)律?;蛘呶铱梢杂胏lass等元素定位,這樣我往往能找到同樣的class元素出現(xiàn)7次,然后用for循環(huán)遍歷。
幾種元素定位方式我都嘗試過了,在本項(xiàng)目中我最終決定用xpath定位的方式。讀者不信邪的話可以嘗試下其他定位元素的方式。
查找七個(gè)元素xpath的規(guī)律,發(fā)它們只是在li[i]中的i依次增加而已,可以用format函數(shù)進(jìn)行格式化。
搜到菜鳥教程里對format函數(shù)的講解。
格式化字符串的函數(shù) str.format(),它增強(qiáng)了字符串格式化的功能。
基本語法是通過 {} 和 : 來代替以前的 % 。
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ù)來構(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ù)沒有送達(dá)人數(shù),有閱讀數(shù)。猜想是送達(dá)人數(shù)的數(shù)據(jù)被隱藏了,需要點(diǎn)擊送達(dá)人數(shù)按鈕,才能調(diào)用數(shù)據(jù)。
于是在每次循環(huán)的開始,都設(shè)置點(diǎn)擊送達(dá)人數(shù)處。結(jié)果是第一行數(shù)據(jù)的送達(dá)人數(shù)有數(shù)據(jù)了,但是之后的六行都沒有數(shù)據(jù)。
于是發(fā)現(xiàn)點(diǎn)擊送達(dá)人數(shù)按鈕后,生成的新數(shù)據(jù)框正好擋住了第二行數(shù)據(jù),導(dǎo)致提取不到之后的數(shù)據(jù)。
于是設(shè)置在每一次提取完數(shù)據(jù)后,鼠標(biāo)點(diǎn)擊頁面的某個(gè)位置,并且這個(gè)位置點(diǎn)擊后可以無反應(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)擊頁面某個(gè)無反應(yīng)的位置,然后continue繼續(xù)程序的循環(huán)。
運(yùn)行后,發(fā)現(xiàn)100條數(shù)據(jù)里,有兩條數(shù)據(jù)沒有提取到,再次運(yùn)行程序,發(fā)現(xiàn)又是有兩條數(shù)據(jù)沒有提取到,并且和上一次的兩條數(shù)據(jù)不完全一樣。猜想是因?yàn)槌绦驁?zhí)行過快,服務(wù)器沒來得及返回?cái)?shù)據(jù)。于是設(shè)置了每次循環(huán)睡眠1秒鐘。
最終獲取每一頁的送達(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),依次抓取每頁的7條數(shù)據(jù)
代碼和Python從放棄到入門那一篇差不多。
<pre>#用webdriver啟動谷歌瀏覽器
chrome_driver = r"C:\Users\jiansi\PycharmProjects\jiansidata\venv\Lib\site-packages\selenium\webdriver\chrome\chromedriver.exe"
driver = webdriver.Chrome(executable_path=chrome_driver)
"""需要手動輸入個(gè)人微信公眾號的賬號,密碼,要導(dǎo)出的公眾號名稱"""
username = '' # 賬號
password = '' # 密碼
login(username, password)
page_num = int(driver.find_elements_by_class_name('weui-desktop-pagination__num__wrp')[-1].text.split('/')[-1])
點(diǎn)擊下一頁
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>
通過pandas導(dǎo)入csv數(shù)據(jù),并進(jìn)行數(shù)據(jù)清洗
從這一步開始,我新建了一個(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>
可能會報(bào)錯(cuò),原因和encoding的編碼格式有關(guān),可是嘗試改變編碼格式,從gbk換為gbk18030,或者再換位utf-8,unicode等。
df.info()是查看數(shù)據(jù)的基本情況,方便觀察數(shù)據(jù)有沒有空值等錯(cuò)誤。這次數(shù)據(jù)沒有空值,所以處理空值等錯(cuò)誤的操作這里就沒有采用。
因?yàn)槲姨崛×藘蓚€(gè)公眾號的數(shù)據(jù),要將兩個(gè)公眾號的數(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)都沒有較如何對某一列的數(shù)據(jù)進(jìn)行處理。
后來才知道用pandas里的apply()函數(shù)可以實(shí)現(xiàn)。并且,apply函數(shù)還可以實(shí)現(xiàn)對某些列進(jìn)行運(yùn)算生成新的列,所以計(jì)算閱讀率的任務(wù)也可以通過apply()函數(shù)完成了。實(shí)際上Excel里面使用函數(shù)的各種操作,在pandas里面基本就可以用apply()函數(shù)完成了。
鏈接這篇文章對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>
在這串代碼中,我通過定義一個(gè)變換方法,然后用apply函數(shù)引用這種變換方法,按列刷新,把生成的數(shù)據(jù)改到原來那一列。
類似地,我生成了閱讀率數(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ù)按序號排序,也可以用sort_values()函數(shù)按值排序。
我用兩種排序方式生成了兩個(gè)數(shù)據(jù)。
<pre>"""對dataframe按照postnum從小到大進(jìn)行排序"""
df4 = df3.sort_values(axis=0, ascending=True, by='postnum')
print(df4)
"""對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>
從圖中可見,閱讀率普遍高于8%,也普遍高于50%。
按照思路二,將文章按閱讀率從小到大排序,橫軸為文章序號,縱軸為閱讀率,更直觀展現(xiàn)高于某一閱讀率的文章比例。
從圖中可見,我有約80%的文章閱讀率超過50%,有超過95%的文章閱讀率超過8%,有約20%的文章閱讀率超過400%。以下為這張圖的代碼實(shí)現(xiàn)。
<pre>df7['index'] = np.arange(len(df7))</pre>
df7為按閱讀率排序后的數(shù)組,上面這一句的目的是生成一列index,按照每一條數(shù)據(jù)的行號輸出編號。
<pre>"""用matplotlib生成散點(diǎn)圖,橫軸為文章序號"""
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è)繪圖包。