:從反爬識別到合規(guī)數(shù)據(jù)采集)
做數(shù)據(jù)采集的人幾乎都逃不過“反爬”這兩個字。就在上個月我給自己那個只會聊天的AI助手——代號“小龍蝦”——加了一套爬蟲技能讓它能自己看網(wǎng)頁、提取數(shù)據(jù)、清洗入庫。整個過程踩了不少坑但也整理出了一套相對完整的思路。這篇內(nèi)容我會把這套方案的架構(gòu)、工具選擇和實戰(zhàn)代碼都拆開講清楚尤其會聊一聊反爬機制到底在防什么以及哪些“突破”手段是合規(guī)的、哪些絕對不能碰。如果你也在做數(shù)據(jù)采集或者想給自己的AI應(yīng)用加上聯(lián)網(wǎng)抓取能力這篇文章應(yīng)該能幫你避開我踩過的那些坑。1. 項目全景為什么給AI裝爬蟲技能1.1 “小龍蝦”的初始能力邊界我一開始做“小龍蝦”這個項目其實只是想讓它能回答一些需要實時數(shù)據(jù)的問題。比如用戶問“某款產(chǎn)品最近的價格趨勢”“某個論壇的熱帖標題”它不能憑空編需要有真實數(shù)據(jù)來源。原本的方案很簡單人工寫幾個腳本定時抓數(shù)據(jù)喂給它。但問題很快暴露出來——腳本只針對固定的頁面結(jié)構(gòu)一旦頁面改版選擇器全部失效而且碰到有反爬策略的站點腳本跑不了幾次就會被限制訪問。后來我換了個思路既然“小龍蝦”是AI為什么不讓它自己學(xué)會“看頁面”和“提取信息”于是我給這個AI助手設(shè)計了一套爬蟲技能層。它不再是單純的聊天模型而是可以調(diào)用工具、分析頁面結(jié)構(gòu)、動態(tài)調(diào)整抓取策略的“數(shù)據(jù)小助手”。這個改造聽起來簡單真正做起來卻涉及三個層面的問題反爬機制識別、頁面解析策略、數(shù)據(jù)清洗流程。每一個層面都有不少隱藏的坑。1.2 反爬機制真正想攔住的是誰先說反爬。很多人一聽到“反爬”就覺得是貓鼠游戲非要繞過對方的防護才算贏。但實際動手后你會發(fā)現(xiàn)大部分反爬機制的目標不是阻止所有爬蟲而是阻止“不守規(guī)矩”的爬蟲。常見的幾類反爬手段本質(zhì)都在做同一件事判斷訪問者是不是真人以及訪問行為是否合理。User-Agent校驗服務(wù)器檢查HTTP請求頭里的UA字段非瀏覽器UA直接拒絕。訪問頻率限制同一IP在短時間內(nèi)請求次數(shù)過多觸發(fā)限流甚至封禁。動態(tài)渲染數(shù)據(jù)由JavaScript在瀏覽器里異步生成直接請求HTML拿不到內(nèi)容。行為校驗檢測鼠標軌跡、點擊間隔、頁面停留時間等判斷是否像真人。驗證碼當風(fēng)控系統(tǒng)判斷可疑時彈出驗證碼要求人機驗證。我做的第一版爬蟲一上來就模擬請求去抓一個排行榜頁面結(jié)果連返回的響應(yīng)都變成了“403 Forbidden”。后來我仔細看了對方返回的HTML發(fā)現(xiàn)里面有一行提示大概意思是“請求頻率超限請稍后再試”。這才意識到反爬不是要把所有爬蟲趕盡殺絕而是在逼你用更尊重站點資源的方式去抓數(shù)據(jù)。合規(guī)的應(yīng)對策略其實也簡單模擬真實瀏覽器的基本請求頭、控制抓取頻率、盡量用瀏覽器自動化處理動態(tài)渲染頁面遇到驗證碼就停下來檢查自己的節(jié)奏。這套思路放進去之后“小龍蝦”的抓取成功率立刻高了很多。1.3 方案選型為什么用“瀏覽器自動化AI解析”而不是純請求最開始我傾向于用純請求庫加正則表達式因為性能好、資源占用低。但實際遇到動態(tài)渲染頁面后這個方案直接失效。我試著用接口逆向的方式去找頁面背后的數(shù)據(jù)接口確實能找到一部分但有些站點會對接口參數(shù)做簽名校驗?zāi)嫦虺杀痉浅8叨乙坏Ψ礁潞灻惴ㄓ忠匦路治鼍S護成本完全不可控。后來我轉(zhuǎn)向瀏覽器自動化方案。讓“小龍蝦”像人一樣打開一個無頭瀏覽器等頁面加載完再讀取渲染后的DOM。這個方案的優(yōu)點是不太依賴頁面的具體實現(xiàn)方式不管是Vue還是React最后渲染出來的都是標準HTMLAI可以直接從中提取信息。缺點是速度和資源消耗都遠大于純請求所以它更適合中小規(guī)模的定向采集不適合大規(guī)模爬取。配合AI解析之后方案變得更靈活了。我不需要預(yù)先寫死提取規(guī)則而是把渲染后的HTML片段丟給大語言模型讓它分析出目標數(shù)據(jù)的CSS選擇器或者XPath。這樣即使頁面改版只要把新的HTML片段再給模型看一遍規(guī)則就會自動更新不需要人肉維護一堆正則表達式。2. 技能架構(gòu)與工具選型2.1 整體架構(gòu)請求層、解析層、AI決策層整個“小龍蝦”的爬蟲技能我拆成了三層。第一層是請求層負責獲取頁面內(nèi)容。這個層級要考慮請求頭、超時時間、重試機制、頻率控制。對于靜態(tài)頁面用HTTP請求庫就夠了對于動態(tài)頁面則要啟動瀏覽器自動化框架。第二層是解析層負責從HTML里提取結(jié)構(gòu)化數(shù)據(jù)。以前我習(xí)慣直接用XPath或CSS選擇器但選擇器寫起來麻煩頁面一改就崩?,F(xiàn)在我會把頁面文本和結(jié)構(gòu)交給AI讓它生成提取規(guī)則然后我用一個通用的提取器去執(zhí)行這些規(guī)則。第三層是AI決策層也是“小龍蝦”的靈魂。這一層接收用戶的自然語言指令比如“抓取這個頁面里的商品名稱和價格”然后自動判斷該用什么請求方式、是否需要渲染JS、提取完數(shù)據(jù)后怎么清洗、要不要保存到數(shù)據(jù)庫。AI決策層的輸出是一份可執(zhí)行的任務(wù)計劃而不是一段寫死的代碼。這三層各司其職好處是任何一層出問題都可以單獨替換。比如請求層發(fā)現(xiàn)某個站點需要登錄才能看數(shù)據(jù)那就不用硬闖直接在AI決策層判斷“這個數(shù)據(jù)源不適合自動化抓取”轉(zhuǎn)而搜索公開API。2.2 工具選型每個環(huán)節(jié)的合適選擇工具選型我做了不少對比重點考慮三個維度社區(qū)活躍度、文檔質(zhì)量、和AI模型的配合難度。請求層我用了兩個方案一個是老牌的requests庫適合簡單頁面另一個是httpx支持異步并發(fā)性能更好。requests的優(yōu)點是生態(tài)成熟、代碼簡單但遇到連接復(fù)用和異步請求時有點力不從心。httpx的API和requests很像但原生支持異步我用它來處理需要同時抓取多個頁面的場景。解析層我選過BeautifulSoup、lxml和parsel。BeautifulSoup最直觀適合新手但性能一般。lxml速度最快XPath支持完善可語法太硬。parsel是Scrapy提煉出來的解析庫封裝得干凈既有CSS選擇器也支持XPath內(nèi)存占用也可控。我最后留了parsel因為它和AI生成的選擇器兼容性最好。瀏覽器自動化工具我在Selenium和Playwright之間猶豫了很久。Selenium名氣大、資料多但安裝驅(qū)動麻煩運行也不夠穩(wěn)定。Playwright的API設(shè)計更現(xiàn)代自帶瀏覽器下載和管理還可以通過CDP協(xié)議和AI模型交互所以最終選了Playwright。對于“小龍蝦”來說它只需要加載頁面、等待關(guān)鍵元素出現(xiàn)、導(dǎo)出HTMLPlaywright用起來最省心。2.3 給“小龍蝦”的AI能力接口AI決策層我設(shè)計了一套簡易的函數(shù)調(diào)用機制?!靶↓埼r”本身是一個大語言模型接口但我給它注冊了幾個工具函數(shù)包括“fetch_page”“render_page”“extract_data”“save_dataset”。模型根據(jù)用戶的問題生成JSON格式的工具調(diào)用指令系統(tǒng)去執(zhí)行并把結(jié)果返回給模型模型再組織成回答。這套機制的核心是讓AI學(xué)會“看情況辦事”。比如用戶說“幫我看看這個頁面里有哪些新聞標題”模型會先選擇“render_page”而不是“fetch_page”因為它知道新聞?wù)军c的內(nèi)容很可能由JS渲染。如果用戶要求“每5分鐘抓一次”模型會自動在請求層插入隨機延時降低被封風(fēng)險。工具接口設(shè)計有個很關(guān)鍵的細節(jié)要給模型提供足夠清晰的參數(shù)說明。比如“fetch_page”需要傳入url、headers、timeout“extract_data”需要傳入html_snippet和schema。模型看到這些說明后才知道怎么調(diào)用。沒有詳細的工具說明AI再強也會瞎猜。3. 實戰(zhàn)三類典型反爬場景的合規(guī)應(yīng)對3.1 場景一UA校驗與基礎(chǔ)請求頭先看最簡單的場景。很多網(wǎng)站老舊的防護機制就是檢查User-Agent如果是Python默認的UA直接拒絕。我給“小龍蝦”設(shè)計請求層時把常用瀏覽器的UA維護成一份配置發(fā)起請求前隨機取一個并補上常見的Accept、Accept-Language、Connection頭。下面是一個簡化版的請求函數(shù)用requests實現(xiàn)import requests import random BROWSER_UAS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15, ] def fetch_page(url): headers { User-Agent: random.choice(BROWSER_UAS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp.text這樣改完后原本被UA校驗擋掉的頁面大多能正常返回。但我提醒一句這只是最基本的一步現(xiàn)在很多站點的反爬是組合拳UA校驗往往和頻率限制一起出現(xiàn)。實操心得不要偽造一個根本不存在的UA。有的開發(fā)者隨手寫了“Googlebot”的UA去抓頁面這樣做既不符合規(guī)范也容易觸發(fā)更嚴格的反爬。更穩(wěn)妥的做法是模擬普通用戶的真實環(huán)境比如Windows Chrome或者macOS Safari的UA。3.2 場景二動態(tài)渲染頁面拿不到數(shù)據(jù)第二個場景非常常見。用requests請求某論壇的帖子列表返回的HTML里只有空殼數(shù)據(jù)全是通過JavaScript異步加載的。這時候就需要用Playwright渲染完整頁面再提取。我給“小龍蝦”封裝了一個“render_page”工具代碼如下from playwright.sync_api import sync_playwright def render_page(url, wait_fornetworkidle, timeout30000): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( 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 ), viewport{width: 1280, height: 800}, localezh-CN, ) page context.new_page() page.goto(url, wait_untilwait_for, timeouttimeout) page.wait_for_timeout(2000) html page.content() browser.close() return html我知道很多人在這一步喜歡加“隨機休眠”讓頁面更像真人操作。但更重要的其實是等待策略。Wait_until設(shè)置為“networkidle”可以等網(wǎng)絡(luò)空閑后再返回如果數(shù)據(jù)加載特別慢還可以顯式等待某個選擇器出現(xiàn)比如“page.wait_for_selector(.list-item)”。這個場景的難點不是代碼而是判斷一個頁面到底需不需要渲染。我的經(jīng)驗是先用普通請求抓一遍如果HTML長度正常但目標字段是空的或者頁面結(jié)構(gòu)里有明顯的script標簽包含大量數(shù)據(jù)再切換成渲染模式。“小龍蝦”的AI決策層會先做一次預(yù)判減少不必要的瀏覽器啟動。3.3 場景三訪問頻率限制與風(fēng)控提示第三個場景是高頻訪問觸發(fā)限流。我最早測試某個商品比價站時用單線程連續(xù)抓了100個頁面結(jié)果第80個請求開始頁面返回里出現(xiàn)了一段“請完成安全驗證”的提示。這說明觸發(fā)風(fēng)控了。合規(guī)的應(yīng)對措施不是去破解驗證碼而是讓AI自動降速并暫停。我在請求層加了一個“抓取狀態(tài)檢查器”專門分析返回內(nèi)容里是否有驗證碼、滑塊、異常跳轉(zhuǎn)等關(guān)鍵詞。一旦發(fā)現(xiàn)異常立刻停止當前任務(wù)并進入指數(shù)退避模式。import time import random def smart_request(url, session, max_retries3): for attempt in range(max_retries): resp session.get(url, timeout10) content resp.text if 安全驗證 in content or 滑塊 in content: wait_time (2 ** attempt) random.uniform(0, 1) time.sleep(wait_time) continue if resp.status_code 200: return content raise RuntimeError(請求被風(fēng)控攔截已自動停止)指數(shù)退避的意思是每次重試的等待時間翻倍第一次等2秒第二次等4秒第三次等8秒。這樣既不會立刻激怒風(fēng)控系統(tǒng)也給對方留出恢復(fù)時間。如果重試三次仍然被攔就直接放棄這個數(shù)據(jù)源不硬剛。實操心得在“小龍蝦”里我還會讓AI記錄每個站點的“健康分”。抓取成功一次加一分觸發(fā)風(fēng)控一次扣十分。分數(shù)低于閾值后AI會主動向用戶報告“該站點暫時不適合抓取建議改用官方API”。這個機制讓我避免了很多無謂的封禁。4. 反爬對抗的邊界技術(shù)可行不等于可以亂來4.1 Robots協(xié)議與法律底線標題里寫了“突破反爬”但這個“突破”必須是合規(guī)前提下的技術(shù)突破而不是鉆法律空子。每個正規(guī)站點都會在robots.txt里聲明哪些路徑允許抓取、哪些不允許。開發(fā)者在寫爬蟲之前第一件事應(yīng)該是查看目標站點的robots.txt。我遇到過有開發(fā)者直接抓取某平臺需要登錄才能看到的數(shù)據(jù)還沾沾自喜地覺得“繞過了權(quán)限校驗”。這種做法非常危險已經(jīng)不只是反爬問題而是越權(quán)訪問。即使技術(shù)上能做到一旦被追責個人很難承擔后果。不要試圖繞過驗證碼、不要破解登錄態(tài)、不要對非公開數(shù)據(jù)強行抓取。這是我在團隊里反復(fù)強調(diào)的三條紅線?!靶↓埼r”的AI決策層里也內(nèi)置了這條規(guī)則當頁面出現(xiàn)登錄墻或驗證碼時默認判定為“不可抓”并提示用戶尋找授權(quán)數(shù)據(jù)源。4.2 優(yōu)先用官方API和公開數(shù)據(jù)集很多時候我們想抓的數(shù)據(jù)其實對方已經(jīng)提供了更穩(wěn)定、更合法的獲取途徑。比如很多社交平臺、天氣服務(wù)、地圖服務(wù)都有官方API雖然可能限頻或者收費但數(shù)據(jù)質(zhì)量和穩(wěn)定性遠高于爬蟲。我在設(shè)計“小龍蝦”時把“尋找官方API”作為一個優(yōu)先動作。AI會先根據(jù)用戶描述判斷數(shù)據(jù)所屬領(lǐng)域然后自動搜索有沒有官方接口。只有確認沒有API或者API權(quán)限不能滿足需求時才考慮頁面抓取。這樣做還有一個額外好處維護成本低。頁面結(jié)構(gòu)隨時會變但API一般相對穩(wěn)定。如果你做的是長期數(shù)據(jù)采集項目用API幾乎是唯一的可持續(xù)方案。4.3 把“合規(guī)判斷”寫進AI腳本既然AI已經(jīng)參與到爬蟲決策里那合規(guī)判斷也應(yīng)該交給它。我做了一個簡單的規(guī)則引擎放在AI決策層旁邊。規(guī)則包括檢查robots.txt、檢查頁面是否需要登錄、檢查抓取頻率是否合理、檢查數(shù)據(jù)是否涉及個人隱私。這套規(guī)則引擎會在每次抓取任務(wù)開始前執(zhí)行只要有一項不符合就中止任務(wù)并向用戶說明原因。比如用戶要求抓取某論壇的用戶私人主頁AI會直接拒絕因為那屬于個人隱私數(shù)據(jù)。“小龍蝦”不再只是一個能抓數(shù)據(jù)的工具它更像一個有邊界意識的數(shù)據(jù)助手。偷懶的辦法是只把規(guī)則寫進系統(tǒng)提示詞里讓大模型“自覺”。但我個人建議做成獨立的規(guī)則模塊因為提示詞可以被用戶改寫規(guī)則卻始終獨立存在能夠更可靠地守住底線。5. 常見問題與排查技巧實錄5.1 AI生成的選擇器為什么老失效我在實際使用中遇到最頻繁的問題就是AI生成的CSS選擇器明明在某一時刻有效過幾天再運行就失效了。原因很簡單頁面結(jié)構(gòu)變了。排查時我先登錄瀏覽器開發(fā)者工具手動查看目標元素的當前class和id。然后對比“小龍蝦”上次生成的選擇器看看差異在哪。通常有兩種情況一種是class名帶上了隨機后綴比如“l(fā)ist-item-2398”變成“l(fā)ist-item-7812”另一種是整塊DOM結(jié)構(gòu)移動了層級。解決辦法是讓AI生成“語義更穩(wěn)”的選擇器。不要用純class名而是優(yōu)先用文本內(nèi)容或者結(jié)構(gòu)化屬性。比如// 不穩(wěn).list-item-2398 // 更穩(wěn)[data-typeproduct] .title另外我還會在提取規(guī)則里加一個“自檢步驟”。抓取結(jié)果為空時AI會自動對比頁面里是否有相似結(jié)構(gòu)并嘗試重新生成選擇器。這樣即使頁面改版系統(tǒng)也能在無人干預(yù)的情況下自我修復(fù)。5.2 抓下來的數(shù)據(jù)亂碼或字段錯位亂碼問題多出在編碼識別上。很多網(wǎng)頁沒有聲明charset或者聲明與實際編碼不一致。我用requests時直接使用resp.text可能會得到亂碼因為requests會猜編碼。我的習(xí)慣是用resp.content拿到原始字節(jié)再通過代碼或頁面meta標簽判斷編碼然后手動解碼。import chardet def decode_response(resp): raw resp.content detected chardet.detect(raw[:5000]) encoding detected.get(encoding, utf-8) return raw.decode(encoding, errorsreplace)字段錯位則通常是選擇器匹配到了錯誤元素。比如想抓價格結(jié)果把“原價”和“折扣價”混在一起。這個時候不要只給AI一段HTML而是把目標字段的上下文也喂給它讓它理解哪些是有效數(shù)據(jù)。5.3 請求被風(fēng)控后的恢復(fù)步驟一旦確認被風(fēng)控很多人會焦慮地不停重試結(jié)果越試越黑。正確的恢復(fù)步驟是先完全停止任務(wù)讓IP“冷靜”一段時間然后檢查抓取日志確認觸發(fā)風(fēng)控的原因。在我的項目里“小龍蝦”被風(fēng)控時會自動切斷該站點任務(wù)轉(zhuǎn)去抓其他數(shù)據(jù)源然后等待至少30分鐘再試探性請求一次。如果試探請求返回正常才恢復(fù)抓取但會把原來的并發(fā)數(shù)減半抓取頻率也降到原來的一半。如果你同時抓取了多個站點建議給不同站點設(shè)置獨立的限速配置。有的站點比較寬容每秒10次都沒事有的站點比較敏感每秒2次都會被警告。把這些配置做成可調(diào)節(jié)參數(shù)比寫死在代碼里要靈活得多。5.4 快速排查速查表下面這張表是我在實際排障過程中整理出來的按“現(xiàn)象-原因-解法”的順序羅列希望對你有幫助?,F(xiàn)象可能原因合規(guī)解法返回403UA被校驗攔截設(shè)置真實瀏覽器UA補全請求頭頁面無數(shù)據(jù)動態(tài)渲染未執(zhí)行換用瀏覽器自動化工具渲染第N次請求被攔觸發(fā)頻率限制降低頻率啟用指數(shù)退避重試出現(xiàn)驗證碼行為或頻率異常立即停止等待窗口期提取字段為空當前選擇器失效讓AI重新分析HTML生成新規(guī)則中文亂碼編碼識別錯誤使用chardet檢測編碼后手動解碼登錄墻數(shù)據(jù)非公開放棄抓取尋找API或授權(quán)最后再分享一個小技巧爬蟲技能不是越大越好而是要能進能退。你給AI加的能力越多它跑偏的概率也越大。我在“小龍蝦”上線第二周的時候發(fā)現(xiàn)它會自作主張去抓一些明顯不該碰的節(jié)點后來加上“合規(guī)規(guī)則引擎”才把問題控制住?,F(xiàn)在回頭看真正的“數(shù)據(jù)高手”不是能繞過多少限制而是知道該在哪兒停下。