抓取h1標(biāo)題的入門(mén)實(shí)戰(zhàn)指南)
簡(jiǎn)介一份以實(shí)戰(zhàn)為導(dǎo)向的Python爬蟲(chóng)入門(mén)資源面向具備基本編程基礎(chǔ)、希望快速掌握網(wǎng)絡(luò)數(shù)據(jù)抓取的初學(xué)者可解決從零編寫(xiě)簡(jiǎn)單爬蟲(chóng)的常見(jiàn)困惑。實(shí)例以requests庫(kù)發(fā)送HTTP請(qǐng)求、BeautifulSoup解析HTML文檔完整演示從網(wǎng)頁(yè)中提取標(biāo)題的流程涵蓋GET請(qǐng)求發(fā)送、狀態(tài)碼判斷、文檔解析、標(biāo)簽定位等基礎(chǔ)步驟并延伸介紹了CSS選擇器與異常處理思路。資源為單個(gè)docx文檔體積僅14KB內(nèi)容緊湊無(wú)冗余便于離線閱讀隨堂實(shí)踐。文檔中提供了可復(fù)制的完整代碼包括依賴庫(kù)安裝命令、請(qǐng)求失敗時(shí)的狀態(tài)碼提示、遍歷并打印標(biāo)題文本的具體寫(xiě)法并建議讀者自行替換URL進(jìn)行驗(yàn)證同時(shí)通過(guò)示例強(qiáng)調(diào)實(shí)際操作需遵守robots.txt規(guī)則、控制訪問(wèn)頻率避免對(duì)目標(biāo)網(wǎng)站造成過(guò)大負(fù)載幫助讀者建立規(guī)范的爬蟲(chóng)開(kāi)發(fā)習(xí)慣。目前已有133人瀏覽學(xué)習(xí)適合作為爬蟲(chóng)入門(mén)的第一份手邊參考也適合作為教學(xué)輔助材料快速上手。1. 從Requests到BeautifulSoup一次只抓的Web Scraping入門(mén)不少人學(xué)爬蟲(chóng)第一反應(yīng)是找復(fù)雜的框架、上分布式采集結(jié)果被環(huán)境配置勸退。我的建議正好相反先用 requests 發(fā)一個(gè) HTTP GET 請(qǐng)求再用 BeautifulSoup 從返回的 HTML 里挑出全部標(biāo)簽把這條鏈路跑通再談其他。requests 負(fù)責(zé)把網(wǎng)頁(yè)內(nèi)容原樣搬回來(lái)BeautifulSoup 負(fù)責(zé)從這堆帶尖括號(hào)的文本里精準(zhǔn)切出標(biāo)題文字兩者分工明確加起來(lái)不到二十行代碼。這篇筆記適合有基礎(chǔ) Python 語(yǔ)法、想快速上手網(wǎng)絡(luò)數(shù)據(jù)采集的人代碼可以直接復(fù)制運(yùn)行重點(diǎn)放在參數(shù)怎么調(diào)、失敗時(shí)看什么輸出。2. 搭建環(huán)境與第一個(gè)請(qǐng)求requests的選型理由與關(guān)鍵參數(shù)2.1 為什么是 requests 而不是 urllibPython 標(biāo)準(zhǔn)庫(kù)里其實(shí)自帶 urllib也能發(fā) HTTP 請(qǐng)求但處理 Cookie、重定向、請(qǐng)求頭拼接時(shí)要寫(xiě)不少樣板代碼遇到需要維持會(huì)話的站點(diǎn)更是麻煩。requests 把這些底層細(xì)節(jié)統(tǒng)一封裝成 get、post、put 這類方法調(diào)用一個(gè)方法對(duì)應(yīng)一個(gè)動(dòng)作代碼可讀性高很多。對(duì)于入門(mén)階段requests 幾乎是社區(qū)公認(rèn)的首選。另一個(gè)值得知道的點(diǎn)是requests 底層用的是 urllib3它把連接池復(fù)用、TLS 握手、自動(dòng)解壓這些臟活全接管了。你看到 response requests.get(url) 這一行背后其實(shí)是 DNS 解析、TCP 連接、發(fā)送請(qǐng)求頭、接收響應(yīng)體的完整流程。理解這一點(diǎn)對(duì)排查問(wèn)題很有用——比如狀態(tài)碼返回 200但 response.text 里是一段「訪問(wèn)異?!固崾締?wèn)題大概率不在 requests而在對(duì)方服務(wù)器識(shí)別出這不是瀏覽器發(fā)出的請(qǐng)求。2.2 安裝與最小請(qǐng)求三步驗(yàn)證網(wǎng)絡(luò)通不通安裝很簡(jiǎn)單在終端里執(zhí)行下面這行命令即可pip install requests beautifulsoup4我習(xí)慣先在虛擬環(huán)境里操作避免把全局 Python 環(huán)境弄亂。裝好后別急著寫(xiě)完整爬蟲(chóng)先跑一個(gè)最小請(qǐng)求驗(yàn)證網(wǎng)絡(luò)鏈路通不通import requests url http://example.com response requests.get(url, timeout10) print(response.status_code) print(response.text[:200])這段代碼的作用是發(fā)送 GET 請(qǐng)求并打印狀態(tài)碼和響應(yīng)體前 200 個(gè)字符先確認(rèn)目標(biāo)網(wǎng)站真的能訪問(wèn)。status_code 等于 200 說(shuō)明請(qǐng)求成功如果打印出 403 或 406說(shuō)明服務(wù)器拒絕了這次請(qǐng)求后面需要加請(qǐng)求頭偽裝瀏覽器。先把這一步跑通再往下做解析能省很多排查時(shí)間。2.3 響應(yīng)對(duì)象的關(guān)鍵成員拿數(shù)據(jù)前先讀懂它requests.get() 返回的是一個(gè) Response 對(duì)象里面有幾個(gè)成員在爬蟲(chóng)里幾乎天天要用。response.status_code 是 HTTP 狀態(tài)碼response.text 是響應(yīng)體按推測(cè)編碼解碼后的字符串中文網(wǎng)站偶爾會(huì)解碼錯(cuò)誤需要手動(dòng)指定 encodingresponse.content 是原始字節(jié)下載圖片或二進(jìn)制文件時(shí)用它response.headers 里能看到 Content-Type、Server 字段判斷返回內(nèi)容到底是什么類型。params {page: 1, limit: 20} response requests.get(url, paramsparams, timeout(3, 10)) print(response.url)params 參數(shù)會(huì)自動(dòng)把字典拼成查詢串追加到 URL 后面比如 ?page1limit20這樣不用手動(dòng)拼接字符串。timeout 推薦傳元組 (3, 10)前一個(gè)數(shù)是連接超時(shí)后一個(gè)數(shù)是讀取超時(shí)單位都是秒。只傳一個(gè)數(shù)的話連接和讀取共用同一個(gè)值如果服務(wù)器一直不返回?cái)?shù)據(jù)腳本可能長(zhǎng)時(shí)間卡住這是新手常踩的坑。不同狀態(tài)碼在爬蟲(chóng)場(chǎng)景里的含義也值得記一下?tīng)顟B(tài)碼含義爬蟲(chóng)里常見(jiàn)場(chǎng)景200請(qǐng)求成功正常返回頁(yè)面內(nèi)容301/302重定向requests 默認(rèn)自動(dòng)跟隨403被拒絕缺少 UA 或觸發(fā)反爬策略404地址不存在鏈接失效或?qū)戝e(cuò)路徑429請(qǐng)求過(guò)于頻繁觸發(fā)限流需要放慢速度500服務(wù)器內(nèi)部錯(cuò)誤目標(biāo)站本身出問(wèn)題重試也未必有用實(shí)際調(diào)試時(shí)優(yōu)先看 response.text 的開(kāi)頭幾百字而不是只看狀態(tài)碼。很多反爬頁(yè)面為了穩(wěn)定會(huì)故意返回 200但內(nèi)容已經(jīng)換成了驗(yàn)證提示。提示如果目標(biāo)頁(yè)面需要登錄才能訪問(wèn)requests.get() 里還可以加 cookies 參數(shù)或者先用 requests.post() 提交表單獲取會(huì)話后續(xù)章節(jié)的 Session 對(duì)象會(huì)更適合這種場(chǎng)景。3. 解析HTML提取標(biāo)題find_all與CSS選擇器的搭配用法3.1 find_all 與 Tag 對(duì)象從h1標(biāo)簽開(kāi)始提取BeautifulSoup 的核心作用是把 HTML 文本解析成一棵標(biāo)簽樹(shù)。soup.find_all(h1) 會(huì)返回一個(gè)列表列表里每個(gè)元素是一個(gè) Tag 對(duì)象代表頁(yè)面上的一個(gè)標(biāo)簽。要從 Tag 里取出純文字用 get_text() 方法。from bs4 import BeautifulSoup html_text htmlbodyh1第一個(gè)標(biāo)題/h1h1第二個(gè)標(biāo)題/h1/body/html soup BeautifulSoup(html_text, html.parser) h1_tags soup.find_all(h1) for h1 in h1_tags: print(h1.get_text(stripTrue))這里我用了一段靜態(tài) HTML 字符串做演示效果和解析真實(shí)網(wǎng)頁(yè)響應(yīng)是一樣的。get_text(stripTrue) 會(huì)去掉標(biāo)題首尾的空白字符和換行避免打印出來(lái)帶著一大串空格。注意 strip 參數(shù)只清理首尾不會(huì)動(dòng)標(biāo)題內(nèi)部的空格如果正文里有連續(xù)空格需要另做清洗。find_all 還可以加參數(shù)縮小范圍比如 find_all(h1, limit3) 只取前三個(gè)結(jié)果find_all(h1, class_title) 只查找 class 為 title 的 h1 標(biāo)簽。這里 class_ 后面必須帶下劃線因?yàn)?class 是 Python 的保留關(guān)鍵字直接寫(xiě) class 會(huì)報(bào)語(yǔ)法錯(cuò)誤。我見(jiàn)過(guò)不少人在這一步被卡住報(bào)錯(cuò)信息里明確提示了這一點(diǎn)。3.2 select() 與 CSS Selectors層級(jí)定位比循環(huán)查找更省事find_all 按標(biāo)簽名和屬性篩選時(shí)還夠用但遇到「某個(gè)區(qū)域下的某類元素」這種層級(jí)關(guān)系寫(xiě)起來(lái)就啰嗦了。比如要抓 id 為 content 的 div 里面所有 h2用 find_all 要先定位 div 再在結(jié)果上繼續(xù) find_all代碼會(huì)多一層嵌套。soup.select() 方法接受 CSS Selectors 語(yǔ)法可以直接用一行表達(dá)式完成同樣的定位。# 選取 class 為 title 的所有 h1 soup.select(h1.title) # 選取 id 為 header 的容器內(nèi)部的 h1 soup.select(#header h1) # 選取帶 href 屬性的所有 a 標(biāo)簽 soup.select(a[href]) # 選取 class 為 content 的 div 下的直接子元素 p soup.select(div.content p)select() 返回的同樣是 Tag 對(duì)象列表取值方式和 find_all 沒(méi)有區(qū)別。實(shí)際項(xiàng)目里我通常兩個(gè)方法混用先用 select() 把范圍鎖定到某個(gè)區(qū)塊再在區(qū)塊內(nèi)用 find_all() 提取同類型的多個(gè)元素。CSS Selectors 的另一個(gè)好用的地方是屬性選擇器比如抓所有帶 download 屬性的鏈接一行就能搞定不用寫(xiě)循環(huán)去判斷。3.3 完整腳本與三個(gè)邊界情況空列表、嵌套標(biāo)簽和重復(fù)標(biāo)題把前面的內(nèi)容串成一個(gè)完整腳本加上異常處理和編碼兜底import requests from bs4 import BeautifulSoup url http://example.com try: response requests.get(url, timeout10) response.raise_for_status() except requests.exceptions.RequestException as e: print(請(qǐng)求失敗, e) exit() if text/html not in response.headers.get(Content-Type, ): print(目標(biāo)不是 HTML 內(nèi)容拒絕解析) exit() response.encoding response.apparent_encoding soup BeautifulSoup(response.text, html.parser) h1_tags soup.select(h1) print(f共找到 {len(h1_tags)} 個(gè) h1 標(biāo)題) for h1 in h1_tags: title h1.get_text(stripTrue) if title: print(title)raise_for_status() 會(huì)在狀態(tài)碼為 4xx 或 5xx 時(shí)主動(dòng)拋異常這樣就不用每次手動(dòng)判斷狀態(tài)碼。response.encoding 先賦成 apparent_encoding這是從頁(yè)面內(nèi)容里推測(cè)出的編碼比 requests 默認(rèn)按響應(yīng)頭猜測(cè)的方式更可靠。腳本最后額外判斷了 h1 是否為空文本避免頁(yè)面里存在空標(biāo)簽時(shí)打印一堆空白行。邊界情況要提前想清楚。第一頁(yè)面上可能一個(gè) h1 都沒(méi)有返回空列表代碼不報(bào)錯(cuò)但也沒(méi)有輸出這不代表爬蟲(chóng)壞了而是頁(yè)面結(jié)構(gòu)問(wèn)題。第二h1 內(nèi)部可能嵌套了 a 鏈接或 spanget_text() 會(huì)連同子標(biāo)簽的文字一起取出拿到的是拼接后的完整文本。第三一個(gè)頁(yè)面有多個(gè) h1 時(shí)腳本會(huì)全部打印如果你只想要第一個(gè)改成 soup.select_one(h1) 即可。注意解析器我默認(rèn)用 html.parser它不需要額外安裝第三方庫(kù)。lxml 解析速度更快但要單獨(dú)裝編譯好的二進(jìn)制包入門(mén)階段沒(méi)必要增加這個(gè)變量。4. 爬蟲(chóng)避坑指南五個(gè)會(huì)把新手逼瘋的翻車現(xiàn)場(chǎng)這一章完全來(lái)自實(shí)戰(zhàn)中反復(fù)遇到的故障。每個(gè)問(wèn)題都按「現(xiàn)象 → 原因 → 解決」展開(kāi)照著排查能省下很多翻論壇的時(shí)間。4.1 狀態(tài)碼200但抓到的是提示頁(yè)現(xiàn)象requests.get() 返回的狀態(tài)碼是 200打印 response.text 卻發(fā)現(xiàn)里面不是目標(biāo)內(nèi)容而是「您的訪問(wèn)異?!够蛞坏莉?yàn)證頁(yè)面。原因這是反爬策略的常見(jiàn)手法。服務(wù)器判斷請(qǐng)求頭里的 User-Agent 不像瀏覽器就返回一個(gè)兜底頁(yè)面但狀態(tài)碼仍然保持 200方便前端正常渲染。只看狀態(tài)碼完全發(fā)現(xiàn)不了問(wèn)題。解決先打印 response.text 的前 200 個(gè)字符看一眼實(shí)際內(nèi)容。然后給請(qǐng)求加上瀏覽器請(qǐng)求頭至少帶上 User-Agentheaders { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } response requests.get(url, headersheaders, timeout10)設(shè)置 UA 后大部分提示頁(yè)問(wèn)題都能解決。如果還不行可能需要先訪問(wèn)首頁(yè)拿到 Cookie 再請(qǐng)求目標(biāo)頁(yè)這時(shí)就該用下一章的 Session 對(duì)象了。4.2 中文標(biāo)題亂碼encoding 的黑匣子現(xiàn)象抓回來(lái)的英文和數(shù)字正常但中文標(biāo)題全部變成亂碼比如「?¥????」這種無(wú)法辨認(rèn)的字符。原因requests 在解碼響應(yīng)體時(shí)會(huì)根據(jù)響應(yīng)頭里的 charset 推斷編碼。有些網(wǎng)站響應(yīng)頭里沒(méi)寫(xiě) charsetrequests 會(huì)回退到一個(gè)默認(rèn)值中文頁(yè)面在這種默認(rèn)編碼下解碼必然出亂碼。解決手動(dòng)指定編碼優(yōu)先用 apparent_encoding它是從頁(yè)面內(nèi)容字節(jié)特征推測(cè)出來(lái)的response.encoding response.apparent_encoding也可以在抓取前先查看響應(yīng)頭里的聲明print(response.headers.get(Content-Type))如果里面有 charsetutf-8 之類直接賦給 response.encoding 也行。注意一點(diǎn)亂碼問(wèn)題出在解碼環(huán)節(jié)BeautifulSoup 本身沒(méi)有責(zé)任soup 解析的是已經(jīng)解碼的字符串。4.3 find_all 結(jié)果為空動(dòng)態(tài)渲染的鍋現(xiàn)象代碼沒(méi)有任何報(bào)錯(cuò)response.status_code 是 200但 soup.find_all(h1) 返回空列表或者只能拿到頁(yè)面框架里那幾個(gè)無(wú)關(guān)標(biāo)題。原因頁(yè)面內(nèi)容由 JavaScript 在瀏覽器端動(dòng)態(tài)渲染requests 拿到的 HTML 只是空殼骨架真實(shí)標(biāo)題是腳本執(zhí)行后生成的Requests 不執(zhí)行 JavaScript自然抓不到。還有一種情況是標(biāo)題不在 h1 里而是用 div 套了大量樣式來(lái)模擬標(biāo)題的視覺(jué)效果。解決用瀏覽器打開(kāi)目標(biāo)網(wǎng)頁(yè)右鍵「查看網(wǎng)頁(yè)源代碼」對(duì)比源代碼和渲染后頁(yè)面。如果源代碼里沒(méi)有你要的數(shù)據(jù)說(shuō)明是動(dòng)態(tài)渲染需要改用 Selenium 或 Playwright 這類瀏覽器自動(dòng)化工具。如果數(shù)據(jù)在源代碼里但標(biāo)簽不是 h1就調(diào)整選擇器比如改成 soup.select(div.article-title)。4.4 抓幾次后被拒絕403 和 429 的限流現(xiàn)象腳本前兩次運(yùn)行正常第三次開(kāi)始連續(xù)返回 403或者返回 429 Too Many Requests。原因短時(shí)間高頻率的請(qǐng)求觸發(fā)了服務(wù)器的限流機(jī)制。即使你偽裝了 UA單位時(shí)間內(nèi)訪問(wèn)次數(shù)太多服務(wù)器同樣會(huì)識(shí)別為腳本行為。解決控制抓取節(jié)奏在每次請(qǐng)求之間加延時(shí)常見(jiàn)的做法是 time.sleep(2) 到 time.sleep(5)。同時(shí)查看目標(biāo)網(wǎng)站的 robots.txt路徑一般寫(xiě)在域名根目錄下比如 http://example.com/robots.txt里面會(huì)明確列出哪些路徑不允許爬取。做技術(shù)練習(xí)時(shí)盡量選擇允許采集的站點(diǎn)。4.5 腳本卡死不動(dòng)timeout 缺失的后遺癥現(xiàn)象requests.get() 執(zhí)行后長(zhǎng)時(shí)間沒(méi)有反應(yīng)腳本像掛了最后才拋出一個(gè)超時(shí)異常。原因沒(méi)有顯式設(shè)置 timeoutrequests 會(huì)一直等待服務(wù)器響應(yīng)。有些服務(wù)器接受連接后不返回?cái)?shù)據(jù)連接就一直掛著腳本也就卡住了。解決所有請(qǐng)求都顯式設(shè)置 timeout推薦用元組形式區(qū)分連接和讀取超時(shí)response requests.get(url, timeout(3, 10))再把每個(gè)請(qǐng)求包進(jìn) try-except捕獲 requests.exceptions.RequestException批量任務(wù)里配合循環(huán)實(shí)現(xiàn)失敗重試。從那以后我每次寫(xiě)爬蟲(chóng)都強(qiáng)制檢查三個(gè)地方有沒(méi)有 timeout、有沒(méi)有異常捕獲、有沒(méi)有控制頻率缺一個(gè)都不上線。5. 讓爬蟲(chóng)更健壯Session、Headers與批量抓取三步進(jìn)階前面的腳本每次請(qǐng)求都是獨(dú)立的遇到需要登錄或維持會(huì)話的站點(diǎn)就會(huì)失效。requests 的 Session 對(duì)象能保持連接和 Cookie配合自定義 Headers 可以模擬一個(gè)穩(wěn)定的瀏覽器會(huì)話。下面是一個(gè)批量抓取多個(gè)頁(yè)面標(biāo)題的完整示例import requests from bs4 import BeautifulSoup import time session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) urls [http://example.com, http://example.com/about, http://example.com/contact] all_titles [] for url in urls: try: response session.get(url, timeout(3, 10)) response.raise_for_status() response.encoding response.apparent_encoding soup BeautifulSoup(response.text, html.parser) titles [h.get_text(stripTrue) for h in soup.select(h1) if h.get_text(stripTrue)] all_titles.extend(titles) time.sleep(2) except requests.exceptions.RequestException as e: print(f抓取失敗{url}原因{e}) print(f共采集到 {len(all_titles)} 個(gè)標(biāo)題) print(list(dict.fromkeys(all_titles)))Session 會(huì)復(fù)用底層連接連續(xù)請(qǐng)求同一個(gè)站點(diǎn)時(shí)比每次新建連接更快也更不容易被識(shí)別為異常流量。headers.update() 把 UA 統(tǒng)一設(shè)置到會(huì)話級(jí)別后續(xù)每個(gè)請(qǐng)求自動(dòng)攜帶。time.sleep(2) 是給服務(wù)器留出的喘息時(shí)間批量采集時(shí)這個(gè)間隔建議只增不減。驗(yàn)證環(huán)節(jié)我一般做兩步第一步對(duì)比采集數(shù)量和頁(yè)面實(shí)際數(shù)量是否一致不一致說(shuō)明選擇器漏了元素或頁(yè)面有動(dòng)態(tài)內(nèi)容第二步對(duì)標(biāo)題列表去重檢查是否有重復(fù)抓取。跑完這套流程一個(gè)入門(mén)級(jí)的標(biāo)題采集器就算真正能交付了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取