爬蟲實(shí)戰(zhàn):requests+BeautifulSoup精準(zhǔn)提取元數(shù)據(jù))
前陣子要做個(gè)小型的電影觀察分析缺一批影片的片名、導(dǎo)演、主演、評(píng)分、上映日期、片長(zhǎng)、劇情簡(jiǎn)介這類元數(shù)據(jù)。我一開始想的是手動(dòng)復(fù)制結(jié)果整理了十幾部就受不了了——豆瓣詳情頁(yè)的信息密度高一條影片條目要來(lái)回滾動(dòng)好幾次才能把需要的東西抄完太浪費(fèi)時(shí)間。后來(lái)干脆寫了個(gè) Python 爬蟲把那段時(shí)間要用的詳情頁(yè)元數(shù)據(jù)一次性抓下來(lái)并按 CSV 落盤。這篇文章就是我那套寫法的完整復(fù)盤從頁(yè)面結(jié)構(gòu)分析到請(qǐng)求偽裝從字段精準(zhǔn)提取到 CSV 導(dǎo)出方案再到跑批時(shí)實(shí)際踩過的幾個(gè)坑全部梳理出來(lái)。適合剛學(xué)完 requests 和 BeautifulSoup、想拿真實(shí)網(wǎng)站練手的同學(xué)也適合已經(jīng)能從列表頁(yè)抓到數(shù)據(jù)、但一進(jìn)詳情頁(yè)就各種缺字段的老哥。1. 先摸清目標(biāo)豆瓣詳情頁(yè)的元數(shù)據(jù)到底藏在哪寫爬蟲最忌諱一上來(lái)就寫代碼先把頁(yè)面“解剖”清楚后面的提取邏輯基本是水到渠成的事。我這次的目標(biāo)是詳情頁(yè)元數(shù)據(jù)所以第一步要搞明白詳情頁(yè)和列表頁(yè)到底有什么區(qū)別哪些數(shù)據(jù)在這個(gè)頁(yè)面上能拿到它們各自以什么形式存在。1.1 從列表頁(yè)到詳情頁(yè)URL 結(jié)構(gòu)是采集的第一張地圖豆瓣電影的數(shù)據(jù)組織方式很簡(jiǎn)單。列表頁(yè)比如某個(gè)榜單解決的是“有哪些影片”的問題它返回幾十部影片的條目但每個(gè)條目里只有片名、評(píng)分、評(píng)價(jià)人數(shù)、一句話概述這類精簡(jiǎn)信息。導(dǎo)演、編劇、主演、片長(zhǎng)、劇情簡(jiǎn)介這種“深水區(qū)”數(shù)據(jù)列表頁(yè)根本不給你完整展示往往要到詳情頁(yè)才有。詳情頁(yè)的 URL 長(zhǎng)這樣https://movie.douban.com/subject/xxxx/這里的數(shù)字就是每部影片在豆瓣里的唯一編號(hào)不同影片對(duì)應(yīng)不同 ID。所以一條完整的采集鏈路是先從列表頁(yè)拿到一批影片標(biāo)題和鏈接再?gòu)逆溄又刑崛〕鲞@個(gè)數(shù)字 ID最后用 ID 拼接詳情頁(yè) URL逐個(gè)抓取元數(shù)據(jù)。這一步雖然簡(jiǎn)單但值得注意不要試圖用正則從網(wǎng)頁(yè)里扣a標(biāo)簽來(lái)拼 URL太容易拿到廣告位或隨機(jī)推薦鏈接。我習(xí)慣用 BeautifulSoup 按 CSS 選擇器去過濾比如只選擇標(biāo)題列表里的a標(biāo)簽并且用正則校驗(yàn)鏈接中必須包含/subject/和末尾的數(shù)字串。import re from urllib.parse import urljoin def extract_subject_id(link): match re.search(r/subject/(\d)/, link) return match.group(1) if match else None只有拿到干凈的 subject_id才能保證循環(huán)抓詳情頁(yè)時(shí)不會(huì)突然打到無(wú)關(guān)頁(yè)面。這個(gè)正則看著簡(jiǎn)單但在跑批量任務(wù)時(shí)能幫你過濾掉很多臟數(shù)據(jù)。1.2 詳情頁(yè)里值得抓的三塊內(nèi)容打開任意一個(gè)豆瓣詳情頁(yè)按 F12 簡(jiǎn)單瞄一眼結(jié)構(gòu)會(huì)發(fā)現(xiàn)元數(shù)據(jù)主要分布在三個(gè)區(qū)域我習(xí)慣把它們叫做“標(biāo)題區(qū)”“屬性區(qū)”“腳本區(qū)”。數(shù)據(jù)塊包含字段在頁(yè)面中的位置推薦解析方式標(biāo)題區(qū)片名、年份、導(dǎo)演、編劇、主演h1標(biāo)題、div idinfo信息欄BeautifulSoup 選擇器評(píng)分區(qū)評(píng)分、評(píng)價(jià)人數(shù)、好于百分比strong propertyv:average、評(píng)價(jià)人數(shù)標(biāo)簽屬性選擇器腳本區(qū)劇情簡(jiǎn)介、上傳日期、類型標(biāo)簽script typeapplication/ldjsonast 或 json 解析為什么要把“腳本區(qū)”單獨(dú)拿出來(lái)說(shuō)因?yàn)槎拱暝斍轫?yè)里有一段結(jié)構(gòu)化的 JSON-LD 數(shù)據(jù)里面直接寫了name、description、datePublished這種近乎齊全的元數(shù)據(jù)字段。很多教程只盯 HTML 節(jié)點(diǎn)看繞了半天才把劇情簡(jiǎn)介摳出來(lái)其實(shí)這段 JSON 里已經(jīng)有了。省事又不容易出錯(cuò)。我還是會(huì)從 HTML 節(jié)點(diǎn)里抓一部分字段因?yàn)槟承┱故驹陧?yè)面上的數(shù)據(jù)比如“好于 90% 劇情片”這種橫向?qū)Ρ饶_本里沒有只能老老實(shí)實(shí)解析節(jié)點(diǎn)。兩個(gè)來(lái)源配合使用才能做到“精準(zhǔn)采集”。2. 請(qǐng)求這關(guān)怎么過依賴選型、請(qǐng)求頭偽裝與頻率控制很多新手爬蟲被擋在第一步不是解析代碼寫得不好而是請(qǐng)求一多就被網(wǎng)站識(shí)別了。豆瓣的反爬不算狠但也絕不需要你用毫無(wú)偽裝的裸請(qǐng)求去挑戰(zhàn)它的耐心。這個(gè)環(huán)節(jié)我踩過一次有次趕數(shù)據(jù)用短時(shí)間密集請(qǐng)求連抓幾百個(gè)詳情頁(yè)結(jié)果中途連續(xù)收到好幾條“檢測(cè)到有異常請(qǐng)求”的提示頁(yè)直接把整個(gè)任務(wù)打斷。2.1 依賴安裝與解析器選型別什么都上 Scrapy先說(shuō)說(shuō)工具選型。這個(gè)任務(wù)用 requests BeautifulSoup4 lxml 就夠了沒必要上 Scrapy。Scrapy 確實(shí)強(qiáng)大但對(duì)于“批量抓一批詳情頁(yè)并導(dǎo)出 CSV”這種小體量任務(wù)它的學(xué)習(xí)曲線和調(diào)試成本是多余的。你拆成一個(gè)循環(huán)、一個(gè)解析函數(shù)幾十分鐘就能寫完出了問題也容易定位。requests 負(fù)責(zé)發(fā)請(qǐng)求、接收 HTMLBeautifulSoup4 負(fù)責(zé)結(jié)構(gòu)化解析lxml 作為背后解析器速度比 Python 標(biāo)準(zhǔn)庫(kù)的 html.parser 快不少三件套是這類輕量爬蟲的黃金組合。pip install requests beautifulsoup4 lxml有一點(diǎn)要注意BeautifulSoup 的lxml解析器需要單獨(dú)裝不能因?yàn)?bs4 裝好了就想當(dāng)然地跳過。如果不裝代碼雖然能跑但解析性能會(huì)明顯下降跑批量的時(shí)候一段一段卡頓體驗(yàn)很差。2.2 UA 偽裝與請(qǐng)求頭這幾行頭信息能保命豆瓣詳情頁(yè)對(duì)請(qǐng)求頭里的User-Agent和Accept-Language比較敏感。一個(gè)來(lái)自明顯過時(shí)瀏覽器的 UA或者一個(gè)連語(yǔ)言偏好都沒聲明的請(qǐng)求很容易觸發(fā)風(fēng)控標(biāo)記。我習(xí)慣用一句像樣的瀏覽器 UA比如下面這種headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Referer: https://movie.douban.com/, Connection: keep-alive }其中Accept-Language里的zh-CN很重要豆瓣詳情頁(yè)會(huì)根據(jù)這個(gè)字段決定某些頁(yè)面內(nèi)容是中文還是其他處理過的語(yǔ)言變體。少了它偶爾會(huì)碰上拿到的字段是英文的情況解析邏輯沒問題但數(shù)據(jù)就是不對(duì)勁。我自己就把這個(gè)頭加上了實(shí)測(cè)穩(wěn)定很多。請(qǐng)求頭只是第一道偽裝真正的安全牌是請(qǐng)求頻率。我習(xí)慣每抓完一個(gè)詳情頁(yè)隨機(jī)睡個(gè)2~4秒讓請(qǐng)求間隔看起來(lái)像真人瀏覽。import random import time time.sleep(random.uniform(2, 4))別小看這幾秒。爬蟲數(shù)據(jù)量小的時(shí)候無(wú)所謂一旦抓幾百個(gè)頁(yè)面這種隨機(jī)間隔比固定 3 秒更能模擬真人的節(jié)奏也降低被連續(xù)標(biāo)記的風(fēng)險(xiǎn)。2.3 單線程順序抓取為什么我勸你別上并發(fā)剛接觸爬蟲的時(shí)候我也有一個(gè)執(zhí)念覺得并發(fā)越多越快越好。后來(lái)被教訓(xùn)過幾次才明白對(duì)這種小體量詳情頁(yè)采集場(chǎng)景來(lái)說(shuō)單線程順序抓取反而是最穩(wěn)妥的方案。并發(fā)一旦上到 5 個(gè)、10 個(gè)線程請(qǐng)求頻率曲線會(huì)變得很陡。豆瓣風(fēng)控的標(biāo)記不是看你總計(jì)抓了多少頁(yè)面而是看你在一個(gè)時(shí)間窗口內(nèi)的請(qǐng)求密度。10 個(gè)線程同時(shí)跑每秒鐘可能有 5 個(gè)請(qǐng)求從同一個(gè)出口打過去哪怕加了延遲上限速度還是容易翻車。再說(shuō)了一個(gè)詳情頁(yè)的請(qǐng)求加解析也就半秒左右加上隨機(jī)延遲抓 500 部影片也就一二十分鐘的事。這個(gè)耗時(shí)完全可以接受沒必要為了省那幾分鐘去賭賬號(hào) IP 的安全。3. 精準(zhǔn)提取核心代碼拆解我在代碼里盡量把“解析頁(yè)面的邏輯”收斂到一個(gè)獨(dú)立函數(shù)里這樣頁(yè)面結(jié)構(gòu)一變只需要改這一個(gè)函數(shù)其他邏輯不用動(dòng)。這種做法在爬蟲工程化里非常重要畢竟網(wǎng)站改版完全不打招呼拆得越干凈后續(xù)維護(hù)成本越低。3.1 一個(gè)函數(shù)搞定單個(gè)詳情頁(yè)標(biāo)準(zhǔn)化輸出字典下面的函數(shù)是我在實(shí)際項(xiàng)目里整理出來(lái)的精簡(jiǎn)版輸入 HTML 文本輸出一個(gè)字段干凈的字典。每一步我都加了空值保護(hù)確保拿不到數(shù)據(jù)的字段不會(huì)讓程序崩掉。import ast import json import re import requests from bs4 import BeautifulSoup def parse_detail_html(html: str) - dict: soup BeautifulSoup(html, lxml) item {} # 標(biāo)題區(qū)h1 里通常是 片名 年份 title_node soup.select_one(h1) title_text title_node.get_text(stripTrue) if title_node else # 用空格切出片名和年份注意年份可能不存在 parts title_text.split() item[title] parts[0] if parts else item[year] parts[-1] if len(parts) 1 and parts[-1].isdigit() else # 評(píng)分區(qū) rating_node soup.select_one(strong[propertyv:average]) item[rating] rating_node.get_text(stripTrue) if rating_node else votes_node soup.select_one(span[propertyv:votes]) item[votes] votes_node.get_text(stripTrue) if votes_node else # 屬性區(qū)導(dǎo)演、編劇、主演 item[director] extract_info_field(soup, 導(dǎo)演) item[screenwriter] extract_info_field(soup, 編劇) item[actors] extract_info_field(soup, 主演) # 片長(zhǎng) runtime_node soup.select_one(span[propertyv:runtime]) item[runtime] runtime_node.get_text(stripTrue) if runtime_node else # 劇情簡(jiǎn)介從 ldjson 里優(yōu)先取 item[summary] extract_description(soup) return itemextract_info_field是我專門寫的通用提取函數(shù)思路很直觀在div#info里逐個(gè)找標(biāo)簽帶導(dǎo)演、編劇、主演字樣的span然后收集它后面直到br為止的所有兄弟節(jié)點(diǎn)文本用/連接。def extract_info_field(soup, label: str) - str: info soup.find(div, idinfo) if not info: return for span in info.find_all(span, class_pl): if label in span.get_text(stripTrue): values [] for sibling in span.find_next_siblings(): if sibling.name br: break text sibling.get_text(stripTrue) if text and text not in (更多, 更多?): values.append(text) cleaned [] for value in values: value re.sub(rnbsp;, , value) cleaned.append(value) return / .join(cleaned) return 這里有個(gè)細(xì)節(jié)容易忽略找到標(biāo)簽后它的兄弟節(jié)點(diǎn)會(huì)包含a標(biāo)簽以及一些沒有結(jié)構(gòu)意義的空白節(jié)點(diǎn)。但豆瓣屬性區(qū)里導(dǎo)演名和其他字段是嵌在a標(biāo)簽里的所以直接遍歷兄弟節(jié)點(diǎn)遇到br就停拿到所有a文本基本等于拿到了完整字段值。這個(gè)方法比一次性選div#info下所有a再按位置索引要穩(wěn)。3.2 劇情簡(jiǎn)介走 ldjsonast.literal_eval 比 json.loads 更穩(wěn)劇情簡(jiǎn)介是詳情頁(yè)里很長(zhǎng)的一段文本直接抓節(jié)點(diǎn)當(dāng)然能抓到但更省事的做法是讀取頁(yè)面上那段application/ldjson腳本。這段腳本里已經(jīng)寫好了description字段和頁(yè)面展示的劇情簡(jiǎn)介內(nèi)容一致。def extract_description(soup) - str: script soup.find(script, typeapplication/ldjson) if script and script.string: raw_text script.string.strip() try: data ast.literal_eval(raw_text) description data.get(description, ) if isinstance(description, str): return description.strip() except Exception: pass # 退路直接用頁(yè)面節(jié)點(diǎn) summary soup.select_one(span[propertyv:summary]) if summary: return summary.get_text(stripTrue) return 這里我故意用了ast.literal_eval而不是json.loads。很多人寫爬蟲看到 JSON 字段下意識(shí)用json.loads但豆瓣這個(gè)腳本里偶爾會(huì)出現(xiàn)單引號(hào)、None這類 Python 字面量的寫法嚴(yán)格來(lái)說(shuō)不是合法的 JSON。json.loads會(huì)直接拋異常而ast.literal_eval能容忍這些寫法處理起來(lái)更順手。如果數(shù)據(jù)本身就是標(biāo)準(zhǔn) JSONast.literal_eval也能正常解析所以這個(gè)小選擇總體來(lái)說(shuō)更穩(wěn)健。另外要說(shuō)的是從 HTML 節(jié)點(diǎn)提取劇情簡(jiǎn)介時(shí)務(wù)必留意stripTrue否則很容易把換行和空格粘進(jìn)摘要里。字段里帶一堆\n后續(xù)做數(shù)據(jù)分析還得二次清洗純屬自找麻煩。3.3 空值處理別讓一個(gè)壞頁(yè)面毀掉整批任務(wù)實(shí)際跑批量的時(shí)候一定會(huì)碰到某些影片頁(yè)面缺少某個(gè)字段的情況。比如個(gè)別老片沒有片長(zhǎng)、沒有評(píng)分或者年份信息異常。如果代碼一遇到None就拋異常整個(gè)循環(huán)都會(huì)中斷前面抓的數(shù)據(jù)全部白費(fèi)。我的習(xí)慣是所有字段都做了None兜底取不到就把空字符串放進(jìn)去。表面上看 CSV 里有一列是空的但這總比程序崩掉、任務(wù)中斷要好。后期的數(shù)據(jù)處理可以統(tǒng)一過濾空值根本不耽誤事。def safe_get(node): return node.get_text(stripTrue) if node else 再加上主循環(huán)里對(duì)單頁(yè)解析做異常捕獲一個(gè)頁(yè)面解析出錯(cuò)時(shí)只記錄日志跳過它繼續(xù)抓下一個(gè)。這個(gè)“容錯(cuò)優(yōu)先”的思路是爬蟲從“能跑”到“能長(zhǎng)時(shí)間穩(wěn)定跑”的關(guān)鍵一步。4. CSV 導(dǎo)出的三個(gè)細(xì)節(jié)亂碼、重復(fù)表頭、字段順序抓到的元數(shù)據(jù)最終要落盤到 CSV 才方便后續(xù)用辦公軟件打開、篩選、分析。CSV 導(dǎo)出看著簡(jiǎn)單但有幾個(gè)細(xì)節(jié)不處理好出來(lái)的文件會(huì)很難用。4.1 追加寫入還是每次重寫我推薦追加模式如果你是一次性抓完一批再導(dǎo)出那每次都重寫文件沒問題。但我是邊抓邊寫希望抓完 50 個(gè)頁(yè)面后文件里就有 50 行數(shù)據(jù)中途跑掛了也不至于所有數(shù)據(jù)全部丟失。這種情況下追加寫入最合適。追加寫入的核心是用modea打開文件而不是默認(rèn)的w。同時(shí)要指定newline否則 CSV 里每?jī)尚兄g都會(huì)多出一個(gè)空行這是 Windows 下常見的坑。import csv import os fieldnames [title, year, director, screenwriter, actors, rating, votes, runtime, summary] def save_to_csv(item: dict, filename: str douban_movies.csv): file_exists os.path.isfile(filename) and os.path.getsize(filename) 0 with open(filename, modea, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if not file_exists: writer.writeheader() writer.writerow(item)用os.path.isfile判斷文件是否已存在是為了避免表頭重復(fù)。文件第一次創(chuàng)建時(shí)寫表頭之后就只追加數(shù)據(jù)行。4.2 為什么必須是 utf-8-sigExcel 亂碼的根源很多人導(dǎo)出 CSV 后用 Excel 一打開中文全變成亂碼問題基本出在編碼上。普通utf-8編碼的 CSVExcel 默認(rèn)會(huì)用本地的ANSI解碼中文字符自然對(duì)不上號(hào)。解決辦法是寫入時(shí)用utf-8-sig編碼它會(huì)在文件開頭寫入一個(gè) BOM 標(biāo)記Excel 識(shí)別到之后就自動(dòng)按 UTF-8 解碼。我在第一次寫爬蟲腳本時(shí)就是忘了這一步導(dǎo)出的 CSV 用記事本打開正常、用 Excel 打開一片亂碼當(dāng)時(shí)還以為是代碼寫錯(cuò)了排查了半天才發(fā)現(xiàn)是編碼問題。4.3 字段順序定義清楚CSV 的第一行就是你的數(shù)據(jù)字典csv.DictWriter的fieldnames列表順序就是輸出 CSV 里每一列的順序。我習(xí)慣把主標(biāo)識(shí)類字段放前面標(biāo)題、年份再放人員類導(dǎo)演、編劇、主演然后是評(píng)分類評(píng)分、評(píng)價(jià)人數(shù)最后是長(zhǎng)文本劇情簡(jiǎn)介。這個(gè)順序?qū)罄m(xù)人工審查和機(jī)器讀取都比較友好。如果字段順序不穩(wěn)定比如這次是“標(biāo)題 年份 導(dǎo)演”下次是“導(dǎo)演 標(biāo)題 年份”CSV 打開后表頭亂七八糟不說(shuō)別人接手你的數(shù)據(jù)還得重新對(duì)齊。這一行 fieldnames最好在寫代碼時(shí)就固定下來(lái)一勞永逸。5. 實(shí)測(cè)里繞不開的坑風(fēng)控提示、頁(yè)面結(jié)構(gòu)升級(jí)、臟數(shù)據(jù)過濾前面說(shuō)的方法已經(jīng)夠跑通一個(gè)基礎(chǔ)版本但真正讓腳本“穩(wěn)”起來(lái)的是處理意外情況的能力。這里把我踩過的幾個(gè)坑集中講一下。5.1 風(fēng)控提示怎么識(shí)別與應(yīng)對(duì)豆瓣風(fēng)控上線得比較早當(dāng)你請(qǐng)求頻率過高時(shí)返回的不是攔截頁(yè)面就是一個(gè)看似正常的頁(yè)面里加了一句“檢測(cè)到有異常請(qǐng)求從你的 IP 發(fā)出”。這種情況下HTML 內(nèi)容解析時(shí)不會(huì)報(bào)錯(cuò)但你會(huì)發(fā)現(xiàn)rating、votes這些字段全部消失數(shù)據(jù)開始大量出現(xiàn)空值。排查這個(gè)問題最直接的辦法是打印響應(yīng)狀態(tài)碼和頁(yè)面標(biāo)題。如果狀態(tài)碼是 418 或者頁(yè)面標(biāo)題變成了“頁(yè)面不存在”基本可以斷定是觸發(fā)了風(fēng)控這時(shí)候繼續(xù)硬著頭皮跑沒有意義。我的處理策略也很簡(jiǎn)單降低頻率、按指數(shù)退避重新請(qǐng)求。具體來(lái)說(shuō)第一次遇到異常時(shí) sleep 30 秒重試一次如果還是異常就 sleep 60 秒再試連續(xù)三次不行就跳過當(dāng)前頁(yè)面并記錄日志。注意重試之間要累積延時(shí)不要固定間隔地反復(fù)重試。def fetch_html(url, headers, retries3): for attempt in range(retries): resp requests.get(url, headersheaders, timeout10) if resp.status_code 200 and 檢測(cè)到有異常請(qǐng)求 not in resp.text: return resp.text time.sleep(10 * (attempt 1)) return None這套重試邏輯非常適合中小體量的爬蟲既不會(huì)在風(fēng)控面前死磕也不會(huì)因?yàn)橐粌纱尾▌?dòng)就丟掉數(shù)據(jù)。5.2 頁(yè)面結(jié)構(gòu)升級(jí)選擇器失效時(shí)的兜底方案豆瓣改版過好幾次某些節(jié)點(diǎn)的 class 或?qū)傩詴?huì)調(diào)整。我的解析代碼之所以能快速適應(yīng)靠的是兩個(gè)原則第一優(yōu)先用穩(wěn)定屬性選擇器。比如propertyv:average、propertyv:votes、propertyv:runtime這些屬性是豆瓣對(duì)外輸出的結(jié)構(gòu)化標(biāo)記比classrating_self clearfix這種視覺樣式類穩(wěn)定得多。樣式類說(shuō)改就改結(jié)構(gòu)化屬性通常不會(huì)隨便變動(dòng)。第二解析函數(shù)統(tǒng)一收口。無(wú)論頁(yè)面怎么變解析邏輯全部放在parse_detail_html一個(gè)函數(shù)里。網(wǎng)站改版后只需要修改這個(gè)函數(shù)的內(nèi)部選擇器其他部分完全不用動(dòng)。我把這種設(shè)計(jì)理解成“爬蟲的依賴倒置”頁(yè)面結(jié)構(gòu)與采集邏輯隔離變化只出現(xiàn)在一層。5.3 數(shù)據(jù)清洗過濾掉無(wú)效條目和重復(fù)條目批量跑完一批數(shù)據(jù)后別急著直接用。CSV 里可能有些字段缺失嚴(yán)重的行比如連片名都沒有的空行也可能因?yàn)閿帱c(diǎn)重爬部分條目重復(fù)寫入。我常用的清洗邏輯很簡(jiǎn)單用 pandas 讀進(jìn)來(lái)先按title去重再過濾掉標(biāo)題為空的記錄最后把評(píng)分、年份這類字段的類型轉(zhuǎn)好。排序時(shí)按評(píng)分降序、評(píng)價(jià)人數(shù)降序方便后續(xù)分析。import pandas as pd df pd.read_csv(douban_movies.csv, encodingutf-8-sig) df df[df[title] ! ] df df.drop_duplicates(subset[title], keeplast) df[rating] pd.to_numeric(df[rating], errorscoerce) df[votes] pd.to_numeric(df[votes], errorscoerce) df df.sort_values([rating, votes], ascendingFalse)這一步清理能從粗糙的原始數(shù)據(jù)里快速拎出可用部分。很多爬蟲教程到“導(dǎo)出 CSV”就結(jié)束了但我實(shí)際用下來(lái)的體會(huì)是導(dǎo)出的 CSV 才是數(shù)據(jù)工作的起點(diǎn)清洗和你抓取本身同樣重要。6. 爬蟲合理的邊界頻率、數(shù)據(jù)量與用途要有分寸最后我想認(rèn)真聊聊分寸感。寫爬蟲的都知道“能不能”和“該不該”是兩回事。豆瓣詳情頁(yè)的數(shù)據(jù)本質(zhì)上是由網(wǎng)友和平臺(tái)共同維護(hù)的公開內(nèi)容不以登錄狀態(tài)為前提就可以瀏覽采集它對(duì)個(gè)人學(xué)習(xí)、數(shù)據(jù)分析來(lái)說(shuō)有合理需求。但這不代表可以無(wú)限度地抓。我給自己定的邊界有三條單次采集控制在可解釋的范圍內(nèi)。我的項(xiàng)目需求是幾百部影片的元數(shù)據(jù)這個(gè)規(guī)模用單線程慢慢跑完全合理。但如果你要做全站幾百萬(wàn)條數(shù)據(jù)的鏡像采集那就已經(jīng)完全超出個(gè)人學(xué)習(xí)范疇了該考慮尋求官方數(shù)據(jù)接口或授權(quán)渠道??刂普?qǐng)求頻率。個(gè)人用途正常瀏覽式的頻率可以理解但這不意味著可以用高并發(fā)去挑戰(zhàn)平臺(tái)防守。我在前面反復(fù)提到隨機(jī)延遲這不僅是技術(shù)層面的防封策略也是態(tài)度問題。數(shù)據(jù)只用于個(gè)人分析不二次轉(zhuǎn)賣、不公開完整數(shù)據(jù)集。別人的勞動(dòng)成果值得手下留情這也是所有技術(shù)人生存的基本盤。這套邊界在我的所有爬蟲項(xiàng)目里都是默認(rèn)配置。技術(shù)本身是中性工具關(guān)鍵是使用方式和用途要經(jīng)得起推敲。最后再分享一個(gè)實(shí)操小技巧批量任務(wù)結(jié)束后把抓取到的 CSV 文件名后綴加上當(dāng)天的日期比如douban_movies_20240615.csv。這樣每次重新抓取、清洗都不會(huì)把舊數(shù)據(jù)覆蓋掉如果需要對(duì)比不同時(shí)間的數(shù)據(jù)變化也有據(jù)可查。我后來(lái)很多分析結(jié)果能順利回溯靠的就是這個(gè)小習(xí)慣。