写完一个爬虫其实只是第一步。本地调试的时候,脚本在IDE里跑得飞快,数据一条条入库,心里还挺美。但真放到服务器上,问题就来了:今天忘了手动跑,明天磁盘被日志塞满,后天目标网站改了一个字段,爬虫直接崩在半夜,等你第二天早上发现,一整天的数据全是空的。所以我一直觉得,对零基础入门的人来说,上线与运维才是爬虫真正“毕业”的关卡。
这一篇就专门讲爬虫上线后最基础也最要命的三个词:定时运行、日志轮转、失败告警。尽量用轻量方案,不引入一堆重型组件,思路和代码都能直接抄走。
1. 项目概述:别让爬虫成为“一次性玩具”
1.1 写完爬虫不等于完成项目
我见过很多新手把爬虫写完、跑通一次,就认为任务结束了。实际上,如果这个爬虫只需要跑一次,那确实结束了;但绝大多数场景是长期、定时、无人值守地采集数据——比如每天抓一次天气、每周同步一次商品价格、每小时监听某个页面的更新。这种“长期在线”的需求,才是真正考验代码健壮性的地方。
长期跑和不长期跑,差别非常大。本地跑一次,你盯着终端,报错了立刻能看到;服务器上跑,报错发生在一个你完全不看屏幕的时间点。而且长期跑会积累问题:日志文件越写越大,磁盘早晚被占满;任务一旦崩掉,没有任何人通知你;服务器重启之后,你的定时任务可能跟着一起消失了。这些问题有一个共同特点:它们不会在第一天发生,而是会在你最松懈的时候突然冒出来。
所以我会把“上线与运维”拆成三个核心能力:定时运行解决“谁在什么时间启动爬虫”的问题;日志轮转解决“日志无限增长把磁盘写爆”的问题;失败告警解决“爬虫挂了但没人知道”的问题。这三件事的成本都不高,但能显著提高项目的“存活率”。
1.2 轻量版方案为什么够用
说到运维,很多资料一上来就是 Docker、K8s、Apache DolphinScheduler、Prometheus 全家桶。这些工具当然很强大,但它默认你有服务器集群、有专门的运维时间。对于个人项目、小团队、每天跑一两次的低频任务,引入这些反而是一种负担——光是理解调度平台的概念,就够劝退一大半初学者了。
这一篇的方案组合非常简单:
| 需求 | 轻量方案 | 重量级替代方案 |
|---|---|---|
| 定时运行 | 系统自带 cron / 计划任务 | DolphinScheduler、Airflow |
| 日志管理 | Python logging + 文件轮转 | ELK、Loki |
| 失败告警 | 企业微信 / 钉钉机器人 Webhook | Prometheus + Alertmanager |
这套组合不需要额外安装服务、不需要维护复杂的配置,一台普通服务器甚至是一台树莓派都能跑。等到项目真的成长到需要多人协作、需要可视化调度、需要监控大盘的时候,再平滑迁移过去也不迟。先用最小的成本解决问题,而不是一开始就上最重的架构,这是我现在做项目的一个基本判断标准。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 定时运行:给爬虫上一根“机械闹钟”
2.1 为什么选 cron 而不是自己写定时循环
有不少同学习惯在代码里写 time.sleep(3600) 或者用 schedule 库来实现定时执行。这种方式本身没问题,但它有一个致命缺陷:你的 Python 进程必须一直活着。一旦进程因为内存溢出、服务器重启、或者某次异常退出,整个定时循环就断了。而且这种“进程内定时”不适合多个任务独立管理,排查问题也不直观。
cron 是 Linux 系统自带的定时任务工具,它的设计思路是“到点把命令启动一下”,命令跑完就结束,没有常驻进程的负担。就算任务崩了,下一个周期 cron 还是会照样启动,不用担心“定时器跟着主进程一起死掉”。这就像定闹钟和盯着表看时间的区别——前者到点了自然会响,后者只要你看走神一秒就完蛋。
macOS 和 Linux 都自带 cron,Windows 用户也可以用“任务计划程序”,思路完全一致。对于服务器部署,绝大多数情况都是 Linux,所以下面主要说 cron。
2.2 cron 表达式与一套可以直接抄的配置
cron 的表达式有五个字段,顺序是“分 时 日 月 周”。举个例子:
code复制0 2 * * * cd /opt/crawler && python3 run.py >> logs/crawl.log 2>&1
这段的意思是每天凌晨 2 点整,先进入 /opt/crawler 目录,然后执行 python3 run.py,把标准输出和错误输出都追加到 logs/crawl.log 里。常见写法有这些:
*/10 * * * *:每 10 分钟执行一次30 8 * * 1-5:工作日早上 8:30 执行0 */2 * * *:每 2 小时整点执行15 3 * * 0:每周日凌晨 3:15 执行
配置的方法是执行 crontab -e,把上面那行贴到文件里保存,然后用 crontab -l 查看。这里有几个关键点需要解释:
第一,命令里写 cd /opt/crawler && 不是强迫症,而是为了防止“相对路径找不到文件”的经典坑。cron 执行任务时的工作目录不是你手动执行脚本时那个目录,不切换目录的话,代码里的 open("data.json") 就会把文件写到莫名其妙的地方去,甚至直接报 FileNotFoundError。
第二,>> logs/crawl.log 2>&1 非常值得养成习惯。2>&1 表示把错误输出也重定向到同一个文件,这样脚本报错时你至少能在日志里看到信息,而不是让错误变成一条在 cron 邮件里躺着的死消息。
第三,如果你用的是 venv 虚拟环境,python 路径要写完整,比如 /opt/crawler/venv/bin/python3。因为 cron 运行时的 PATH 环境变量非常精简,你手动在终端里能敲 python3 是因为 shell 帮你加载了环境变量,cron 不会。
2.3 定时任务最容易踩的三个坑
第一个坑是时区不一致。服务器默认可能是 UTC 时间,你以为是凌晨 2 点跑,实际是北京时间上午 10 点跑。建议部署时先执行 timedatectl 确认系统时区,需要的话用 sudo timedatectl set-timezone Asia/Shanghai 改过来。否则你会很奇怪“为什么任务老是白天才跑”。
第二个坑是cron 里执行失败了你不知道。cron 的机制是如果命令有标准错误输出,会尝试发邮件通知,但大多数服务器根本没配邮件服务,等于这些错误直接沉底了。所以我上面的建议是把日志写进文件,这样至少排查时有据可查。配合后面的失败告警,才能真正形成闭环。
第三个坑是脚本里用了相对路径。我建议代码里统一用绝对路径,或者至少用这样的方式来定位:
python复制from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
这样无论你从哪个目录启动脚本,资源文件都能正确找到。这属于典型的“在终端里好好的,在 cron 里就炸了”的问题,提前规避能省很多麻烦。
3. 日志轮转:日志只写不清理,磁盘迟早撑爆
3.1 一个简单的算术题:日志膨胀有多可怕
很多人写爬虫时不写日志,觉得无所谓;也有一部分人写了日志,但只写不清理。前者遇到问题两眼一抹黑,后者则是在给自己埋一颗定时炸弹。
算一笔账:假设你的爬虫每天运行一次,每次输出 500 行日志,每行平均 100 字节,一天也就是 50KB,看起来完全不可怕。但如果爬虫改成每小时跑一次,并且你顺手打印了每次请求的响应内容,那一天的日志可能就涨到 50MB 以上。一台小服务器磁盘通常只有 40GB,一两个月就满了。磁盘满之后会发生什么?爬虫还在定时触发,但连日志文件都写不进去了,甚至整个系统开始报错,其他服务也跟着遭殃。
“日志轮转”这个概念解决的就是这个问题:按时间或按大小把当前日志文件切分成历史文件,只保留最近 N 份,更旧的自动删除。这么一来,日志永远占着固定的体量,不会无限膨胀。
3.2 Python 自带轮转:logging + TimedRotatingFileHandler
Python 的 logging 模块内置了轮转处理器,不需要装任何第三方库。我的常用写法是这样:
python复制import logging
from logging.handlers import TimedRotatingFileHandler
logger = logging.getLogger("crawler")
logger.setLevel(logging.INFO)
handler = TimedRotatingFileHandler(
"logs/crawler.log",
when="midnight",
backupCount=7,
encoding="utf-8"
)
handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s"
))
logger.addHandler(handler)
几个参数的具体含义:when="midnight" 表示每天零点切分一次日志;backupCount=7 表示只保留最近 7 个历史文件,最老的会被自动删除;encoding="utf-8" 是为了防止 Linux 下日志里的中文变成乱码。
我习惯在代码里用 logger.info("开始抓取...")、logger.exception("抓取失败") 这样的方式记录运行状态。注意,logger.exception 只能在 except 块里用,它会自动把当前异常堆栈一起打出来,比 logger.error(str(e)) 信息量大得多。
3.3 另一条路:用系统的 logrotate 做轮转
除了在 Python 代码里做轮转,还可以用 Linux 自带的 logrotate 从外部管理日志文件。适合的场景是:日志由多个程序写同一个文件,或者你想对日志做压缩,又不想改 Python 代码。
配置方式是新建一个文件 /etc/logrotate.d/crawler,内容类似这样:
code复制/opt/crawler/logs/*.log {
daily
rotate 7
compress
missingok
copytruncate
}
解释一下:daily 是每天轮转一次,rotate 7 保留 7 份,compress 把历史日志压缩成 gz 格式,missingok 表示日志文件不存在时不要报错,copytruncate 则是先复制内容再清空原文件。最后这个参数特别重要,因为如果用默认的 rename 方式,Python 进程手里还握着旧文件的句柄,日志会继续写到被 rename 掉的文件里,磁盘空间根本不会释放。copytruncate 能避免这种“日志文件消失了,空间却没有回来”的诡异情况。
两种方案怎么选?我的经验是:单脚本、低频任务,用 Python 自带 handler 最省事;多脚本共目录或者需要压缩归档,用 logrotate 更统一。另外提醒一句,如果日志文件是被多个进程同时写入的,Python 内置的 TimedRotatingFileHandler 在切分时有可能出现竞争问题,这种场景直接上 logrotate 更省心。
4. 失败告警:第一时间知道爬虫挂了,而不是第二天
4.1 告警渠道怎么选:企微机器人是轻量首选
失败告警这件事的核心价值在于缩短故障发现时间。没有告警机制时,你的流程是:某天突然发现数据不对——去查日志——发现爬虫三天前就挂了——想办法补救——可能数据已经补不回来了。有告警机制后,流程变成:爬虫挂了——群机器人推消息——你掏出手机看到——赶紧修。前者的数据缺失窗口可能是三天,后者可能只有几分钟。
告警渠道可以选邮件、短信、企业微信机器人、钉钉机器人等。我的对比结论是这样的:
| 渠道 | 成本 | 配置难度 | 到达率 | 适用场景 |
|---|---|---|---|---|
| 邮件 SMTP | 免费 | 中 | 一般 | 能接受延迟的场景 |
| 企业微信机器人 | 免费 | 低 | 高 | 团队内部告警首选 |
| 钉钉机器人 | 免费 | 低 | 高 | 用钉钉办公的团队 |
| 短信(商业API) | 收费 | 中 | 最高 | 核心业务 7x24 必达 |
对于大多数爬虫项目,我推荐企业微信机器人或钉钉机器人,二选一就行。原因很简单:手机装个 App 就能收到推送,Webhook 配置只要复制粘贴一个 URL,而且 markdown 格式支持把消息排版得清清楚楚。
4.2 企微机器人告警的完整代码
企业微信机器人的使用步骤:先建一个群,然后在群设置里添加“群机器人”,把 Webhook 地址复制下来。这个地址形如 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxx。
然后写一个通用的发送函数:
python复制import requests
WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxx"
def send_alert(title: str, content: str):
data = {
"msgtype": "markdown",
"markdown": {
"content": f"**{title}**\n\n{content}"
}
}
resp = requests.post(WEBHOOK_URL, json=data, timeout=10)
if resp.json().get("errcode") != 0:
print("告警消息发送失败:", resp.text)
在爬虫主逻辑里这样用:
python复制try:
run_crawler()
except Exception as e:
send_alert(
"爬虫任务异常",
f"> 异常类型:{type(e).__name__}\n> 错误信息:{e}\n> 时间:{now()}"
)
else:
send_alert("爬虫任务完成", f"> 本次抓取成功,新增 {count} 条数据")
注意两个细节。第一,Webhook 地址别硬编码到代码里,更不要提交到公开仓库,否则任何人都能往你的群里发消息。建议写到环境变量或者单独的配置文件里。第二,告警消息里要带上上下文信息,比如爬的是哪个 URL、哪个环节出的错。否则你深夜收到一条“异常”,还得先登录服务器查日志才能知道发生了啥,告警的价值就大打折扣了。
钉钉机器人和企业微信机器人的逻辑几乎一模一样,只是推送的时候多了一个可选的安全设置:如果你在钉钉机器人配置了“加签”,就需要在请求头里额外带上 X-DingTalk-Sign 参数。没配置加签的话,直接 POST JSON 就行。
4.3 告警降噪:别让告警本身变成噪音
上过线的人都知道,告警机制做不好,很快就会变成“狼来了”的故事。如果爬虫因为网站一条临时波动就触发几十条告警,大家第一周还认真看,第二周就把群消息免打扰了,真正的问题反而被淹没。
我的经验是做三重降噪:
第一,失败后先重试再告警。不要一次失败就立刻发消息,可以连续重试 3 次,每次间隔递增,比如 5 秒、10 秒、30 秒。三次都失败再告警,大概率能过滤掉瞬时网络抖动。
python复制for attempt in range(3):
try:
run_crawler()
break
except Exception as e:
last_exc = e
time.sleep(5 * (attempt + 1))
else:
send_alert("爬虫任务失败", str(last_exc))
第二,同一窗口期只告警一次。最粗暴但有效的方法是用一个小文件记录上一次告警时间,在时间窗口内(比如 10 分钟)不重复推送。这能避免“进程反复崩溃又被 cron 拉起”导致的告警轰炸。
第三,恢复时发一条通知。故障结束后发一条“已恢复”的消息,让所有人知道事情过去了,不用反复去确认。实际体验下来,这个“开始—结束”的闭环比一堆失败消息更有价值。
5. 常见问题与排查技巧实录
5.1 cron 明明配置了,为什么不执行
这个问题可以按顺序排查:
- 先执行
crontab -l确认配置确实存在,很多人改了文件但没保存成功。 - 检查 cron 服务状态:
systemctl status cron或者service crond status,服务器重启后 cron 服务如果没有自启动,任务就不会跑。 - 手动执行一遍 crontab 里那条命令,看是否报错。很多“不执行”其实是“执行了但秒退”,因为脚本本身有 bug。
- 检查脚本的权限:cron 执行任务的用户如果是普通用户,脚本需要有对应的执行权限,记得
chmod +x run.py。
另外一个很隐蔽的问题是脚本开头没有 shebang。如果你在 crontab 里写的是 python3 /opt/crawler/run.py 还好;万一你写的是 /opt/crawler/run.py 直接执行,而文件开头没有 #!/usr/bin/env python3,内核会尝试用 shell 解析 Python 文件,然后报语法错误。
5.2 日志文件轮转了,但磁盘空间没释放
这个问题在高频写入的场景下经常出现。原因是程序持有的是旧文件的句柄,logrotate 把文件 rename 走了,但进程还在往那个已经看不出名字的文件里写数据,所以 df -h 看到空间没了,ls 却找不到大文件。
排查技巧:用 lsof | grep deleted 查看被删除但仍被进程占用的文件。解决方法是把 logrotate 配置改成 copytruncate,或者让 Python 进程定期重新打开文件句柄。如果你是用 TimedRotatingFileHandler,它自己会处理 reopen,一般不会遇到这个问题;反而是在用外部 logrotate 时才要特别注意。
5.3 告警一直发,群都快被刷屏了
通常分两种情况。一种是爬虫被目标网站封了 IP,每次请求都 403,并且你的重试逻辑是“失败一次告警一次”。解法就是上面说的降噪三板斧:重试 + 时间窗口限制 + 恢复通知。
另一种更隐蔽:你发送告警的代码本身会因为网络问题连不上 Webhook,然后在 except 块里又触发发送告警,形成递归风暴。我见过一次真实的案例,告警代码把进程打崩了。所以发送告警的函数里一定要有超时控制(我上面代码里的 timeout=10),并且发送失败后只打印日志,绝对不要再去触发新的告警。
5.4 一个简单的上线自检清单
按照我的习惯,每次上线爬虫前会花十分钟过一遍这张清单:
crontab -l能看到任务,时间表达式是你想要的时区- 手动执行一次命令,日志文件正常生成,内容可读
- 故意制造一次异常(比如关闭网络),确认告警能收到
- 查看日志目录,确认轮转配置生效,
backupCount符合预期 - 服务器
df -h确认磁盘有余量 - 确认代码没有硬编码 Webhook 或数据库密码
这一套走下来,不敢说项目“万无一失”,但至少能把最常见的坑提前排掉一大半。
落到实操上的个人习惯是:新脚本上线后的第一个星期,我会每天定时看一眼日志和磁盘占用,顺手验证一下告警通道是否通畅。等确认稳定了,才敢真正放手不管。这期讲的这三个功能,代码量加起来不超过 200 行,却是我认为爬虫项目里性价比最高的一部分投入。你的爬虫到今天为止,是“写完就扔的玩具”,还是能安静跑大半年的小工具,区别往往就在这几个不起眼的运维细节上。
