習(xí)框架:從urllib到Scrapy的完整路線與避坑指南)
簡(jiǎn)介這份PDF面向零基礎(chǔ)到進(jìn)階的Python爬蟲學(xué)習(xí)者系統(tǒng)梳理了從入門到實(shí)戰(zhàn)的完整學(xué)習(xí)框架幫助初學(xué)者解決「學(xué)什么、怎么學(xué)、在哪里學(xué)」的困惑。內(nèi)容圍繞爬蟲三部曲展開獲取數(shù)據(jù)環(huán)節(jié)講解urllib、requests、aiohttp等請(qǐng)求庫(kù)及異步抓取思路解析數(shù)據(jù)環(huán)節(jié)覆蓋BeautifulSoup、pyquery、XPath與正則表達(dá)式的定位技巧存儲(chǔ)數(shù)據(jù)環(huán)節(jié)則涉及TXT、JSON、CSV及MySQL、MongoDB等方案并延伸至代理IP、請(qǐng)求頭偽裝、Selenium動(dòng)態(tài)渲染與分布式爬蟲等進(jìn)階話題同時(shí)提醒Robots協(xié)議與合規(guī)邊界。資源為單個(gè)PDF文件壓縮包約2.1MB輕量便攜適合隨時(shí)查閱。目前已有174人學(xué)習(xí)可作為爬蟲知識(shí)體系搭建與查漏補(bǔ)缺的參考框架。1. 從一份學(xué)習(xí)框架說(shuō)起為什么有人兩周上手有人卡在環(huán)境配置很多人第一次接觸 Python 爬蟲都是從收藏一份「完整學(xué)習(xí)框架」開始的。這份《2023年最新Python爬蟲的完整學(xué)習(xí)框架.pdf》就是典型代表——它不是某個(gè)具體項(xiàng)目的源碼包而是一張把「請(qǐng)求庫(kù)、解析庫(kù)、存儲(chǔ)方案、工程化框架」串起來(lái)的學(xué)習(xí)路線圖。我見(jiàn)過(guò)太多人把這類 PDF 存進(jìn)網(wǎng)盤吃灰也見(jiàn)過(guò)有人照著框架兩周就能穩(wěn)定抓取主流站點(diǎn)數(shù)據(jù)。差別不在天賦在于有沒(méi)有把框架里的每個(gè)節(jié)點(diǎn)落到可運(yùn)行的代碼上。這份資料解決的核心問(wèn)題是告訴你學(xué)什么、按什么順序?qū)W、每個(gè)階段用什么庫(kù)。它把爬蟲拆成「獲取數(shù)據(jù) → 解析數(shù)據(jù) → 存儲(chǔ)數(shù)據(jù)」三部曲再往上延伸到 Scrapy、PySpider、Nutch 這類工程化框架。適合兩類人一是剛學(xué)完 Python 基礎(chǔ)語(yǔ)法、想找個(gè)明確路徑的初學(xué)者二是寫過(guò)零散腳本、但沒(méi)形成體系、遇到反爬就翻車的半熟手。如果你指望它是一份「復(fù)制粘貼就能跑」的代碼合集那會(huì)失望但如果你想少走彎路、知道每個(gè)階段該翻哪份文檔這份框架的參考價(jià)值很高。2. 請(qǐng)求層選型urllib、requests、aiohttp 到底先學(xué)哪個(gè)2.1 從標(biāo)準(zhǔn)庫(kù) urllib 起步的理由框架里明確建議先從 Python 自帶的 urllib 開始這個(gè)順序是有道理的。urllib 是內(nèi)置的 HTTP 請(qǐng)求庫(kù)不需要額外安裝能讓你直接看到請(qǐng)求構(gòu)造的底層細(xì)節(jié)——URL 編碼、請(qǐng)求頭設(shè)置、超時(shí)處理、異常捕獲。很多人一上來(lái)就用 requests寫起來(lái)確實(shí)爽但遇到編碼問(wèn)題、重定向異常時(shí)完全不知道底層發(fā)生了什么。用 urllib 發(fā)一個(gè)最基礎(chǔ)的 GET 請(qǐng)求代碼長(zhǎng)這樣import urllib.request import urllib.parse # 構(gòu)造請(qǐng)求頭模擬瀏覽器 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 } # 目標(biāo) URL帶查詢參數(shù) params {q: python爬蟲, page: 1} url https://example.com/search? urllib.parse.urlencode(params) # 構(gòu)造 Request 對(duì)象并發(fā)送 req urllib.request.Request(url, headersheaders) try: with urllib.request.urlopen(req, timeout10) as resp: html resp.read().decode(utf-8) print(html[:500]) except urllib.error.HTTPError as e: print(fHTTP錯(cuò)誤: {e.code}) except urllib.error.URLError as e: print(fURL錯(cuò)誤: {e.reason})這段代碼的關(guān)鍵點(diǎn)有三個(gè)urlencode負(fù)責(zé)把字典轉(zhuǎn)成合法的查詢字符串避免手動(dòng)拼接出錯(cuò)Request對(duì)象承載請(qǐng)求頭User-Agent是最基礎(chǔ)的偽裝timeout參數(shù)必須設(shè)否則遇到不響應(yīng)的服務(wù)器會(huì)一直掛著。參數(shù)方面timeout10是秒數(shù)一般設(shè) 5 到 15 之間太短容易誤判超時(shí)太長(zhǎng)會(huì)拖慢整體節(jié)奏。2.2 requests 的簡(jiǎn)潔與 aiohttp 的異步提速當(dāng)你用 urllib 寫過(guò)十幾個(gè)請(qǐng)求之后切到 requests 會(huì)明顯感覺(jué)到效率提升。requests 自帶 JSON 解析、會(huì)話保持、編碼自動(dòng)識(shí)別同樣的請(qǐng)求三行就能搞定import requests session requests.Session() session.headers.update({ 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 }) resp session.get(https://example.com/api/list, params{page: 1}, timeout10) data resp.json() # 自動(dòng)解析 JSON print(data.get(items, [])[:3])Session對(duì)象的好處是復(fù)用 TCP 連接和請(qǐng)求頭批量請(qǐng)求時(shí)比每次新建連接快不少。resp.json()直接返回字典省去手動(dòng)json.loads。但同步請(qǐng)求的天花板很明顯。框架里提到一個(gè)數(shù)據(jù)請(qǐng)求同一個(gè)頁(yè)面 100 次同步方式最少 30 秒異步方式只需 3 秒左右。這個(gè)差距在批量抓取時(shí)就是能不能跑完的區(qū)別。aiohttp 配合async/await寫法能把并發(fā)請(qǐng)求壓到很低的時(shí)間成本import aiohttp import asyncio async def fetch(session, url): async with session.get(url, timeoutaiohttp.ClientTimeout(total10)) as resp: return await resp.text() async def main(urls): async with aiohttp.ClientSession() as session: tasks [fetch(session, u) for u in urls] results await asyncio.gather(*tasks) return results urls [fhttps://example.com/page/{i} for i in range(1, 21)] htmls asyncio.run(main(urls)) print(f抓取完成共 {len(htmls)} 頁(yè))asyncio.gather把 20 個(gè)請(qǐng)求同時(shí)發(fā)出去總耗時(shí)接近最慢的那一個(gè)而不是累加。注意ClientTimeout要顯式設(shè)置aiohttp 默認(rèn)超時(shí)是 5 分鐘不設(shè)的話一個(gè)卡住的請(qǐng)求會(huì)拖死整批任務(wù)。常見(jiàn)做法是把并發(fā)數(shù)控制在 10 到 50 之間太高容易被目標(biāo)站點(diǎn)限流。提示異步爬蟲的調(diào)試比同步麻煩異常堆棧不直觀。建議先用同步版本跑通邏輯再改寫成異步。3. 解析層實(shí)戰(zhàn)BeautifulSoup、lxml、pyquery 與正則的邊界3.1 四種解析方式的適用場(chǎng)景拿到 HTML 之后下一步是從中提取目標(biāo)數(shù)據(jù)??蚣芾锪辛怂姆N主流方案BeautifulSoup、pyquery、XPath配合 lxml、正則表達(dá)式。它們不是互相替代的關(guān)系而是各有各的舒適區(qū)。BeautifulSoup 的優(yōu)勢(shì)是容錯(cuò)性強(qiáng)遇到標(biāo)簽不閉合、屬性缺失的臟 HTML 也能解析而且自動(dòng)處理編碼轉(zhuǎn)換。缺點(diǎn)是速度偏慢大文檔解析時(shí)明顯。pyquery 用 jQuery 風(fēng)格的 CSS 選擇器前端基礎(chǔ)好的人上手極快底層用 lxml 解析器速度比 BeautifulSoup 快。XPath 是功能最全的支持超過(guò) 100 個(gè)內(nèi)置函數(shù)能做字符串處理、數(shù)值比較、節(jié)點(diǎn)定位適合結(jié)構(gòu)復(fù)雜的頁(yè)面。正則表達(dá)式速度最快但寫起來(lái)最費(fèi)勁頁(yè)面結(jié)構(gòu)一變就得重寫。我一般會(huì)這樣選頁(yè)面結(jié)構(gòu)規(guī)整、需要快速出結(jié)果用 pyquery頁(yè)面臟亂、標(biāo)簽不規(guī)范用 BeautifulSoup需要按文本內(nèi)容、屬性值做復(fù)雜篩選用 XPath只需要提取幾個(gè)固定格式的字符串比如電話號(hào)碼、日期用正則。3.2 用 pyquery 提取列表數(shù)據(jù)的完整示例假設(shè)要抓一個(gè)商品列表頁(yè)每個(gè)商品有標(biāo)題、價(jià)格、鏈接。用 pyquery 的寫法from pyquery import PyQuery as pq import requests html requests.get(https://example.com/products, timeout10).text doc pq(html) items [] for li in doc(.product-list .item).items(): title li.find(.title).text().strip() price li.find(.price).attr(data-price) # 從屬性取價(jià)格 link li.find(a).attr(href) if title and price: items.append({title: title, price: price, link: link}) print(f共提取 {len(items)} 條) for it in items[:3]: print(it)doc(.product-list .item)是 CSS 選擇器層級(jí)關(guān)系用空格表示。.items()返回可迭代對(duì)象逐個(gè)處理。.text()取文本內(nèi)容.attr()取屬性值。這里有個(gè)細(xì)節(jié)價(jià)格放在>from lxml import etree html requests.get(https://example.com/products, timeout10).text tree etree.HTML(html) # XPath篩選>import csv data [ {title: 商品A, price: 199, link: https://example.com/a}, {title: 商品B, price: 299, link: https://example.com/b}, ] with open(products.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, price, link]) writer.writeheader() writer.writerows(data)encodingutf-8-sig是為了讓 Excel 打開時(shí)不亂碼這個(gè)坑我踩過(guò)好幾次。newline防止 Windows 下多出空行。DictWriter按字典鍵寫入字段順序由fieldnames控制。4.2 MongoDB 存非結(jié)構(gòu)化數(shù)據(jù)的優(yōu)勢(shì)當(dāng)你要存的是評(píng)論、文章正文、圖片鏈接這類長(zhǎng)度不固定、結(jié)構(gòu)不統(tǒng)一的數(shù)據(jù)時(shí)MongoDB 比 MySQL 更合適。它不需要預(yù)先定義表結(jié)構(gòu)直接存 JSON 文檔。用 PyMongo 操作from pymongo import MongoClient, errors client MongoClient(mongodb://localhost:27017/, serverSelectionTimeoutMS5000) db client[spider_db] col db[products] # 插入前先建唯一索引防止重復(fù)抓取 col.create_index(link, uniqueTrue) docs [ {title: 商品A, price: 199, link: https://example.com/a, tags: [數(shù)碼, 熱銷]}, {title: 商品B, price: 299, link: https://example.com/b, tags: [家居]}, ] for doc in docs: try: col.update_one({link: doc[link]}, {$set: doc}, upsertTrue) except errors.PyMongoError as e: print(f寫入失敗: {e}) print(f當(dāng)前集合文檔數(shù): {col.count_documents({})})update_one配合upsertTrue是冪等寫入的關(guān)鍵——同一個(gè)鏈接重復(fù)抓取時(shí)更新而不是重復(fù)插入。create_index(link, uniqueTrue)在數(shù)據(jù)庫(kù)層面兜底即使代碼邏輯有漏洞也不會(huì)產(chǎn)生重復(fù)數(shù)據(jù)。serverSelectionTimeoutMS設(shè)短一點(diǎn)連不上時(shí)快速失敗而不是干等。Redis 在框架里被定位為分布式爬蟲的數(shù)據(jù)存儲(chǔ)主要用來(lái)做請(qǐng)求隊(duì)列去重和緩存。單機(jī)爬蟲用不上但如果你要擴(kuò)展到多臺(tái)機(jī)器協(xié)同抓取Redis 的 set 結(jié)構(gòu)做 URL 去重非常高效。5. 避坑與排查請(qǐng)求被拒、解析為空、數(shù)據(jù)重復(fù)的常見(jiàn)原因5.1 返回 403 或驗(yàn)證碼頁(yè)面現(xiàn)象請(qǐng)求返回 403 狀態(tài)碼或者拿到的是一個(gè)驗(yàn)證碼頁(yè)面而不是目標(biāo)內(nèi)容。原因最常見(jiàn)的是請(qǐng)求頭缺少User-Agent或者User-Agent用了默認(rèn)的 Python-urllib 標(biāo)識(shí)。其次是請(qǐng)求頻率太高觸發(fā)了目標(biāo)站點(diǎn)的限流。還有一種情況是目標(biāo)站點(diǎn)檢查了Referer或Cookie。解決補(bǔ)全請(qǐng)求頭至少包含User-Agent、Referer、Accept-Language。降低請(qǐng)求頻率在請(qǐng)求之間加time.sleep(random.uniform(1, 3))。如果目標(biāo)站點(diǎn)需要登錄態(tài)用Session保持 Cookie??蚣芾锾岬降?Selenium 方案適合處理 JavaScript 渲染后才能拿到數(shù)據(jù)的頁(yè)面但它的開銷比純 HTTP 請(qǐng)求大得多能不用就不用。5.2 解析結(jié)果為空列表現(xiàn)象代碼沒(méi)報(bào)錯(cuò)但提取到的數(shù)據(jù)是空列表或空字符串。原因三種可能——選擇器寫錯(cuò)了數(shù)據(jù)是 JavaScript 動(dòng)態(tài)加載的初始 HTML 里根本沒(méi)有頁(yè)面編碼不是 UTF-8中文變成亂碼導(dǎo)致匹配失敗。解決先在瀏覽器里用CtrlU查看頁(yè)面源代碼不是 Elements 面板確認(rèn)目標(biāo)數(shù)據(jù)是否在初始 HTML 里。如果不在說(shuō)明需要抓包分析真實(shí)請(qǐng)求或者用 Selenium 渲染。選擇器問(wèn)題可以用print(doc)打印前 500 個(gè)字符肉眼確認(rèn)結(jié)構(gòu)。編碼問(wèn)題檢查resp.encoding必要時(shí)手動(dòng)設(shè)為resp.apparent_encoding。5.3 數(shù)據(jù)重復(fù)寫入現(xiàn)象數(shù)據(jù)庫(kù)里出現(xiàn)大量重復(fù)記錄同一個(gè)商品被存了多次。原因爬蟲重跑時(shí)沒(méi)有做去重判斷或者去重邏輯只檢查了標(biāo)題而標(biāo)題有細(xì)微差異。解決用唯一標(biāo)識(shí)通常是 URL 或商品 ID做去重鍵。MongoDB 建唯一索引配合upsertTrueMySQL 用INSERT ... ON DUPLICATE KEY UPDATECSV 在寫入前用 set 記錄已處理的鏈接。不要依賴標(biāo)題去重標(biāo)題可能因?yàn)榭崭瘛?biāo)點(diǎn)差異導(dǎo)致判斷失效。5.4 異步爬蟲報(bào)「Event loop is closed」現(xiàn)象aiohttp 腳本跑完一批請(qǐng)求后再跑第二批時(shí)報(bào)RuntimeError: Event loop is closed。原因asyncio.run()每次調(diào)用都會(huì)創(chuàng)建新的事件循環(huán)并在結(jié)束時(shí)關(guān)閉如果ClientSession在循環(huán)外創(chuàng)建第二次使用時(shí)循環(huán)已經(jīng)關(guān)了。解決把ClientSession的創(chuàng)建和關(guān)閉都放在async函數(shù)內(nèi)部用async with管理生命周期?;蛘哒麄€(gè)腳本只調(diào)用一次asyncio.run()把所有任務(wù)放在同一個(gè)main函數(shù)里。5.5 Robots 協(xié)議與訪問(wèn)頻率現(xiàn)象爬蟲跑了一段時(shí)間后 IP 被封鎖或者收到站點(diǎn)的警告郵件。原因沒(méi)有遵守robots.txt的抓取規(guī)則或者請(qǐng)求頻率遠(yuǎn)超正常用戶行為。解決抓取前先訪問(wèn)https://目標(biāo)站/robots.txt確認(rèn)Disallow的路徑不要碰。控制并發(fā)數(shù)單機(jī)爬蟲建議不超過(guò) 10 個(gè)并發(fā)請(qǐng)求間隔至少 1 秒??蚣芾锾岬降?Robots 協(xié)議是爬蟲的基本規(guī)矩不是技術(shù)限制而是行為邊界越過(guò)這條線帶來(lái)的麻煩遠(yuǎn)大于省下的那點(diǎn)時(shí)間。6. 從腳本到工程Scrapy 的落地姿勢(shì)與一個(gè)驗(yàn)證習(xí)慣當(dāng)你用 requests BeautifulSoup 寫了十幾個(gè)針對(duì)不同站點(diǎn)的腳本之后會(huì)發(fā)現(xiàn)大量代碼在重復(fù)請(qǐng)求重試、去重、并發(fā)控制、數(shù)據(jù)管道。這時(shí)候就該上 Scrapy 了??蚣芾锇?Scrapy 定位為「最靈活的定制化爬取」這個(gè)評(píng)價(jià)很準(zhǔn)確——它的學(xué)習(xí)曲線比 requests 陡但一旦跑通擴(kuò)展性和可維護(hù)性是腳本比不了的。Scrapy 的核心概念是 Spider、Item、Pipeline、Middleware。Spider 定義抓取邏輯Item 定義數(shù)據(jù)結(jié)構(gòu)Pipeline 負(fù)責(zé)清洗和存儲(chǔ)Middleware 處理請(qǐng)求和響應(yīng)的攔截。一個(gè)最小可運(yùn)行的 Spiderimport scrapy class ProductSpider(scrapy.Spider): name product start_urls [https://example.com/products?page1] custom_settings { DOWNLOAD_DELAY: 1.5, # 請(qǐng)求間隔秒 CONCURRENT_REQUESTS: 8, # 并發(fā)數(shù) RETRY_TIMES: 3, # 重試次數(shù) 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, } def parse(self, response): for item in response.css(.product-list .item): yield { title: item.css(.title::text).get(default).strip(), price: item.attrib.get(data-price, ), link: response.urljoin(item.css(a::attr(href)).get()), } # 翻頁(yè)找到下一頁(yè)鏈接繼續(xù)抓 next_page response.css(a.next::attr(href)).get() if next_page: yield response.follow(next_page, callbackself.parse)custom_settings里的DOWNLOAD_DELAY和CONCURRENT_REQUESTS是最需要調(diào)的兩個(gè)參數(shù)。DOWNLOAD_DELAY1.5表示每個(gè)請(qǐng)求之間等 1.5 秒配合CONCURRENT_REQUESTS8實(shí)際對(duì)目標(biāo)站點(diǎn)的壓力大約是每秒 5 個(gè)請(qǐng)求。如果目標(biāo)站點(diǎn)響應(yīng)慢或者有限流跡象把并發(fā)降到 4、延遲加到 3 秒。RETRY_TIMES3讓失敗的請(qǐng)求自動(dòng)重試省去手動(dòng)寫重試邏輯。response.follow會(huì)自動(dòng)處理相對(duì) URL 和域名過(guò)濾比手動(dòng)拼urljoin再yield Request簡(jiǎn)潔。callbackself.parse表示下一頁(yè)返回的數(shù)據(jù)仍然用同一個(gè)解析函數(shù)處理形成遞歸抓取。Scrapy 跑起來(lái)之后驗(yàn)證數(shù)據(jù)質(zhì)量有個(gè)習(xí)慣我一直在用先跑-s CLOSESPIDER_ITEMCOUNT20限制只抓 20 條把輸出導(dǎo)成 JSON 檢查字段完整性和去重情況確認(rèn)沒(méi)問(wèn)題再放開全量抓取。scrapy crawl product -s CLOSESPIDER_ITEMCOUNT20 -o sample.jsonCLOSESPIDER_ITEMCOUNT20表示抓到 20 條就自動(dòng)停止-o sample.json把結(jié)果輸出到文件。打開sample.json檢查標(biāo)題有沒(méi)有空值、價(jià)格格式是否統(tǒng)一、鏈接是否完整。這個(gè)習(xí)慣幫我省過(guò)很多次「跑了幾萬(wàn)條才發(fā)現(xiàn)字段全錯(cuò)」的后悔藥。從那以后我每次寫新 Spider都強(qiáng)制先跑 20 條樣本驗(yàn)證再上全量。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取