“臺部落”對簡書部分文章的未授權(quán)轉(zhuǎn)載行為,想必大家已經(jīng)有所耳聞了吧。
有簡友查了一些關(guān)于爬蟲的資料,里面提到”通過網(wǎng)絡(luò)爬蟲批量獲取數(shù)據(jù)的成本較低“,這段話我部分認同。
既然”臺部落“爬我們的文章,這次我們就以”臺部落”為例,帶大家了解一下網(wǎng)絡(luò)爬蟲的基本開發(fā)流程。
測試連通性
先給出臺部落的網(wǎng)址:https://www.twblogs.net
如果大家直接訪問的話應(yīng)該會報錯,因為這個網(wǎng)站使用了一個叫做 Cloudflare 的 CDN,通過設(shè)置禁止了所有大陸地區(qū)用戶的訪問。
CDN,全稱 Content Delivery Networks,內(nèi)容分發(fā)網(wǎng)絡(luò),是一種加速網(wǎng)絡(luò)資源訪問的技術(shù)。
假設(shè)簡書的圖片存放在上海的一個機房中,如果你在西藏,訪問這張圖片的速度就會比在上海慢一些,因為圖片需要從更遠的地方傳輸過來,這一方面是電信號的傳輸速度上限導致的,另一方面是因為數(shù)據(jù)在傳輸中會經(jīng)過更多次處理,每次處理都要消耗一定時間。
所以,簡書會將圖片上傳到 CDN 網(wǎng)絡(luò)中,這樣當西藏的用戶訪問時,就可以就近從西藏的服務(wù)器獲取數(shù)據(jù),加快了訪問速度。當然,CDN 還有其它作用,比如降低數(shù)據(jù)傳輸成本、在一定程度上抵御網(wǎng)絡(luò)攻擊等。
直接訪問網(wǎng)頁都看不到數(shù)據(jù),用爬蟲當然也是采集不到的,所以我們需要通過一些特殊手段,將自己的 IP 更改成非大陸區(qū)域。出于監(jiān)管原因,我不能描述具體方式,技術(shù)人自然會懂,普通用戶不需要過多了解。
總之,經(jīng)過一些操作后,我們可以正常訪問臺部落網(wǎng)站了。
爬蟲程序一般使用 Python 開發(fā)。首先,我們需要確保程序可以正常訪問到臺部落網(wǎng)站。編寫如下代碼:
import requests
proxies = {
"http": "socks5://127.0.0.1:10808",
"https": "socks5://127.0.0.1:10808"
}
response = requests.get("https://www.twblogs.net", proxies=proxies)
print(response.text)
在這段代碼中,我們導入了一個叫做 requests 的庫,可以簡單理解成別人寫好的代碼,然后設(shè)置本地代理,并通過代理訪問臺部落網(wǎng)站。
訪問結(jié)果(稱為“請求”)存儲在 reponse 變量中,最后,獲取請求到的文本內(nèi)容(網(wǎng)頁代碼),通過 print 函數(shù)將其打印出來。
運行代碼,輸出如下:

現(xiàn)在我們已經(jīng)成功通過程序訪問了臺部落。
分析網(wǎng)絡(luò)請求
我們把目光放回到網(wǎng)頁上:

這次我們要爬取的內(nèi)容是每篇文章的標題、作者和發(fā)布時間。
對信息流內(nèi)容的爬取,數(shù)據(jù)加載方式往往是突破口。
這個網(wǎng)站觸發(fā)新內(nèi)容加載的途徑是下滑,也就是說,一定存在這樣一個邏輯,在頁面下拉到一定位置時觸發(fā)新數(shù)據(jù)的加載。
而要加載內(nèi)容,就不可避免地要發(fā)送網(wǎng)絡(luò)請求。
我們按下 F12,打開瀏覽器的開發(fā)者工具,切換到網(wǎng)絡(luò)選項卡,然后下滑頁面觸發(fā)刷新:

界面中顯示出了非常多的網(wǎng)絡(luò)請求,簡單查看一下,大多數(shù)都是資源請求,比如網(wǎng)頁中的圖標和一些代碼文件。
點擊上圖紅圈中的“Fetch/XHR”按鈕,篩選出所有異步網(wǎng)絡(luò)請求,這是我們獲取數(shù)據(jù)的關(guān)鍵:

注意這幾條請求的網(wǎng)址,改變的只有最后的一部分,前面的“path=index&postoffset=”是沒有變化的(這里的第一條請求來自一個瀏覽器擴展,可以忽略)。
點開第一條請求:

現(xiàn)在我們看到了數(shù)據(jù)加載背后請求的網(wǎng)址,我們一般將這種通過異步方式調(diào)用,返回數(shù)據(jù)的網(wǎng)址稱為 API(應(yīng)用程序編程接口,簡稱“接口”)。
接口地址下面是請求方式,這里是 GET,HTTP 請求中最常用的一種,我們平時訪問某個網(wǎng)站,其實就是對網(wǎng)站發(fā)起 GET 請求。
接下來切換到“Payload”(負載)選項卡:

這些是請求所攜帶的參數(shù),這個網(wǎng)站沒有什么反爬措施,兩個參數(shù)都不難理解。有些網(wǎng)站會在發(fā)送請求時攜帶一個驗證用的參數(shù)(Token),而這個參數(shù)是會根據(jù)一定規(guī)則變化的,這時我們就需要對網(wǎng)頁的源代碼進行分析,找出 Token 的生成邏輯,這里不做展開。
“響應(yīng)”選項卡,很明顯是一段 HTML 代碼。我們平時看到的網(wǎng)頁,就是通過這種語言編寫而成的。
另外,HTML 不是編程語言,而是一種“標記語言”,只靠它一個也不能做出美觀的網(wǎng)頁,還需要有 CSS 為網(wǎng)頁“穿上衣服”,也就是調(diào)整格式,以及 JavaScript 實現(xiàn)網(wǎng)站的交互邏輯,比如點擊標題時進入對應(yīng)的文章頁面。

使用程序發(fā)送網(wǎng)絡(luò)請求
有了這些信息,我們就可以通過編寫程序請求這個接口了。代碼如下:
import requests
proxies = {
"http": "socks5://127.0.0.1:10808",
"https": "socks5://127.0.0.1:10808"
}
response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
proxies=proxies)
print(response.text)
程序輸出如下:

很明顯又是一段 HTML,我們略微修改代碼,將數(shù)據(jù)保存到一個 HTML 文件中:
with open("twblogs_mainpage.html", "w") as f:
f.write(response.text)
解析網(wǎng)頁內(nèi)容
用瀏覽器打開這個 HTML 文件:

這就是接口返回的結(jié)果,只這樣看是看不出什么的,我們還是要回到源代碼上,切換到“元素”選項卡:

開發(fā)者工具的左上角有一個方框和指針的按鈕,點擊一下,它會變成藍色,然后點擊網(wǎng)頁上要采集的元素,開發(fā)者工具中的代碼會自動展開,并顯示出與這個元素對應(yīng)的代碼:

這里需要補充一點知識,HTML 是由一個個“標簽”嵌套而成的,例如上圖中的“html”、“body”、“div”、“section”。
每個標簽都由尖括號包裹起來,不同種類的標簽嵌套在一起,就構(gòu)成了網(wǎng)頁的骨架。
不難發(fā)現(xiàn),我們的目標在body > section > h4 > a中,具體來說,是這個 a 標簽的內(nèi)容。
這里要引入一種叫做 xPath 的表達式,它可以幫助我們快速地在 HTML 中定位內(nèi)容。
上面的嵌套關(guān)系,用 xPath 表示是這樣的:
//section[@class="list-item"]/div/h4/a/text()
雙斜杠的意思是查找網(wǎng)頁中所有符合條件的標簽,單斜杠則代表嵌套關(guān)系,中括號內(nèi)可以指定這個標簽的屬性。
所以,這段 xPath 的含義是這樣的:在整個 HTML 文件中查找 class 屬性為 list-item 的 section 標簽,定位到它下面的 div 標簽,然后是 h4 標簽,再里面是 a 標簽,獲取這個標簽的內(nèi)容。
我們可以通過一個叫做 xPath Helper 的瀏覽器插件來驗證這段 xPath 的正確性:

result 中顯示出了每篇文章的標題,接下來就是用程序?qū)崿F(xiàn)解析流程了,我們需要用到一個叫做 lxml 的庫,它可以幫助我們通過 xPath 表達式提取網(wǎng)頁內(nèi)容:
import requests
from lxml import etree
proxies = {
"http": "socks5://127.0.0.1:10808",
"https": "socks5://127.0.0.1:10808"
}
response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
proxies=proxies)
html_obj = etree.HTML(response.text)
print(html_obj.xpath("http://section[@class='list-item']/div/h4/a/text()"))

程序以一個 Python 列表的方式返回了頁面中所有的文章標題。
接下來我們再寫一些 xPath 表達式:
# 文章鏈接
//section[@class="list-item"]/div/h4/a/@href
# 作者昵稱
//section[@class="list-item"]/div/div/a/span/text()
# 作者鏈接
//section[@class="list-item"]/div/div/a/@href
# 發(fā)布時間
//section[@class="list-item"]/div/div/span/text()
然后修改我們的代碼,使程序能獲取到這些數(shù)據(jù):
from datetime import datetime
from pprint import pprint
import requests
from lxml import etree
proxies = {
"http": "socks5://127.0.0.1:10808",
"https": "socks5://127.0.0.1:10808"
}
response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
proxies=proxies)
html_obj = etree.HTML(response.text)
titles = html_obj.xpath("http://section[@class='list-item']/div/h4/a/text()")
article_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("http://section[@class='list-item']/div/h4/a/@href"))
author_nicknames = html_obj.xpath("http://section[@class='list-item']/div/div/a/span/text()")
author_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("http://section[@class='list-item']/div/div/a/@href"))
publish_times = (datetime.fromisoformat(x) for x in html_obj.xpath("http://section[@class='list-item']/div/div/span/text()"))
result = []
for title, article_link, author_nickname, author_link, publish_time in zip(
titles, article_links, author_nicknames, author_links, publish_times):
result.append({
"title": title,
"article_link": article_link,
"author_nickname": author_nickname,
"author_link": author_link,
"publish_time": publish_time
})
pprint(result)

在一番神奇的操作之后,程序以列表中嵌套字典的方式返回了我們要爬取的所有數(shù)據(jù)。
分頁處理
但這只是一頁,怎么獲取其它頁的內(nèi)容呢?
還記得我們在前文看到的請求參數(shù)么?

offset,意思是“偏移”,看看前面爬取到的數(shù)據(jù),這個字段像什么?
對,文章鏈接。每一個新請求所攜帶的 offset 參數(shù)值,正是上一個請求最后一組數(shù)據(jù)中,文章鏈接的最后一部分。沿用簡書的叫法,我們將這一段稱為 slug。
而 path 參數(shù)是不變的,這樣問題就簡單了,只需要存儲每次請求獲得的 slug,然后在下一次請求中作為參數(shù)傳入即可。
簡單修改一下代碼,爬它十頁:
from datetime import datetime
from pprint import pprint
import requests
from lxml import etree
proxies = {
"http": "socks5://127.0.0.1:10808",
"https": "socks5://127.0.0.1:10808"
}
slug = None
result = []
for i in range(10):
params = {"path": "index", "postoffset": slug}
response = requests.get("https://www.twblogs.net/api/list/?path=index&postoffset=620602826c6f797dd5fadd7a",
proxies=proxies)
html_obj = etree.HTML(response.text)
titles = html_obj.xpath("http://section[@class='list-item']/div/h4/a/text()")
article_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("http://section[@class='list-item']/div/h4/a/@href"))
author_nicknames = html_obj.xpath("http://section[@class='list-item']/div/div/a/span/text()")
author_links = (f"https://www.twblogs.net{x}" for x in html_obj.xpath("http://section[@class='list-item']/div/div/a/@href"))
publish_times = (datetime.fromisoformat(x) for x in html_obj.xpath("http://section[@class='list-item']/div/div/span/text()"))
for title, article_link, author_nickname, author_link, publish_time in zip(
titles, article_links, author_nicknames, author_links, publish_times):
result.append({
"title": title,
"article_link": article_link,
"author_nickname": author_nickname,
"author_link": author_link,
"publish_time": publish_time
})
slug = result[-1]["article_link"].split("/")[-1]
print(f"爬取第 {i + 1} 頁成功!")
print(f"數(shù)據(jù)條數(shù):{len(result)}")
輸出結(jié)果:

一百五十條數(shù)據(jù),只用了十秒鐘。
數(shù)據(jù)存儲
result 列表在程序運行完畢之后就會消失,所以我們需要把數(shù)據(jù)進行存儲。
一般情況下,我們使用數(shù)據(jù)庫進行數(shù)據(jù)的存儲,但考慮到代碼復雜度和理解難度,我們這次使用 CSV 格式存儲數(shù)據(jù),可以理解為 Excel 表格。
只需要在最前面加上這行代碼:
import pandas as pd
然后在最后加上這幾行:
df = pd.DataFrame(result)
df.to_csv("data.csv", encoding="utf-8")
print("數(shù)據(jù)保存成功!")
程序運行完畢后,目錄下會多出一個 data.csv 文件,我們打開它:

完美。
回頭看看代碼量,只有 44 行。
結(jié)語
十秒鐘一百五十條數(shù)據(jù),乍一看很快,但對于整個臺部落的數(shù)據(jù)量來說,效率還是不盡如人意。
但我們編寫的這種,是單線程同步爬蟲。
在真正的爬蟲程序中,可以同時發(fā)起數(shù)十個網(wǎng)絡(luò)請求,可以讓程序在發(fā)送一個請求后不在原地死等結(jié)果,而是去處理其它請求。
真實的數(shù)據(jù)庫可以讓多個程序同時寫入數(shù)據(jù),存儲的數(shù)據(jù)量可達千萬。
通過分布式技術(shù),我們可以在多臺服務(wù)器上部署爬蟲程序,讓結(jié)果統(tǒng)一存儲到一個數(shù)據(jù)庫內(nèi)。
運用成熟的爬蟲框架,我們可以更輕松地完成開發(fā),并通過網(wǎng)頁界面監(jiān)控每個采集任務(wù)的狀態(tài)。
生產(chǎn)數(shù)據(jù)的成本很高,但采集數(shù)據(jù)的成本很低。在技術(shù)防御之外,總需要有法律法規(guī),對數(shù)據(jù)的使用方式進行限制。
利用無版權(quán)的商業(yè)數(shù)據(jù)獲利,是不正當?shù)纳虡I(yè)競爭行為,做出這種行為的團體和公司,應(yīng)當受到相應(yīng)的處罰。
未經(jīng)授權(quán)轉(zhuǎn)載他人內(nèi)容,屬于侵犯著作權(quán)的行為。著作權(quán)的主體是公民,無論是否成年,你獨立創(chuàng)作的內(nèi)容,其著作權(quán)都屬于你,你有權(quán)決定是否允許他人轉(zhuǎn)載。
技術(shù)是否無罪,我不想做一個明確的判定,但我們共同期待著,期待著在創(chuàng)作領(lǐng)域,能有一束溫暖的陽光穿過烏云,給予每個人應(yīng)得的權(quán)利。