ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scrapy自定义命令与扩展:打造批量调度和监控系统

Scrapy自定义命令与扩展:打造批量调度和监控系统 我最早用Scrapy的时候日常操作路径固定得乏味写个spider然后scrapy crawl xxx跑完盯着终端看几眼再手动把csv导走。爬虫少的时候这套流程没问题可一旦你的工作变成“管理一支爬虫队伍”问题就全冒出来了——几十个爬虫的启动参数散落在shell历史里跑完的结果没有人统一汇总状态只能靠肉眼盯日志想改哪个爬虫都要全局搜索命令。后来我把Scrapy的自定义命令和扩展机制吃透把原生的scrapy crawl换成了属于自己的批量调度命令又在进程内部挂了一圈监控扩展整个爬虫工具才真正称得上“专属”二字。这篇文章是我从只会写spider到把Scrapy改装成自带调度和遥测系统的完整记录。内容全部基于我实际维护过的一套爬虫项目适合已经在用Scrapy、平时被多爬虫调度和运行状态监控折磨的开发者。文章会从命令行扩展的原理讲起接着给出一份批量调度命令的完整实现再讲扩展如何给爬虫装上进度汇报、统计入库和主动熔断能力最后把我在这个过程中踩过的坑交代清楚。文中代码都比较短可以直接抄进项目里改。1. 为什么要把手伸进Scrapy的命令行和扩展系统1.1 默认命令在真实生产里的三个尴尬时刻先说三个我真实遇到的场景如果你也中招了说明你需要看下去。第一个场景是“多爬虫批量运行”。我手里有新闻、论坛、评论三类爬虫每天晚上要一起跑。原生Scrapy一次只能crawl一个spider于是我在crontab里写了三四行shell循环每个循环都要带着一堆-a参数。时间一长这些调度脚本和项目本身完全脱节改一个参数要同时改好几处还容易漏。第二个场景是“运行过程中不可观测”。爬虫跑起来之后我只能看到Scrapy默认的日志在滚动。到底抓了多少条Item、当前请求成功率是多少、有没有异常堆积全部要靠人脑在下班前盯着屏幕判断。偶尔一次凌晨跑挂第二天早上打开日志发现任务早就死了但中间那四五个小时没人知道。第三个场景是“结果没有沉淀”。任务跑完数据进了数据库但每次任务的统计信息丢了。过了三天想查一下“昨天新闻爬虫到底抓了多少条、有没有失败”只能靠回忆和推断没有任何一张表能给出答案。这三个痛点的共同根源是Scrapy默认暴露出来的操作入口和管理能力太薄。主程序只给了你一个crawl命令而真正生产需要的批量调度、运行监控、结果统计全都得自己补齐。1.2 命令与扩展的分工一个管出口一个管过程Scrapy在设计上其实把“扩展能力”分得很清楚自定义命令管理的是暴露给使用者的操作入口扩展管理的是框架运行过程中的插件逻辑。你可以把Scrapy想象成一台卡车。出厂时只有“启动发动机”这一个按钮就是scrapy crawl。你想给这台车加一个方向盘让操作者能决定跑哪条路线、拉几趟货这就是自定义命令。你想在发动机内部装传感器实时感知转速、油温、故障码在异常时自动干预这就是扩展。这两个机制是互补的。命令是给“人”用的解决的是“我想让爬虫系统做什么”扩展是给“引擎”用的解决的是“爬虫运行过程中该怎么自我感知、自我管理”。一套顺手的专属爬虫工具其实就是把这两端都改成自己能驾驭的样子。1.3 先划清的合规底线聊技术之前必须先把合规前提说清楚。自定义命令和扩展只是框架层的开发能力怎么用完全取决于场景。下面所有代码和方案都只适用于你有权采集的数据比如自己公司的站点、有授权的第三方数据源、公开接口且遵守服务条款的内容。使用前请确认目标站点的robots协议和适用法律同时控制好请求频率不要因为工具顺手就给对方服务器造成不必要的压力。这些红线比任何技术方案都重要。2. 从scrapy命令到你的代码命令行扩展的工作原理2.1 一条命令在Scrapy里的完整旅程想写自定义命令先要明白Scrapy命令行的执行链路。你每次敲scrapy xxx入口都在scrapy.cmdline的execute()。它会做几件事加载配置、扫描可用的命令模块、解析参数、找到对应的Command类、调用它的run()方法。Scrapy里命令分两类全局命令和项目命令。全局命令不依赖具体项目不怎么需要项目的settings和爬虫列表比如scrapy version、scrapy startproject。项目命令则必须在某个Scrapy项目目录下执行因为它需要加载项目的settings、spider loader和扩展配置典型例子就是scrapy crawl。我们自定义的业务命令基本都是项目命令。因为它要访问项目里的爬虫列表、读取settings、用CrawlerProcess拉起爬虫这些只有在项目环境里才有意义。对应到代码上就是Command类的requires_project True。2.2 注册自定义命令COMMANDS_MODULE 的接法Scrapy提供了官方的命令扩展点在settings.py里配置COMMANDS_MODULE指向我们自己写的命令模块即可。目录结构如下myproject/ ├── scrapy.cfg └── myproject/ ├── __init__.py ├── settings.py ├── commands/ │ ├── __init__.py │ ├── hello.py │ └── run_batch.py └── spiders/settings.py里加一行COMMANDS_MODULE myproject.commands与Django的App类似commands目录必须是一个Python包也就是要有__init__.py文件。每个.py文件对应一个命令文件名就是命令名比如hello.py对应scrapy hello。文件内部必须且只能有一个名为Command的类Scrapy会实例化这个类来执行命令。很多新手命令不生效第一反应是settings没改对实际上更常见的问题是目录少了__init__.py或者类名不叫Command。这两个细节在Scrapy源码里是硬编码约定绕不过去。2.3 Command类的核心接口逐个拆给你看一个命令类主要由下面这些成员组成理解它们各自的职责写命令就顺手了。成员作用requires_project是否为项目命令True表示需要加载项目配置syntax()返回命令的语法说明会显示在帮助信息里short_desc()一句话命令描述帮助信息会用到add_options(parser)往参数解析器里添加自定义选项process_options(args, opts)参数解析完成后做校验和预处理run(args, opts)命令的真正入口写核心逻辑有一点必须注意Scrapy命令行用的是标准库optparse不是后来主流Python生态里的argparse。所以添加参数时用parser.add_option(...)对应的方法名和风格都和argparse不一样别搞混。run(args, opts)里能通过self.crawler_process拿到当前的CrawlerProcess实例这是自定义命令能和爬虫交互的关键。启动爬虫、遍历爬虫列表、读取settings都是通过它完成的。2.4 最小可运行命令scrapy hello光看理论不如直接跑一个最小例子。在commands/hello.py里写from scrapy.commands import ScrapyCommand class Command(ScrapyCommand): requires_project False def syntax(self): return def short_desc(self): return 一个用于验证自定义命令是否生效的最小命令 def run(self, args, opts): print(hello from custom command)然后在项目根目录执行scrapy hello正常情况下会输出hello from custom command。如果没有任何输出而是报“Unknown command”按顺序排查四件事COMMANDS_MODULE字符串有没有拼错、commands目录下有没有__init__.py、文件里的类名是不是Command、当前所在的目录是不是有scrapy.cfg的项目根目录。这四个检查点能解决九成以上的命令不生效问题。3. 实现一个批量调度命令run_batch 的完整开发记录3.1 需求整理与参数设计最小命令跑通之后就可以做一个真正生产中能用的批量调度命令。我的需求很具体每天凌晨把一批固定的爬虫跑一遍支持给所有爬虫传公共参数支持限制每个爬虫的任务量跑完后能把运行记录沉淀下来供后续查询。命令的最终形态长这样scrapy run_batch --spider news --spider forum -a date2024-06-01 --limit 500 --name daily参数设计如下选项说明默认值--spider要运行的爬虫名可多次传入传all表示运行项目里全部爬虫无必须指定-a传递给爬虫的公共参数格式keyvalue可多次传入空--limit每个爬虫最多抓取多少条Item0表示不限制0--name本次运行的批次名用于统计查询自动生成时间戳选择“一条命令控制多个spider”而不是写shell for循环最大的好处是复用同一个CrawlerProcess。所有爬虫共享同一套settings、同一个Twisted事件循环日志格式统一扩展也能统一感知。shell脚本能做到多进程并发但状态共享和统计汇总要额外做复杂度反而更高。3.2 参数解析与校验参数解析的代码放在add_options和process_options里。--spider用actionappend这样同一个选项可以出现多次形成一个列表。-a父类ScrapyCommand已经帮我们注册好了解析后存在opts.spargs里是一个[keyvalue, ...]格式的列表需要手动转成字典。process_options里做参数转换和校验from scrapy.commands import ScrapyCommand from scrapy.utils.conf import arglist_to_dict class Command(ScrapyCommand): requires_project True def syntax(self): return --spider news --spider forum -a date2024-06-01 --limit 500 --name daily def short_desc(self): return 批量运行指定爬虫并统一记录运行状态 def add_options(self, parser): super().add_options(parser) parser.add_option( --spider, destspiders, actionappend, default[], metavarSPIDER, help需要运行的爬虫名可多次传入all 表示全部爬虫, ) parser.add_option( --limit, destlimit, typeint, default0, help每个爬虫最多抓取多少条 Item0 表示不限制, ) parser.add_option( --name, destrun_name, default, help本次运行批次名默认自动生成, ) def process_options(self, args, opts): super().process_options(args, opts) self.spargs arglist_to_dict(opts.spargs) if opts.spargs else {} if not opts.spiders: self.parser.error(至少指定一个爬虫使用 --spider spider_name 传入) self.spider_names [all] if all in opts.spiders else opts.spiders self.limit opts.limit self.run_name opts.run_name or datetime.now().strftime(%Y%m%d_%H%M%S) # 提前检查爬虫名是否存在避免跑到一半才报错 if self.spider_names ! [all]: available set(self.crawler_process.spider_loader.list()) unknown set(self.spider_names) - available if unknown: raise ValueError(f未知的爬虫名称: {sorted(unknown)})这里之所以用self.parser.error(...)而不是抛异常是因为optparse的parser.error()会输出帮助信息并正确退出进程使用体验更像原生命令。3.3 run() 核心逻辑多爬虫启动与统一收尾run()里最关键的几行代码是这样的def run(self, args, opts): process self.crawler_process if self.spider_names [all]: self.spider_names list(process.spider_loader.list()) if self.limit: # 在创建爬虫前设置让 CloseSpider 扩展能读到最新配置 process.settings.set(CLOSESPIDER_ITEMCOUNT, self.limit, prioritycmdline) for name in self.spider_names: process.crawl(name, batchself.run_name, **self.spargs) process.start()这里有几个值得说清楚的点。第一process.crawl(name, batchself.run_name, **self.spargs)里的batch和-a参数最终都会传给每个spider的__init__成为spider实例的属性。后续扩展想要知道“这条运行记录属于哪个批次”直接从spider.batch取就行不需要通过全局变量传递。第二process.start()内部会启动Twisted的reactor并且一个进程只能调用一次。这意味着自定义命令天然适合“一次运行、跑完退出”的场景不要试图在一个命令里循环调用start()跑第二批那是后续章节要讲的坑。第三CLOSESPIDER_ITEMCOUNT是Scrapy内置的CloseSpider扩展在读取的设置项。我想在命令行里统一限制任务量所以在创建爬虫之前把它写进settings。用prioritycmdline是为了覆盖项目配置文件里可能存在的同名设置让命令行的--limit真正生效。3.4 让结果可查配套 recent 命令批量调度命令解决的是“怎么跑”但跑完之后总要能查历史。我在项目里加了一个配套命令recent读取运行记录表把最近N条展示出来。展示需要数据支撑数据来源是下一章会实现的RunRecorder扩展它会在每个spider关闭时把统计信息写入数据库。这里先看命令侧怎么写。# commands/recent.py import os from datetime import datetime from scrapy.commands import ScrapyCommand from sqlalchemy.orm import sessionmaker from myproject.models import engine, RunRecord class Command(ScrapyCommand): requires_project True def syntax(self): return -n 10 def short_desc(self): return 查看最近N条爬虫运行记录 def add_options(self, parser): super().add_options(parser) parser.add_option( -n, --num, destnum, typeint, default10, help显示最近多少条记录默认10, ) def run(self, args, opts): Session sessionmaker(bindengine) session Session() rows ( session.query(RunRecord) .order_by(RunRecord.id.desc()) .limit(opts.num) .all() ) if not rows: print(还没有运行记录) session.close() return for r in rows: print( f{r.batch:16} {r.spider:20} f{r.start_time:%Y-%m-%d %H:%M:%S} fitems{r.items:6} errors{r.errors:4} felapsed{r.elapsed:.1f}s reason{r.reason} ) session.close()配合SQLAlchemy记录表日常查询运维记录就变成了一条命令的事scrapy recent -n 20。3.5 接入定时调度命令设计成“跑完即退出”是有意的因为这样才能干净地和系统调度器配合。我在服务器上用crontab做定时任务每天凌晨两点执行批量命令0 2 * * * cd /opt/myproject /usr/bin/python3 -m scrapy.cmdline run_batch --spider news --spider forum --limit 500 --name daily /var/log/scrapy_batch.log 21如果项目用了虚拟环境直接把/usr/bin/python3换成虚拟环境里的python路径即可。这样做的好处是调度器只负责到点拉起进程进程内部怎么跑完全由自定义命令控制跑完自动退出不会残留常驻进程。4. 用扩展为爬虫装上遥测和生命周期钩子4.1 扩展、中间件、信号三者的边界别搞混很多Scrapy用户对扩展、中间件、信号三个概念分不清这直接导致想写插件时不知道用哪个机制。先理清楚。机制定位典型用途扩展Extension框架级插件挂在引擎生命周期上全局监控、统计上报、定时任务、爬虫开关中间件Middleware介入请求/响应/Item的处理链加Header、改URL、清洗Item、处理异常信号Signal事件通知机制让扩展订阅某个事件比如spider_opened、item_scraped用一个比喻来记中间件是流水线上的工位每个Request或Item都要经过信号是生产车间的广播喇叭某个事件发生时就响一下扩展是广播的管理员它负责听广播并做出反应。所以在Scrapy里写扩展核心就是把自己的函数注册到信号上。4.2 进度哨兵 ProgressReporter我写的第一版扩展叫ProgressReporter作用很直接每个爬虫运行期间定时打印当前抓取量、请求量、错误数量。它解决的问题是之前提到的“运行过程中不可观测”。# extensions/progress_reporter.py from datetime import datetime from twisted.internet import task from scrapy import signals from scrapy.exceptions import NotConfigured class ProgressReporter: def __init__(self, crawler, interval): self.stats crawler.stats self.interval interval self._tasks {} self._start {} classmethod def from_crawler(cls, crawler): interval crawler.settings.getfloat(STATUS_REPORT_INTERVAL, 15) ext cls(crawler, interval) crawler.signals.connect(ext.spider_opened, signalsignals.spider_opened) crawler.signals.connect(ext.spider_closed, signalsignals.spider_closed) return ext def spider_opened(self, spider): self._start[spider.name] datetime.now() t task.LoopingCall(self._report, spider) t.start(self.interval) self._tasks[spider.name] t def _report(self, spider): s self.stats.get_stats() elapsed (datetime.now() - self._start.get(spider.name, datetime.now())).total_seconds() spider.logger.info( progress items%s requests%s errors%s elapsed%.1fs, s.get(item_scraped_count, 0), s.get(response_received_count, 0), s.get(log_count/ERROR, 0), elapsed, ) def spider_closed(self, spider, reason): t self._tasks.pop(spider.name, None) if t and t.running: t.stop() self._start.pop(spider.name, None)这里用到了Twisted的LoopingCall。很多人会疑惑为什么不直接用time.sleep加循环。因为Scrapy引擎跑在Twisted的事件循环里任何阻塞调用都会卡住整个爬虫进程。LoopingCall是异步定时器不会阻塞网络请求这才是官方推荐的做定时任务的方式。注意我用字典按spider.name隔离每个爬虫的任务而不是只用一个self.task。否则多个spider并发时前一个spider的定时任务会永远停不下来最后一堆僵尸定时器。4.3 运行记录入库 RunRecorder有了进度哨兵解决了运行中不可观测的问题。接着就要把每次任务的最终统计沉淀下来这就是上一章recent命令的数据来源。先定义一个简单的数据库模型放在models.py里# models.py from sqlalchemy import create_engine, Column, String, Integer, Float, DateTime from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class RunRecord(Base): __tablename__ run_record id Column(Integer, primary_keyTrue) batch Column(String(64), indexTrue) spider Column(String(100), indexTrue) start_time Column(DateTime) finish_time Column(DateTime) reason Column(String(32)) items Column(Integer, default0) requests Column(Integer, default0) errors Column(Integer, default0) elapsed Column(Float, default0.0) engine create_engine(sqlite:///run_records.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine)然后扩展RunRecorder在spider_closed时把统计信息写进去# extensions/run_recorder.py from datetime import datetime from scrapy import signals from scrapy.exceptions import NotConfigured from myproject.models import Session, RunRecord class RunRecorder: classmethod def from_crawler(cls, crawler): # 未配置数据库地址时直接跳过优雅降级 if not crawler.settings.get(RUN_REPORT_DB_URL): raise NotConfigured(未配置 RUN_REPORT_DB_URLRunRecorder 不启用) ext cls() crawler.signals.connect(ext.spider_opened, signalsignals.spider_opened) crawler.signals.connect(ext.spider_closed, signalsignals.spider_closed) return ext def spider_opened(self, spider): self._start getattr(self, _start, {}) self._start[spider.name] datetime.now() def spider_closed(self, spider, reason): stats spider.crawler.stats.get_stats() start self._start.get(spider.name, datetime.now()) rec RunRecord( batchgetattr(spider, batch, ), spiderspider.name, start_timestart, finish_timedatetime.now(), reasonreason, itemsstats.get(item_scraped_count, 0), requestsstats.get(response_received_count, 0), errorsstats.get(log_count/ERROR, 0), elapsed(datetime.now() - start).total_seconds(), ) session Session() session.add(rec) session.commit() session.close()这个扩展的关键是from_crawler类方法它是Scrapy扩展的标准入口。扩展加载时框架会调用from_crawler在方法里注册信号处理函数。如果扩展不满足启用条件直接抛NotConfiguredScrapy就会跳过它不会影响爬虫主流程。4.4 从扩展里主动干预停止卡死爬虫与上游告警遥测只是第一步扩展还能干更主动的事。我在项目里遇到过某个爬虫因为上游站点响应异常卡在某个页面重试队列里迟迟不结束。后来在扩展里加了一个“空闲熔断”逻辑记录最近一次成功抓取Item的时间如果超过设定阈值没有任何新结果就主动关闭这个spider。核心逻辑类似下面这段只展示判断部分def _check_idle(self, spider): last self._last_item.get(spider.name) if last is None: return idle_seconds (datetime.now() - last).total_seconds() if idle_seconds self.max_idle_seconds: spider.logger.warning(spider idle too long, closing by extension) spider.crawler.engine.close_spider(spider, idle_timeout)主动关闭要用engine.close_spider而不是直接抛异常。这样Scrapy会把剩余队列正常收尾并把关闭原因写进最后的统计里和手动CtrlC的效果不一样。如果想做异常通知可以用spider_error信号在信号回调里统计错误次数超过阈值就调用内部告警接口发通知。注意不同Scrapy版本信号参数略有差异写之前查一下你所用版本的官方API签名。4.5 扩展启用细节EXTENSIONS优先级与NotConfigured扩展写好后在settings.py里启用EXTENSIONS { myproject.extensions.ProgressReporter: 500, myproject.extensions.RunRecorder: 600, }字典里的数字表示加载顺序和优先级。Scrapy会按数字从小到大依次实例化扩展同一声明周期内数字小的先执行。日常写业务扩展用500左右的数字最稳不会和内置扩展的默认优先级产生奇怪冲突。另外扩展最好按职责拆分不要把所有逻辑堆在一个扩展类里。进度上报、结果入库、异常熔断各自是一个扩展通过settings开关控制。这样排查问题的时候能快速定位也不会因为某一个扩展抛异常导致整个爬虫不能用。5. 把命令和扩展缝起来一套轻量爬虫运维工具链5.1 最小落地架构到这里命令和扩展各司其职整个工具链已经可以串起来了。最小落地方案如下crontab或系统计划任务负责定时拉起进程。**scrapy run_batch**负责按批次运行多个spider并接收公共参数。ProgressReporter扩展负责运行过程中的实时日志让你随时知道爬虫有没有在干活。RunRecorder扩展负责在spider关闭时把统计信息写入SQLite。**scrapy recent**负责查询历史运行记录。这套架构的核心思路是“进程级隔离”。每个批次任务都是独立的Scrapy进程跑完就退出不常驻、不互相影响。只要数据库连接正常即使某个批次中途崩了已写入的记录也不会丢。整个项目需要新增的文件也就四五个commands/目录、extensions/目录、models.py再加settings里几行配置。比起引入一整套管理平台这个成本低得多。5.2 后续可以继续加的东西这套骨架搭好之后往上加东西非常顺手因为操作入口和数据出口都已经打通了。比如报表生成。RunRecord表里存了每次任务的批次、爬虫、耗时、Item数、错误数我后来写了一个scrapy report --day 2024-06-01命令专门从表里聚合出当天的数据概况输出一个Markdown文件直接贴到群里当日报。再比如可视化。数据库表结构固定之后用FastAPI或者Streamlit包一层就是一个爬虫运行看板。前端只查run_record表完全不需要碰爬虫代码。还有很多爬虫内部用的是playwright渲染动态页面、处理iframe嵌套内容和这套命令扩展机制完全不冲突。命令负责调度进程扩展负责感知状态爬虫内部用什么技术抓数据框架层无感知。5.3 什么情况下你才需要上重型编排平台自定义命令加扩展的组合非常轻但并不是万能药。我的判断标准是爬虫数量在几十个以内、运行环境就是一台或几台服务器、调度逻辑主要是定时批次执行这套方案完全够用一旦需要分布式调度、多人协作操作、Web管理界面、细粒度权限控制就应该换重型平台。傻瓜式判断法如果你发现自己在给自定义命令不断地加“远程部署”“任务依赖”“失败重试”之类的功能而这些东西平台里早就有那就是该换平台的时候了。工具和平台没有优劣只有合不合适。6. 踩坑记录自定义命令和扩展最容易翻车的几个地方6.1 命令不生效的四个常见原因这个前面提过值得再汇总一次。命令写出来却执行不了90%是这四个原因COMMANDS_MODULE配错或字符串路径少了包名层级。commands目录下缺少__init__.pyPython不认它是包。文件里的类名不叫CommandScrapy加载时找不到。没有在项目根目录下执行命令Scrapy找不到scrapy.cfg加载不了项目配置。排查顺序建议先看scrapy help里有没有新命令没有就检查注册配置有但运行报错再查代码逻辑。6.2 crawler_process.start() 只能调一次的坑这是所有写自定义命令的人都会撞上的坑。CrawlerProcess.start()底层是reactor.run()Twisted的reactor一旦停止同一个进程里不能再次启动。如果你在run()里写了一个for循环想分批跑完爬虫A再跑爬虫B在循环里两次调用start()第二次必然抛ReactorNotRestartable。解决办法有两个。第一把“跑第二批次”的诉求交给外部调度器也就是每次新起一个进程这是最简单也最符合命令设计的方式。第二如果真的需要在同一个进程里串行跑多批放弃CrawlerProcess改用CrawlerRunner自己管理reactor生命周期。第二种方案复杂度高不少我建议普通业务场景直接选第一种。6.3 扩展里做阻塞操作导致事件循环卡死扩展代码运行在Twisted的reactor线程里所有爬虫的请求调度都在同一个事件循环上。如果你在扩展的信号回调里用requests.get()同步请求外部接口或者执行一个需要好几秒的数据库批量操作整个爬虫进程都会被卡住表现就是“日志不动、请求不发、看起来像死了”。解决办法也很明确轻量快速的操作比如写一条SQLite记录可以直接做耗时长、涉及网络I/O的操作放到twisted.internet.threads.deferToThread里执行或者把数据投递到消息队列由独立消费者处理。记住一个原则扩展里不要做会让事件循环等待超过几十毫秒的事。6.4 日志重复、信号处理与Windows兼容自定义命令里的print、扩展里的spider.logger、Scrapy自身的日志三个日志来源如果不统一终端输出会非常混乱。建议业务代码统一走logging或spider.loggerprint只用于命令里最后的结果展示比如recent命令打印表格因为它本身就是给人看的输出。信号处理的坑也值得注意。信号回调如果出异常Scrapy默认会捕获并记日志不会导致进程崩溃但会让人误以为“回调没执行”。排查时先看有没有被吞掉的异常日志再怀疑信号压根没注册上。Windows环境下自定义命令的体验会差一些CtrlC终止爬虫时进程不一定能干净退出Twisted在Windows上的信号处理能力有限。如果你的生产环境是Windows建议把爬虫运行部分放到WSL或Linux容器里否则你会花很多时间在和信号处理打架上。6.5 工具化之后的一点体会回头看我最初写这套东西的动机其实就是“不想每天重复同样的手工操作”。真正把自定义命令和扩展用起来之后最大的感受不是省了多少敲命令的时间而是整个爬虫项目的“可运营性”上来了新爬虫接入只需要在项目里加一个spider文件批量调度命令自动就能--spider all跑起来出问题先查run_record表再翻对应批次的日志定位时间从小时级降到分钟级给同事交接也不需要发一大段shell命令只需要告诉他们两个命令scrapy run_batch ...和scrapy recent ...。这种“工具感”才是Scrapy让我觉得真正值得投入时间去折腾的地方。
返回列表