網路資料抓取-簡書文章閱讀量分析-案例
ofollow,noindex">智慧決策上手系列教程索引
以前在簡書發了一些文章,涉及的分類特別雜亂,有TensorFlow的,有Web開發的,還有一些小學生程式設計教程和繪圖設計教程...最近又在做人工智慧通識專題和智慧決策系列教程的文章。
這些天很多簡友關注我,但我很迷茫,並不知道哪些文章最受大家重視,對大家更有用些,而簡書也沒有這方面的統計功能開放給作者們使用。
我就想能不能自己把這些變化資料抓取下來,自己分析一下,於是就開動寫這個案例教程了。
這個教程推薦使用Chrome瀏覽器和Jupyter Notebook編輯器。Notebook的安裝請參照 安裝Anaconda:包含Python程式設計工具Jupyter Notebook
有哪些資料可以獲取?
從自己的文章列表頁面可以看到總體【關注數】和每篇文章的【觀看數】都是直接獲取的, 我們只要彙總每天哪些文章觀看數增加了,再對比關注數的變化,就能知道哪些文章引發的關注最多 。

image.png
因為目測我的文章每天總閱讀量的增加數,和每天關注的增加數相差不太大,也就是說, 大部分閱讀都引發了關注 ,所以兩者之間是強關聯的。
如果不是這樣,比如每天增加閱讀1萬,關注增加100,那就不好說了,因為可能A文章被觀看100次都引發了關注,而B文章被觀看了9000次卻沒有引發一個關注,那麼就沒辦法從單個文章閱讀量上分析關注變化,也就猜不出哪些文章更受喜歡。
爬蟲資料是在html裡還是在動態json請求裡?
首先我們要知道頁面上這些資料是怎麼來的,是直接html標籤顯示的?還是通過JavaScript動態填充的?請參閱系列教程的前4節。
我們的套路:
-
右鍵【顯示網頁原始碼】,開啟的就是瀏覽器位址列裡面的地址請求直接從伺服器拉取到的html資料,如果這裡可以Ctrl+F搜尋到需要的資料(比如可以搜到“人工智慧通識-AI發展簡史-講義全篇”),那麼用最簡單的html資料提取就可以。
-
如果上面一個辦法搜不到,那就右鍵【檢查元素】,然後檢視Network面板裡面type為xhr的請求,點選每一個,看哪個Response裡面可以Ctrl+F搜到我們需要的資料。(很多時候可以從請求的英文名字裡面猜個八九不離十)
在這個案例裡,我們需要的資料看上去就在網頁原始碼裡面,暫且是這樣。

image.png
怎麼用header和params模擬瀏覽器?
為了不讓網站的伺服器知道我們是爬蟲,就還要像瀏覽器一樣傳送附加的額外資訊,就是header和params。
我們右鍵【檢查】,然後切換到【Network】網路面板,然後重新整理網頁,我們會看到一個和網頁地址一致的請求。
如下圖,我的主頁地址是 https://www.jianshu.com/u/ae784c57b353 ,就看到Network最頂上的是ae784c57b353:

image.png
如果我們切換到請求的Response響應結果面板,就可以看到這個請求獲取的實際就是網頁原始碼。它的type型別是document,也就是html文件。
就是它了,我們需要它的header頭資訊和params引數。
【右擊-Copy-Copy Request Headers】就能複製到這個請求的頭資訊了。

image.png
但是,如果你留意,就會發現這個請求的Response結果(也就是網頁原始碼)並不是包含所有文章,而只是只包含了9個文章。但是如果我們用滑鼠往下滾動頁面,發現文章就會越來越多的自動新增進來。(右側的滾動條越變越短)
我們重新整理頁面重置,然後清空Network網路面板,一點點輕輕往下滾動,直到列表裡出現了一個xhr請求:

image.png
點選這個請求,可以在Headers裡看到它的Parameters引數,其實就是請求名稱的問號後面的部分:

image.png
order_by
是排序, page
是第幾頁。所以不是簡書文章列表不分頁,預設載入的是 page=1
,而當你往下滾動的時候自動新增下一頁的內容 page=2,page=3...
。
我們檢視它的Response也會發現,它所得到的內容和我們上面的網頁原始碼格式是一致的:

image.png
設定引數
開啟Jupyter Notebook,在第一個cell單元編寫程式碼,設定相關引數(headers欄位涉及到個人隱私已經被我簡化了,你須要在瀏覽器裡面複製自己的):
url = 'https://www.jianshu.com/u/ae784c57b353' params = {'order_by': 'shared_at', 'page': '1'} headers = ''' GET /u/ae784c57b353 HTTP/1.1 Host: www.jianshu.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7 Cookie: read_mode=day; default_font=font2; locale=zh-CN;....5b9fb068=1538705012 If-None-Match: W/"31291dc679ccd08938f27b1811f8b263" '''
但是這樣的headers格式是個長串字元,我們需要把它改寫成params那樣的字典,手工改太麻煩也容易改錯,我們再新增一個cell使用下面程式碼自動改寫(不熟悉的話可以暫時不用理解它,以後隨著學習深入就很快會看懂了):
def str2obj(s, s1=';', s2='='): li = s.split(s1) res = {} for kv in li: li2 = kv.split(s2) if len(li2) > 1: res[li2[0]] = li2[1] return res headers = str2obj(headers, '\n', ': ')
發起Request請求
先用最簡單的程式碼傳送請求檢查是否正確:
import requests html = requests.get(url, params=params, headers=headers) print(html.text)
正常的話應該輸出和網頁原始碼差不多的內容。
獲取標題資料
在頁面一個文章上【右鍵-檢查】開啟Elements元素面板,我們來仔細看每個文章標準的一段:

image.png
從圖上可以看到每個 <li>
標籤對應一個文章,我們需要的三個內容(紅色框):
/p/0fed5efab3e5 人工智慧通識-AI發展簡史-講義全篇 272
因為資料都是在html標籤裡面,所以我們需要使用BeautifulSoup功能模組來把html變為容易使用的資料格式,先嚐試抓到標題。把上面的程式碼改進一下:
import requests from bs4 import BeautifulSoup html = requests.get(url, params=params, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') alist = soup.find_all('div', 'content') for item in alist: title = item.find('a', 'title').string print(title)
全部執行輸出結果如下有9個文章:

image.png
獲取文章編號和閱讀量
我們對上面的程式碼改進一下:
import requests from bs4 import BeautifulSoup html = requests.get(url, params=params, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') alist = soup.find_all('div', 'content') for item in alist: line = [] titleTag = item.find('a', 'title')#標題行a標記 line.append(titleTag['href'])#編號 line.append(titleTag.string)#標題 read = item.find('div', 'meta').find('a').contents[2] line.append(str(int(read)))#編號,先轉int去掉空格回車,再轉str才能進line print(','.join(line))
在這裡我們使用 [href]
的方法獲取了 <a class="wrap-img" href="/p/0fed5efab3e5" target="_blank">
這個標記內的屬性,這個方法同樣適用於更多情況,比如 [class]
可以獲得 warp-img
欄位。
另外 item.find('div', 'meta').find('a').contents[2]
這裡,我們利用了 find
只能找到內部第一個符合條件的標記的特點; contents[2]
這是由於a標記內包含了多個內容, <i class="iconfont ic-list-read"></i> 20
,試了幾下,發覺 [2]
是我們想要的內容。
以上程式碼執行全部可以輸出以下內容:

image.png
獲取關注數和文章總數
我們把流程分成兩步走:
- 獲取文章總數和關注總數
- 根據文章總數迴圈獲取每頁的資料
把上面的cell內容都選中,按 Ctrl+/
都臨時註釋掉。然後在這個cell上面新增一個新的cell,用來讀取文章總數 acount
和關總數 afocus
:
import requests from bs4 import BeautifulSoup html = requests.get(url, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') afocus = soup.find('div', 'info').find_all('div','meta-block')[0].find('p').string acount = soup.find('div', 'info').find_all('div','meta-block')[2].find('p').string afocus=int(afocus) acount=int(acount) print('關注:',afocus,'文章', acount)
find
只獲取第一個符合條件的標記, find_all
是獲取所有符合條件的標記。
要對比著html原始碼來看:

image.png
正常應該輸出兩個數字。
獲取全部文章資料
選擇剛才遮蔽掉的程式碼,再次按 ctrl+/
恢復可用。
然後修改成以下內容:
import math import time pages=math.ceil(acount/9) data=[] for n in range(1,pages+1): params['page']=str(n) html = requests.get(url, params=params, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') alist = soup.find_all('div', 'content') for item in alist: line = [] titleTag = item.find('a', 'title')#標題行a標記 line.append(titleTag['href'])#編號 line.append(titleTag.string)#標題 read = item.find('div', 'meta').find('a').contents[2] line.append(str(int(read)))#編號,先轉int去掉空格回車,再轉str才能進line data.append(','.join(line)) print('已獲取:',len(data)) time.sleep(1) print('\n'.join(data))
這裡使用了 math.ceil(acount/9)
的方法獲取總頁數,ceil是遇小數就進1,比如 ceil(8.1)
是9, ceil(9.0)
也是9,這樣即使最後一頁只有1個文章也不會被遺漏。
for n in range(1,12)
這個for迴圈中,每一次n都被自動加1,獲取第一頁時候n是1,第二頁時候n是2...所以 params['page']=str(n)
就可以自動變頁。
print('已獲取:',len(data))
這行其實沒有用,因為獲取頁面需要十幾秒鐘,如果中間不列印點什麼會看上去像是無反應或宕機。 len(data)
是指data這個列表的長度length。
time.sleep(1)
每讀取一頁就停1秒,以免被伺服器發覺我們是爬蟲而封禁我們。
儲存文章資料
我們上面使用逗號分開文章的序號、標題和閱讀量,然後再加入data列表, data.append(','.join(line))
;同樣,最後我們輸出時候使用回車把data所有文章連在一起, print('\n'.join(data))
。
實際上,我們可以直接把它儲存為excel可以讀取的.csv檔案。最下面新建一個cell新增以下程式碼:
with open('articles.csv', 'w', encoding="gb18030") as f: f.write('\n'.join(data)) f.close()
這裡注意, w
是write寫入模式, encoding="gb18030"
是為了確保中文能正常顯示。
執行後就能在你對應的Notebook資料夾內多出一個articles.csv檔案,用excel開啟就能看到類似下面的資料:

image.png
每天使用不同的檔名儲存
我們每天爬取一下所有文章的資料,每天存為一個excel表,都叫做 articles.csv
肯定會重名。我們應該用不同的日期來命名就好很多,比如 articles-201810061230.csv
表示2018年10月6日12點30分統計的記錄,這樣看起來就清楚多了。
如何獲取當前電腦的日期時間?計算機裡面記錄時間的最簡單方法就是,只記錄從某一年開始經過了多少毫秒,大多是從1970年開始算的。只要知道距離1970年1月1日0時0分0秒0毫,過了多少毫秒,那麼這個時間就能計算出是哪年哪月哪日。
我們修改一下上面的程式碼:
tm = str(int(time.time())) fname = 'articles_' + tm + '.csv' with open(fname, 'w', encoding="gb18030") as f: f.write('\n'.join(data)) f.close()
這樣儲存的就是類似 articles_1538722108.csv
檔名的檔案了。
如果要變回年月日的顯示,需要使用datatime功能模組,例如
import time,datetime
tm=int(time.time())
print(datetime.datetime.fromtimestamp(tm).strftime('%Y-%m-%d %H:%M:%S'))
這個會輸出類似 2018-10-05 14:47:11
這樣的結果
增量儲存關注數
我們需要把每一天抓取到的關注數和文章數儲存到一個excel表裡,而不是分開的,我們新增一個cell,新增如下程式碼,實現每次執行就向 total.csv
檔案增加一行資料:
from os.path import exists alabels = ['time', 'focus', 'articles'] adata = [tm, afocus,str(acount)] #acount是數字,需要轉化 afname='./articles_total.csv' if not exists(afname): with open(afname, 'a', encoding="gb18030") as f: f.write(','.join(alabels)+'\n') f.close() with open(afname, 'a', encoding="gb18030") as f: f.write(','.join(adata)+'\n') f.close()
exists
是存在的意思,如果檔案存在,就正常往裡新增新資料,如果不存在就先新增一個表頭 time,focus,articles
。
最後回顧
這個文章裡我們做了下面幾個練習:
- 根據問題思考需要哪些資料,能不能抓取到
- 分析頁面,找到這些資料在哪裡,是html文件裡還是單獨的請求
- 找到請求,複製headers,搞清楚params
- 傳送請求,用beautifulsoup幫助找到需要的資料
- 根據文章總數,迴圈處理分頁
- 把獲取到的資料儲存為excel可以識別的csv檔案
- 利用時間自動建立不同的檔案
- 檔案的增量新增寫入,每次新增一行資料
最終整理到一起的程式碼如下,添加了afuns、aword、alike等資料。
注意必須更換自己的headers才能使用:
#cell-1 設定引數 url = 'https://www.jianshu.com/u/ae784c57b353' params = {'order_by': 'shared_at', 'page': '1'} headers = ''' GET /u/ae784c57b353 HTTP/1.1 Host: www.jianshu.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7 Cookie: read_mode=day; default_font=font2; locale=zh-CN; ......b9fb068=1538705012 If-None-Match: W/"31291dc679ccd08938f27b1811f8b263" ''' #cell-2 轉化headers def str2obj(s, s1=';', s2='='): li = s.split(s1) res = {} for kv in li: li2 = kv.split(s2) if len(li2) > 1: res[li2[0]] = li2[1] return res headers = str2obj(headers, '\n', ': ') # cell-3 傳送整體請求,獲取基本資訊、文章總數 import requests from bs4 import BeautifulSoup html = requests.get(url, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') afocus = soup.find('div', 'info').find_all('div','meta-block')[0].find('p').string afuns = soup.find('div', 'info').find_all('div','meta-block')[1].find('p').string acount = soup.find('div', 'info').find_all('div','meta-block')[2].find('p').string awords = soup.find('div', 'info').find_all('div','meta-block')[3].find('p').string alike = soup.find('div', 'info').find_all('div','meta-block')[4].find('p').string acount=int(acount) print('>>文章總數', acount) #cell-4 迴圈獲取每一頁資料 import math import time aread = 0 pages = math.ceil(acount / 9) data = [] for n in range(1, pages + 1): params['page'] = str(n) html = requests.get(url, params=params, headers=headers) soup = BeautifulSoup(html.text, 'html.parser') alist = soup.find_all('div', 'content') for item in alist: line = [] titleTag = item.find('a', 'title')#標題行a標記 line.append(titleTag['href'])#編號 line.append(titleTag.string)#標題 read = item.find('div', 'meta').find('a').contents[2] aread += int(read)#計算總閱讀量 line.append(str(int(read)))#編號,先轉int去掉空格回車,再轉str才能進line data.append(','.join(line)) print('已獲取:', len(data)) time.sleep(1) #cell-5 儲存文章資料新檔案 tm = str(int(time.time())) fname = './data/articles_' + tm + '.csv' with open(fname, 'w', encoding="gb18030") as f: f.write('\n'.join(data)) f.close() #cell-6 增量儲存基礎資訊 from os.path import exists alabels = ['time', 'focus', 'funs', 'articles', 'words', 'like', 'read'] adata = [tm, afocus, afuns, str(acount), awords, alike, str(aread)] afname='./articles_total.csv' if not exists(afname): with open(afname, 'a', encoding="gb18030") as f: f.write(','.join(alabels)+'\n') f.close() with open(afname, 'a', encoding="gb18030") as f: f.write(','.join(adata)+'\n') f.close() #cll-7 提示完成 print('>>完成,儲存在%s'%fname)
過幾天收集到一些資料之後再分享資料分析相關的內容,請留意我的文章更新~
智慧決策上手系列教程索引
每個人的智慧決策新時代
如果您發現文章錯誤,請不吝留言指正;
如果您覺得有用,請點喜歡;
如果您覺得很有用,歡迎轉載~
END