總結(jié))
前言很多人一上手就問(wèn)「爬蟲要裝哪幾個(gè)庫(kù)」然后照著某篇清單把 requests、BeautifulSoup、lxml、Scrapy、selenium 全裝上結(jié)果一個(gè)也沒(méi)用明白。庫(kù)不是越多越好關(guān)鍵是搞清楚每一類庫(kù)解決的是哪一步的問(wèn)題有的負(fù)責(zé)發(fā)請(qǐng)求有的負(fù)責(zé)解析 HTML有的負(fù)責(zé)調(diào)度和去重有的專門對(duì)付 JavaScript 渲染的頁(yè)面。另一個(gè)常見誤解是把標(biāo)準(zhǔn)庫(kù)當(dāng)成「玩具」。事實(shí)上urllib.request能發(fā)完整的 HTTP 請(qǐng)求html.parser能可靠地遍歷標(biāo)簽urllib.robotparser能解析 robots.txtjson、csv、sqlite3、re、gzip、concurrent.futures覆蓋了存取、清洗和并發(fā)。一個(gè)不需要安裝任何依賴的爬蟲是真實(shí)可用的。需要提前聲明本文涉及第三方庫(kù)時(shí)只講它們?cè)诹鞒汤锏亩ㄎ缓痛笾掠梅ň唧w參數(shù)名和返回類型請(qǐng)以各庫(kù)官方文檔為準(zhǔn)。因?yàn)榄h(huán)境里沒(méi)有安裝這些庫(kù)任何具體到參數(shù)名的調(diào)用都不應(yīng)被當(dāng)作可直接照抄的成品。另外Python 2 已于 2020 年 1 月 1 日停止維護(hù)本文一律用 Python 3 寫法。一、標(biāo)準(zhǔn)庫(kù)一行安裝命令都不需要標(biāo)準(zhǔn)庫(kù)適合「抓取量不大、頁(yè)面是服務(wù)端渲染、能自己控制流程」的場(chǎng)景。它最大的價(jià)值是零依賴、可移植、行為穩(wěn)定。# 適用于 Python 3.8import urllib.requestimport urllib.parseimport urllib.errorimport urllib.robotparserimport http.clientimport gzipimport jsonimport csvimport sqlite3import reimport timeimport concurrent.futuresprint(標(biāo)準(zhǔn)庫(kù)導(dǎo)入完成無(wú)需 pip 安裝)模塊負(fù)責(zé)的環(huán)節(jié)典型用途urllib.request發(fā)請(qǐng)求urlopen、Request設(shè)置請(qǐng)求頭urllib.parseURL 處理urljoin拼相對(duì)地址、urlencode編碼查詢串urllib.error異常區(qū)分HTTPError帶狀態(tài)碼與URLErrorurllib.robotparser合規(guī)解析 robots.txtcan_fetch判斷http.client底層 HTTP需要精細(xì)控制連接時(shí)用html.parser解析 HTML事件驅(qū)動(dòng)覆寫handle_starttag等回調(diào)json/csv存數(shù)據(jù)讀寫 JSON、CSVsqlite3存數(shù)據(jù)單文件數(shù)據(jù)庫(kù)天然適合去重和增量re文本清洗從已取出的文本里摳日期、數(shù)字gzip解壓處理Content-Encoding: gzip的響應(yīng)concurrent.futures并發(fā)線程池處理 I/O 密集抓取注意re的位置它適合清洗已經(jīng)抽出來(lái)的純文本不適合直接套在整頁(yè) HTML 上。HTML 的嵌套和屬性順序不是正則能穩(wěn)定描述的。二、第三方 HTTP 客戶端requests / httpx / aiohttp標(biāo)準(zhǔn)庫(kù)的urllib.request能用但接口偏底層編碼要自己解、會(huì)話連接復(fù)用要自己配、超時(shí)和重試要自己寫。這類庫(kù)的價(jià)值就是把這些樣板收起來(lái)。requests同步、接口直觀是事實(shí)上的入門首選。它的定位是「給人用的 HTTP 客戶端」會(huì)話復(fù)用用Session請(qǐng)求參數(shù)、表單、JSON 都有對(duì)應(yīng)入口。具體參數(shù)以 requests 官方文檔為準(zhǔn)。httpx接口風(fēng)格接近 requests同時(shí)提供同步和異步兩套 API適合既要簡(jiǎn)單又要異步的場(chǎng)景。aiohttpasyncio生態(tài)里的 HTTP 客戶端異步抓取大量頁(yè)面時(shí)常用。它與asyncio事件循環(huán)綁定寫法與同步庫(kù)差別較大。這里只給出定位不給出具體調(diào)用——因?yàn)橛洸粶?zhǔn)參數(shù)名而寫一個(gè)「看起來(lái)對(duì)」的簽名比不寫更危險(xiǎn)。需要時(shí)請(qǐng)查對(duì)應(yīng)庫(kù)的官方文檔。三、解析庫(kù)BeautifulSoup / lxml / html.parser解析層解決的是「從 HTML 里把字段摳出來(lái)」。選擇取決于頁(yè)面是否規(guī)整、你更習(xí)慣 CSS 選擇器還是 XPath。html.parser標(biāo)準(zhǔn)庫(kù)容錯(cuò)性尚可事件驅(qū)動(dòng)。適合結(jié)構(gòu)簡(jiǎn)單或需要邊下邊解析的場(chǎng)景。BeautifulSoup在某個(gè)底層解析器之上提供統(tǒng)一的樹形接口容錯(cuò)性強(qiáng)對(duì)殘缺 HTML 友好。用 CSS 選擇器取元素很方便。以 BeautifulSoup 官方文檔為準(zhǔn)。lxml底層用 libxml2解析快、支持 XPath。缺點(diǎn)是它是 C 擴(kuò)展安裝依賴較多。瀏覽器自動(dòng)化selenium / playwright頁(yè)面內(nèi)容由 JavaScript 動(dòng)態(tài)渲染時(shí)才需要。代價(jià)是慢、重只在確實(shí)必要且目標(biāo)站允許時(shí)使用。一個(gè)務(wù)實(shí)的原則能用服務(wù)端渲染的靜態(tài) HTML 解決的就不要上瀏覽器自動(dòng)化。四、工程化框架Scrapy當(dāng)抓取規(guī)模變大你要處理的就不只是「抓一個(gè)頁(yè)面」而是調(diào)度、去重、限速、失敗重試、數(shù)據(jù)管道。這類需求由爬蟲框架承擔(dān)。Scrapy是最常見的 Python 爬蟲框架把「引擎、調(diào)度器、下載器、爬蟲、管道」拆成獨(dú)立組件用Request/Response和回調(diào)callback組織解析流程。適合規(guī)則明確、站點(diǎn)多、需要長(zhǎng)期運(yùn)行的采集任務(wù)如果只是抓幾十個(gè)頁(yè)面用它反而是過(guò)度設(shè)計(jì)。調(diào)度與存儲(chǔ)層面還會(huì)用到 Redis、消息隊(duì)列、關(guān)系型數(shù)據(jù)庫(kù)等通用組件它們不是爬蟲專用庫(kù)這里只提定位。實(shí)戰(zhàn)只用標(biāo)準(zhǔn)庫(kù)搭一個(gè)抓取小工具下面這段不依賴任何第三方庫(kù)演示「請(qǐng)求—解碼—解析—存庫(kù)」這條鏈路的骨架。把數(shù)據(jù)落到 sqlite3順便用主鍵天然去重。# 適用于 Python 3.8import sqlite3import timeimport urllib.errorimport urllib.requestfrom html.parser import HTMLParserclass PageTitleScanner(HTMLParser):def __init__(self):super().__init__(convert_charrefsTrue)self.in_title Falseself.title_parts []def handle_starttag(self, tag, attrs):if tag title:self.in_title Truedef handle_endtag(self, tag):if tag title:self.in_title Falsedef handle_data(self, data):if self.in_title:self.title_parts.append(data)def title(self):return .join(self.title_parts).strip()def fetch_title(url, timeout10):req urllib.request.Request(url, headers{User-Agent: LibSummaryBot/0.1 (contact: meexample.com),})with urllib.request.urlopen(req, timeouttimeout) as resp:raw resp.read()charset resp.headers.get_content_charset() or utf-8text raw.decode(charset, errorsreplace)parser PageTitleScanner()parser.feed(text)parser.close()return parser.title()def main():urls [https://www.example.com/, https://www.example.org/]conn sqlite3.connect(titles.db)conn.execute(CREATE TABLE IF NOT EXISTS pages (url TEXT PRIMARY KEY, title TEXT))for url in urls:try:title fetch_title(url)except urllib.error.HTTPError as e:print(HTTP 錯(cuò)誤, e.code, url)continueexcept urllib.error.URLError as e:print(網(wǎng)絡(luò)錯(cuò)誤, e.reason, url)continueconn.execute(INSERT OR REPLACE INTO pages (url, title) VALUES (?, ?),(url, title),)conn.commit()print(已保存:, url, -, title)time.sleep(1) # 限速conn.close()if __name__ __main__:main()INSERT OR REPLACE配合url主鍵同一地址重復(fù)抓到也只保留一條比手工維護(hù)去重集合更省事。常見坑點(diǎn)先裝庫(kù)再想需求? 一次性裝上 requests、Scrapy、selenium卻不知道該用哪層。 ? 先明確頁(yè)面是否需要 JS 渲染、規(guī)模多大再按「請(qǐng)求—解析—調(diào)度」分層選庫(kù)。把re當(dāng)成 HTML 解析器?re.findall(ra href(.*?), html)—— 屬性順序、單雙引號(hào)、換行一變就漏。 ? 用html.parser、BeautifulSoup 或 lxml 做結(jié)構(gòu)解析re只清洗抽出的純文本。用第三方庫(kù)卻硬記參數(shù)名? 憑印象寫一個(gè)「看起來(lái)合理」的調(diào)用參數(shù)名或順序其實(shí)是錯(cuò)的。 ? 記不準(zhǔn)就只寫用法思路以該庫(kù)官方文檔為準(zhǔn)。html.parser子類忘記調(diào)用父類構(gòu)造?def __init__(self): self.links []—— 不解構(gòu)內(nèi)部狀態(tài)feed()會(huì)出問(wèn)題。 ? 先super().__init__(convert_charrefsTrue)再初始化自己的屬性。硬編碼 UTF-8 解碼第三方庫(kù)返回的內(nèi)容? 拿到 bytes 就.decode(utf-8)GBK 頁(yè)面直接亂碼。 ? 先取響應(yīng)頭的 charset取不到再退回 UTF-8 并加errorsreplace。忽略 gzip 壓縮? 直接解碼響應(yīng)體得到一堆二進(jìn)制亂碼。 ? 檢查Content-Encoding用gzip模塊解壓后再解碼。把iteritems()/xrange當(dāng)成還能用的寫法?for k, v in d.iteritems():、for i in xrange(10):—— Python 3 均已移除。 ? 用d.items()返回視圖、range本身就是惰性的。上多線程就以為能提速? CPU 密集任務(wù)用ThreadPoolExecutor指望加速。 ? 爬蟲主要受 I/O 限制線程池有意義CPU 密集要換多進(jìn)程??偨Y(jié)層次標(biāo)準(zhǔn)庫(kù)方案第三方方案何時(shí)升級(jí)發(fā)請(qǐng)求urllib.requestrequests / httpx / aiohttp需要會(huì)話、重試、異步時(shí)解析 HTMLhtml.parserBeautifulSoup / lxml頁(yè)面復(fù)雜、要 XPath/CSS 時(shí)渲染 JS無(wú)selenium / playwright內(nèi)容確實(shí)由 JS 生成時(shí)調(diào)度工程化自己寫循環(huán)Scrapy站點(diǎn)多、需長(zhǎng)期運(yùn)行時(shí)存儲(chǔ)sqlite3/json/csv數(shù)據(jù)庫(kù)、消息隊(duì)列數(shù)據(jù)量大、要并發(fā)寫時(shí)選庫(kù)的順序應(yīng)該是「先讓標(biāo)準(zhǔn)庫(kù)跑通再按瓶頸補(bǔ)庫(kù)」。絕大多數(shù)入門任務(wù)標(biāo)準(zhǔn)庫(kù)就夠了真正需要第三方庫(kù)的時(shí)候你已經(jīng)能說(shuō)清自己缺的是哪一步。