Scrapy全站爬取日志体系与工程实践:从入门到稳定运行

发布时间:2026/10/10 9:08:22
Scrapy全站爬取日志体系与工程实践:从入门到稳定运行 做全站爬取这件事我一开始是被日志坑惨了的。跑了几百万个 URL爬到一半程序崩了控制台刷了几万行红色堆栈愣是找不到是从哪个链接开始出问题的。后来我痛定思痛把 Scrapy 的日志体系和 logging 模块彻底理顺再加上一套合理的全站遍历策略才真正体会到什么叫“爬虫跑得稳日志看得清”。这篇东西不是教科书是我自己踩坑踩出来的实操总结适合那些已经用 Scrapy 写过几个简单爬虫、但想把它做成能上规模、能排查、能维护的项目的朋友。1. 全站爬取的项目设计与思路拆解1.1 全站爬取和定向爬取的根本区别很多人理解的爬虫是“给我一个列表页我把里面的详情页字段抓出来”这叫定向爬取目标 URL 是已知的、有限的、可枚举的。但全站爬取不一样它的核心问题是你怎么知道一个站到底有哪些页面你没有一个预先写好的 URL 清单有的只是一个域名和一堆未知的链接关系网。我见过有人试图在 start_urls 里堆几百个 URL以为这就是全站了结果站点一改版爬虫就直接哑火。真正意义上的全站爬取是把“发现链接—过滤链接—入队—请求—解析—再发现链接”这个回路跑起来让爬虫自己顺着站内结构一路漫游下去直到没有新的合法链接为止。这个过程有点像你在一座迷宫里走路手里拿的不是地图而是一团线和一支笔每走到一个岔路口就把看到的新路口记下来回头再挨个走进。设计阶段最需要想清楚的几件事边界在哪同域名才算站内还是 CDN 域名、二级域名也算外链是顺手保存还是直接丢弃去重粒度URL 是否要去掉锚点、去除追踪参数同样的内容是不是有多个 URL 指向规模预期这站是几千页还是几百万页决定你用单机还是考虑分布式决定你要不要引入消息队列。合规边界robots.txt 是否允许抓取目标站点的使用条款怎么写的数据拿到之后用途是什么我个人的做法是先看 robots.txt再抓一个样本页面分析链接密度估算总量级然后再决定方案要不要上分布式。一上来就铺分布式集群的多半站点规模用不到纯属给自己找麻烦。1.2 为什么选 Scrapy 而不是 requests BeautifulSoup很多人问过我全站爬取用 requests 手写不行吗不是不行是维护成本高得离谱。全站爬取最考验的就是并发调度、去重、失败重试、限速这四件事。requests 写起来这几个功能全要自己造轮子造到后面你会发现你其实就是在写一个残缺的 Scrapy。Scrapy 天生自带的东西我列一下异步并发框架Twisted 底层几十个并发请求轻轻松松不用你自己折腾线程池。内置去重队列基于请求指纹重复 URL 自动过滤不需要你维护一个几十万长度的 set。中间件机制不管是换 UA、加代理、重试、还是接 Playwright都是在下载器中间件里加一段代码的事不动主逻辑。Item Pipeline解析完数据之后可以统一做清洗、去重、落库职责分得很清楚。最关键的是它的日志和统计系统是打包好的配合 logging 模块能让你对爬虫的运行状态一目了然。所以我最终定的方案是 Scrapy CrawlSpider 自定义扩展 logging 分流管理。CrawlSpider 负责链接发现扩展负责监控logging 负责把整个运行过程变成可追溯的记录。1.3 项目目录结构与代码模块划分一个能上规模的全站爬虫项目目录结构应该一眼望过去就知道哪块是干什么的。我习惯这样组织fullsite_crawler/ ├── scrapy.cfg ├── fullsite/ │ ├── __init__.py │ ├── items.py # 数据模型定义 │ ├── middlewares.py # 下载器中间件UA轮换/代理/Playwright │ ├── pipelines.py # 数据清洗与入库 │ ├── settings.py # 全局配置 │ ├── spiders/ │ │ ├── __init__.py │ │ └── site_spider.py # CrawlSpider 核心爬虫 │ ├── extensions/ │ │ ├── __init__.py │ │ └── monitor.py # 自定义扩展统计、日志、告警 │ └── utils/ │ ├── logger.py # logging 统一配置入口 │ └── url_tools.py # URL 规范化与过滤这个结构里extensions和utils是我后来加上的。早期我图省事把所有功能塞在 spider 里结果 spider 文件膨胀到 1000 多行改一个字段映射要滚半天屏幕。拆出来之后清爽很多而且每个模块可以单独测试。2. Scrapy 日志体系的正确打开方式2.1 先搞懂 Scrapy 的日志到底是谁在打Scrapy 日志基础其实不是 Scrapy 自己写的它用的就是 Python 标准库的 logging。Scrapy 启动的时候会去配置 logging 的 root logger所以你直接在代码里用logging.getLogger(__name__)拿到的 logger是能和 Scrapy 自带的日志混在一起输出的。我的经验是与其在 settings.py 里配 LOG_LEVEL 和 LOG_FILE不如自己在项目入口做一次完整的 logging 配置。因为 Scrapy 的默认配置太简单了一个输出到控制台一个输出到文件格式固定没有轮转没有按级别分流。日志文件跑几个小时就能到几个 GB排查问题的时候连 grep 都卡。我在utils/logger.py里做的事情就是彻底接管日志的生命周期统一格式化、统一分流、统一轮转。2.2 一套可落地的 logging 配置模板直接上我自己一直在用的配置你们可以直接抄# utils/logger.py import logging import sys from logging.handlers import RotatingFileHandler def setup_logging(debug: bool False) - None: root_logger logging.getLogger() root_logger.setLevel(logging.DEBUG if debug else logging.INFO) fmt logging.Formatter( fmt%(asctime)s | %(levelname)-8s | %(name)s | %(lineno)-4d | %(message)s, datefmt%Y-%m-%d %H:%M:%S ) # 控制台输出 INFO 以上 console logging.StreamHandler(sys.stdout) console.setLevel(logging.DEBUG if debug else logging.INFO) console.setFormatter(fmt) root_logger.addHandler(console) # 主日志文件INFO 以上按大小轮转 main_handler RotatingFileHandler( logs/crawler.log, maxBytes50 * 1024 * 1024, # 50MB backupCount5, # 保留 5 个历史文件 encodingutf-8 ) main_handler.setLevel(logging.DEBUG if debug else logging.INFO) main_handler.setFormatter(fmt) root_logger.addHandler(main_handler) # 错误日志独立文件方便快速定位崩溃点 error_handler RotatingFileHandler( logs/error.log, maxBytes20 * 1024 * 1024, backupCount3, encodingutf-8 ) error_handler.setLevel(logging.ERROR) error_handler.setFormatter(fmt) root_logger.addHandler(error_handler)这段配置解决了我三个痛点日志按大小轮转不会无限膨胀把磁盘塞满。错误单独一个文件排查崩溃不用在几十万行 INFO 日志里翻。格式里带 logger 名和行号定位代码位置一秒钟的事。然后你需要在项目的settings.py里把 Scrapy 默认的日志配置关掉不然它会和你的配置打架# settings.py LOG_ENABLED False LOG_LEVEL INFO接着在scrapy.cfg同级的入口脚本里比如run.py先调用setup_logging()再启动爬虫。2.3 自定义 logger 的分流与使用技巧项目里不是只有一个 logger而是应该每个模块有自己的命名空间。我在 spider、middleware、pipeline 里面都是这样拿 logger 的import logging logger logging.getLogger(__name__)__name__在这个例子里就是fullsite.spiders.site_spider这样日志里天然带上了模块路径。我习惯在关键节点打不同的日志级别logger.info()请求发出、收到响应、成功写入数据库。logger.warning()响应异常但可以重试、数据字段缺失但能容忍。logger.error()请求重试耗尽、数据库写入失败、页面结构变化导致解析失败。logger.critical()整个爬虫需要停下来比如发现目标站点加验证码导致连续 200 次请求失败。这里有一个很多人忽略的细节Scrapy 的 spider 内部不要用 print 做调试。print 的内容不会进日志文件爬虫一重启就找不到了。我在代码 review 的时候看到 print 一律要求改成 logger这条规矩帮团队省了无数排查时间。2.4 请求级日志中间件的写法光靠 Scrapy 自带的 DEBUG 日志你只能看到“抓了哪个 URL、状态码多少”看不到请求耗时、重试次数、响应体大小。我写了一个轻量级的下载器中间件把这些信息以结构化的方式打进日志# middlewares.py import logging import time logger logging.getLogger(__name__) class RequestLogMiddleware: def process_request(self, request, spider): request.meta[start_time] time.time() return request def process_response(self, request, response, spider): start_time request.meta.get(start_time, time.time()) elapsed time.time() - start_time logger.info( f[{response.status}] {request.url} | f耗时 {elapsed:.2f}s | 大小 {len(response.body):,}B | f深度 {request.meta.get(depth, 0)} ) return response def process_exception(self, request, exception, spider): logger.error(f[EXCEPTION] {request.url} | {type(exception).__name__}: {exception}) return None这段日志跑起来之后你能在文件里看到整个爬虫的“心电图”哪个 URL 慢了、哪个域名开始拒绝服务了全都有迹可循。后来我还加了一个统计每 1000 个请求打一个汇总日志包含平均耗时、错误率、当前爬取速度相当于在日志里嵌了一个迷你监控面板。3. 从种子 URL 到全站覆盖的实操路径3.1 入口选择不止是 start_urls全站爬取的入口我总结下来有三种拿法start_urls 硬编码适合小站你手动列几个入口页面。robots.txt 里的 Sitemap 标签很多站点暴露了 sitemap 地址这是最香的入口。Sitemap 里直接列了一堆 URL合法又高效。Scrapy 自带SitemapSpider直接继承就能用。从搜索引擎缓存或历史数据里反推这个不展开合规风险高个人不建议。我对有 sitemap 的站点一律优先用 SitemapSpider。它的好处除了拿到高质量 URL配合全站漫游还能避免漏掉那些只有深层入口才能到达的页面。# spiders/site_spider.py from scrapy.spiders import SitemapSpider class SiteSpider(SitemapSpider): name site_crawler sitemap_urls [https://example.com/robots.txt] sitemap_rules [ (/news/, parse_article), (/product/, parse_product), ] def parse_article(self, response): # 解析文章页... pass def parse_product(self, response): # 解析商品页... pass但注意sitemap 往往只覆盖“重要的、需要被搜索收录的”页面很多详情页、分页、用户生成页面不在里面。所以我的策略是双轨制sitemap 提供基础 URL 池CrawlSpider 的 Rule 规则再负责把 sitemap 漏掉的链接捡回来。3.2 CrawlSpider 的 Rule 规则怎么配才能不重不漏CrawlSpider 是全站爬取的核心武器。它用一套规则从每个响应里提取链接、过滤链接、决定哪些给哪个回调函数处理。我最常用的一套配置是这个样子# spiders/site_spider.py from scrapy.spiders import CrawlSpider, Rule from scrapy.linkextractors import LinkExtractor class SiteCrawlerSpider(CrawlSpider): name site_crawler allowed_domains [example.com] start_urls [https://example.com/] rules ( # 列表分页链接交给 parse_list 继续发现链接 Rule( LinkExtractor( allow(/list/, /news/, /category/), deny(/user/, /login), uniqueTrue, ), callbackparse_list, followTrue, ), # 文章详情页解析数据不再继续跟进 Rule( LinkExtractor( allow(/article/\\d\\.html, /detail/\\d), uniqueTrue, ), callbackparse_article, followFalse, ), )这里有几个关键经验allow与deny必须同时用。allow只做正向匹配你永远不知道站点里还有/m/、/mobile/这种移动版目录加deny可以提前把已知的垃圾区段排除掉。uniqueTrue很重要它保证同一个 URL 在同一个页面里只提取一次减少后面去重的压力。follow参数的含义容易搞混followTrue表示这个链接提取出来之后如果它本身也含有可以继续爬取的链接依然继续跟进followFalse表示提取出来后只交给回调函数处理不再继续往下钻。列表页要followTrue详情页一般followFalse。3.3 URL 规范化与自定义去重指纹全站爬取跑起来之后第一个让你崩溃的问题同一篇文章有三四个 URL。https://example.com/article/123、https://example.com/article/123?fromhome、https://example.com/article/123#comment、https://example.com/article/123/你说它们是同一篇还是不同篇Scrapy 默认的去重是基于完整 URL 的上面四个 URL 会被当成四个不同请求结果就是数据表里出现四条重复记录。我写了一个 URL 规范化函数在链接提取之后、入队之前做一次清洗# utils/url_tools.py from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode def normalize_url(url: str, strip_paramsNone): strip_params strip_params or {from, utm_source, utm_medium, utm_campaign, spm, src} parsed urlparse(url) # 去掉锚点 parsed parsed._replace(fragment) # 过滤指定查询参数 query [(k, v) for k, v in parse_qsl(parsed.query) if k not in strip_params] # 排序参数避免顺序不同导致重复 query.sort(keylambda x: x[0]) parsed parsed._replace(queryurlencode(query)) # 去掉尾部斜杠非根路径时 path parsed.path if len(path) 1 and path.endswith(/): parsed parsed._replace(pathpath.rstrip(/)) return urlunparse(parsed)配合这个函数我还会自定义去重类让指纹基于规范化后的 URL 生成# middlewares.py / custom_dupefilter.py from scrapy.dupefilters import RFPDupeFilter class CustomDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): from utils.url_tools import normalize_url normalized normalize_url(request.url) # 用规范化后的 URL 重组 request 再算指纹 from scrapy.utils.request import request_fingerprint new_request request.replace(urlnormalized) return request_fingerprint(new_request)然后在settings.py里指定DUPEFILTER_CLASS fullsite.middlewares.CustomDupeFilter这一步做完全站爬虫的请求量通常会砍掉 20% 到 40%数据质量提升一个档次。3.4 调度优先级与遍历策略全站爬取的遍历顺序直接影响目标站点的压力感受和你自己的抓取效率。Scrapy 默认的调度器是后进先出的栈式调度Depth First深度优先。深度优先的好处是实现简单、内存占用低坏处是你会一头扎进某个最深的列表分支把其他板块晾在一边。如果你是全站漫游我建议改成广度优先让爬虫像波浪一样在整个站点均匀推进# settings.py SCHEDULER_ORDER BFO # Breadth-First Order改完之后爬虫会先抓完首页这一层再抓列表页这一层然后才是详情页。这有一个实际的好处详情页的数据是独立的列表页的解析逻辑如果出问题了你能在早期就发现并修正而不会等深度优先跑到第 N 层才暴露。另外Scrapy 里可以通过priority参数给请求加权。我把列表页的优先级设为 10详情页设为 1这样列表页永远会被优先消费详情页数据随后跟进。这个策略在百万级站点的爬取里非常管用。3.5 限速与反爬应对不能等到被 ban 才后悔全站爬取对目标站点的访问压力是定向爬虫的几十倍限速是必修课不是选修课。我标配以下配置# settings.py DOWNLOAD_DELAY 0.5 # 同一域名下请求间隔秒 DOWNLOAD_TIMEOUT 15 CONCURRENT_REQUESTS 16 CONCURRENT_REQUESTS_PER_DOMAIN 8 # 自动限速 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY 4.0AUTOTHROTTLE_ENABLED是 Scrapy 里最被低估的一个功能。它会根据目标服务器的响应速度动态调整请求频率。服务器响应快就加速响应慢就自动降速比一个固定的 DOWNLOAD_DELAY 智能得多。UA 轮换也是必须的。全站爬虫跑起来同一个 UA 连续请求几千个页面被封是迟早的事。写一个最简单的轮换中间件# middlewares.py class UserAgentMiddleware: UA_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ..., # 其他 UA 自己补齐 ] def process_request(self, request, spider): import random request.headers[User-Agent] random.choice(self.UA_LIST) return request关于代理池我的观点是业务站点、合规抓取先用限速 UA 轮换撑住实在不行再上代理池。一上来就用代理池那是给大厂反爬体系准备的普通站点用不着而且代理质量参差不齐反而拉低抓取效率。4. extensions 扩展机制与动态 iframe 页面处理4.1 Scrapy 中 extensions 到底是什么很多 Scrapy 用户没见过extensions因为爬虫 Demo 从来不提它。但等到项目跑到生产环境你会发现没有 extensions爬虫就像一个没有仪表盘的飞机——飞得再高你看不见速度和油量。Scrapy 的扩展机制本质是在爬虫生命周期关键节点挂载自定义逻辑的一套钩子系统。它通过信号signal来感知事件比如爬虫启动、spider 打开、item 被抓取、请求异常、爬虫关闭。扩展和中间件的区别在于中间件管的是“请求/响应流”扩展管的是“爬虫本身的运行状态”。最常见的扩展用途每 5 分钟把爬虫的抓取速度、错误率打进日志或发给监控系统。检测到连续 N 个请求失败时主动暂停爬虫或者发告警。爬虫正常关闭时统一生成一份抓取报告。4.2 自定义监控扩展写一个 100 行以内的运行追踪器我写了一个简单的统计扩展它做的事情就是每隔一段时间打一条汇总日志# extensions/monitor.py import logging from scrapy import signals logger logging.getLogger(__name__) class CrawlerMonitor: def __init__(self, stats): self.stats stats self.report_interval 60 classmethod def from_crawler(cls, crawler): ext cls(crawler.stats) crawler.signals.connect(ext.spider_opened, signalsignals.spider_opened) crawler.signals.connect(ext.spider_closed, signalsignals.spider_closed) crawler.signals.connect(ext.item_scraped, signalsignals.item_scraped) crawler.signals.connect(ext.request_scheduled, signalsignals.request_scheduled) return ext def spider_opened(self, spider): logger.info(f[Monitor] Spider {spider.name} started) from twisted.internet import task self.task task.LoopingCall(self.report, spider) self.task.start(self.report_interval) def spider_closed(self, spider): logger.info(f[Monitor] Spider {spider.name} closed | fpages: {self.stats.get_value(response_received_count)} | fitems: {self.stats.get_value(item_scraped_count)} | ferrors: {self.stats.get_value(log_count/ERROR)}) if self.task.running: self.task.stop() def report(self, spider): logger.info( f[Monitor] {spider.name} | f已请求 {self.stats.get_value(downloader/request_count)} | f已接收 {self.stats.get_value(downloader/response_received_count)} | fitem 入库 {self.stats.get_value(item_scraped_count)} | f错误 {self.stats.get_value(log_count/ERROR)} )这段代码在你跑全站爬取的时候会特别有用。因为爬虫一旦跑起来控制台刷屏刷得飞快你根本看不清运行趋势有个 60 秒一次的汇总日志就像装了一个简易的 Grafana。要启用这个扩展在 settings.py 里加一行EXTENSIONS { fullsite.extensions.monitor.CrawlerMonitor: 500, }数字 500 是优先级正常情况下不会和其他扩展冲突如果你启用了 Telnet Console 之类的就注意错开。4.3 动态 iframe 页面为什么普通 Scrapy 抓不到全站爬取最烦的一类页面就是内容躲在 iframe 里的页面。普通的 requests 和 Scrapy 直接拿到的是外层 HTML 文档iframe 的内容是另一个独立的 HTML 文档是通过浏览器第二次请求加载进来的。你不把内层文档的内容抓回来字段永远缺失。iframe 一般分两类同源 iframeiframe 的 src 指向同一个域名的另一个 URL。这种好解决你在解析响应的时候用response.urljoin(iframe_src)拿到完整 URL再 yield 一个 Request 去抓内层页面就行。不用上 Playwright。跨域 iframesrc 指向别的域名或者更变态的是嵌套多层 iframe。这种用纯 Scrapy 就非常难搞因为外层页面在浏览器渲染完后iframe 里可能还有一层 iframe。我的经验是先尝试纯 Scrapy 方案——从 HTML 源码里解析 iframe 标签得到 src 后直接向该 src 发请求。这个方法 70% 的情况都够用。但如果页面里 iframe 的 src 是 JS 动态生成、或者 iframe 文档里还有一层同级的动态加载那纯 Scrapy 就不行了得上 Playwright。4.4 Scrapy 集成 Playwright 处理动态 iframe 的实践Scrapy 2.0 之后配合scrapy-playwright这个库可以在 Downloader 层面让请求走真实的浏览器环境。它不是无头浏览器模拟请求而是真的启动一个 Chromium 实例去渲染页面渲染完了把最终 HTML 返回给你的爬虫解析。接入步骤很短pip install scrapy-playwright # 还要安装 playwright 并下载 chromium playwright install chromiumsettings.py 里加上DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor然后在请求里通过 meta 启用yield scrapy.Request( urlpage_url, meta{playwright: True}, callbackself.parse_product, )如果页面里有 iframe 需要渲染可以在 meta 里加playwright_page_methods或者用page对象操作def parse_list(self, response): page response.meta.get(playwright_page) if page: # 等待 iframe 完全加载 page.wait_for_selector(iframe.example-class) # 获取所有 iframe 的 URL frame_urls [f.url for f in page.frames if f.url] for url in frame_urls: yield scrapy.Request(urlurl, callbackself.parse_iframe_page) yield scrapy.Request( urlresponse.url, callbackself.parse_outer_page )不过要泼一盆冷水Playwright 不是万能药代价极其昂贵。一个带浏览器渲染的请求耗时是普通请求的 10 倍以上内存开销也大并发只能压低。我通常的做法是按需启用先跑一轮纯 Scrapy 模式把能抓的页面全抓了剩下那些字段缺失严重的 URL收集起来放到待重抓列表里再单独用带 Playwright 的蜘蛛跑一轮补抓。这样既控制了成本又解决了动态渲染页面。如果是跨域 iframe 且有登录态要求还需要在 launch 参数里配置storage_state把登录 cookie 传进去from scrapy_playwright.page import PageMethod yield scrapy.Request( urlpage_url, meta{ playwright: True, playwright_context_kwargs: { storage_state: auth/state.json, }, playwright_page_methods: [ PageMethod(wait_for_selector, div#content), ], }, callbackself.parse_product, )配合PageMethod你可以在页面加载完成后依次执行“滚动到底部”“点击展开更多”“等待某个元素出现”等操作。这一步让全站爬虫的通吃能力大幅提升也解释了为什么现在团队一定是 Scrapy Playwright 双修。5. 常见问题与排查技巧实录5.1 日志看不到输出或文件为空这个坑我刚开始折腾 logging 配置时反复踩过。原因一般是Scrapy 启动时重设了 logging。如果你在 settings.py 里LOG_ENABLED True默认就是Scrapy 会在初始化时logging.basicConfig()一遍把你的 handler 全部清掉。解决办法有两个任选其一settings.py里设LOG_ENABLED False彻底让 Scrapy 不碰 logging。你还想保留 Scrapy 自带日志就在自己的配置里给scrapy命名空间单独挂 handler。或者不关 LOG_ENABLED但用scrapy.utils.log.configure_logging()来接管它内部会尊重你已经配置好的 handler。我建议直接选第一个LOG_ENABLED False 自己的setup_logging()。这样你拿到 100% 的控制权。5.2 全站爬虫重复抓取同一个 URL如果自定义去重做了还有重复排查这几个点你是不是在 spider 内部又自己写了if url not in seen的代码如果有先把这段删了。你自己维护的 set 和 Scrapy 的去重队列是两套体系内存占用大不说一旦 set 和指纹不一致重复就在所难免。URL 里是不是带着#锚点这个锚点不会发到服务器但会参与指纹计算。规范化函数里必须去掉fragment。Cookie 导致同一 URL 返回不同状态时Scrapy 会不会重新入队如果你的dont_filterTrue那 Scrapy 默认不去重这个参数要慎用。全站爬取阶段不要轻易给请求加dont_filter。5.3 爬虫突然停滞日志也没有报错典型的“安静地死掉”。我的排查思路是先看downloader/request_count和downloader/response_received_count这两个统计是否还在上涨。如果上涨网络层是通的问题在解析层或 item 处理层。如果请求数不涨了多半是调度器队列空了、或者被限速卡住了。检查DOWNLOAD_DELAY和AUTOTHROTTLE_MAX_DELAY是不是被调得太大。还有一种情况你的回调函数抛了异常但异常被 Scrapy 吞了只是在日志里打一条 ERROR。如果你日志分流没做这些 ERROR 混在几万行 INFO 里根本看不到。这也是为什么我 insist 错误日志单独一个文件的原因。最后大招把日志级别调到 DEBUG观察下载器的内部状态看到底卡在哪个环节。5.4 动态 iframe 页面抓回来是空的如果你已经上了 Playwright但解析出来还是空先检查这两件事等待时机iframe 是异步加载的你不等它加载完就去取值当然是空。wait_for_selector是基础操作还可以配合page.wait_for_load_state(networkidle)。frame 定位Playwright 里如果你直接用page.inner_text(div.content)只能拿到外层文档的内容。嵌入的 iframe 要用page.frame_locator(iframe).locator(div.content)。这是新手最容易搞错的地方。def parse_dynamic(self, response): page response.meta.get(playwright_page) if page: # 等待 iframe 中的内容渲染 frame page.frame_locator(iframe#main-frame) title frame.locator(h1.product-title).inner_text(timeout5000) # 注意inner_text 在旧版本 Playwright 可能是 inner_text() yield {title: title}5.5 日志文件占满磁盘日志轮转配置了但还是爆盘一般是你同时开了多个 handler并且每个 handler 都没有设置 maxBytes。再检查一遍RotatingFileHandler 的 maxBytes 是单次条件backupCount 要大于 1否则轮转一两次就没有空间了。还有如果你用 Docker 跑爬虫注意挂载的目录是不是无限增长建议把 logs 目录也加日志清理的定时任务。5.6 全站爬取如何优雅断点续爬全站爬完一半挂了重启却从头开始这是大忌。Scrapy 自带断点续爬的机制用JOBDIR# 启动时指定 job 目录 scrapy crawl site_crawler -s JOBDIRjobs/site_crawler这个特性的原理是Scrapy 会把已经入队但未完成的请求、去重指纹、间隔器状态全部序列化到 JOBDIR 目录下。下次用同一个 JOBDIR 启动爬虫会接着上次的进度继续跑不会重新抓已经完成的部分。我建议每次跑全站任务都带上一个独立的 JOBDIR命名方式按日期# run.py 里动态生成 from datetime import date job_dir fjobs/{date.today().isoformat()} os.environ[SCRAPY_JOBDIR] job_dir配合打开DUPEFILTER_PERSIST True和SCHEDULER_PERSIST True断点续爬才算完整SCHEDULER_PERSIST True DUPEFILTER_PERSIST True要清空历史数据重新爬怎么办删掉 JOBDIR 目录爬虫自然从零开始。6. 一个完整的全站爬取启动脚本参考把所有配置合到一起我用一个run.py作为入口避免每次敲一长串 scrapy crawl 命令还容易漏参数# run.py import sys import logging from datetime import date from utils.logger import setup_logging def main(): setup_logging(debugFalse) job_dir fjobs/{date.today().isoformat()} logging.getLogger(__name__).info(fJob 目录{job_dir}) from scrapy.crawler import CrawlerProcess from scrapy.utils.project import get_project_settings settings get_project_settings() settings.set(JOBDIR, job_dir) settings.set(SCHEDULER_PERSIST, True) settings.set(DUPEFILTER_PERSIST, True) process CrawlerProcess(settings) process.crawl(site_crawler) process.start() if __name__ __main__: sys.exit(main())这个脚本把 logging 配置、断点目录、爬虫启动全部打包部署到服务器上用nohup python run.py 就能在后台长时间运行。日志写到 logs 目录崩溃了重启就自动续爬监控数据已经在日志里躺好了。我在实际项目里用这套方案爬过一个数十万页的垂直资讯站和一个小型电商站全站覆盖率从手写 requests 时代的 60% 提升到 95% 以上而且排查问题的时间大幅减少。logging Scrapy 组合的真谛不在于某一个配置项有多神奇而在于你通过日志真正理解爬虫每一秒在干什么。最近我在尝试的是把日志里的请求耗时、错误率指标接进 Prometheus 那套链路里让爬虫的“心电图”不再只躺在文件里而是变成一条实时曲线。Scrapy 的扩展机制允许你这么玩前提是先把项目的 logging 基础打牢不然一切都是空中楼阁。希望这篇东西能帮你少踩几个我正在替你踩的坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询