戰(zhàn):用requests和BeautifulSoup高效抓取網(wǎng)頁標(biāo)題)
簡介面向具備基本編程基礎(chǔ)、對 Python 爬蟲感興趣的開發(fā)者文檔圍繞“抓取網(wǎng)頁文章標(biāo)題”任務(wù)展開系統(tǒng)演示了用 requests 發(fā)起 HTTP 請求、BeautifulSoup 解析 HTML并通過 find_all(h2) 提取標(biāo)題后保存到本地 txt 文件的完整流程。內(nèi)容包含庫的安裝命令、fetch_titles 與 save_titles 函數(shù)的逐段講解、運(yùn)行實(shí)例與結(jié)果展示還提醒讀者遵守 robots.txt 與網(wǎng)站使用條款、在請求頻繁時(shí)加入延時(shí)并討論如何根據(jù)頁面結(jié)構(gòu)調(diào)整選擇器、做好異常處理與數(shù)據(jù)存儲、靈活應(yīng)對反爬策略等問題對初學(xué)者搭建真實(shí)可擴(kuò)展的爬蟲腳本很有參考價(jià)值。資源為單個(gè) docx 文檔壓縮包大小約 17KB輕量易讀便于隨時(shí)查閱對照。已有 176 人學(xué)習(xí)瀏覽適合跟隨示例親手實(shí)踐并在此基礎(chǔ)上調(diào)整目標(biāo)站點(diǎn)與選擇器逐步構(gòu)建自己的抓取工具。1. 從文章標(biāo)題抓取聊起一個(gè)能跑通的最小爬蟲骨架日常接需求時(shí)我經(jīng)常遇到“幫我把某幾個(gè)欄目的文章標(biāo)題整理成表格”這類活兒。手動(dòng)復(fù)制當(dāng)然能做但頁面一多就讓人崩潰。用 Python 寫個(gè)爬蟲去抓標(biāo)題是最快能落地、也最容易讓新手建立信心的場景。這篇文章要拆的是一份標(biāo)題抓取資源基于 requests 發(fā)起請求用 BeautifulSoup 解析頁面里的標(biāo)題標(biāo)簽最后把結(jié)果寫到本地文本文件。它能解決“批量獲取文章列表、快速做內(nèi)容盤點(diǎn)、給后續(xù)數(shù)據(jù)分析做前置采集”這類具體問題。適合有基礎(chǔ) Python 語法、想上手爬蟲但還沒系統(tǒng)學(xué)過的人也適合需要快速抄一個(gè)簡化版采集腳本的熟手。我會(huì)把完整代碼、參數(shù)含義、適配真實(shí)網(wǎng)頁的調(diào)整方法以及我實(shí)際踩過的幾個(gè)坑都放出來。讀完你能得到一個(gè)可以直接改 URL 就跑的腳本也能知道標(biāo)題不在預(yù)期標(biāo)簽里時(shí)該怎么兜底。2. 最小可用版本requests 取網(wǎng)頁BeautifulSoup 拆標(biāo)題2.1 為什么選 requests BeautifulSoup而不是別的組合做標(biāo)題抓取這種輕量采集技術(shù)選型的原則是“夠用就好”。requests 是 Python 生態(tài)里最成熟的 HTTP 客戶端API 簡單get 一下就能拿到響應(yīng)文本BeautifulSoup 則是把 HTML 解析成可遍歷對象的老牌庫對新手特別友好查找標(biāo)簽的寫法很像是在寫自然語言。有人會(huì)問怎么不用 urllib我的判斷是 urllib 雖然內(nèi)置但處理 Cookie、超時(shí)、請求頭時(shí)要寫的樣板代碼更多。還有人會(huì)推薦 Scrapy可抓標(biāo)題這種單頁、低頻的任務(wù)上 Scrapy 有點(diǎn)大材小用。requests 加 BeautifulSoup 的組合代碼量最少調(diào)試時(shí)哪里出錯(cuò)一眼就能看到出了問題也容易在社區(qū)搜到答案。這套組合的邊界也要說清楚它適合頁面內(nèi)容在 HTML 源碼里直接可見的靜態(tài)頁面。如果目標(biāo)網(wǎng)站是前端 JS 動(dòng)態(tài)渲染數(shù)據(jù)requests 拿到的 HTML 里根本沒有標(biāo)題那種情況我在第四章會(huì)講怎么繞。2.2 最小可運(yùn)行版本完整代碼與參數(shù)說明先看一個(gè)能直接跑的腳本。它做的事很簡單請求指定 URL找到頁面里所有h2標(biāo)簽提取文本打印出來并保存。import requests from bs4 import BeautifulSoup def fetch_titles(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } try: response requests.get(url, headersheaders, timeout10) except requests.RequestException as e: print(f請求失敗: {e}) return [] if response.status_code ! 200: print(f頁面返回異常狀態(tài)碼: {response.status_code}) return [] soup BeautifulSoup(response.text, html.parser) title_tags soup.find_all(h2) return [tag.get_text(stripTrue) for tag in title_tags] def save_titles(titles, filenametitles.txt): with open(filename, w, encodingutf-8) as f: for title in titles: f.write(title \n) if __name__ __main__: url https://example.com titles fetch_titles(url) if titles: print(f共抓取 {len(titles)} 個(gè)標(biāo)題) save_titles(titles) print(已保存到 titles.txt) else: print(未獲取到標(biāo)題)這段代碼里有幾個(gè)參數(shù)值得單獨(dú)說。headers里的User-Agent是標(biāo)識客戶端類型的字符串。很多網(wǎng)站會(huì)攔截不帶 UA 的請求或者直接返回 403加了這一項(xiàng)能偽裝成普通瀏覽器降低被拒概率。timeout10是請求超時(shí)時(shí)間單位是秒超過這個(gè)時(shí)間還沒響應(yīng)就主動(dòng)放棄避免腳本卡死。response.status_code ! 200是狀態(tài)碼檢查。200 表示正常其他常見狀態(tài)碼比如 302 是重定向、404 是地址不存在、403 是禁止訪問。這一層判斷雖然簡單但能讓你在第一時(shí)間知道是網(wǎng)站拒絕了你還是解析邏輯出了問題。soup.find_all(h2)返回所有符合條件的標(biāo)簽對象get_text(stripTrue)提取標(biāo)簽內(nèi)的純文本stripTrue會(huì)去掉首尾空白和換行。這里有個(gè)限制它默認(rèn)只認(rèn)h2真實(shí)網(wǎng)站的標(biāo)題結(jié)構(gòu)未必如此第三章細(xì)說。2.3 從控制臺輸出到本地落盤文件處理的細(xì)節(jié)很多入門教程會(huì)在控制臺打印完就結(jié)束但實(shí)際采集場景里結(jié)果往往要交給下游使用所以落盤是剛需。上面代碼里我用了open(filename, w, encodingutf-8)其中w模式表示覆蓋寫入每次運(yùn)行都會(huì)重建文件。如果希望多次運(yùn)行結(jié)果不互相覆蓋可以改成a追加模式。encodingutf-8這一項(xiàng)不能省。Windows 下 Python 默認(rèn)編碼可能是 gbk寫中文標(biāo)題時(shí)會(huì)直接 UnicodeEncodeError導(dǎo)致程序中斷。指定 utf-8 之后文件內(nèi)容也能被大多數(shù)編輯器和后續(xù)處理工具正常讀取。寫文件時(shí)我習(xí)慣在每條標(biāo)題末尾加\n保證每個(gè)標(biāo)題占一行。如果你后續(xù)想把標(biāo)題導(dǎo)入 Excel這個(gè)格式可以直接用 Excel 的“分列”功能處理。如果想存成 CSV可以用 csv 模塊替代直接寫文件但對純標(biāo)題場景文本文件已經(jīng)足夠清晰。3. 適配真實(shí)網(wǎng)頁標(biāo)題不在 h2 里時(shí)怎么定位3.1 先用瀏覽器開發(fā)者工具確認(rèn)標(biāo)題的真實(shí)結(jié)構(gòu)把示例代碼往真實(shí)網(wǎng)站上一跑最常見的情況就是“腳本沒報(bào)錯(cuò)但標(biāo)題列表是空的”。原因幾乎都是同一個(gè)目標(biāo)網(wǎng)站的標(biāo)題標(biāo)簽根本不是h2。我一般的做法是先用瀏覽器打開目標(biāo)頁面按 F12 打開開發(fā)者工具點(diǎn)左上角的箭頭圖標(biāo)再點(diǎn)擊頁面上的任意一個(gè)標(biāo)題DevTools 會(huì)直接高亮對應(yīng)的 HTML 元素。這一步能精確看到標(biāo)題包在什么標(biāo)簽里、帶什么 class、有沒有嵌套結(jié)構(gòu)。比如有的網(wǎng)站標(biāo)題是h1 classarticle-title有的是a classtitle-link還有一些是h3搭配內(nèi)層a。拿到真實(shí)結(jié)構(gòu)后再回頭改爬蟲的查找語句。這個(gè)過程是整個(gè)適配工作的核心不要猜網(wǎng)頁結(jié)構(gòu)要看結(jié)構(gòu)。3.2 多選擇器兜底從 find_all 到 select確定真實(shí)結(jié)構(gòu)之后可以用更精準(zhǔn)的方式定位。下面這段代碼演示了如何通過 CSS 選擇器查找標(biāo)題并做多套選擇器兜底。def fetch_titles_flexible(url): headers {User-Agent: Mozilla/5.0} response requests.get(url, headersheaders, timeout10) soup BeautifulSoup(response.text, html.parser) # 按優(yōu)先級嘗試不同的選擇器哪個(gè)命中用哪個(gè) selectors [ h2, h3 a, a.title-link, .article-title ] for selector in selectors: elements soup.select(selector) if elements: print(f使用選擇器 {selector} 命中 {len(elements)} 個(gè)標(biāo)題) return [el.get_text(stripTrue) for el in elements] return []soup.select接收 CSS 選擇器h3 a表示 “h3 內(nèi)部的 a 標(biāo)簽”.article-title表示classarticle-title的標(biāo)簽。按順序逐個(gè)嘗試哪個(gè)先找到結(jié)果就用哪個(gè)這種兜底策略能應(yīng)對同一站點(diǎn)不同頁面結(jié)構(gòu)不完全一致的情況。get_text(stripTrue)在嵌套結(jié)構(gòu)下會(huì)把子標(biāo)簽里的文字也拼進(jìn)來。如果標(biāo)題里混有分類名、日期等其他信息你要在循環(huán)里加上過濾邏輯比如只保留長度在 5 到 100 字之間的文本。這一步?jīng)]有統(tǒng)一標(biāo)準(zhǔn)完全看目標(biāo)頁面的實(shí)際結(jié)構(gòu)。3.3 去重、排序與亂碼處理頁面標(biāo)題可能出現(xiàn)重復(fù)比如置頂文章在列表里出現(xiàn)兩次。文本范圍抓取時(shí)通常需要去重。最簡單的方式是轉(zhuǎn)換為集合再變回列表def clean_titles(titles): seen set() result [] for title in titles: title title.strip() if title and title not in seen: seen.add(title) result.append(title) return result這段代碼通過一個(gè)set記錄已出現(xiàn)的標(biāo)題既去掉了空字符串也保留了第一次出現(xiàn)的順序。很多列表頁的標(biāo)題本身就是按時(shí)間倒序排列的所以我不建議在做完解析后輕易重新排序保持頁面原始順序往往更符合業(yè)務(wù)直覺。亂碼是另一個(gè)高頻問題。requests拿到響應(yīng)后會(huì)用response.encoding去解碼但某些網(wǎng)站的響應(yīng)頭沒聲明編碼或聲明的編碼與實(shí)際不符就會(huì)出現(xiàn)滿屏a€“之類的亂碼。這時(shí)可以采用以下策略response.encoding response.apparent_encodingapparent_encoding是從響應(yīng)內(nèi)容里推測出的編碼準(zhǔn)確率通常不錯(cuò)。但注意用它之后要在實(shí)際項(xiàng)目中多做幾個(gè)頁面的驗(yàn)證因?yàn)橥茰y不是百分百可靠。建議在腳本里先打印response.encoding和response.apparent_encoding做個(gè)對比再?zèng)Q定用哪種方式解碼。4. 標(biāo)題爬蟲避坑四個(gè)我翻過車的真實(shí)問題4.1 訪問被拒返回 403 Forbidden現(xiàn)象運(yùn)行腳本后控制臺輸出頁面返回異常狀態(tài)碼: 403一個(gè)標(biāo)題都沒抓到。原因絕大部分情況是請求沒有帶 User-Agent。網(wǎng)站服務(wù)器認(rèn)出了請求不是來自真實(shí)瀏覽器直接拒絕了。另一個(gè)可能是請求頻率太高被服務(wù)器臨時(shí)封禁。解決先加上模擬瀏覽器的請求頭這是最簡單的止損手段。如果還不行就把目標(biāo)網(wǎng)站的robots.txt找出來看一眼路徑通常是域名/robots.txt確認(rèn)你要抓的路徑?jīng)]有被明確禁止。再不行就把請求之間的間隔拉長加入time.sleep(2)這類延時(shí)。遇到 403 我從不死磕單次請求優(yōu)先從“偽裝程度”和“請求頻率”兩個(gè)方向同時(shí)調(diào)整。4.2 頁面源碼里根本沒有標(biāo)題現(xiàn)象代碼不報(bào)錯(cuò)狀態(tài)碼是 200但soup.find_all(h2)返回空列表打印結(jié)果為空。原因目標(biāo)頁面是前端框架動(dòng)態(tài)渲染的標(biāo)題數(shù)據(jù)由 JavaScript 在瀏覽器端發(fā)起異步請求然后渲染到頁面上。requests 拿到的只是空殼 HTML里面沒有實(shí)際數(shù)據(jù)。解決優(yōu)先去瀏覽器的開發(fā)者工具里看 Network 面板找到返回標(biāo)題數(shù)據(jù)的 XHR 或 Fetch 請求。直接請求那個(gè)數(shù)據(jù)接口通常能拿到結(jié)構(gòu)更干凈的 JSON 數(shù)據(jù)這也比解析 HTML 更穩(wěn)定。如果接口加密復(fù)雜才考慮用自動(dòng)化工具但那是更重的方案對入門場景來說找接口是性價(jià)比最高的路徑。4.3 中文標(biāo)題亂碼成謎現(xiàn)象標(biāo)題內(nèi)容變成??°é??一類看不懂的字符。原因response.encoding從響應(yīng)頭讀到的編碼與頁面實(shí)際使用的編碼不一致。常見于沒有聲明 charset 的站點(diǎn)或者聲明了 gbk 但實(shí)際是 utf-8 的頁面。解決用我在第三章提到的response.apparent_encoding兜底。更穩(wěn)妥的做法是檢查一下 HTML 里meta charset...標(biāo)簽的實(shí)際聲明手動(dòng)設(shè)置響應(yīng)編碼。比如頁面是 GBK 編碼就寫response.encoding gbk再繼續(xù)解析。這類問題沒有萬能鑰匙遇到亂碼就多打印幾個(gè)響應(yīng)頭信息能少走彎路。4.4 請求太頻繁賬號或 IP 被限制現(xiàn)象腳本連續(xù)運(yùn)行一段時(shí)間后突然所有請求都返回 503 或驗(yàn)證碼頁面要求輸入驗(yàn)證碼才能繼續(xù)。原因短時(shí)間內(nèi)的請求數(shù)量超過了網(wǎng)站設(shè)定的閾值被識別為爬蟲行為。解決降低請求頻率加入隨機(jī)延時(shí)而不是固定延時(shí)。比如time.sleep(random.uniform(1, 3))模擬人工操作的自然間隔。對于大量頁面可以分批跑每批之間停一段時(shí)間。此外采集數(shù)據(jù)也要尊重目標(biāo)網(wǎng)站的條款明確禁止采集的內(nèi)容不要強(qiáng)行突破。我是把它當(dāng)成長期運(yùn)行腳本的基本禮儀來看的。5. 把腳本加固成可復(fù)用小工具重試機(jī)制、增量保存與參數(shù)調(diào)節(jié)5.1 請求重試應(yīng)對網(wǎng)絡(luò)抖動(dòng)與臨時(shí)限制網(wǎng)絡(luò)請求很難做到百分百成功斷網(wǎng)、服務(wù)器 5xx、超時(shí)都可能導(dǎo)致單次抓取失敗。與其失敗后手動(dòng)重跑不如在腳本里內(nèi)置重試邏輯。import time import random import requests from requests.adapters import HTTPAdapter def fetch_with_retry(url, headers, retries3, timeout10): session requests.Session() session.mount(https://, HTTPAdapter(max_retriesretries)) try: response session.get(url, headersheaders, timeouttimeout) response.raise_for_status() return response.text except requests.RequestException as e: print(f第 {retries} 次重試后仍失敗: {e}) return None這里用requests.Session配合HTTPAdapter設(shè)置最大重試次數(shù)raise_for_status()會(huì)在狀態(tài)碼為 4xx 或 5xx 時(shí)主動(dòng)拋出異常觸發(fā)重試邏輯。注意重試不是無腦重復(fù)初次請求失敗后下一次請求前等待一個(gè)隨機(jī)間隔能避免在服務(wù)器過載時(shí)立刻加重負(fù)擔(dān)。我會(huì)建議重試次數(shù)設(shè)為 3超過 3 次直接把 URL 記錄下來等這一輪跑完再單獨(dú)處理。5.2 增量保存只抓新標(biāo)題不重復(fù)落盤當(dāng)腳本需要定時(shí)運(yùn)行時(shí)全量覆蓋寫入會(huì)丟掉之前抓到的標(biāo)題追加寫入則會(huì)產(chǎn)生大量重復(fù)。這時(shí)增量保存是更實(shí)用的策略。做法是先讀取本地已有的標(biāo)題集合再過濾掉已經(jīng)存在的部分。def load_existing_titles(filenametitles.txt): try: with open(filename, encodingutf-8) as f: return set(line.strip() for line in f if line.strip()) except FileNotFoundError: return set() def incremental_save(titles, filenametitles.txt): existing load_existing_titles(filename) new_titles [t for t in titles if t not in existing] with open(filename, a, encodingutf-8) as f: for title in new_titles: f.write(title \n) return len(new_titles)load_existing_titles把文件里已有的標(biāo)題讀成一個(gè)集合incremental_save先計(jì)算差值再把新標(biāo)題追加寫入文件返回值是新寫入的數(shù)量。這個(gè)函數(shù)可以直接接在上一章的去重邏輯后面防止同一標(biāo)題被反復(fù)保存。用追加模式a而不是覆蓋模式是為了保住歷史數(shù)據(jù)。5.3 參數(shù)調(diào)節(jié)清單超時(shí)、間隔、編碼、請求頭最后整理一份我在實(shí)際使用中會(huì)反復(fù)調(diào)整的參數(shù)清單。網(wǎng)上很多現(xiàn)成代碼跑不動(dòng)多數(shù)是因?yàn)檫@些參數(shù)沒有針對目標(biāo)頁面做適配。參數(shù)常見取法作用timeout10~30 秒避免請求長時(shí)間無響應(yīng)導(dǎo)致腳本掛起請求間隔隨機(jī) 1~3 秒降低服務(wù)器壓力防止封禁encoding頁面 meta 聲明或 apparent_encoding解決中文亂碼User-Agent本機(jī)瀏覽器的 UA 字符串提高請求通過率選擇器瀏覽器 DevTools 實(shí)測得到的 CSS 選擇器精確定位標(biāo)題抓標(biāo)題這類任務(wù)真正花時(shí)間的往往不是寫爬蟲而是調(diào)這些參數(shù)。比如我上次做一個(gè)列表頁采集標(biāo)題標(biāo)簽有三種情況我先后試了五個(gè)選擇器才確認(rèn)了穩(wěn)定的定位方式。從那以后我每次寫爬蟲都強(qiáng)制自己先打開瀏覽器看結(jié)構(gòu)再?zèng)Q定代碼怎么寫。建議你也保留這個(gè)習(xí)慣能省掉不少排查時(shí)間。希望這篇文章能幫你順利抓到自己想要的內(nèi)容。本文還有配套的精品資源點(diǎn)擊獲取