简介:一份系统讲解 Python 爬虫开发的 DOC 格式文档,面向从零起步的初学者和希望进阶的开发者,内容从爬虫基本概念、工作流程(发送请求、解析网页、存储数据、循环抓取)逐步延伸到 requests、BeautifulSoup、Scrapy、Selenium 四大常用工具库的安装与实战用法,并覆盖数据分析、信息监控、舆情分析等典型应用场景,帮助读者建立完整的爬虫知识体系。压缩包内仅含 1 个 doc 文件,大小 38KB,但内容组织紧凑,集中呈现了请求发送、HTML 解析、数据存储、分页循环等核心环节的代码示例与思路说明,便于按需查阅与快速上手。目前已有 295 人学习使用,特别适合需要独立完成网页数据采集任务的开发者。文档针对 IP 封锁、验证码、JavaScript 渲染等常见反爬措施,介绍了代理池切换、请求头伪装、验证码识别与人工处理等绕过技巧,并附带了电商商品信息抓取的完整实战项目,覆盖从页面结构分析、数据解析到 CSV 存储的每一步,帮助读者在真实场景中巩固技能、少走弯路。
1. Python 爬虫开发:先弄清楚数据在哪,再决定用哪套工具
如果你给自己定下的目标是「全面掌握 Python 爬虫开发」,我建议你先把「发请求、拿 HTML、解析、存库」这条惯性路径放一放。真正让新手和熟练开发者拉开差距的,不是会不会写 requests.get,而是能不能在十分钟内判断出目标数据到底在 HTML 里、在异步接口里、还是在 JS 渲染之后。判断错了,后面所有代码都是白写。这套资源把这条路完整走了一遍:从 requests 和 BeautifulSoup 搭静态页面抓取,到 Selenium 处理动态渲染,再到 Scrapy 做规模化采集,最后落在一个电商商品数据的实战项目上。它适合刚入门、想系统建立爬虫知识框架的开发者,也适合写过不少脚本、却总在反爬面前翻车的进阶者。
2. requests 与 BeautifulSoup:静态页面抓取的最小闭环
2.1 请求从哪儿发出去:headers、Session 与超时设置
requests 是 Python 里最常用的 HTTP 请求库,它的 API 设计得很直接:get 一个网址,拿 response,读 text。但实际抓取时,直接裸请求很容易被服务器识别为脚本。常见做法是先把自己的请求头伪装成浏览器,再考虑带不带 Cookie。
import requests s = requests.Session() s.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", }) resp = s.get("https://example.com/page", timeout=10) resp.raise_for_status() # 状态码不是 2xx 时直接抛异常 html = resp.text print(html[:200])这段代码里有几个关键点需要说明。Session()对象会帮你自动管理 Cookie,多次请求同一站点时,服务器返回的 Set-Cookie 会被保留并在后续请求中带上,这是模拟用户连续访问的基础。headers.update()设的是会话级请求头,后续所有请求都会带上,不用每个请求都重复写。timeout=10很关键,没设超时的话,遇到慢响应或连接被重置,脚本可能卡死很久。raise_for_status()是给请求加一道保险,遇到 403、404、500 时立即抛异常,比你自己判断resp.status_code更早暴露问题。
2.2 BeautifulSoup 解析:find_all 与 CSS 选择器怎么取舍
拿到 HTML 之后,解析和提取数据是第二件事。BeautifulSoup 是这类任务最稳的选择,它不依赖正则,而是把 HTML 解析成对象树,再通过节点关系取内容。
from bs4 import BeautifulSoup soup = BeautifulSoup(html, "lxml") items = soup.find_all("div", class_="product-item") for item in items: name_node = item.find("h2", class_="product-title") price_node = item.select_one("span.product-price") if name_node and price_node: print(name_node.get_text(strip=True), price_node.get_text(strip=True))解析器我一般用lxml,速度比html.parser快不少,容错也更好。如果环境受限装不了,退回html.parser也能跑,只是慢一些。find_all适合按标签名、class 批量找节点;select_one则走 CSS 选择器语法,定位嵌套结构时更省代码。这里注意 class 在 Python 里是保留字,所以 BeautifulSoup 的class_带下划线。另外,get_text(strip=True)会把节点内的换行和空格去掉,取出来的价格、标题基本是干净的。写解析逻辑时,一定要先判空再做提取,很多页面会有一两个商品缺字段,不判空的话脚本会在第 N 条数据上直接抛 AttributeError。
2.3 落地 CSV:编码与字段清洗的细节
数据提取出来之后,落到 CSV 是大多数人第一个想到的存储方式。这里有两个固定坑:一是写 CSV 时指定newline="",不然行与行之间会有空行;二是编码用utf-8-sig而不是utf-8,否则用 Excel 打开文件时中文会直接乱码。
import csv rows = [] for item in items: title = item.select_one("h2.product-title") price = item.select_one("span.product-price") if not title or not price: continue # 字段缺失时跳过,而不是让脚本崩溃 rows.append([ title.get_text(strip=True), price.get_text(strip=True).replace("¥", "").replace(",", ""), ]) with open("products.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["title", "price"]) writer.writerows(rows)这里对价格字段做了两层清洗:先去掉货币符号,再去掉千分位逗号,保证落库后的字段是纯数字。后面做数据分析或写进数据库时,就不用再单独写一遍清洗逻辑。每次抓完数据,我习惯先wc -l products.csv或者直接打开前几行看一遍,确认条数对得上、字段没有错位,再做下一步。
3. 动态渲染与反爬应对:从请求头伪装到 Selenium 的边界
3.1 现象先识别:被限流、验证码、与空响应
写爬虫最怕的不是被封,而是不知道发生了什么。同样是拿不到数据,背后的原因可能完全不一样。我习惯先把现象分门别类记下来,再决定用哪套应对方案。
| 现象 | 常见原因 | 优先处理方向 |
|---|---|---|
| 返回 403 / 418 | 请求头被识别或触发频控 | 补全 headers,降频率,带重试 |
| 返回 200 但页面没有目标数据 | 数据由 JS 动态加载 | 找异步接口,或换浏览器自动化 |
| 出现验证码页面 | 访问行为异常 | 检查频率与登录态,考虑人工介入 |
| 前半段正常,后半段全部超时 | IP 被限制 | 拉大请求间隔,检查出口 IP 是否被拉黑 |
这里想强调一个原则:爬虫的目标是获取公开数据,不是对抗安全体系。遇到验证码时,先想有没有正规途径,比如目标网站提供的 API、需要登录的合理会话;如果都没有,果断人工处理或者放弃该数据源,硬破验证码性价比非常低,而且容易把自己账号和 IP 都搭进去。
3.2 请求间隔与重试:让爬虫节奏接近人工
控制频率是爬虫活下去的基本功。很多新手习惯用for url in urls: requests.get(url)一口气跑完,结果爬到第三页就触发风控。常见做法是加随机延迟和指数退避重试。
import requests import random import time urls = [f"https://example.com/products?page={i}" for i in range(1, 6)] for url in urls: for attempt in range(3): try: resp = s.get(url, timeout=10) if resp.status_code == 200: break except requests.RequestException: time.sleep(2 * (attempt + 1)) # 第1次等2秒,第2次等4秒 time.sleep(random.uniform(1.5, 3.5)) # 每次请求之间随机休息这段代码里的重试逻辑是「失败后越等越久」,第一次失败等 2 秒,第二次等 4 秒,避免频繁重试给服务器造成压力。请求之间的随机等待用random.uniform(1.5, 3.5),保证节奏不规律,更接近真人浏览。如果你运营着固定的出口 IP 池,通常还需要按权重分配不同 IP 的使用频率,避免某个出口在短时间被集中使用。这部分各家服务配置差异很大,按你所用通道的文档来调整。务必遵守目标站点 robots 协议和访问条款,控制并发,这也是爬虫能长期跑的前提。
3.3 Selenium 与浏览器自动化:什么时候上、什么时候别上
遇到 JS 动态渲染的页面,requests 拿到的 HTML 往往是空壳,这时候就轮到 Selenium 出场。它能启动一个真实浏览器,执行完页面里的 JavaScript 后再读取渲染结果,拿到的就是用户肉眼看到的内容。
from selenium import webdriver from selenium.webdriver.chrome.options import Options opts = Options() opts.add_argument("--headless") # 无头模式,不弹窗口 driver = webdriver.Chrome(options=opts) try: driver.get("https://example.com/dynamic") html = driver.page_source soup = BeautifulSoup(html, "lxml") title = soup.select_one("h1.product-title") print(title.get_text(strip=True) if title else "未找到") finally: driver.quit()Selenium 的优点是能拿到真实渲染后的 DOM,缺点是慢、吃内存、而且对机器环境敏感:浏览器版本和驱动版本不匹配时启动直接报错。我的判断标准是,优先在开发者工具里找异步接口,能直接请求 JSON 接口就绝不上浏览器。只有当接口参数被加密、加密过程复杂到不值得分析时,才用 Selenium 兜底。它在这里更像是一块「后悔药」,不是默认方案。
4. Scrapy 框架:单页脚本升级为可持续爬虫的工程化之路
4.1 项目初始化与爬虫骨架
requests+BeautifulSoup 的组合写两三个页面很顺手,但页面量一大,并发、去重、重试、数据落库都要自己搭,代码很快变成一堆补丁。Scrapy 的价值在于把这些事都做成了框架的一部分:请求调度、并发控制、去重、管道处理全都不用自己写。它适合的是「要长期维护、要规模化抓取」的项目。
scrapy startproject demo_spider cd demo_spider scrapy genspider products example.com生成项目后,核心的爬虫文件在demo_spider/spiders/products.py里。一个最简单的爬虫如下:
import scrapy class ProductsSpider(scrapy.Spider): name = "products" allowed_domains = ["example.com"] start_urls = ["https://example.com/products"] def parse(self, response): for item in response.css("div.product-item"): yield { "title": item.css("h2.product-title::text").get(), "price": item.css("span.product-price::text").get(), } next_page = response.css("a.next::attr(href)").get() if next_page: yield scrapy.Request( response.urljoin(next_page), callback=self.parse )parse方法里用 yield 返回数据,Scrapy 会把每个 dict 当作一条 item 交给管道,而不是攒够一批再处理,内存占用低很多。翻页用response.urljoin(next_page)把相对路径拼成完整 URL,再yield scrapy.Request把下一个页面重新交给同一个parse方法处理,这样就形成了一个自动翻页的循环。CSS 选择器里::text取该节点的纯文本,::attr(href)取链接属性,比在 requests 脚本里手动拼接字符串更直观。
4.2 Item 与 Pipeline:数据清洗和落库的工程化写法
Scrapy 项目里数据流是分层的。Spider 里只负责提取和 yield,清洗和存储交给 Item Pipeline。这样换存储方式时不用改爬虫逻辑,反过来改解析规则时也不用碰存储代码。
import scrapy class ProductItem(scrapy.Item): title = scrapy.Field() price = scrapy.Field()上面是items.py里声明的 item 结构。下面用管道把数据写进 CSV:
import csv class CsvPipeline: def open_spider(self, spider): self.f = open("products.csv", "w", newline="", encoding="utf-8-sig") self.writer = csv.writer(self.f) self.writer.writerow(["title", "price"]) def process_item(self, item, spider): self.writer.writerow([item["title"], item["price"]]) return item def close_spider(self, spider): self.f.close()管道要生效,还得在settings.py里登记,并配上执行优先级:
ITEM_PIPELINES = { "demo_spider.pipelines.CsvPipeline": 300, } DOWNLOAD_DELAY = 2 # 每个请求间隔至少 2 秒 CONCURRENT_REQUESTS = 8 # 同时最多 8 个请求优先级数字越小越先执行,如果挂多个管道,前一个管道处理完会 return item,后一个继续收。DOWNLOAD_DELAY会自动在设定值上下做随机抖动,实际间隔在 1.5 到 2.5 秒之间,比固定 2 秒更安全。CONCURRENT_REQUESTS = 8配上 2 秒延迟,是大多数普通网站能接受的上限。要不要从 requests 升到 Scrapy,就看目标站点页面是不是超过几十个、数据更新是不是周期性发生;一次性的小任务用框架反而要写更多配置。
5. 实战:电商商品数据抓取与常见问题排查
5.1 先做侦查:开发者工具里看请求,别急着写代码
拿到一个商品列表页,不要直接打开编辑器写爬虫。我一般先在浏览器里按下 F12,切到 Network 面板,刷新页面后观察请求。重点看两种类型:文档请求(Doc)里返回的是不是完整 HTML,XHR/Fetch 请求里返回的是不是 JSON。如果商品数据躺在某个 JSON 接口里,直接用 requests 请求那个接口,解析和性能都比解析 HTML 好得多。
看接口参数也一样,右键复制请求为 cURL,把里面的 query string 拆出来对照页面页码,基本能还原分页逻辑。这个习惯能帮你省掉大量「写完解析却发现数据不全」的时间。目标页面的 HTML 结构越复杂,这一步越值得做。
5.2 一套能跑的抓取脚本:从列表页到详情页
下面这套脚本对应典型的电商列表页加详情页场景。列表页提取商品标题、价格、链接,详情页补充评分字段,最后统一落 CSV。每一步都做了容错,不至于因为个别商品缺字段就中断。
import requests import csv import time import random from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36", "Referer": "https://example.com/", } def fetch_page(url): resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text return None def parse_list(html): soup = BeautifulSoup(html, "lxml") results = [] for card in soup.select("div.product-card"): link = card.select_one("a.product-link") title = card.select_one("h2.product-title") price = card.select_one("span.product-price") if link and title and price: results.append({ "title": title.get_text(strip=True), "price": price.get_text(strip=True).replace("¥", "").replace(",", ""), "url": link.get("href"), }) return results rows = [] for page in range(1, 6): url = f"https://example.com/products?page={page}" html = fetch_page(url) if html: rows.extend(parse_list(html)) print(f"page {page} done, total rows: {len(rows)}") time.sleep(random.uniform(1, 2.5)) # 补充详情页字段 for row in rows: detail_html = fetch_page(row["url"]) if detail_html: soup = BeautifulSoup(detail_html, "lxml") rating = soup.select_one("span.product-rating") row["rating"] = rating.get_text(strip=True) if rating else "N/A" time.sleep(random.uniform(0.8, 1.8)) with open("ecommerce_products.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "price", "rating", "url"]) writer.writeheader() writer.writerows(rows) print("done, saved rows:", len(rows))fetch_page统一处理请求和超时,页面异常时返回 None,解析函数就不用每次写 try。parse_list里三个字段必须都取到才放进结果集,缺任何一个就丢弃这条商品,这比拿到空值后再清洗更省事。详情页循环里对每个商品都做了一次网络请求,所以同样加了随机 sleep,避免连续打爆详情接口。最后用DictWriter写 CSV,表头和行数据自动对齐,不怕字段顺序错位。
5.3 避坑记录:四类高频问题的现象、原因与解决
第一类:返回 403 Forbidden。现象是请求能发出去,但服务器拒绝响应。原因通常是请求头不完整,缺少 User-Agent、Referer、Accept-Language 这些浏览器默认带上的字段。解决方法是先手动访问页面,在开发者工具里复制真实请求头,逐个补到代码里,而不是只加一个 User-Agent 就完事。
第二类:HTTP 200 但 BeautifulSoup 提取结果为空。现象是页面能打开,代码也执行了,就是 find 不到目标节点。原因是目标数据由 JavaScript 动态加载,HTML 源码里根本没有对应结构。解决方法是回到 Network 面板,找返回 JSON 的 XHR 请求,直接抓接口;如果接口加密严重,再考虑 Selenium。
第三类:CSV 里价格出现错乱。现象是价格列混入货币符号、千分位,或者用 Excel 打开直接乱码。原因是原始字符串没有清洗干净,或者编码没用对。解决方法是用re.sub(r"[^\d.]", "", text)只保留数字和小数点;写文件时固定用utf-8-sig加newline=""。
第四类:爬到一半开始超时或出现验证码。现象是前几十条正常,后面请求全部报错,或者页面跳转到验证码页。原因是访问频率太高,出口 IP 被临时限制。解决方法是降低并发,把随机间隔拉大到 2 到 4 秒,并在请求异常时退避重试。遇到验证码,先查登录态和官方接口,硬破或依赖第三方打码都容易把账号封掉。
提示:以上四类问题在爬虫项目里出现频率极高。排查顺序永远是先看请求头、再看请求频率、最后才怀疑解析逻辑。把这三件事理清楚,半数以上抓取异常都能自己定位。
6. 优化与验证:并发、去重、断点续爬与数据校验
6.1 并发不是越大越好
多线程能提升抓取速度,但并发太高反而触发风控。我自己用 requests 做多线程时,ThreadPoolExecutor(max_workers=4)加上随机延迟是安全起点;Scrapy 场景则优先调CONCURRENT_REQUESTS = 8,再配合DOWNLOAD_DELAY控制节奏。爬虫不是抢火车票,稳定比速度重要。
6.2 去重与断点续爬
大型任务最怕跑到一半中断,重跑时又把已抓的页面重复请求一遍。我的做法是维护一个已处理 URL 的集合,并用本地文件做持久化。项目重启后先读文件,已存在的 URL 直接跳过。
seen = set() SEEN_FILE = "seen_urls.txt" try: with open(SEEN_FILE, encoding="utf-8") as f: seen = {line.strip() for line in f} except FileNotFoundError: pass for row in rows: if row["url"] in seen: continue # 这里放抓取逻辑 with open(SEEN_FILE, "a", encoding="utf-8") as f: f.write(row["url"] + "\n") seen.add(row["url"])这段代码把去重和断点续爬放在一起:每条 URL 抓取前检查,抓取后立即写入文件,下次启动时就能从上次中断的位置继续。注意写入文件的时机要放在抓取成功之后,而不是放入队列时,否则失败重试时会把没抓成功的链接误判为已完成。
6.3 验证数据完整性的习惯
任务跑完后别急着收工,先做三道检查:看条数是否和预期一致,看关键字段的缺失率是否在可接受范围,抽样几条数据和线上页面手工对比。我会把每次抓取的日志存成单独文件,记录时间、请求总数、失败数、耗时,方便下次任务前对比。从那次因为价格字段没清洗导致整表数据作废之后,我每次抓完都会强制跑一遍字段校验脚本,宁愿多花五分钟确认,也不想把脏数据交出去。这套流程配合上面的去重逻辑,已经帮我在多个模拟项目 X 里稳定跑完整条链路,希望帮到你。
本文还有配套的精品资源,点击获取