☰
Python Scrapy爬虫实战:从反爬拦截到稳定采集的工程化配置指南
2026/10/14 14:46:59 网站建设 项目流程

简介:这份资源面向具备一定Python基础、希望入门或巩固爬虫开发的学习者,聚焦于使用Scrapy框架抓取BBS论坛数据这一典型场景。内容围绕Scrapy的核心组件展开,涵盖爬虫定义、数据项建模、管道处理与项目配置等模块,适合用于数据挖掘、信息处理或历史数据存储等练习方向。压缩包共14个文件,以6个py源码与6个pyc编译文件为主,另含1个docx说明文档和1个cfg配置文件,整体约18KB,体量轻便,便于快速阅读与本地运行。目前已有5622人学习下载,说明该案例在爬虫入门群体中具有一定参考价值。通过这份资料,读者可以了解一个完整Scrapy项目的目录组织方式,理解items、pipelines、settings等模块之间的协作关系,并借助说明文档梳理抓取BBS数据的实现思路,为后续自行编写爬虫、调试选择器与处理数据存储打下基础。

1. 从一次被反爬拦到怀疑人生的抓取任务说起

有个做数据分析的朋友接了个活儿,要定期从几个公开信息页面上把结构化数据扒下来做趋势对比。他一开始用 requests 加 BeautifulSoup 硬怼,前两百条还挺顺,到后面开始出现 403、验证码、返回空列表,甚至拿到一堆看起来像数据其实是模板占位符的 HTML。他问我是不是得换个语言,我说不用,问题不在语言,在你没把抓取当成一个工程问题来拆。Python scrapy 爬虫这套东西之所以在数据采集场景里一直被用,不是因为它语法多花哨,而是它把请求调度、去重、解析、管道输出这几件事拆成了可配置的组件,你遇到反爬、限速、数据清洗这些破事时,有地方下手改,而不是在一坨脚本里到处打补丁。这份资源适合两类人:一类是刚学完 Python 基础、想找个完整项目把语法用起来的新手;另一类是从业者,手头有周期性采集需求,想看看一个能跑通的爬虫骨架长什么样、哪些参数值得调、哪些坑必须提前绕。

2. 拆开一个 Scrapy 工程:五个核心组件到底各管什么

2.1 引擎、调度器、下载器、Spider、管道

Scrapy 的运行链路可以理解成一条流水线。引擎是总控,负责在调度器、下载器、Spider 之间传递指令和数据;调度器维护一个请求队列,同时做去重,避免同一个 URL 被反复抓;下载器真正发出 HTTP 请求,拿回响应;Spider 是你写解析逻辑的地方,决定从响应里提取什么、下一步请求哪些链接;管道负责对提取出来的数据做清洗、校验、写库或写文件。这五个组件里,新手最常忽略的是调度器和管道,觉得能抓到就行,结果要么重复抓同一页浪费配额,要么数据里混着空值、乱码、多余空格直接进了数据库。

常见做法是先用scrapy startproject生成骨架,再在settings.py里把并发、延迟、去重这些开关调一遍。下面是一个最小可跑的 Spider 示例,目标是从一个列表页提取条目名称和详情链接,再跟进详情页拿描述。

import scrapy class ItemSpider(scrapy.Spider): name = "item_spider" # 起始 URL,实际使用时替换为目标列表页 start_urls = ["https://example.com/list?page=1"] def parse(self, response): # 提取列表页中每个条目的链接 for link in response.css("div.item a::attr(href)").getall(): yield response.follow(link, callback=self.parse_detail) # 翻页:找到下一页按钮并跟进 next_page = response.css("a.next::attr(href)").get() if next_page: yield response.follow(next_page, callback=self.parse) def parse_detail(self, response): # 在详情页提取字段,交给管道处理 yield { "title": response.css("h1::text").get(default="").strip(), "desc": response.css("div.desc::text").get(default="").strip(), "url": response.url, }

这段代码里parse负责列表页的链接提取和翻页,parse_detail负责详情页字段提取。response.follow会自动处理相对链接和绝对链接的拼接,比手动拼 URL 稳。get(default="")的作用是当选择器没匹配到内容时返回空字符串而不是 None,避免后续管道里做字符串操作时报类型错误。翻页逻辑里判断next_page是否存在,不存在就自然停止,不需要额外写终止条件。

2.2 请求调度与去重:为什么你的爬虫会重复抓同一页

调度器的去重默认基于请求指纹,包括 URL、方法、请求体等。但如果你在 Spider 里手动构造请求时带了随机参数,或者 URL 里带了每次都会变的时间戳,去重就会失效。另一个常见情况是翻页逻辑写错,比如下一页链接提取到了当前页的链接,导致死循环。我一般会在settings.py里把DUPEFILTER_DEBUG临时打开,看看到底哪些请求被判重了,确认没问题再关掉。

# settings.py 中与调度和去重相关的常用配置 # 并发请求数,默认 16,新手建议先降到 8 观察目标站点反应 CONCURRENT_REQUESTS = 8 # 同一域名下的并发请求数,进一步限制压力 CONCURRENT_REQUESTS_PER_DOMAIN = 4 # 下载延迟,单位秒,给目标站点留出响应时间 DOWNLOAD_DELAY = 1.5 # 随机化延迟,避免固定间隔被识别为机器行为 RANDOMIZE_DOWNLOAD_DELAY = True # 开启去重调试,排查重复请求时临时设为 True DUPEFILTER_DEBUG = False # 请求超时时间,避免卡死 DOWNLOAD_TIMEOUT = 15

CONCURRENT_REQUESTS和CONCURRENT_REQUESTS_PER_DOMAIN这两个参数是新手最容易调过头的地方。默认值在本地测试没问题,但对着一个响应慢的站点跑,并发太高会导致大量超时和重试,反而拖慢整体速度。我一般先把并发压到 4 到 8,延迟设 1 到 2 秒,跑一轮看成功率,再逐步往上加。RANDOMIZE_DOWNLOAD_DELAY打开后,实际延迟会在设定值的 0.5 到 1.5 倍之间浮动,比固定间隔自然一些。

2.3 数据管道:从原始字段到可用数据的最后一公里

管道是很多人写完 Spider 就扔一边的部分,但实际项目里数据质量问题大多出在这里。比如标题里混着换行符和多余空格,描述字段里混着 HTML 实体,价格字段是字符串需要转成浮点数。下面是一个管道示例,做三件事:去空白、过滤空标题、把价格转成数值。

import re class CleanPipeline: def process_item(self, item, spider): # 去除标题首尾空白和内部连续空白 if "title" in item: item["title"] = re.sub(r"\s+", " ", item["title"]).strip() # 标题为空的条目直接丢弃 if not item.get("title"): raise DropItem(f"empty title: {item.get('url')}") # 价格字段转数值,失败则置为 None if "price" in item: raw = re.sub(r"[^\d.]", "", item["price"]) try: item["price"] = float(raw) except ValueError: item["price"] = None return item

process_item是管道的入口方法,每个 item 都会经过这里。DropItem是 Scrapy 提供的异常,抛出后该条目会被丢弃,不会进入后续管道或输出。价格清洗里先用正则去掉非数字和非小数点的字符,再尝试转浮点,转失败就置 None,这样下游做统计时不会因为一个脏字段整个流程崩掉。管道在settings.py里通过ITEM_PIPELINES注册,数字越小越先执行。

3. 把抓取跑稳:请求头、限速与重试的实操配置

3.1 请求头与中间件:别让默认 UA 出卖你

Scrapy 默认的 User-Agent 里带着框架标识,很多站点看到这个直接拒绝。最直接的做法是在settings.py里换一个常见浏览器的 UA,再补上 Accept、Accept-Language 这些头。如果目标站点对请求头检查比较细,可以写一个下载中间件,在每次请求前随机换一组头。

# settings.py 中设置默认请求头 DEFAULT_REQUEST_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", "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", }

如果需要在中间件里动态换 UA,可以维护一个 UA 列表,在process_request里随机选一个塞进request.headers。注意不要用网上那些来路不明的 UA 列表,里面很多是过时或伪造的,反而容易触发风控。我一般只准备三到五个主流浏览器的近期版本 UA,轮换使用。

3.2 自动限速与重试:让爬虫自己学会踩刹车

Scrapy 自带一个 AutoThrottle 扩展,可以根据目标站点的响应时间自动调整延迟。开启后,你不需要手动猜延迟设多少,它会根据实际响应动态调整。

# settings.py 中开启自动限速 AUTOTHROTTLE_ENABLED = True # 起始延迟,单位秒 AUTOTHROTTLE_START_DELAY = 1.0 # 最大延迟上限,避免被拖到无法接受 AUTOTHROTTLE_MAX_DELAY = 10.0 # 目标并发,实际并发会根据响应时间向这个值靠拢 AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 # 调试时打开,可以看到每次请求的实际延迟 AUTOTHROTTLE_DEBUG = False

重试配置同样重要。默认情况下 Scrapy 会对某些失败请求做重试,但重试次数和触发条件可以调。

# 重试中间件配置 RETRY_ENABLED = True # 最大重试次数,超过后放弃 RETRY_TIMES = 3 # 触发重试的 HTTP 状态码 RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 429]

429 是请求过多的状态码,把它加进重试列表后,遇到限流会等一段时间再试。但要注意,如果目标站点持续返回 429,说明你的请求频率已经超出对方容忍范围,这时候应该降并发、加延迟,而不是靠重试硬扛。我见过有人把RETRY_TIMES设到 10,结果爬虫卡在一个页面上反复重试,整体进度反而更慢。

3.3 翻车现场:三个我踩过的配置坑

第一个坑是DOWNLOAD_DELAY设了但没生效。原因是在 Spider 里用了meta覆盖了延迟,或者开了 AutoThrottle 后手动延迟被自动调整覆盖。排查方法是打开AUTOTHROTTLE_DEBUG看实际延迟值。第二个坑是管道里做了网络请求。有人在管道里调外部 API 做数据补全,结果管道变成同步阻塞,整个爬虫速度被拖垮。正确做法是把这类操作放到异步任务里,或者单独起一个后处理流程。第三个坑是日志级别设成 DEBUG 后,控制台被大量请求日志淹没,真正的错误信息被冲掉。我一般把LOG_LEVEL设成 INFO,只在排查特定问题时临时开 DEBUG。

4. 避坑与排查:抓不到、抓得慢、抓得脏的常见问题

4.1 返回 403 或验证码页面

现象是请求发出后返回 403,或者响应内容里出现验证码相关关键词。原因通常是请求头不完整、请求频率过高、或者目标站点对特定 IP 段有策略。解决步骤:先换 UA 和补全请求头,再把并发降到 2 以下、延迟加到 3 秒以上,观察是否恢复。如果仍然不行,检查是不是需要携带 Cookie 或特定 token,这类信息一般需要从首次访问的响应里提取。

4.2 选择器返回空列表

现象是response.css或response.xpath返回空列表,但浏览器里明明能看到内容。原因可能是页面内容由 JavaScript 动态渲染,Scrapy 拿到的原始 HTML 里没有这些节点。解决方式是确认目标数据是否在初始 HTML 中,如果不在,需要换用能执行 JS 的方案,或者找到页面背后的数据接口直接请求。我一般先用scrapy shell打开目标 URL,手动跑一遍选择器,确认是选择器写错还是页面本身没有数据。

4.3 数据重复或遗漏

现象是输出文件里出现重复条目,或者某些页面的数据没抓到。重复的原因多半是去重失效或翻页逻辑重复抓了同一页;遗漏的原因可能是翻页条件写错导致提前终止,或者某些请求失败后没有重试。排查时先把DUPEFILTER_DEBUG打开看重复请求,再检查翻页选择器是否匹配到了多个元素。遗漏问题可以看日志里的失败请求数量,对比预期总数。

4.4 爬虫跑着跑着卡住不动

现象是日志停止输出,进程还在但没进展。原因可能是某个请求超时后卡在重试循环里,或者管道里某个操作阻塞了。解决方式是设置DOWNLOAD_TIMEOUT限制单次请求时间,检查管道里是否有同步阻塞操作,必要时给管道加超时控制。另外,CLOSESPIDER_TIMEOUT可以给整个爬虫设一个最大运行时间,到点自动关闭,避免无限期挂着。

4.5 输出文件编码乱码

现象是 CSV 或 JSON 文件里的中文显示为乱码。原因通常是写入时没有指定编码,或者目标页面本身编码与解析时假设的编码不一致。解决方式是在settings.py里设置FEED_EXPORT_ENCODING = "utf-8",并在 Spider 里确认response.encoding是否正确。如果页面编码是 GBK 而 Scrapy 按 UTF-8 解析,中文就会乱。可以在 Spider 里手动设置response.encoding = "gbk"再解析。

5. 进阶技巧:用 Scrapy Shell 和日志把调试效率提上去

5.1 Scrapy Shell 的三种用法

scrapy shell是调试选择器最顺手的工具。第一种用法是直接打开一个 URL,进入交互环境后可以反复试选择器。第二种用法是配合fetch命令,在 shell 里换 URL 重新抓取。第三种用法是把响应保存到本地,用scrapy shell file:///path/to/page.html打开,适合在没有网络或需要反复调试同一页面时用。

# 打开目标页面进入交互调试 scrapy shell "https://example.com/list?page=1" # 在 shell 中测试选择器 # response.css("div.item a::attr(href)").getall() # response.xpath("//h1/text()").get() # 把响应保存到本地后再调试 scrapy shell "file:///tmp/sample.html"

进入 shell 后,response对象就是当前页面的响应,你可以直接在上面跑 CSS 或 XPath 选择器,看返回结果是否符合预期。确认选择器没问题后再写进 Spider,能省掉大量反复跑爬虫的时间。

5.2 日志分级与关键信息定位

Scrapy 的日志级别从低到高有 DEBUG、INFO、WARNING、ERROR、CRITICAL。日常运行设 INFO 就够,能看到抓取进度和统计信息。排查问题时临时开 DEBUG,但要注意日志量会很大。我一般会在 Spider 里用self.logger.info打关键节点,比如翻页到第几页、当前页提取了多少条,这样即使不开 DEBUG 也能追踪进度。

# 在 Spider 中打自定义日志 def parse(self, response): items = response.css("div.item") self.logger.info(f"page={response.url} items={len(items)}") for link in items.css("a::attr(href)").getall(): yield response.follow(link, callback=self.parse_detail)

这行日志会输出当前页面的 URL 和提取到的条目数,跑起来后一眼就能看出哪一页开始数量异常,快速定位是页面结构变了还是请求被拦了。

5.3 一个我常用的收尾习惯

每次爬虫跑完,我不会只看输出文件条数,而是会把日志里的统计信息拉出来对一遍:请求总数、成功响应数、失败数、重试数、去重数。这几个数字对不上,说明中间有环节出了问题。比如请求总数远大于成功响应数,说明失败率太高,需要检查网络或目标站点策略;去重数异常高,说明有重复请求,需要检查翻页逻辑。从那以后我每次上线新爬虫前,都强制走一遍 shell 调试、小批量试跑、统计核对这三步,确认没问题再放量。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询