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