☰
Scrapy+Scrapyd+Gerapy实战:从部署调度到动态页面采集
2026/10/3 1:10:53 网站建设 项目流程

1. 为什么2024年还要折腾scrapyd+gerapy这套组合

先说个可能让新手意外的事实:2024年市面上的爬虫调度方案已经多到挑花眼,有Docker+Airflow的、有K8s+Argo的、还有一堆商业平台在用,但我在团队里做技术选型时,还是把scrapy+scrapyd+gerapy这套组合捡了回来。原因很简单——它依然是中小规模爬虫项目里性价比最高的一条路。

先给没接触过的朋友划个重点:Scrapy是爬虫框架,解决"怎么爬"的问题;Scrapyd是部署和HTTP调度服务,解决"爬虫怎么跑起来、怎么被远程调用"的问题;Gerapy是可视化管理和监控界面,解决"这么多爬虫项目我怎么看得过来"的问题。三层各管一摊,合在一起就是一条完整的爬虫生产线。

有人可能会问,现在都容器化了,直接用Docker Compose编排不就行了?当然可以,但有一个很现实的问题:团队里不是每个人都熟悉容器和编排工具的。我在的实际项目里,运营和部分业务同学也需要偶尔看一眼爬虫状态、手动触发一次采集,如果只给他们一个Docker命令行,基本等于没给。Gerapy的可视化界面把这些操作变成了鼠标点击,门槛一下子就降下来了。

还有个关键点是Scrapyd的HTTP API非常简单。部署用addversion.json,启动用schedule.json,查看状态用listjobs.json,任何语言只要会发HTTP请求就能接入。这意味着你后续想接定时任务、想接消息队列、想写个运维脚本,都非常顺手。相比直接把爬虫进程裸跑在服务器上再用systemd管理,Scrapyd多了一层标准的进程管理和日志查看能力,排查问题时方便太多。

当然,这套组合也不是没有槽点。Scrapyd官方已经很久没更新了,Gerapy的维护也不算活跃,这些问题后面会详细说。但结论先放在这里:如果你的项目规模在几十个爬虫以内、服务器数量不超过几台、团队需要一定程度的可视化操作,这套组合2024年依然能用,而且用好了非常稳。

2. 环境搭建里最容易翻车的几个细节

这一节先解决"跑起来"的问题。我假设你用的是Linux服务器(Ubuntu/CentOS都可以),Python版本建议3.8到3.11之间。为什么不建议太新的版本?因为Scrapyd和Gerapy的依赖里有些老包,Python 3.12以上偶尔会碰到编译报错,没必要在这个环节浪费生命。

2.1 Python虚拟环境是底线

我见过太多人图省事,直接把scrapy、scrapyd、gerapy全部pip装到系统Python里,结果三个月后系统一升级,依赖全崩。强烈建议每个项目建一个独立虚拟环境,或者至少用一个统一的虚拟环境把爬虫相关的东西都隔离出来。

python3 -m venv /data/envs/spider_env source /data/envs/spider_env/bin/activate pip install --upgrade pip pip install scrapy scrapyd gerapy

这里有个坑:Gerapy和Scrapyd在某些版本组合下会依赖不同版本的Twisted,直接pip install三者可能触发依赖冲突。如果遇到这种情况,先不要慌,可以手动指定Twisted版本,我这边的稳定组合是:

pip install Twisted==22.10.0 pip install scrapy==2.11.0 pip install scrapyd==1.2.1 pip install gerapy==0.9.12

这个组合我在多台服务器上验证过,兼容性比较稳。如果后面想用Playwright配合Scrapy做动态页面采集,Scrapy 2.11也有比较好的支持,这一点后面专门讲。

2.2 Scrapyd的监听地址与端口设置

Scrapyd默认只监听127.0.0.1:6800,如果你打算在本地开发、服务器部署,这个默认值其实是安全的。但如果你有多台服务器需要互相调度,或者想从另一台机器访问Scrapyd的API,就得改配置。

Scrapyd的配置文件在虚拟环境路径下的scrapyd.conf,一般在/data/envs/spider_env/lib/python3.10/site-packages/scrapyd/scrapyd.conf。找到[scrapyd]这一段:

[scrapyd] bind_address = 0.0.0.0 http_port = 6800 max_proc = 4

把bind_address改成0.0.0.0,然后重启Scrapyd。注意,如果你直接暴露在公网且没有加防火墙规则,务必加上鉴权或IP白名单,否则任何人都可以往你的服务器上部署和运行任意代码,这等于裸奔。

提示:Scrapyd本身没有用户认证机制,生产环境建议用Nginx做反向代理并加Basic Auth,或者用安全组规则只放行特定IP的6800端口。

2.3 Gerapy的初始化与数据库

Gerapy安装完后,先要执行初始化命令:

gerapy init cd gerapy gerapy migrate gerapy createsuperuser gerapy runserver 0.0.0.0:8000

gerapy init会生成一个gerapy目录,里面包含projects目录和gerapy.sqlite3数据库文件。gerapy migrate会创建数据表,这一步不能跳过,否则后面访问页面会报错。createsuperuser用来创建管理员账号,登录后台管理界面需要用到。

我在实际操作中发现的几个问题:

  • Gerapy的projects目录就是爬虫项目的存放目录,但Gerapy本身不直接识别里面的scrapy项目,需要通过Gerapy管理界面的"项目部署"功能导入。导入时它会把整个项目打包并上传到Scrapyd,这一步在网页上操作即可。
  • 如果你在服务器上跑Gerapy,希望外网也能访问,runserver 0.0.0.0:8000的写法没问题,但Gerapy是一个Django应用,生产环境建议用gunicorn或者uwsgi来跑,单纯用runserver在并发高时会不够稳定。
  • 默认的gerapy.sqlite3是SQLite,数据量小完全够用。如果爬虫任务多到每天几万条调度记录,再考虑迁移到MySQL,常规规模没这个必要。

3. Scrapy项目如何接入Scrapyd:部署与调度的核心链路

很多人把scrapy项目部署到scrapyd这一步搞不清楚,关键在于Scrapyd并不是把源代码文件夹复制到某个目录就能跑的,它需要接收一个打包好的Egg文件。Gerapy做的事情,本质上就是帮你完成这个打包上传过程,然后再去调用Scrapyd的API。

3.1 用Gerapy部署的完整流程

  1. 在Gerapy管理页面的"项目"菜单中,选择"创建项目",填写项目名称和项目路径。
  2. 编辑项目配置,主要是指定Scrapyd服务器的地址,比如http://127.0.0.1:6800。
  3. 点击"部署",Gerapy会读取项目里的scrapy.cfg,将项目打包为Egg并上传到Scrapyd。

这个流程看似简单,但有三个配置细节决定成败:

  • scrapy.cfg里的[deploy]段需要正确配置。通常用默认的url = http://localhost:6800/和project = 项目名即可。如果你用的是Gerapy的部署界面,它会在上传时自动拼接,不需要手动改。
  • 项目的spiders目录下必须有爬虫文件,否则打包后没有可用的spider,调度时铁定报错。常见坑是新建了一个scrapy项目但还没写爬虫,就去部署,结果列表里找不到任何爬虫名。
  • Egg打包时Scrapy版本要一致。如果开发机用Scrapy 2.11打包后传到服务器上,而服务器上Scrapyd运行的是Scrapy 1.8,很可能出现兼容问题。所以建议开发环境和服务器环境的Scrapy版本保持一致,或者直接从服务器上用Gerapy拉取代码、在服务器上打包部署,这样版本必然一致。

3.2 绕过Gerapy,直接用Scrapyd API

Gerapy虽然方便,但如果你有自动化需求——比如写个脚本在每天凌晨定时调度爬虫——直接调API会更顺手。

部署项目到Scrapyd的标准流程是:

  1. 在项目目录下执行:
scrapyd-deploy default -p 项目名

这会把项目打包并上传,返回一个json,里面有project、version、spiders等信息。注意,scrapyd-deploy命令来自scrapyd-client包,不是Scrapyd自带的,要额外安装:

pip install scrapyd-client
  1. 查看当前部署的项目:
curl http://127.0.0.1:6800/listprojects.json
  1. 调度爬虫:
curl http://127.0.0.1:6800/schedule.json -d project=项目名 -d spider=爬虫名

返回结果类似:

{"node_name": "ubuntu", "status": "ok", "jobid": "..."}
  1. 查看任务状态:
curl http://127.0.0.1:6800/listjobs.json?project=项目名

这里能看到pending、running、finished三类任务,分别对应排队中、运行中、已完成的任务。

3.3 Scrapyd的进程管理和并发控制

Scrapyd默认支持max_proc = 4,意思是同时最多跑4个spider进程。如果你的爬虫任务很多,会发现后面的任务全部在pending状态排队。这个数字可以根据服务器性能往上调,但要留足内存给爬虫进程。我一般用1核2G的服务器跑2个进程,4核8G的服务器跑8个进程,具体还要看爬虫本身吃多少内存。

还有一个重要的点是Scrapyd的超时设置。默认情况下,Scrapyd不会主动杀掉运行时间过长的爬虫,任务会一直挂着。如果某个爬虫因为目标网站反爬或者网络异常卡住了,它会一直占用进程和内存。解决方案有两个:

  • 在爬虫代码里用DOWNLOAD_TIMEOUT设置请求超时。
  • 写一个定时脚本,调用cancel.json接口去取消运行时间过长的任务。
curl http://127.0.0.1:6800/cancel.json -d project=项目名 -d job=jobid

这个脚本我放在Crontab里,每10分钟跑一次,超过30分钟没结束的任务直接取消并记录日志。实测下来对稳定性提升非常明显。

4. 调度层面的扩展玩法:定时任务、参数传递、去重策略

光能部署和手动调度还不够,生产环境里最常用的是定时任务和灵活的爬虫参数传递。这一节讲几个我在项目中高频使用的技巧。

4.1 使用Crontab定时调度

Scrapyd本身没有内置定时调度功能,但结合Crontab是极简方案:

0 2 * * * /data/envs/spider_env/bin/python /data/scripts/schedule_daily.py

schedule_daily.py里面就干一件事,调用schedule.json接口:

import requests def schedule(): data = { "project": "my_project", "spider": "daily_spider" } resp = requests.post("http://127.0.0.1:6800/schedule.json", data=data) print(resp.json()) if __name__ == "__main__": schedule()

为什么不用Gerapy的定时任务?Gerapy本身也有定时调度功能,但我尝试下来它的界面定时配置在部分版本中有Bug,配置了不生效。用Crontab的优点是稳定、直白、可版本化管理。如果你需要更复杂的调度逻辑,比如按依赖顺序执行多个爬虫,Crontab就有点力不从心了,这时候可以引入我后面会说到的简单DAG脚本。

4.2 向爬虫传递自定义参数

有很多场景需要动态传参,比如根据日期爬取数据:

curl http://127.0.0.1:6800/schedule.json -d project=my_project -d spider=daily_spider -d date=2024-06-01

在Scrapy爬虫里通过__init__方法接收即可:

import scrapy class DailySpider(scrapy.Spider): name = "daily_spider" def __init__(self, date=None, *args, **kwargs): super().__init__(*args, **kwargs) self.date = date or datetime.now().strftime("%Y-%m-%d") def start_requests(self): yield scrapy.Request( url=f"https://example.com/data?date={self.date}", callback=self.parse )

这个date参数会在HTTP请求的POST字段中自动传递给爬虫,非常方便。注意参数名不能和Scrapy内置参数(如name、log、settings等)冲突,否则会出现意外行为。

4.3 多爬虫之间的依赖调度

我碰到过一种典型场景:爬虫A先采集列表页,爬虫B再根据A的结果去采集详情页。这种前后依赖关系,用Crontab写死时间不可靠,最稳妥的做法是写一个简单的爬虫编排脚本。

思路其实不复杂:先用listjobs.json确认B没有在运行,再调用schedule.json启动B,然后每隔几秒轮询B的状态,确认结束后再启动下一个。我在脚本里还会加一个失败重试机制,连续失败三分钟就发告警。

这个编排脚本本质上就是一个小型调度器。如果以后爬虫数量上来了、依赖关系变复杂了,可以考虑用Airflow或者Prefect来替代,但对当前规模来说,一个300行的Python脚本完全够用,还不需要额外维护基础设施。

4.4 爬虫去重策略与增量爬取

Scrapy默认的去重是基于Request的URL指纹,同一URL在同一个任务里不会重复请求。但跨任务去重,比如每天定时爬取同一个网站,URL指纹是存放在内存里的,任务结束就没了,第二天再跑会全部重新爬。这在数据量大的场景会浪费资源。

解决方案是在Scrapy里配置一个自定义的DupeFilter,把去重指纹持久化到Redis或MySQL中。我常用的做法是接入scrapy-redis,或者直接自己写一个RFPDupeFilter子类,把request_fingerprint存到Redis的Set里。

import redis from scrapy.dupefilters import RFPDupeFilter class RedisDupeFilter(RFPDupeFilter): def __init__(self, path=None, redis_host="127.0.0.1", redis_port=6379, redis_key="dupefilter"): self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True) self.redis_key = redis_key super().__init__(path) def request_seen(self, request): fp = self.request_fingerprint(request) added = self.redis.sadd(self.redis_key, fp) return added == 0

然后在settings.py里指定:

DUPEFILTER_CLASS = "my_project.dupefilters.RedisDupeFilter"

这样一来,同一个URL在跨任务、跨天的爬取中就不会被重复请求。这个改动对长期运行的爬虫项目几乎是必修课,否则数据量起来之后,网络请求和存储压力都会成倍增加。

5. 动态页面采集:Scrapy如何配合Playwright绕过iframe的地狱

标题里带了"scrapy playwright 动态 iframe"这个热词,说明很多人正在被动态页面折磨。这一节我就把这块单独拿出来讲透。

5.1 什么情况需要引入Playwright

Scrapy原生是一个基于HTTP请求的同步/异步爬虫框架,它拿到的是服务器直接返回的HTML。如果页面里的关键数据是通过JavaScript动态渲染出来的,尤其是数据藏在iframe里,或者需要滚动加载、点击按钮后才出现,纯Scrapy是搞不定的。

之前我做过一个数据采集项目,目标页面是典型的登录后查看报表的站点,整个报表区域被嵌在iframe里,iframe内部又通过Ajax请求异步拉数据。直接请求外层的HTML只能拿到一个iframe标签,什么都抓不到。那时候我尝试过scrapy-splash(一个基于Splash渲染服务的中间件),但Splash需要在服务器上额外跑一个Docker容器,而且对复杂的JS交互支持不算好。后来我把方案切到了scrapy-playwright,问题迎刃而解。

5.2 scrapy-playwright实际配置流程

首先安装:

pip install scrapy-playwright playwright install chromium

这里有个容易踩的坑:只执行pip install scrapy-playwright是不够的,必须再执行playwright install来下载浏览器内核。如果下载很慢或者失败,可以考虑设置镜像环境变量,或者直接指定浏览器路径。

然后在Scrapy项目的settings.py中启用DownloaderMiddleware:

DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

第二步的TWISTED_REACTOR非常关键,不设置这行,Scrapy启动时会报错,因为Playwright的异步机制依赖AsyncioReactor。

接下来在爬虫的Request中指定meta:

import scrapy class IframeSpider(scrapy.Spider): name = "iframe_spider" def start_requests(self): yield scrapy.Request( url="https://example.com/dashboard", meta={"playwright": True}, callback=self.parse ) async def parse(self, response): # 页面已经用浏览器渲染完成,可以拿到iframe内的内容 title = response.xpath("//title/text()").get() yield {"title": title}

如果想直接获取iframe内的完整HTML,可以在Request中指定playwright_page_methods来等待页面加载完成,然后在parse里用response.meta['playwright_page']获取Page对象:

import scrapy class IframeSpider(scrapy.Spider): name = "iframe_spider" async def parse(self, response): page = response.meta["playwright_page"] # 切换进iframe frame = page.frame(name="mainFrame") if frame: content = await frame.content() text = await frame.inner_text("body") yield {"frame_text": text}

这里用的是Playwright的Page对象操作方法,在异步parse函数里可以执行await,非常灵活。Playwright的Page对象功能远不止取iframe内容,还能模拟点击、滚动、输入文字、等待某个元素出现,相当于把整个浏览器自动化能力都搬进了Scrapy的爬虫代码里。

5.3 scrapy-playwright的性能与注意事项

引入Playwright后,每个Request都会启动一个浏览器上下文(BrowserContext),这个开销比纯HTTP请求大很多。实测下来,纯Scrapy每秒能跑几十个请求,加了Playwright后基本每秒只能处理一到三个页面。所以不要对所有请求都无脑开启Playwright,而是只对必要的入口页面开启,其他数据接口请求还是走原生HTTP。

另外,目标网站如果对浏览器指纹有检测,Playwright默认的浏览器指纹可能会被识别为自动化工具。常规的绕法是把--disable-blink-features=AutomationControlled这个参数加上:

PLAYWRIGHT_LAUNCH_OPTIONS = { "headless": True, "args": [ "--disable-blink-features=AutomationControlled", ] }

再看一个常见问题:如果目标站点的iframe和主页面不同域,Playwright默认是可以访问的,因为它在浏览器内部直接处理,但如果需要跨域存储Cookie或携带登录态,要记得用storage_state参数来加载已有的登录信息。这个细节在处理需要登录的动态页面时非常实用。

6. 常见问题排查思路:从404到Timeout的完整排障链路

运营了快一年这套框架,积累了不少排查经验。这一节把我遇到的高频问题整理出来,按照从下到上的排查顺序写,方便你照着操作。

6.1 部署成功但Spider列表为空

问题的现象是:Gerapy显示部署成功,但在调度页面找不到任何spider。第一步先确认项目里有没有真正的spider文件。Scrapy识别spider的方式是扫描spiders包里的所有类,并检查是否继承自scrapy.Spider。如果这个类被写在一个非spiders目录的模块里,Scrapy是认不出来的。

再进一步,用命令行验证:

scrapy list

如果这里能列出spider,说明项目本身没问题。接着在项目目录下重新执行:

scrapyd-deploy default -p 项目名

仔细观察返回的JSON,里面会列出spiders字段。如果这个字段是空的,问题多半出在打包环节。有可能是setup.py里没正确配置包名,导致打包时没有包含spiders模块。检查项目根目录下的setup.py:

from setuptools import setup, find_packages setup( name="my_project", version="1.0.0", packages=find_packages(), )

find_packages()会自动找到所有包,但如果项目里有一个空的__init__.py缺失,也会导致模块识别失败。这个坑很隐蔽,单独看代码完全没问题,但打包后就找不到模块。

6.2 调度任务一直Pending不启动

任务显示pending状态说明Scrapyd接收到了调度请求,但当进程数量达到max_proc上限时,新任务就会排队。这时先用:

ps aux | grep scrapyd

看Scrapyd主进程是否在运行,再看是否有多个Scrapyd进程在监听同一个端口。我遇到过在开发环境分组测试时,一不小心在另一个虚拟环境又起了一个Scrapyd,两个进程抢同一个端口,结果新部署的任务全部失败。

还有一个隐蔽原因是Scrapyd的max_proc是针对每个项目生效的,不是全局的。如果你有多个项目,A项目的进程占满了4个名额,B项目的任务依然可以跑。但如果服务器内存已经耗尽,操作系统会频繁触发OOM,Scrapyd进程本身可能被杀死,进程PID变了,通过listjobs.json看到的反而是一切正常,但实际任务根本跑不起来。这种时候看dmesg和journalctl日志最有效。

提示:如果发现Scrapyd经常挂,优先检查服务器内存和Swap配置,别急着改并发数。

6.3 爬虫抛错但Gerapy日志里看不到详情

Gerapy能看到任务状态是运行中还是完成,但看不到具体的spider日志。排查时优先通过Scrapyd日志目录查看:

tail -f /data/logs/scrapyd/my_project/spider_name/日志文件名.log

Scrapyd默认把每个任务的日志写到logs目录,具体路径与项目名、spider名和jobid有关。如果你在scrapyd.conf里配置了logs_dir = /data/logs/scrapyd,那日志就在这个目录下按项目名/spider名/jobid.log存放。

如果日志文件是空的,一种可能是spider还没真正启动就崩了,另一种可能是Scrapy的日志级别被设置成了WARNING,正常输出被过滤掉了。排查时可以在settings.py临时把LOG_LEVEL改成DEBUG,重新部署后看详细日志。

6.4 网络超时与反爬识别

爬虫运行过程中最常见的错误就是Timeout、403、418等。优先用Scrapy的retry中间件来兜底,默认已经开启,但重试次数和重试策略需要按目标网站调整:

RETRY_ENABLED = True RETRY_TIMES = 3 RETRY_HTTP_CODECS = [500, 502, 503, 504, 408, 429] DOWNLOAD_TIMEOUT = 15

DOWNLOAD_TIMEOUT很关键,如果一个请求卡在网络I/O上,默认可达几十秒,导致整个爬虫运行时间变长。把Timeout压到15秒,配合重试3次,既不会太激进,也不会因为短暂网络抖动就丢数据。

如果目标网站返回403,说明服务器识别了请求头。这时候要配置User-Agent中间件,最好配合scrapy-fake-useragent,在每次请求时轮换User-Agent。我还会同步设置延时,比如DOWNLOAD_DELAY = 1,控制请求频率,避免触发更严厉的封禁策略。

6.5 磁盘被日志占满

这是个很容易被忽略的运维问题。Scrapyd会为每个任务写一份日志,长时间运行下来日志文件占用几个GB很正常。我的解决方法是写一个日志轮转脚本,每天凌晨压缩并清理超过7天的日志:

find /data/logs/scrapyd -name "*.log" -mtime +7 -exec gzip {} \; find /data/logs/scrapyd -name "*.log.gz" -mtime +30 -exec rm -f {} \;

把这个脚本放进Crontab,基本不用再操心磁盘告警。

7. Karapy遇到瓶颈时:Docker化改造与保留现有代码的迁移思路

当你把这套组合跑到一定规模后,可能会遇到性能和维护上的瓶颈。这一节聊聊怎么在保留现有Scrapy爬虫代码的前提下,逐步把部署层优化成Docker方案。

7.1 什么时候该考虑迁移

几个信号比较明显:

  • Scrapyd频繁重启或卡死:任务量高并发时,Scrapyd单进程性能开始吃紧。
  • 环境不统一:多台服务器上Python版本、系统库版本各不相同,每次换机器都要重新排错。
  • 需要弹性扩容:比如大促期间爬虫任务量翻倍,手动去几台服务器上分别启动任务,太费人力。
  • 日志和监控没法统一:Scrapyd自带的日志分散在各台机器上,想看全局状态很费劲。

如果你的情况符合其中两三条,就可以考虑迁移了。

7.2 Docker化的最小改造路径

最粗暴但有效的方式:把Scrapy项目自身容器化,Scrapyd和Gerapy先保持原状不动。这样对现有代码的侵入为零,只是把之前手动创建虚拟环境、安装依赖的过程变成了构建Docker镜像。

一个最小可用的Dockerfile长这样:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["scrapyd"]

然后在服务器上:

docker build -t my_spider_image:latest . docker run -d -p 6800:6800 --name scrapyd my_spider_image:latest

这样Scrapyd就跑在Docker里了,外部通过6800端口访问和以前没有任何区别。Gerapy可以继续跑在宿主机上,通过http://127.0.0.1:6800连接Scrapyd,完全无缝衔接。

如果你连Gerapy也想容器化,官方其实没有提供现成镜像,但Gerapy本质是个Django应用,基于python:3.11-slim再安装gerapy,然后跑gerapy runserver就可以了。要注意的是Django的ALLOWED_HOSTS需要设置成*或者你自己的域名,否则访问时会出现Bad Request。

7.3 编排层要不要上框架

当爬虫数量过了几十个、依赖关系也复杂起来以后,Crontab和手写脚本会越来越难维护。这时候可以引入一个轻量级的调度框架。

我试过的方案中,Prefect比Airflow更适合爬虫场景,因为它的学习成本低,对Python项目的侵入性小。你可以在现有脚本基础上加一个装饰器:

from prefect import flow, task @task def crawl_list(): # 调用scrapyd schedule.json pass @task def crawl_detail(): # 等list跑完再调用 pass @flow def daily_pipeline(): crawl_list() crawl_detail() if __name__ == "__main__": daily_pipeline()

如果不想引入重型框架,还有一个折中方案:用RabbitMQ或Redis做简单的任务队列,把爬虫调度请求发到队列里,由一组Worker去消费并调用Scrapyd。这个方案的优点是灵活、可控、不依赖外部调度服务,缺点是很多逻辑需要自己写。

我个人对架构演进的态度是:不要因为"别人都用K8s"就去上K8s。你只需要在业务真正需要的时候,往前迈一步就好。对大部分中小团队来说,Scrapyd+Gerapy这套组合跑到几十个爬虫、千级别的日调度量,真的没那么容易撞上瓶颈。

8. 从面试角度聊聊这套框架的高频考点

既然标题里带了"字节跳动面试时间",不得不说一句:爬虫调度框架在面试中问到的概率其实不低,尤其是当你简历上写了爬虫项目时。面试官很少直接问你"Scrapyd和Gerapy怎么用",更多的是让你从架构角度阐述你对爬虫系统设计的理解。

8.1 高频问题:Scrapyd的核心机制是什么

这个问题考察的不是API的熟练度,而是你是否理解Scrapyd的进程模型。我会这样回答:

Scrapyd是一个守护进程,它监听HTTP端口,接收打包好的项目版本,用Twisted的进程管理能力来调度Scrapy爬虫进程。每次调度请求会生成一个jobid,Scrapyd根据max_proc控制并发数,超出并发限制的任务进入pending队列。它把"项目部署"和"任务运行"两个环节解耦了,所以才能通过HTTP API做到远程部署和远程调度。

这个回答里,Twisted进程管理、jobid、并发队列是三个关键词,能体现出你不是只看过文档,而是真的拆过它的实现。

8.2 高频问题:如果目标网站数据是动态加载的,你会怎么爬

这类问题在面大厂时几乎必问。我的答题思路分三层:

  1. 先分析数据来源,优先找后端API接口,能直接请求JSON就绝不走浏览器渲染。
  2. 如果必须渲染,用scrapy-playwright或者专门的浏览器渲染服务,同时注意控制并发,避免资源耗尽。
  3. 最后讲反爬对抗策略:请求头伪装、Cookie维持、代理IP池、验证码识别等,但要注意分寸,不要说绕过法律边界的事情。

在讲反爬的时候,可以自然地提到scrapy-playwright的实际使用经验,比如如何切换iframe、如何等待元素加载,这些细节比干巴巴讲理论更有说服力。

8.3 高频问题:爬虫系统的架构设计

如果面试官让你白板画一个完整的爬虫系统,我会画成这样:

  • 数据源层:URL列表、种子URL、API接口
  • 采集层:Scrapy实例集群,每个实例跑若干spider
  • 调度层:Scrapyd管理进程和任务队列,Gerapy负责可视化管理
  • 数据层:MySQL存结构化数据,Redis做缓存和去重,文件系统存储原始HTML
  • 监控层:日志采集、异常告警、任务状态页面

这其实就是本文讲的一套东西,只不过面试中要把它当作品架构来讲,突出每层的职责和层与层之间的接口。

8.4 关于面试准备的一点点个人看法

很多人面大厂之前喜欢刷一堆LeetCode,对爬虫项目的复盘却很潦草。但就我观察,面试官对项目细节的追问深度往往会超出预期,他会问你某次线上事故是怎么排查的、某个反爬措施是怎么失效的、日志爆炸时你怎么处理的。这些只有真正动手做过的人才答得自然,临时背答案很容易露馅。

所以我会建议准备爬虫方向面试的朋友,把本文涉及的部署、调度、动态页面采集、反爬对抗、日志排查都亲手过一遍,哪怕项目规模很小,也要把每个环节遇到的问题和解决过程记录下来,这样面试时讲出来的东西才有足够的细节。

9. 一套可以直接参考的生产级部署清单

最后整理一份部署清单,照着做基本不会漏。这份清单是我在自己服务器上反复验证过的,你可以直接抄。

9.1 服务器初始配置

# 更新系统 apt update && apt upgrade -y # 安装基础软件 apt install -y python3 python3-venv python3-pip nginx cron # 创建虚拟环境 python3 -m venv /data/envs/spider_env source /data/envs/spider_env/bin/activate # 安装核心依赖 pip install --upgrade pip pip install Twisted==22.10.0 pip install scrapy==2.11.0 pip install scrapyd==1.2.1 pip install gerapy==0.9.12 pip install scrapyd-client pip install scrapy-playwright playwright install chromium

9.2 启动与配置

# 启动Scrapyd,后台运行 nohup scrapyd > /data/logs/scrapyd.log 2>&1 & # 初始化Gerapy gerapy init cd gerapy gerapy migrate gerapy createsuperuser nohup gerapy runserver 0.0.0.0:8000 > /data/logs/gerapy.log 2>&1 &

9.3 Nginx反向代理

为了让Gerapy可以通过子路径访问,并且加一层Basic Auth,我通常会在Nginx里配一个server块:

server { listen 80; server_name your_domain_or_ip; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; } }

.htpasswd用htpasswd命令生成,Nginx的Basic Auth做一层简单的访问保护,避免Gerapy后台直接暴露在公网。

9.4 Crontab任务示例

# 每天凌晨2点执行日报爬虫 0 2 * * * /data/envs/spider_env/bin/python /data/scripts/schedule_daily.py >> /data/logs/cron.log 2>&1 # 每10分钟清理超时任务 */10 * * * * /data/envs/spider_env/bin/python /data/scripts/cancel_timeout_jobs.py >> /data/logs/cron.log 2>&1 # 每天凌晨3点压缩旧日志 0 3 * * * /bin/bash /data/scripts/rotate_logs.sh >> /data/logs/cron.log 2>&1

9.5 需要提前写好的脚本

我把几个常用脚本放在/data/scripts/目录下:

  • schedule_daily.py:调度指定spider。
  • cancel_timeout_jobs.py:调用listjobs.json查运行中任务,如果任务运行时间超过设定阈值,调用cancel.json取消。
  • rotate_logs.sh:清理旧日志。

这几个脚本都不复杂,加起来不到200行,但在服务器稳定性上起到的作用很大。

10. 最后说几句实在话

如果你完整看完了前面的内容,会发现这套scrapy+scrapyd+gerapy组合本身没有太多高深的东西,它的价值在于把爬虫开发中的部署、调度、查看、集成等常见需求用最简单的方式串起来。正因为每层都简单,所以出了问题才容易定位,团队里新人也容易上手,这恰恰是很多重型调度框架做不到的。

我个人在实际操作中的体会是:别急着追逐新框架,先把已有方案用到极致。Scrapyd确实老,但它稳定的HTTP API和进程管理模型至今没被淘汰;Gerapy虽然维护不频繁,但它的可视化部署能力依然好用。只要你的项目规模没有大到必须上容器编排,这套组合就能继续为你稳定服务。

最后再分享一个小技巧:如果你打算长期维护这套环境,建议在代码仓库里把部署配置、脚本、Nginx配置、Crontab全部版本化管理。换服务器或者来了新同事,执行一遍脚本就能复现环境,不用每次都在命令行里重新敲N遍同样的操作。踩过几次坑之后你就会明白,让部署过程可重复,比任何花哨的功能都重要。

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

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

立即咨询