:從requests到Selenium的完整數(shù)據(jù)采集方案)
簡介面向需要撰寫網(wǎng)絡爬蟲方向畢業(yè)設(shè)計或課題開題報告的學生與研究者這份基于Python的網(wǎng)絡爬蟲開題報告PDF提供了完整可參考的框架與內(nèi)容。報告系統(tǒng)梳理了國內(nèi)外動態(tài)網(wǎng)頁抓取、聚焦爬蟲、驗證碼識別等研究現(xiàn)狀并結(jié)合課題任務給出了可行性分析能幫助讀者快速把握開題報告的關(guān)鍵模塊。其中重點討論了反爬策略、登錄驗證、驗證碼處理、數(shù)據(jù)庫優(yōu)化等技術(shù)難點及解決思路同時列出開發(fā)所需的Windows環(huán)境、Firefox調(diào)試組件、Elasticsearch、MySQL與Python環(huán)境等工作條件具體詳實。資源包為1個PDF文件大小僅59KB便于下載閱讀目前已有816人學習適合正在籌備爬蟲類開題或需要技術(shù)方案參考的讀者。1. 動態(tài)網(wǎng)頁與聚焦爬蟲為什么 requests 拿不到數(shù)據(jù)某個下午我對著一個看似普通的資訊站發(fā)起 requests 請求返回的 HTML 里卻找不到任何一條新聞標題——所有內(nèi)容都藏在 JS 異步加載的動態(tài) DOM 里。這是當前互聯(lián)網(wǎng)環(huán)境的常態(tài)也是爬蟲開發(fā)中最常見的翻車現(xiàn)場。所謂網(wǎng)絡爬蟲本質(zhì)是一段按照既定規(guī)則自動抓取網(wǎng)頁數(shù)據(jù)并構(gòu)建索引的程序它解決了手動復制粘貼效率極低、信息面過窄的問題。真正讓爬蟲開發(fā)變得復雜的是三個現(xiàn)實障礙動態(tài)網(wǎng)頁內(nèi)容不可見、登錄驗證攔截、驗證碼識別困難。許多搜索不到的頁面并非不存在而是需要經(jīng)過瀏覽器渲染、登錄授權(quán)甚至輸入驗證碼才能看到。Python 之所以成為爬蟲開發(fā)的首選語言是因為它的語法簡潔且擁有 requests、BeautifulSoup、Scrapy、Selenium 等成熟的庫覆蓋了從請求發(fā)送到數(shù)據(jù)解析的全鏈路。而傳統(tǒng)通用搜索引擎在面對用戶特定領(lǐng)域查詢時往往返回大量無關(guān)結(jié)果于是聚焦爬蟲成為研究熱點——它只抓取與某個主題相關(guān)的頁面并通過數(shù)據(jù)清洗、去重、入庫和可視化把分析結(jié)果反饋給用戶。本文要拆解的就是這樣一個基于 Python 的聚焦爬蟲系統(tǒng)從選型到落地的完整過程。2. 爬蟲選型requests、Scrapy、Selenium 的分工與落地2.1 三類抓取工具的適用邊界開發(fā)爬蟲的第一步不是寫代碼而是判斷目標站點屬于哪種類型。靜態(tài)頁面直接返回 HTML動態(tài)頁面則需要執(zhí)行 JavaScript 后才能看到數(shù)據(jù)這對工具選型影響極大。我在實際項目里一般按下面的邊界做決策工具適用場景優(yōu)點主要限制requests BeautifulSoup靜態(tài)頁面、接口直出 JSON輕量、可控性強、速度最快無法執(zhí)行 JS動態(tài)頁面拿不到渲染后內(nèi)容Scrapy大規(guī)模分布式抓取、結(jié)構(gòu)清晰的中大型項目自帶調(diào)度器、去重、管道擴展性好學習曲線陡動態(tài)渲染仍需配合其他組件Selenium動態(tài)頁面、需要模擬用戶操作、復雜登錄流程直接驅(qū)動真實瀏覽器所見即所得資源占用高速度慢容易被檢測這三個工具并不是互斥關(guān)系。一個成熟的聚焦爬蟲往往是requests 負責接口請求Selenium 兜底渲染的組合。先嘗試用 requests 直接請求頁面地址或分析出來的 Ajax 接口拿不到再降級到 Selenium這樣做的好處是大部分數(shù)據(jù)可以通過輕量請求完成采集只有少數(shù)核心頁面需要付出渲染成本。2.2 動態(tài)內(nèi)容渲染抓取的最小可行方案當目標站點的內(nèi)容通過 AJAX 異步加載時直接用 requests 拿到的 HTML 里只有空殼。我一般用 Selenium 驅(qū)動 Firefox 來執(zhí)行渲染關(guān)鍵代碼長這樣from selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC opts Options() opts.add_argument(--headless) # 無頭模式不彈出瀏覽器窗口 driver webdriver.Firefox(optionsopts) driver.get(https://example.com/topic/1) # 等待某個關(guān)鍵節(jié)點出現(xiàn)而不是盲目 sleep WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, div.news-list)) ) items driver.find_elements(By.CSS_SELECTOR, div.news-list a) for it in items: print(it.text, it.get_attribute(href)) driver.quit()這段代碼的要點在于顯式等待而不是time.sleep(3)。確定性的頁面交互應該等待元素出現(xiàn)后再繼續(xù)固定休眠只會讓程序要么等不夠、要么白白浪費時間。對 5 年以上從業(yè)者來說這里真正值得關(guān)注的是無頭模式的選擇——部分站點會檢測navigator.webdriver屬性遇到這種情況可以改用真實瀏覽器模式配合手動介入或者對 WebDriver 特征做修正但這需要根據(jù)目標站點的檢測強度單獨評估。2.3 聚焦爬蟲的主題判定邏輯聚焦爬蟲和通用爬蟲最大的區(qū)別在于取舍。通用爬蟲什么都抓聚焦爬蟲只抓與主題相關(guān)的頁面。判定邏輯通常包含兩個層面URL 層面的粗過濾和內(nèi)容層面的相關(guān)度打分。粗過濾通過正則或關(guān)鍵詞列表剔除明顯無關(guān)的鏈接內(nèi)容打分則對標題和正文中的關(guān)鍵詞進行加權(quán)統(tǒng)計。import re from collections import Counter TOPIC_WORDS {python, 爬蟲, 數(shù)據(jù)采集, selenium, scrapy} def url_allowed(url: str) - bool: # 剔除靜態(tài)資源和外部鏈接只保留站內(nèi)資訊頁 if re.search(r\.(jpg|png|css|js|pdf)$, url): return False return /news/ in url or /article/ in url def topic_score(title: str, text: str) - float: words re.findall(r[\u4e00-\u9fa5a-zA-Z], title text) counter Counter(w.lower() for w in words if len(w) 1) score sum(counter.get(w, 0) * 2 for w in TOPIC_WORDS if w in counter) return score這里的topic_score函數(shù)對標題命中的關(guān)鍵詞給予雙倍權(quán)重避免正文中偶然出現(xiàn)的詞干擾判定。實際部署時可以設(shè)定一個閾值比如分數(shù)大于 3 才入庫這樣能顯著減少無關(guān)數(shù)據(jù)。注意 URL 過濾一定要在所有請求之前執(zhí)行它是最便宜的剪枝手段能節(jié)省大量無效請求。3. 反爬對抗請求頭、代理輪換與模擬登錄實戰(zhàn)3.1 請求頭與訪問間隔的正確配置大部分站點最早期的防護就是校驗User-Agent和Referer。很多新手只換了 UA 就被識別原因在于請求特征不一致——瀏覽器會發(fā)送Accept-Language、Accept-Encoding、Connection等十余個字段而 requests 默認只帶少數(shù)幾個。常見做法是準備一個 UA 池在每次請求時隨機選擇并設(shè)置合理的間隔時間。import random import time import requests UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15, ] def make_session(): sess requests.Session() sess.headers.update({ User-Agent: random.choice(UA_POOL), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) return sess def fetch_with_retry(sess, url, retries3): for i in range(retries): try: resp sess.get(url, timeout8) if resp.status_code 200: return resp.text except requests.RequestException: pass time.sleep(random.uniform(2, 4)) # 隨機間隔避免規(guī)律性 return None間隔時間的設(shè)置有個原則均勻固定的間隔反而比隨機間隔更容易被識別。用random.uniform(2, 4)讓請求時間在 2 到 4 秒之間抖動模擬真人閱讀節(jié)奏。對規(guī)模較大的抓取任務還可以引入自適應限速——發(fā)現(xiàn)頻繁返回 403 時自動把間隔翻倍恢復 200 后再逐漸縮短。3.2 代理 IP 的輪換策略當同一個 IP 在短時間內(nèi)發(fā)起大量請求時站點會觸發(fā)頻率限制。代理 IP 是解決這個問題的通用手段但代理并非設(shè)置得越多越好。我一般維護一個可用代理列表每次請求前先發(fā)一個探測請求驗證代理可用性再用它抓取目標頁面。PROXY_LIST [ http://proxy1.example.com:8080, http://proxy2.example.com:8080, ] def get_proxy(): # 簡單輪換生產(chǎn)環(huán)境建議用 redis 維護代理池并定期剔除失效項 return random.choice(PROXY_LIST) def fetch_via_proxy(url, headers): proxy get_proxy() try: resp requests.get(url, headersheaders, proxies{http: proxy, https: proxy}, timeout8) if resp.status_code 200: return resp.text except requests.RequestException: # 代理失效則換下一個注意記錄失敗次數(shù) return None需要提醒的是代理 IP 的質(zhì)量直接決定抓取成功率。免費代理大多短命高匿代理和透明代理的區(qū)別在于是否向目標服務器暴露真實 IP。對數(shù)據(jù)量要求高的項目我傾向于采購穩(wěn)定代理并在本地做延遲排行而不是盲目依賴免費資源。另外所有帶認證的代理要在 URL 中顯式帶上用戶名密碼格式為http://user:passhost:port。3.3 模擬登錄與驗證碼處理流程需要登錄的站點比匿名訪問多一層防護。模擬登錄的核心是復現(xiàn)瀏覽器在登錄時的請求參數(shù)。我在第一次登錄時一定會打開瀏覽器的開發(fā)者工具勾選 Preserve log 選項然后輸入賬號密碼點擊登錄查看網(wǎng)絡面板中l(wèi)ogin或signin相關(guān)的 POST 請求參數(shù)。絕大多數(shù)站點使用表單提交參數(shù)無非是用戶名、密碼、CSRF token 和驗證碼字段。import requests sess requests.Session() # 先 GET 登錄頁提取 CSRF token login_page sess.get(https://example.com/login, timeout8) csrf_token extract_csrf(login_page.text) # 從 form 隱藏域中解析 login_data { username: your_account, password: your_password, csrf_token: csrf_token, captcha: input(請查看本地驗證碼圖片并輸入: ), # 人工介入 remember: 1, } resp sess.post(https://example.com/login, datalogin_data, allow_redirectsTrue, timeout8) print(登錄后頁面:, resp.url) # 后續(xù)請求直接用 sesscookie 會自動攜帶驗證碼是整個流程中最不確定的環(huán)節(jié)。我的經(jīng)驗是第一版先用人工輸入落地跑通確認登錄鏈路沒有其他問題后再評估是否接入打碼平臺。人工打碼每次需要中斷程序等待輸入只適合低頻使用高頻場景下考慮接入第三方打碼接口或訓練簡單的 OCR 模型。注意保持Session對象的復用登錄后的 Cookie 和會話狀態(tài)都綁定在這個對象上每次新建 Session 就意味著重新登錄。4. 數(shù)據(jù)管線去重、垂直水平分表與 Elasticsearch 搜索優(yōu)化4.1 數(shù)據(jù)清洗與去重策略抓取回來的原始數(shù)據(jù)不能直接入庫里面充斥著廣告位文本、重復頁面、亂碼和無關(guān)鏈接。清洗階段要做三件事HTML 標簽剝離、空白符規(guī)范化、字段裁剪。去重則要分兩個維度——URL 去重和內(nèi)容指紋去重URL 相同不代表內(nèi)容相同但內(nèi)容相同 URL 一定不同時存在。import hashlib import re from bs4 import BeautifulSoup def clean_html(raw_html: str) - str: soup BeautifulSoup(raw_html, html.parser) for tag in soup([script, style, nav, footer]): tag.decompose() text soup.get_text(separator ) return re.sub(r\s, , text).strip() def content_fingerprint(text: str) - str: # 對標題 正文前 200 字取 SHA1用于內(nèi)容級去重 sample text[:200] return hashlib.sha1(sample.encode(utf-8)).hexdigest()這里的內(nèi)容指紋用文本前 200 個字符計算原因是不同頁面如果正文轉(zhuǎn)載自同一來源前幾段往往完全相同。如果對全文做哈希任何微小的版權(quán)聲明差異都會導致判重失敗。實際應用中還要維護一個已見指紋的集合判斷是否撞庫時直接查這個集合即可。4.2 表結(jié)構(gòu)設(shè)計與存儲引擎選擇數(shù)據(jù)庫性能受表結(jié)構(gòu)設(shè)計的影響極大字段數(shù)量、類型選擇和索引策略決定了查詢效率。垂直分表是把一張寬表按字段訪問頻率拆成多張窄表高頻字段放主表低頻大字段放擴展表水平分表則是把數(shù)據(jù)量大的表按時間或 ID 范圍拆分到多張結(jié)構(gòu)相同的表中。-- 垂直分表文章主表存儲高頻訪問字段 CREATE TABLE article_main ( id BIGINT PRIMARY KEY AUTO_INCREMENT, url_md5 CHAR(32) NOT NULL, title VARCHAR(255) NOT NULL, publish_time DATETIME NOT NULL, INDEX idx_pub_time (publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 擴展表存儲正文等低頻大字段 CREATE TABLE article_content ( id BIGINT PRIMARY KEY, content MEDIUMTEXT NOT NULL, fingerprint CHAR(40) NOT NULL, FOREIGN KEY (id) REFERENCES article_main(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;存儲引擎的選擇上InnoDB 支持事務和行級鎖適合寫入頻繁且需要數(shù)據(jù)一致性的爬蟲場景MyISAM 只適合純讀且對崩潰恢復不敏感的應用爬蟲數(shù)據(jù)通常錄入后即只讀某些分析場景下 MyISAM 的壓縮表反而節(jié)省空間。我一般統(tǒng)一用 InnoDB因為爬蟲進程可能在寫入中途崩潰事務回滾能避免半截數(shù)據(jù)落庫。4.3 連接池與異步寫入爬蟲是典型的 IO 密集應用把每條數(shù)據(jù)單次寫入數(shù)據(jù)庫會頻繁建立連接性能損耗極大。連接池解決的是連接復用問題異步寫入則把爬取和落庫解耦。DBUtils 是 Python 生態(tài)里常用的連接池方案配合生產(chǎn)者消費者模式可以顯著提升吞吐。from dbutils.pooled_db import PooledDB import pymysql POOL PooledDB( creatorpymysql, maxconnections20, mincached5, host127.0.0.1, usercrawler, passwordsecret, databasenews_db, charsetutf8mb4, ) def save_article(article: dict): conn POOL.connection() try: with conn.cursor() as cur: cur.execute( INSERT IGNORE INTO article_main (url_md5, title, publish_time) VALUES (%s, %s, %s), (article[url_md5], article[title], article[publish_time]), ) conn.commit() finally: conn.close() # 歸還連接而不是真正關(guān)閉maxconnections建議設(shè)置為預估并發(fā)數(shù)的兩倍太小會阻塞寫入太大會壓垮數(shù)據(jù)庫。INSERT IGNORE配合 url 的唯一索引可以實現(xiàn)數(shù)據(jù)庫層面的去重省去應用層先查后寫的邏輯。如果數(shù)據(jù)量達到日均百萬條就要考慮批量提交比如把 100 條攢成一個executemany再提交。4.4 Elasticsearch 索引與搜索高亮數(shù)據(jù)入庫 MySQL 后只解決了存儲問題搜索體驗還需要 Elasticsearch 來增強。ES 的倒排索引天然適合做關(guān)鍵詞檢索結(jié)合 IK 分詞插件可以正確處理中文分詞。建立索引后將 MySQL 中已清洗的數(shù)據(jù)同步到 ES即可實現(xiàn)搜索建議和關(guān)鍵字高亮。# 創(chuàng)建索引并指定分詞器 curl -X PUT localhost:9200/news -H Content-Type: application/json -d { settings: { analysis: { analyzer: { default: { type: ik_max_word } } } }, mappings: { properties: { title: { type: text, analyzer: ik_max_word }, content: { type: text, analyzer: ik_max_word } } } }搜索時使用highlight字段讓匹配到的關(guān)鍵詞被包裹在標簽中前端拿到帶標簽的結(jié)果就能渲染紅色高亮字。熱門搜索詞可以額外用significant_terms聚合從大量搜索日志中提取再配合定時任務刷新就能實現(xiàn)展示熱門搜索的功能。這里需要留意 ES 的版本差異7.x 之后去掉了 type 概念上面的 mapping 結(jié)構(gòu)在 6.x 之前需要額外指定doc類型遷移時容易踩坑。5. 調(diào)試與驗證Firebug 定位、抓包分析與反爬觸發(fā)自查5.1 用 Firebug 和 FirePath 快速定位元素路徑早期在 Firefox 上做爬蟲調(diào)試時Firebug 加 FirePath 是我最常用的組合。Firebug 負責查看網(wǎng)絡請求和 DOM 結(jié)構(gòu)FirePath 則能直接輸入 XPath 表達式并高亮匹配的網(wǎng)頁元素。雖然這兩個組件后來被 Firefox 原生的開發(fā)者工具取代但定位思路一脈相承先找到目標元素的唯一標識再驗證 XPath 或 CSS 選擇器的準確性。使用 Selenium 時我習慣先在 FirePath 里測試 XPath 的匹配數(shù)量確認只在預期位置命中再寫進代碼。這個習慣幫我省掉了大量運行時找不到元素的調(diào)試時間。5.2 登錄參數(shù)抓包分析的完整流程模擬登錄失敗時最有效的排查方式是回到抓包工具里核對請求參數(shù)。打開瀏覽器的網(wǎng)絡面板勾選 Preserve log重新走一遍登錄流程找到狀態(tài)碼為 200 或 302 的 POST 請求逐個字段對照代碼里的login_data。最常見的錯誤是遺漏了某個隱藏字段——比如_token、_t這種帶時間戳的參數(shù)或者login_from這類固定值字段。我的做法是用抓包結(jié)果直接導出為 curl 命令再轉(zhuǎn)換為 requests 代碼保證參數(shù)完整性。另外注意部分站點登錄后會下發(fā)新的 Cookie 并重定向Python 的 requests 默認會自動處理重定向但需要確認最終停留在的是登錄成功頁而不是錯誤頁。5.3 反爬觸發(fā)的自查清單爬蟲寫完并不代表可以穩(wěn)定運行上線前建議按以下清單逐項驗證第一連續(xù)請求 50 次頁面觀察是否出現(xiàn) 403、418 等狀態(tài)碼出現(xiàn)則說明 IP 已被注意優(yōu)先檢查 UA 和間隔第二用 Selenium 打開目標站點人工核對頁面是否正常排除網(wǎng)站反爬策略變化導致的誤判第三檢查 MySQL 中是否有大量重復數(shù)據(jù)這往往是去重邏輯失效而非反爬問題第四觀察 ES 搜索返回的響應時間超過 500 毫秒就需要檢查索引分片數(shù)和查詢語句是否合理。驗證完成后用crontab設(shè)置定時任務并配合日志記錄每個任務的抓取數(shù)量和失敗率出現(xiàn)異常時能第一時間定位到具體環(huán)節(jié)。關(guān)于抓取頻率始終記住一個原則在目標站點的可承受范圍內(nèi)獲取數(shù)據(jù)這是爬蟲程序能長期穩(wěn)定運行的前提。本文還有配套的精品資源點擊獲取