戰(zhàn):知乎回答文本與圖片采集源碼解析)
簡(jiǎn)介這份資源面向具備Python基礎(chǔ)、希望入門網(wǎng)絡(luò)數(shù)據(jù)采集的開發(fā)者與數(shù)據(jù)分析學(xué)習(xí)者圍繞知乎問答社區(qū)場(chǎng)景講解如何抓取回答中的文本與圖片。內(nèi)容涉及HTTP請(qǐng)求發(fā)送、HTML解析、文件讀寫與延時(shí)策略等環(huán)節(jié)幫助讀者理解從問題ID定位頁面到提取回答正文、圖片鏈接及作者信息的完整思路。壓縮包共3個(gè)文件包含2個(gè)py腳本與1個(gè)txt依賴清單分別承擔(dān)數(shù)據(jù)抓取、結(jié)果保存和運(yùn)行環(huán)境說明的職責(zé)整體約4KB結(jié)構(gòu)精簡(jiǎn)便于快速閱讀與二次修改。目前已有1144人學(xué)習(xí)下載適合作為爬蟲練手項(xiàng)目參考。讀者可從中獲得請(qǐng)求構(gòu)造、解析提取、圖片下載與本地存儲(chǔ)的代碼范例并了解通過time.sleep控制請(qǐng)求頻率、設(shè)置計(jì)數(shù)器限制下載數(shù)量等防封禁思路為后續(xù)數(shù)據(jù)分析和信息挖掘打下實(shí)踐基礎(chǔ)。1. 從一份 Python 爬蟲源碼說起知乎回答文本與圖片怎么落地知乎回答頁面的內(nèi)容結(jié)構(gòu)是很多做數(shù)據(jù)采集的開發(fā)者繞不開的一道坎。正文里混著富文本、代碼塊、外鏈圖片圖片還分「點(diǎn)開大圖」和「縮略圖」兩套地址直接拿 requests 抓 HTML 往往只能撈到一堆懶加載占位符。這份 Python 爬蟲源碼包針對(duì)的就是「把某個(gè)問題下所有回答的文本和圖片完整扒下來」這個(gè)具體訴求輸出結(jié)構(gòu)化的文本文件和本地圖片目錄。它適合做輿情分析、內(nèi)容歸檔、數(shù)據(jù)集構(gòu)建的從業(yè)者也適合剛學(xué)完 requests 和 BeautifulSoup、想找一個(gè)真實(shí)站點(diǎn)練手的開發(fā)者。源碼里對(duì)請(qǐng)求頭、分頁、圖片下載做了封裝但知乎的反爬策略這幾年一直在變直接跑之前得先搞清楚它的邊界在哪。2. 拆解爬取鏈路從請(qǐng)求構(gòu)造到圖片落盤2.1 為什么不能只靠 requests BeautifulSoup知乎回答列表頁在未登錄狀態(tài)下只返回前幾條回答往下滾動(dòng)觸發(fā)的是 XHR 接口返回的是 JSON 結(jié)構(gòu)而不是完整 HTML。這意味著純靜態(tài)解析的路子走不通。源碼里采用的是「先拿頁面里的初始數(shù)據(jù)再走接口翻頁」的混合策略。初始頁面里嵌了一個(gè)js-initialData的 script 標(biāo)簽里面是 JSON 格式的初始狀態(tài)包含前幾個(gè)回答的完整內(nèi)容后續(xù)回答則通過api/v4/questions/{id}/answers這個(gè)接口按limit和offset分頁拉取。這個(gè)接口返回的 JSON 里data數(shù)組每一項(xiàng)的content字段是 HTML 字符串excerpt是純文本摘要。圖片信息藏在content的img標(biāo)簽里>import requests import json import re import os from urllib.parse import urlparse # 基礎(chǔ)請(qǐng)求頭Cookie 需要替換成自己瀏覽器里抓到的有效值 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.zhihu.com/, Cookie: 你的_cookie_字符串, # 關(guān)鍵字段缺失會(huì)導(dǎo)致分頁受限 } def fetch_answers(question_id, offset0, limit20): 按偏移量拉取回答列表返回 JSON 中的 data 數(shù)組 api fhttps://www.zhihu.com/api/v4/questions/{question_id}/answers params { include: data[*].content,excerpt,author,created_time, limit: limit, offset: offset, sort_by: default, } resp requests.get(api, headersHEADERS, paramsparams, timeout15) if resp.status_code ! 200: print(f請(qǐng)求失敗狀態(tài)碼{resp.status_code}) return [] return resp.json().get(data, [])這段代碼里include參數(shù)決定了返回字段的豐富程度不傳的話content可能不返回。limit單次最大一般不超過 20設(shè)太大容易被截?cái)?。offset按 20 遞增直到返回的data為空或paging.is_end為 true 為止。實(shí)際跑的時(shí)候建議在每次請(qǐng)求之間加 1 到 2 秒延時(shí)源碼里用的是time.sleep(random.uniform(1, 2))這個(gè)習(xí)慣能省掉很多封禁麻煩。2.3 文本清洗與圖片地址提取拿到content字段后它是一段 HTML。文本提取不能直接上正則去標(biāo)簽因?yàn)榇a塊里的會(huì)被誤傷。源碼里用的是 BeautifulSoup 的get_text()配合分隔符把塊級(jí)元素之間補(bǔ)上換行。from bs4 import BeautifulSoup def extract_text_and_images(html_content): 從回答 HTML 中提取純文本和原圖地址列表 soup BeautifulSoup(html_content, html.parser) # 把 br 和 p 替換成換行避免文本粘連 for tag in soup.find_all([br, p, div]): tag.append(\n) text soup.get_text(separator\n, stripTrue) image_urls [] for img in soup.find_all(img): # 優(yōu)先取>import hashlib import os def download_images(image_urls, save_dir): 下載圖片到指定目錄返回成功數(shù)量 os.makedirs(save_dir, exist_okTrue) success 0 for url in image_urls: try: resp requests.get(url, headersHEADERS, timeout20, streamTrue) if resp.status_code 200: # 用 URL 的 MD5 做文件名避免重名 ext os.path.splitext(urlparse(url).path)[1] or .jpg fname hashlib.md5(url.encode()).hexdigest() ext fpath os.path.join(save_dir, fname) with open(fpath, wb) as f: for chunk in resp.iter_content(1024): f.write(chunk) success 1 except Exception as e: print(f下載失敗 {url}{e}) return successstreamTrue配合iter_content是為了避免大圖一次性讀進(jìn)內(nèi)存。超時(shí)設(shè) 20 秒比文本請(qǐng)求寬松一些因?yàn)閳D片服務(wù)器響應(yīng)偶爾會(huì)慢。如果某張圖反復(fù)失敗源碼里不會(huì)中斷整個(gè)流程只是記錄日志繼續(xù)下一張這個(gè)設(shè)計(jì)在批量任務(wù)里很實(shí)用。3. 避坑與排查那些跑起來才會(huì)遇到的問題3.1 現(xiàn)象接口返回 403 或空數(shù)據(jù)原因通常是 Cookie 過期或請(qǐng)求頭里缺少Referer。知乎對(duì)接口的校驗(yàn)不只看 Cookie還會(huì)檢查來源頁。解決方法是重新從瀏覽器開發(fā)者工具里復(fù)制最新 Cookie并確保Referer指向https://www.zhihu.com/。如果換了 IP 或短時(shí)間內(nèi)請(qǐng)求過多也可能觸發(fā)風(fēng)控這時(shí)候需要降低頻率把延時(shí)拉到 3 秒以上。3.2 現(xiàn)象圖片下載下來是 1KB 左右的空白圖這是典型的懶加載占位圖。原因在于提取時(shí)取了src而不是>import yaml import os def load_config(pathconfig.yaml): with open(path, r, encodingutf-8) as f: cfg yaml.safe_load(f) # 環(huán)境變量?jī)?yōu)先級(jí)高于配置文件 cfg[cookie] os.getenv(ZHIHU_COOKIE, cfg.get(cookie, )) return cfg4.2 斷點(diǎn)續(xù)爬與日志記錄批量任務(wù)最怕跑到一半中斷重新跑又得從頭來。源碼里用了一個(gè)簡(jiǎn)單的狀態(tài)文件記錄已完成的 offset 和已下載的圖片 URL 集合。每次啟動(dòng)先讀狀態(tài)跳過已完成部分。日志用logging模塊輸出到文件格式包含時(shí)間戳、級(jí)別和消息方便事后排查哪一頁出了問題。參數(shù)建議值說明請(qǐng)求間隔1.53 秒太短觸發(fā)風(fēng)控太長效率低單次 limit20超過 20 容易被截?cái)鄨D片超時(shí)20 秒大圖下載留足時(shí)間重試次數(shù)2 次失敗后間隔 3 秒重試狀態(tài)保存頻率每頁一次避免頻繁寫盤4.3 驗(yàn)證抓取完整性的方法跑完之后怎么確認(rèn)沒漏可以對(duì)比接口返回的paging.totals字段和實(shí)際保存的回答數(shù)量。如果差距超過 5%說明中間有失敗頁需要檢查日志里的 403 或超時(shí)記錄。圖片方面統(tǒng)計(jì)每個(gè)回答目錄下的文件數(shù)和提取階段的 URL 數(shù)量做對(duì)比差值就是下載失敗的圖片。我一般會(huì)在腳本最后打印一份匯總總回答數(shù)、成功文本數(shù)、圖片總數(shù)、下載成功數(shù)、失敗列表。這份匯總比任何日志都直觀。從那以后我每次寫采集腳本都強(qiáng)制先跑一個(gè)「只抓一頁、只下三張圖」的冒煙測(cè)試確認(rèn)鏈路通了再放開批量。這個(gè)習(xí)慣幫我省下了不少因?yàn)?Cookie 過期或接口變更導(dǎo)致的整夜空跑。希望這份源碼的拆解能幫到你拿到手之后先改配置、再跑冒煙、最后開批量順序別亂。本文還有配套的精品資源點(diǎn)擊獲取