为什么选择 Changedetection.io
在自动化运维和内容巡检中,经常需要盯着某些网页看状态变化。比如第三方 API 文档更新、竞品价格调整、或者内部灰度页面的发布确认。过去我试过写 Cron 脚本抓 HTML diff,后来发现维护成本很高:CSS 结构一变,正则就全废;还要自己处理反爬、等待渲染、解析 JSON 响应。换了几个商业服务后,还是决定用开源方案。
Changedetection.io 的核心逻辑很直接:定时抓取目标页面,提取指定区域的内容,和上次快照做比对。如果超过阈值就触发通知。它支持 CSS 选择器、JSONPath、甚至 XPath,底层默认用无头浏览器(Playwright)处理动态加载,也支持纯 HTTP 请求。项目完全免费,架构轻量,正好贴合我需要的"低侵入、易扩展"的需求。它不提供复杂的任务编排或分布式调度,但在"盯网页"这个小切口上,已经足够锋利。
Docker 环境与核心参数配置

官方已经提供了 Dockerfile,直接拉取镜像是最省事的路径。我当前使用的是 ghcr.io/dgtlmoon/changedetection.io 镜像。这里有一个细节需要注意:官方默认启用了 Playwright 的 Chromium 内核,这意味着容器内会额外安装一个完整的浏览器环境,体积会比基础版大不少。如果你的监控目标全是静态 HTML 或固定接口的 JSON 响应,可以在启动时通过环境变量关闭浏览器依赖,从而节省近半的存储空间和内存。
具体部署命令如下:
bash
docker run -d \
--name changedetection \
-p 6555:6555 \
-v ${PWD}/datastore:/datastore \
-e PUID=1000 \
-e PGID=1000 \
ghcr.io/dgtlmoon/changedetection.io:latest
这里 -v ${PWD}/datastore:/datastore 是核心。Changedetection 的所有配置、历史快照、触发器记录都写在这个目录里。一旦容器重建或升级,不映射这个卷,之前的监控列表和通知设置就会全部丢失。我遇到过一次因为磁盘空间不足导致容器无法写入 /datastore 的情况,重启后直接进入白屏状态。查看日志提示 Permission denied,实际上是因为宿主机的 datastore 目录权限被 Docker 自动创建的 root 用户覆盖。解决办法是在宿主机提前创建目录并赋予对应 UID/GID,或者在 run 命令中补充 -u 1000:1000。
关于端口,默认是 6555。如果本地有其他服务冲突,可以直接改宿主机映射端口,容器内部端口保持 6555 即可。环境变量方面,除了 PUID/PGID,我还加了一个 BASE_URL,用于生成 Webhook 回调时的绝对路径。如果不配置,系统默认使用相对路径,在反向代理环境下容易拿到错误的回调地址。
添加监控目标与触发规则设计

容器跑起来后,打开 http://localhost:6555 就能看到管理界面。添加监控目标的操作很简单:粘贴 URL,选择提取方式,设定检查间隔。但真正决定效果的是"触发规则"和"过滤条件"。
很多新手会直接选"整个页面",这在实际工程中几乎不可用。网页通常包含广告、时间戳、随机 Token 或滚动加载的推荐内容。直接比对整个 DOM,只要这些浮动元素变化,就会疯狂报警。我的做法是优先使用 CSS 选择器或 JSONPath 锁定核心数据区。例如监控一个天气 API 的返回结果,我会选中 JSONPath:$.data.current.temp。如果是传统网页,就用 Chrome 开发者工具找到目标 div 的 class 或 id,填入 CSS Selector。
另外两个容易被忽略的参数是"等待时间"和"变更阈值"。等待时间(Seconds to wait before checking for changes)针对的是依赖 JS 渲染的页面。有些站点点击加载更多后才出内容,默认几秒根本拿不到完整 DOM。我一般设成 5~8 秒,配合 Playwright 的隐式等待,能抓到稳定状态。变更阈值(Trigger comparison threshold)默认是 5%,意思是只有当提取内容的差异比例超过 5% 才报警。对于金额、库存这类关键指标,我会把它调低到 1% 甚至 0.5%;对于新闻标题类,调到 10% 以上,避免排版微调触发误报。
这里提一句,Changedetection 默认开启了"指纹哈希"(Fingerprint Hashing)。它不会逐字对比文本,而是计算 MD5 摘要。这意味着即使你修改了一个空格,摘要也会变。如果你只想监控特定字段的变化,必须在提取阶段就把无关内容过滤掉,而不是依赖底层的 diff 算法。这一点我没有验证其他哈希算法的效果,目前 MD5 满足需求。
打通通知通道:Webhook 与邮件

监控到变化只是第一步,把结果推送到我的工作流才是目的。Changedetection 内置的通知渠道挺全,我主要用 Webhook 和 SMTP 邮箱。
Webhook 配置非常直白。在触发器详情页下拉找到"Notify when changed",输入你的接收端点即可。它发送的是 POST 请求,Payload 格式固定为 JSON,包含 title、url、previous_md5、current_md5 以及一段 diff 字段。这段 diff 是标准的 HTML 片段,左右两侧分别标红标绿显示变更位置。我在自己的后端服务里写了一个简单的 Flask 路由来接收:
python
from flask import Flask, request
import json
app = Flask(__name__)
@app.route('/webhook/change', methods=['POST'])
def receive_change():
data = request.json
print(f"[Changedetection] URL: {data['url']}")
print(f"[Changedetection] Diff Preview: {data['diff'][:200]}")
# 这里可以接钉钉机器人、飞书 webhook 或者写入数据库
return {"status": "ok"}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
这段代码不需要复杂解析,主要利用 diff 字段快速定位变更区域。如果你的业务系统对格式要求严格,可以在 payload 到达后做二次清洗。需要注意的是,Webhook 默认有重试机制,但重试间隔是固定的,无法自定义退避策略。如果下游服务偶尔超时,可能会收到重复通知。我在测试时发现,连续多次失败后,系统会暂停该触发器,直到下次检查周期重新尝试。这一点我没有验证具体的失败次数阈值,实测观察到的现象是约 3 次失败后进入静默。
邮件通知配置相对繁琐一点,需要填写 SMTP 服务器、端口、发件人账号和密码。现在很多企业邮箱开启了 OAuth2 或独立客户端授权码,直接填主密码会被拒收。我推荐使用 Gmail 的应用专用密码,或者自建 Postfix。SMTP 的缺点是延迟高、不易做富文本排版,而且垃圾邮件过滤器容易拦截自动生成的监控邮件。如果只是个人开发测试,Webhook 接 Telegram Bot 或微信转发的效率远高于邮件。
公网访问与 HTTPS 证书配置
容器默认监听在内网端口,要对外提供服务必须经过反向代理。我习惯用 Nginx 做入口,因为它对 WebSocket 长连接的支持更成熟(虽然 Changedetection 主要走 HTTP,但后台实时刷新会用到 SSE 或轮询)。
以下是 Nginx 配置的核心片段:
nginx
server {
listen 443 ssl http2;
server_name monitor.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location / {
proxy_pass http://127.0.0.1:6555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 关键:允许大请求体,防止上传截图或附件过大被截断
client_max_body_size 50m;
# 保持连接,适用于后台状态轮询
proxy_read_timeout 90s;
proxy_send_timeout 90s;
}
}
配置完后,域名解析指向服务器 IP,HTTP 自动 301 跳转到 HTTPS。这里有一个隐藏坑:Changedetection 在某些功能(比如生成 PDF 快照或导出历史记录)时会发起内部重定向。如果 proxy_set_header X-Forwarded-Proto 没传,应用层会认为自己是 HTTP,导致生成的回调链接变成 http://。这在公网环境下会被浏览器拦截混合内容(Mixed Content),直接报错。务必保证 X-Forwarded-Proto 正确透传。
另外,如果你使用 Cloudflare Tunnel 或其他零信任网关暴露服务,记得在网关层面放行 /api/* 和 /static/* 的路由。Changedetection 的前端资源较多,漏放 CDN 或缓存规则会导致管理后台白屏或样式错乱。这一点我在初期调试时卡了半小时,最后通过浏览器 Network 面板发现大量 .js 文件返回 404,才补全了路由匹配。
实际运行中的内存与误报问题

跑了一周后,我注意到宿主机内存占用持续上升。用 docker stats 查看,进程常驻内存维持在 400MB 左右,且随监控数量线性增长。分析后发现,每个活跃的触发器都会在内存里保留一份最近的 DOM 快照和哈希值。加上 Playwright 实例没有及时释放临时句柄,累积下来容易触达容器的 OOM 限制。
解决思路有两个方向。一是合理控制并发检查频率。不要把所有目标都设为 1 分钟检查一次。Changedetection 支持全局检查和单条覆盖。我把非核心页面调到每小时,核心接口保留 5 分钟。二是启用清理机制。在设置里找到"Prune old snapshots",开启后可以自动删除 7 天前的历史数据,显著降低存储压力。对于内存泄漏问题,我目前采取定期重启容器的妥协方案。官方 Issue 里有讨论过 Playwright 上下文未销毁的问题,社区建议升级到 latest 标签的最新构建版本。这一点我没有深入抓包验证底层堆栈,仅通过版本迭代观察到内存曲线趋于平稳。
误报方面,最头疼的是"动态 ID"和"时间戳"。有些网站的结构类名带哈希后缀,每次加载都会变。单纯靠 CSS 选择器必死。我的应对策略是改用 XPath 的 contains() 函数,或者直接用 Regex 过滤掉时间段落。例如监控商品页,用 CSS 选择器定位 .price,然后用 Filter out 功能填入 \d{4}-\d{2}-\d{2} 排除发布时间。Changedetection 的过滤语法支持标准 PCRE,熟练后基本能过滤掉 90% 的噪声。剩下的 10% 属于业务强相关变动,手动标记为"已知变更"即可。
后续建议与边界说明
Changedetection.io 定位清晰:轻量、专注、开箱即用。它不适合替代专业的爬虫调度平台,也不具备复杂的任务编排能力。但对于"盯网页"这个小切口,它的性价比极高。
在实际落地时,建议把监控对象分级管理。核心链路走 Webhook 实时告警,边缘页面走每日汇总邮件。容器部署尽量绑定固定数据卷,配合 CI/CD 流水线做配置备份。如果遇到大规模并发抓取需求,可以考虑多实例部署并通过 Redis 共享队列,不过这就脱离了官方单机架构的设计初衷。
技术选型永远存在取舍。如果你需要深度定制抓取逻辑、处理极高频的接口轮询,或者对数据留存有合规审计要求,可能需要回到自研 Scrapy + Celery 的老路上。但如果只是解决日常的信息差焦虑,Changedetection 已经把泥活干干净了。跑通流程后,你会发现它更像是一个稳定的信号中继站,而不是另一个需要维护的黑盒系统。