做爬虫的朋友应该都经历过这个阶段:本地脚本跑得好好的,requests一梭子下去数据哗哗往库里写。结果把代码扔到服务器上,要么提示ModuleNotFoundError,要么MySQL连不上,要么爬着爬着进程没了,日志里啥也没留下。我第一次做爬虫部署的时候,就被这种“环境的诅咒”折磨得够呛,后来彻底切到Docker容器化部署,才算是把这一整套流程捋顺了。这篇文章,我想把Docker容器化部署爬虫项目的完整流程从头到尾写一遍——从为什么要容器化、怎么写Dockerfile,到docker-compose编排、定时调度、反爬应对、上线之后的运维,全部串起来讲。适合那些已经会写爬虫,但部署经验还停留在“scp加python xxx.py”阶段的朋友参考。我能保证的是,里面每个步骤都是我实际跑过、踩过坑之后沉淀下来的东西,不是网上那种贴个Dockerfile就完事的教程。
1. 先聊聊爬虫部署里那些“换台机器就翻车”的事
1.1 环境不一致是最大的坑
我最早部署爬虫,流程很简单:本地把脚本写完,scp传到服务器,装个Python,pip install一堆依赖,然后nohup python main.py &。第一台服务器这么搞勉强能用,第二台就开始出幺蛾子。
最常见的翻车现场有这么几类:
- Python版本不一致。本地是3.11,服务器上是系统自带的3.6,某些语法直接不兼容,比如
dict的合并运算、dataclass、f-string的一些新特性。报错不会指出版本问题,只会莫名其妙地SyntaxError。 - 系统级依赖缺失。爬虫项目最怕的不是pure-python的requests,而是lxml、mysqlclient、cryptography这些带C扩展的库。lxml需要libxml2和libxslt,mysqlclient需要libmysqlclient-dev和gcc,装不装得上看运气。
- Windows和Linux的差异。本地开发用Windows,代码里的路径是
C:\\data\\output.csv,放到Linux上直接崩。还有文件编码,本地默认gbk,写的CSV到了服务器上中文乱码。 - Selenium方案更惨。本地装了个Chrome浏览器,驱动版本匹配得好好的,服务器上一看连浏览器都没有。就算装了Chrome,还得操心chromedriver版本和Chrome版本对不对得上。
这些问题单看都不难解决,但每次换环境、换机器、给同事交接,都要重新踩一遍。而且踩完还不一定记得住所有坑,因为"当时是怎么解决的"往往只存在于聊天记录里。
1.2 容器化解决的核心问题
Docker容器化把这套问题整个打包封死了。核心逻辑很朴素:把运行环境连同代码一起做成镜像,到哪台机器上都是同一个运行环境。
镜像一旦构建成功,里面就固定了Python版本、系统依赖、pip依赖、时区设置、工作目录。服务器上只需要装一个Docker引擎,什么Python、什么lxml编译环境,统统不用管。这带来的实际好处是:
- 一次构建,到处运行。本地构建好的镜像推到镜像仓库,服务器上pull下来直接跑。换服务器、扩容、迁移,成本瞬间降到原来的十分之一。
- 版本完全锁定。requirements.txt锁了依赖版本,镜像本身就锁了一个不可变的依赖组合,再也不存在“那台老服务器上装的还是旧版requests”这种诡异问题。
- 依赖关系可视化。爬虫项目通常要配MySQL、Redis,用Docker Compose可以把这些中间件和爬虫一次性拉起来,谁依赖谁、怎么通信,全部声明在文件里,新人接手也能一目了然。
- 故障恢复更省心。容器挂了自动重启,日志统一走
docker logs,内存爆了有资源限制,不会把整台服务器拖垮。
这里要说一句安装的事情。Windows和macOS上直接用Docker Desktop,装完基本开箱即用;Linux服务器一般装docker-ce,apt install docker.io也行,两三个命令的事。网上教程很多,我就不展开写了,但有一个提醒:装完Docker Desktop如果报"virtualization support not detected",大概率是BIOS里虚拟化没开,去主板设置里把VT-x/AMD-V打开就好,别急着重装系统。
2. 把爬虫装进镜像:Dockerfile从基础到能用
2.1 基础镜像选择:slim与alpine的取舍
写Dockerfile的第一步是选基础镜像。爬虫项目最常见的选择是python:3.11-slim和python:3.11-alpine。很多人一上来就选alpine,因为它小——python:3.11-alpine大概50MB,而slim版本有120MB左右。
但我要泼一盆冷水:爬虫项目真的不建议用alpine。alpine用的是musl libc,和大多数Linux发行版的glibc不一样。lxml、cryptography、pandas、mysqlclient这些带C扩展的库,在musl环境下经常要现场编译。现场编译就要装gcc、musl-dev、python3-dev,装完镜像体积反而比slim还大,而且编译时间长得让人怀疑人生。我踩过一次alpine装pandas,整整编译了二十多分钟,最后还因为某个头文件路径不对直接失败。
python:3.11-slim基于Debian,glibc体系,绝大多数pip包都有现成的wheel,装起来是下载即用。体积多个几十MB,换来的是一路顺畅的安装体验,这个性价比非常高。
如果项目还在用Python 3.9或更老,也建议选对应版本的slim镜像。爬虫这种脚本型项目,老版本镜像完全够用,不需要追着3.13这种最新版跑——等第三方库兼容性跟上,你早就升级了。
2.2 依赖安装与构建缓存的讲究
写Dockerfile有一条重要的经验:把不常变的操作放在前面,把经常改的代码放在最后。Docker构建是有层缓存的,每一行指令会生成一层,只要这层之前的层没变,构建时就直接复用缓存,不会重新执行。
明白了这个原理,Dockerfile的结构就很清晰了。我实际项目里用的Dockerfile长这样:
FROM python:3.11-slim ENV TZ=Asia/Shanghai \ PYTHONUNBUFFERED=1 \ PYTHONIOENCODING=utf-8 \ PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \ PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends \ tzdata curl iputils-ping \ && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "main.py"]几个关键点说明一下:
第一,PIP_INDEX_URL指向国内pip镜像。Docker构建时是在容器环境里执行的,默认访问官方PyPI,速度时快时慢,有时候干脆超时。设成清华镜像后,构建速度能快好几倍。这里提一句,构建服务器在国外的话就不需要设这个,但在国内部署项目时我建议无条件加上。
第二,先COPY requirements.txt,再COPY整个项目。这样只要requirements.txt没变,pip install这层就会命中缓存。如果先把整个项目COPY进去,哪怕你只改了一个爬虫脚本,构建时都要把全部依赖重新装一遍,那酸爽谁试谁知道。
第三,PYTHONUNBUFFERED=1必须加。Python的输出默认是行缓冲的,print的内容在日志文件里可能滞后很久,容器一旦崩溃,最后一屏日志就丢了。设了这个变量之后,print和logging会立即刷新出去,排查问题的时候能看到最实时的状态。
第四,装上curl和iputils-ping。这俩不是为了跑爬虫,是为了排查问题。容器之间网络不通的时候,你得能进容器里curl一下、ping一下。没有这两个工具,排查网络问题全靠猜,非常痛苦。
2.3 时区、编码、系统依赖一个都不能少
新手最容易忽略的两个坑,就是时区和编码。
时区问题表现为:容器默认是UTC时间,你的爬虫代码里用了datetime.datetime.now(),存到数据库的时间比北京时间慢8个小时。很多人在线上跑了一两个月,直到发现报表数据不对才注意到。解决办法就是Dockerfile里那两行ln -snf和echo,把时区软链指到Asia/Shanghai。Debian系的slim镜像还要先装tzdata,不然没有时区数据文件。
编码问题表现为:爬取回来的数据里有中文,print到日志是好的,但写入文件就报UnicodeEncodeError,或者写成UTF-8文件后Windows上打开是乱码。我的建议是在Python脚本里做两层保障:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')不过更稳妥的是用环境变量把整个运行环境的编码钉死——Dockerfile里已经写了PYTHONIOENCODING=utf-8,这会让标准输出、标准错误都用UTF-8处理,绝大多数编码问题都能一次性消掉。
系统依赖方面,爬虫项目经常需要额外的库:要用lxml解析HTML,就需要libxml2和libxslt;要操作图片,需要libjpeg、zlib;要跑Selenium,需要Chrome和一堆浏览器运行时依赖。这些依赖写在Dockerfile的apt-get install里,但要记得最后加一句rm -rf /var/lib/apt/lists/*,清理apt缓存,不然镜像体积会膨胀不少。
3. 用docker-compose一次性拉起爬虫全家桶
3.1 compose文件怎么设计才不踩雷
爬虫项目很少是单打独斗的——数据要存MySQL,请求排队要用Redis,有的还要接调度系统。如果一个个docker run,光是记参数就够头疼。docker-compose把多个容器编排成一个整体,一起构建、一起启动、一起停止。
我常用的compose文件结构是这样:
services: mysql: image: mysql:8.0 container_name: crawler-mysql environment: MYSQL_ROOT_PASSWORD: "ChangeMe123" MYSQL_DATABASE: spider_data command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-pChangeMe123"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: crawler-redis command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data crawler: build: . container_name: crawler-app depends_on: mysql: condition: service_healthy redis: condition: service_started env_file: .env restart: unless-stopped volumes: - ./logs:/app/logs networks: - crawler-net volumes: mysql-data: redis-data: networks: crawler-net: driver: bridge这里有几个细节非常关键。
MySQL容器我显式指定了utf8mb4字符集。爬虫存的数据里什么都有,表情符号、生僻字、各种全角符号,默认的latin1或者utf8都会出问题。--character-set-server=utf8mb4是必加的,不然存emoji的时候直接报错,存中文也可能出现问号。
depends_on这个参数,我强烈建议配合healthcheck用。很多人写depends_on: [mysql, redis]就觉得搞定了,但这个参数只保证容器启动了,不保证MySQL初始化完成。MySQL第一次启动要初始化数据目录,可能要十几秒,这期间爬虫进程连数据库必然失败。加了healthcheck和condition: service_healthy,compose会等MySQL真正可用了再启动爬虫,这个坑基本就补上了。
Redis那个容器,我用--appendonly yes开启了AOF持久化。爬虫项目里的Redis常常用来存待抓取的URL队列、去重集合,丢了可就全白干了。
还有一个多年的老梗要提:compose文件里写不写version字段。现在用Docker Compose V2的话,这个字段是废弃的,写了反而可能触发警告。我上面那份文件就没有version,直接用最新语法,干净利落。
3.2 docker网络不通?完整的排查链路在这里
容器编排起来之后,最常遇到的问题就是"网络不通"。爬虫容器连不上MySQL,报错往往是这样的:
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on 'localhost' (connect refused)")凡是遇到这个,我第一个检查的永远是连接地址。容器内部的localhost指的是容器自己,不是你宿主机上的MySQL。代码里写死localhost的,在本地跑没问题,一进容器必死无疑。正确的做法是改用compose里的服务名,也就是mysql——Docker内置DNS会把服务名解析成对应容器的IP。
排网络问题,我有一套固定的排查链路,分享给你:
- 确认容器在同一个网络里。执行
docker network ls看有没有自定义网络,再看docker inspect crawler-app的Networks字段里有没有包含crawler-net。两个容器不在同一网络,是无法用服务名互通的。 - 进容器里实测连通性。
docker exec -it crawler-app bash,然后ping mysql。如果ping不通,检查是不是网络配错了;如果ping得通但业务代码连不上,那就是端口或者认证的问题。 - 检查端口映射和服务监听。MySQL容器如果只想被其他容器访问,可以不映射端口;但宿主机上的客户端要连,就得有
ports: "3306:3306"。另外注意云服务器上的安全组,很多情况下容器端口开了、映射也写了,但云平台安全组没放行,宿主机外面依然连不上。 - 看DNS配置。容器里
cat /etc/resolv.conf,如果DNS不对,外网请求会解析失败。可以在daemon.json里自定义dns,或者用docker run --dns 8.8.8.8,compose里配dns:字段也行。
我把常见现象列了个表,方便你对照定位:
| 现象 | 最可能原因 | 解决办法 |
|---|---|---|
| 爬虫容器连不上MySQL | 代码里用了127.0.0.1/localhost | 改成服务名mysql |
| 容器间ping不通 | 没加入同一自定义网络 | 在compose里定义共享网络 |
| 宿主机访问不了MySQL | 端口未映射或云安全组未放行 | 写ports映射,再检查安全组 |
| 容器内外网DNS解析失败 | 宿主DNS异常或容器dns配置不对 | 修改daemon.json自定义DNS |
3.3 MySQL与Redis的数据持久化方案
容器是极其"无情"的——删掉就没了,容器里的数据也跟着消失。爬虫攒的数据是核心资产,绝对不能放在容器文件系统里。
解决方式是宿主机挂载卷。上面compose文件里的mysql-data:/var/lib/mysql就是named volume,由Docker管理,存在宿主机的/var/lib/docker/volumes/下。这种方案的好处是数据文件不受容器生命周期影响,docker-compose down都不会丢,重建容器后数据原样回来。
除了数据库,爬虫还会产生很多文件类产物:CSV、JSON、图片、日志。这些我推荐用bind mount,把宿主机目录直接挂进容器:
volumes: - ./logs:/app/logs - ./data:/app/data这里有个权限的坑。bind mount的目录所有权取决于宿主机上目录的UID/GID,容器内进程可能没有写入权限。我遇到过多次:爬虫在容器里跑,写文件报Permission denied,一看是宿主机目录属于root,容器内是非root用户。解决办法很简单,宿主机上先chmod -R 777 logs data,或者chown -R 1000:1000(大部分Python镜像默认用户是root,但如果你在Dockerfile里切换了非root用户,就得改对UID)。
4. 定时任务、代理与反爬策略在容器里的落地
4.1 定时调度:宿主cron还是容器内cron?
爬虫项目几乎都离不开定时调度。每天凌晨爬一次、每小时抓一轮、周末跑全量。在本地可以用系统的计划任务,容器里怎么搞?
方案一:宿主机的cron + docker exec。这是我最推荐的做法,简单、可靠、好排查。宿主机上crontab -e,写一行:
0 2 * * * docker exec crawler-app python /app/scripts/daily_task.py >> /var/log/crawler_cron.log 2>&1优点是调度逻辑和容器完全解耦。宿主机cron不会跟容器一起挂掉,日志也能直接写到宿主机的文件里。要注意cron默认的环境变量很少,PATH里往往没有docker——建议写成/usr/bin/docker的全路径,或者在cron脚本开头export PATH=/usr/bin:/bin:$PATH。
方案二:容器内装cron + crond运行。这种方式把调度逻辑也封装进镜像里,迁移机器不用再配宿主机cron。Dockerfile里装cron,把定时任务文件放进容器,最后用supervisord同时拉起cron和爬虫进程:
RUN apt-get install -y cron COPY cron.d/spider-cron /etc/cron.d/spider-cron RUN crontab /etc/cron.d/spider-cron0 2 * * * root python /app/scripts/daily_task.py >> /app/logs/cron.log 2>&1cron在容器里跑有两个坑。一是时区,前面Dockerfile里已经设了Asia/Shanghai,cron会继承这个时区,但如果没设,定时任务会比预期晚8小时。二是环境变量,cron执行时不会自动加载/etc/profile,Python解释器路径、PYTHONIOENCODING这些都要在脚本或cron行里显式指定,否则可能跑一个残缺的环境。
我现在的项目是两个方案混用的:简单的定时任务用宿主cron,因为改crontab不用重新构建镜像;涉及多进程编排的用supervisord方案,让容器内自己管理。
4.2 代理池接入容器的正确姿势
很多爬虫场景需要代理。要么是目标接口对单一IP有频率限制,要么是需要访问第三方API服务,要么是想让请求从多个出口IP轮询发出。不管哪种情况,容器里接代理都有讲究。
如果你的代理服务是O口一个固定地址的,直接在容器里设环境变量就行:
environment: - HTTP_PROXY=http://proxy-host:8080 - HTTPS_PROXY=http://proxy-host:8080注意这里有一个很容易踩的坑:HTTP_PROXY只对代码里用requests这类库发请求时生效吗?不是的。requests默认不会自动读取HTTP_PROXY环境变量——它有自己的proxies参数,但如果你不显式传,它也会检查环境变量。所以要稳,最好在代码里显式写:
import os proxies = { "http": os.getenv("HTTP_PROXY", ""), "https": os.getenv("HTTPS_PROXY", ""), } session = requests.Session() session.proxies.update(proxies)如果项目维护了一个代理池,IP列表存在Redis里,爬虫每次请求前随机取一个代理,这个逻辑在容器里跑完全没问题,只要Redis连接地址用服务名redis就行。
还要提醒一句:代理质量直接决定爬虫的稳定性。我遇到过代理返回200但内容是乱七八糟的跳转页、代理超时却消耗了大量请求时间、代理IP源乱返回跨域数据等。建议在代码里加个代理校验逻辑——取到代理先发个轻量的测试请求,不可用就换下一个,别把坏代理送到正式抓取流程里。
另外,任何时候都要守住合规底线:爬目标站之前先看对方的robots.txt和服务条款,控制请求频率,别把站点打挂。技术上能做的不代表应该做,这是我在这个领域做了很多年项目后的最核心心得。
4.3 降低被封概率的容器内实践
反爬是爬虫绕不开的话题。最基本的反爬三道锁:User-Agent检测、请求频率限制、IP封禁。容器化部署后,这些策略的实现方式稍微调整一下。
UA池可以换成文件放在镜像里,代码每次随机选一个:
USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", ]随机延迟放Redis里做全局频控更优雅。多个爬虫实例同时跑的时候,光靠每个实例内部随机延迟是不够的,总的请求量可能还是很高。用Redis的INCR加过期时间,在代码里做一个简单的令牌桶或滑动窗口:
import redis import time r = redis.Redis(host="redis", port=6379, db=0) def check_rate_limit(key, max_requests=10, window=60): count = r.incr(key) if count == 1: r.expire(key, window) return count <= max_requests对于依赖JavaScript渲染的站点,容器里跑Selenium是家常便饭。这里我建议要么在爬虫容器里装Chromium浏览器加chromedriver,要么直接用selenium/standalone-chrome镜像单独起一个浏览器服务,爬虫容器通过Remote WebDriver连接它。后者的好处是浏览器崩溃了重启浏览器容器就行,不影响爬虫逻辑。但要注意这个镜像体积很大,内存占用也不小,部署的服务器最好是2G内存起步。
前端加密的站点,比如要逆向JS逻辑算签名的,容器化之后倒没有额外难度——加密逻辑是代码的一部分,Python和Node在容器里都能跑,只是要把Node环境预先装进Dockerfile里。这部分我不展开,但有一个原则:先确认目标站点的服务条款和采集边界,再做技术选型。
5. 上线只是开始:日志、资源与故障自愈
5.1 日志别只print,要能被docker logs捞出来
爬虫挂着没数据、凌晨任务失败没人发现,这些问题的根子往往是日志管理太随意。本地开发的时候随手print没关系,容器环境里要让日志流动起来。
我要求项目的日志输出到标准输出而不是文件,这样docker logs crawler-app --since 10m就能直接看最近的运行日志。Python的logging配置非常简单:
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[logging.StreamHandler()], ) logger = logging.getLogger("spider")日志格式里一定要带时间和级别。容器的docker logs本来就会打时间戳,但你自己脚本里的日志最好也带时间——docker logs的时间戳是Docker加的,日志内容里的时间是业务层面的,两者对得上,排查问题才能定位到具体哪次请求、哪个环节。
如果确实需要把日志落到文件,别忘了在compose里挂载volume,并且给logging加上RotatingFileHandler,不然日志文件会无限膨胀,把磁盘塞爆。
还有一个实用技巧:定时任务的执行结果,最好在代码结束时打印一个明确的状态。爬虫跑完打印spider job finished, total: 123, failed: 0,cron重定向到宿主日志,每天早上扫一眼就知道昨晚跑没跑完。这个习惯帮我在线上避免了无数次"半夜爬起来看日志"。
5.2 给容器装上"内存保险丝"
Python爬虫的内存泄漏是常见病。requests的session没释放、循环里不断累积列表、Selenium打开了一个个标签页不关闭,时间一长内存占用就蹭蹭往上涨。容器的好处是Docker可以给你限制资源,不让它把宿主机拖死。
compose里限制资源有两种写法。如果是Docker Compose直接跑,用mem_limit:
mem_limit: 512m cpus: 1.0如果是Swarm模式,用deploy段:
deploy: resources: limits: cpus: "1.0" memory: 512M设了内存限制之后,容器一旦超限会被内核OOM杀掉,然后restart: unless-stopped会把它拉起来。这看起来粗暴,但比让进程在宿主机上无限吃内存、最后把整台机器拖到无响应要好得多。
实际操作中我会配合docker stats来观察:
docker stats crawler-app如果发现内存持续增长、稳定上升,比如从200M慢慢涨到400M再到接近上限被重启,那就基本可以断定代码里有内存泄漏点。常见的处理是:在爬虫主循环里定期gc.collect()、限制列表长度、用requests.Session().close()或者上下文管理器。实在查不出来,就写一个守护逻辑,每N轮抓取完成后主动os._exit(0)让容器重启,虽然土,但确实管用。
5.3 崩溃自愈与告警通知
容器是自己重启了,但重启之后任务没跑完怎么办?比如凌晨两点爬到一半容器OOM,重启后爬虫继续跑,但任务状态已经乱了。这时候需要两个层面的保障。
第一层是代码层面的任务断点续跑。把"已抓取的URL集合"存在Redis里,启动时加载,每次抓取前先检查是否已抓过。这样即使容器中途重启,重新跑起来也不会重复抓取大量数据。
第二层是告警。爬虫挂了没人知道,等于白挂。我现在的做法是在爬虫容器的守护进程里加健康检测:
import requests def send_alert(message): requests.post("https://your-alert-webhook", json={"text": message}) def main(): while True: try: run_spider() logger.info("spider completed") except Exception as exc: logger.exception("spider crashed") send_alert(f"爬虫异常: {exc}") time.sleep(60) # 继续下一轮,而不是退出进程这样即使某个任务抛异常,主进程也不会退出,而是记录日志、发出告警、等待下一轮继续。配合restart: unless-stopped,容器层面和代码层面双保险。
告警渠道可以用企业微信机器人、钉钉机器人或者邮件,随便选一个。核心诉求是:异常发生时有一个能主动找到你的通道,而不是你等日志从哪冒出来。
6. 我沉淀下来的一套爬虫容器化模板与避坑清单
6.1 一份可直接抄的目录结构
写了这么多,最后分享一套我实际项目中沉淀下来的模板。结构清晰,扩展方便,新项目直接复制改改就能上。
spider-project/ ├── Dockerfile ├── docker-compose.yml ├── .dockerignore ├── .env ├── requirements.txt ├── config.py # 全局配置,读取环境变量 ├── main.py # 手动跑一次入口 ├── crawlers/ │ ├── __init__.py │ ├── base.py # 通用抓取逻辑、UA池、延迟、代理 │ └── job_a.py # 具体站点的爬虫 ├── storage/ │ ├── __init__.py │ ├── db.py # SQLAlchemy连接与session │ └── redis_client.py # Redis连接封装 ├── scripts/ │ └── daily_task.py # 定时任务脚本 └── logs/ # 宿主机挂载的日志目录.dockerignore别偷懒不写,把logs/、data/、.git/、__pycache__/、.venv/都忽略掉,不然构建上下文会把你本地几G的缓存文件全打进镜像,构建慢到怀疑人生。
数据库访问我建议直接用SQLAlchemy,别手写一堆裸SQL。storage/db.py里放一个engine和session_factory,爬虫代码里统一从这里拿session,数据入库的字段校验、批量插入、去重逻辑都集中在这一层,后面维护会很顺手。
6.2 高频踩坑记录表
最后把我这些年容器化部署爬虫项目高频翻车的点整理成一张表,算是压箱底的避坑清单:
| 坑 | 现象 | 规避方案 |
|---|---|---|
| MySQL 8认证插件不兼容 | 老客户端报caching_sha2_password错误 | pip安装cryptography,或建用户时指定IDENTIFIED WITH mysql_native_password |
| 容器时区不对 | 入库时间比北京时间慢8小时 | Dockerfile装tzdata并设置TZ=Asia/Shanghai |
| cron环境变量缺失 | 定时任务报python: command not found | crontab里写全路径或用bash -lc包一层 |
| bind mount权限不够 | 容器内写文件报Permission denied | 宿主机目录chmod/chown,或Dockerfile里切换用户前先建好目录权限 |
| pip install慢或超时 | 构建卡在安装依赖阶段 | 设置PIP_INDEX_URL指向国内镜像 |
| 代码改了但镜像没更新 | 跑的还是旧逻辑 | 确保.dockerignore没忽略源码目录,重新docker compose build --no-cache crawler |
| 容器端口映射冲突 | 宿主机端口被占用,compose起不来 | 换宿主端口或停掉占用进程,用docker ps先查 |
| Selenium容器内存飙升 | 标签页不关闭,内存持续增长 | 每轮任务后driver.quit(),限制容器内存并允许自动重启 |
这张表里的每一条,我都真金白银地在线上踩过。很多坑看起来不起眼,但它们共同的特点是:报错信息不直接,都藏在日志深处,一不小心就是通宵排障。
套用这套模板,正常一个爬虫项目从零到上线,快的话半天就能完成。本地写完爬虫,配上Dockerfile和compose,构建镜像推到服务器,docker compose up -d跑起来,数据就能稳定地往库里流了。
如果你现在还在用"scp加nohup"的方式部署爬虫,我建议你花一个下午把项目迁到Docker里。迁完你会在下一次换服务器、下一次任务凌晨崩溃自动恢复、下一次给同事交接环境时,感谢自己做了这个决定。别问我为什么说得这么笃定——我就是那个被环境问题折磨了几十次之后才彻底投奔容器化的人。