简介:SiteMonitor是一款面向企业市场研究、品牌监控及个人兴趣追踪的网站资讯监控工具,支持同时监控多个网站,能够实时检测页面结构、文本、图片等内容的更新,并按需开启关键字监控触发提醒。这份RAR压缩包共包含3个文件,其中exe文件为软件安装程序,html文件提供详细的安装升级必读指南,txt文件则补充了监控频率、提醒方式等配置建议,压缩包整体约10.51MB,轻量易部署。完成安装后,可将竞品站点、行业新闻源和个人关注页面统一纳入监控,设置关键词后,一旦网站出现更新或命中词,即可通过微信接收实时推送,快速掌握外部信息变化。配套文档能帮助用户顺利完成部署、升级与参数调优,减少试错成本,适合需要及时跟踪网站内容的运维人员、运营编辑和独立站管理者。目前已有1178人学习下载,对于希望低成本构建网站内容监控方案的读者而言,是一份实用性较强的工具包。
1. 网站资讯监控不是爬虫,是“盯梢”:SiteMonitor 要解决什么
很多人第一次听到“网站资讯监控工具SiteMonitor”,会下意识觉得这就是个爬虫。实际上完全不是一回事。爬虫的目标是“尽量全地抓下来”,监控的目标是“只抓变化的那一小块”。你盯着一个行业资讯页、一个公告栏、一个竞品新闻列表,真正关心的是“有没有更新”,而不是把整个网站镜像下来。SiteMonitor 的定位就是这样一个轻量级盯梢工具:周期性地去取页面,跟上一轮快照做对比,发现值得注意的差异就发通知。它适合运维盯文档更新、运营盯竞品动态、开发盯接口公告这类场景,不需要动用重型采集框架,也不需要一台常驻服务器,最小形态一条 crontab 就能活。这篇文章会从选型、最小实现、持久化到排坑,把整条路走一遍。
2. 先定方案再写代码:SiteMonitor 的核心模块与监控粒度选择
2.1 轮询还是推送:抓取频率与目标站点特征怎么匹配
网站资讯监控最常见的驱动方式是轮询,也就是自己定闹钟去访问。另一种是站点主动推送,比如 RSS、Webhook,但现实中大量资讯页既不提供 RSS,也没有 webhook 接口,所以轮询仍然是通用方案。SiteMonitor 的第一步就是决定“多久看一次”。这个周期和目标站点的更新规律强相关。更新频繁的科技媒体可以每 5 分钟查一次;只看法律条文修订的页面,一天一次都嫌多。我一般会先手工观察目标站点几天,确认更新峰值时段,再定初始频率。
频率设定有个容易被忽略的原则:宁可漏一次,不可每轮都打太重。站点往往有缓存或反爬策略,频繁请求会带来封禁风险。常见做法是把初始轮询间隔设为目标站点平均更新间隔的一半,比如它平均每小时更新一次,那就 30 分钟查一次。再配合条件请求头If-Modified-Since或ETag,让服务器在没变化时直接返回 304,这样既不浪费带宽,也能减少 IP 特征。SiteMonitor 里可以配置request_headers来携带这些字段,后续在抓取层会讲到。
2.2 变更检测的三种常见做法:全文比对、哈希指纹、DOM 结构 diff
这是 SiteMonitor 的核心分水岭。你要监控的页面有好几种变化形态:正文文字变了、某区块新增一条链接、整个页面重新排版但内容没动、广告区域滚动变化。不同的检测方式对应不同的问题。
第一种是全文比对。把两个时间点的 HTML 源码做文本 diff,差异大就告警。这个方案最直观,但噪声极大。页头的时间戳、访问计数、动态广告位都会导致“假更新”。第二种是哈希指纹。对整个页面或某个 CSS 选择器圈出来的区块计算 Hash(比如 SHA-256),只要内容一变指纹就变。这种方法无法告诉你“哪里变了”,只能告诉你“变了没有”,但对大多数“有没有新公告”的场景已经足够。第三种是 DOM 结构 diff。把 HTML 解析成树,比较节点增删和属性变化,能精确定位到是哪条资讯新增,代价是实现复杂,容易在页面结构微调时翻车。
我的建议是:先做选区哈希。也就是用 XPath 或 CSS 选择器把资讯列表区圈出来,只对这个区域的内容算指纹。这能把整页的动态噪音隔离在外,是灵敏度和误报率平衡最好的方案。后期如果需求明确,可以对选区内的每个链接项目单独算指纹,这样就能知道“哪条新闻是新的”。SiteMonitor 的配置里会有一个selector字段,专门干这个。
2.3 存储与通知选型:SQLite 起步还是上 Redis 队列
监控工具的存储需求其实很小:每个被监控页面保存最近几次快照的哈希和抓取时间,再加上一条变更记录表。用 SQLite 单文件就完全够用,不需要单独起数据库服务。只有你要把 SiteMonitor 做成多实例采集、有大量并发任务时才需要换 Redis 队列,把抓取任务和生产消费解耦。对绝大多数个人或小团队场景,SQLite 更合适,备份简单、迁移方便、没有网络故障。
通知渠道常见的就三种:邮件、即时通讯机器人、本地日志。邮件适合正式的通知留痕,但配置 SMTP 有成本;IM 机器人(例如某企业通信工具的群机器人)提供 Webhook 接口,直接发 HTTP POST 即可,实时性最好;本地日志适合初期验证链路,或者和现有日志采集平台对接。SiteMonitor 可以做成插件式通知:一个notifier对象,统一接口是send(title, content),后面想加渠道就实现一个新的子类。存储选型上,如果你预期单表数据超过几十万条,或者要跑复杂的统计查询,再考虑迁移 PostgreSQL。否则 SQLite 那个几百 KB 的文件能撑住一年以上的监控记录。
3. 用 Python 把一个最小 SiteMonitor 跑起来:抓取、指纹、告警一条龙
3.1 抓取层:requests + 解析器,处理编码与动态渲染
先约定一个最小可用的技术栈:Python 3.9+,requests负责 HTTP 抓取,lxml做 HTML 解析,hashlib计算指纹,标准库sqlite3做持久化,crontab做调度。这个组合没有任何重型框架,部署到一台最小的云主机上也能跑得动。
抓取层最容易出问题的不是拿不到内容,而是拿到的内容不能用。第一是编码:很多老网站是 GBK/GB2312,requests的response.text会按 HTTP 头里的 charset 去解码,如果头信息缺失或写错,就会乱码。第二是压缩:某些站点强制返回 gzip,不设置Accept-Encoding会拿到乱码二进制。第三是动态渲染:目标页面如果用 JavaScript 异步加载资讯列表,requests直接拿到的是空壳 HTML。处理办法是在抓取函数里先检查response.history有没有跳转,再看response.encoding,最后用lxml解析前对文本做一次编码修正。
import requests from lxml import etree def fetch_page(url, timeout=15, headers=None): """抓取页面,返回可解析的HTML文本""" # 常见做法:伪装成普通浏览器,但不伪装过度 default_headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'Accept-Encoding': 'gzip, deflate', # 带上条件请求头,服务器没变化时返回304可以省流量 'Cache-Control': 'max-age=0', } if headers: default_headers.update(headers) try: resp = requests.get(url, headers=default_headers, timeout=timeout) resp.raise_for_status() except requests.exceptions.Timeout: # 超时不能算更新,返回None让上层跳过这一轮 return None except requests.exceptions.RequestException as e: # 网络层面的错误也返回None,避免误报 return None # 修正编码:如果服务器声明了编码就用,否则用apparent_encoding推测 if not resp.encoding or resp.encoding.lower() == 'iso-8859-1': resp.encoding = resp.apparent_encoding or 'utf-8' # 处理好gzip/brotli,requests默认会自动解压 return resp.text # 参数说明: # timeout 建议不小于 10,有些老站响应很慢;太小会误判站点挂了。 # headers 里不建议加太多特征字段,某些反爬系统就是靠“过于完整”的UA来识别爬虫。逻辑上,fetch_page返回的是解码后的 HTML 字符串,上层再交给解析器。这里故意没处理 JavaScript 渲染,因为最小版本只面向服务端渲染的资讯页。如果后续遇到动态页面,在第 5 章的避坑里再扩展。
3.2 指纹层:计算内容哈希与提取“资讯条目”的元数据
拿到 HTML 后,下一步就是圈定监控区域。SiteMonitor 的核心价值在于“选区”,而不是整页哈希。假设我们要监控一个公告列表页面,公告列表通常包在一个<ul>或<div>里,每项是带链接的标题。我们可以用 XPath 定位这个列表的容器,然后提取容器内部的text_content()或直接序列化成字符串,再算 SHA-256。
这里有一个取舍:只提取链接文本和 href,能有效忽略按钮、背景图、内联样式的干扰;序列化整段 HTML 则能捕捉到样式类名的变化,但误报率会高。我通常只需要知道“列表有没有新增条目”,所以提取所有<a>标签的(href, text)对,构造一个列表,对列表做序列化后哈希。这样就算页面内嵌了动态时间戳,只要链接没变,指纹也不会变。
import hashlib from lxml import etree def extract_links(html, xpath): """从HTML中提取XPath指向的链接列表,返回排序后的稳定字符串""" parser = etree.HTMLParser() tree = etree.fromstring(html, parser=parser) links = tree.xpath(xpath) items = [] for a in links: href = a.get('href', '') text = ''.join(a.itertext()).strip() # 相对链接转绝对链接,避免被站点改路径误判 if href and href.startswith('/'): href = 'https://example.com' + href # 实际应从配置中取base_url items.append(f"{href}|{text}") # 排序很重要:列表顺序偶尔变化不算内容更新 items.sort() return '\n'.join(items) def make_fingerprint(content): """对内容字符串计算SHA-256,返回十六进制摘要""" return hashlib.sha256(content.encode('utf-8')).hexdigest()参数说明:xpath是监控的精髓,比如//ul[@class="news-list"]//a。写 XPath 前先用浏览器开发者工具确认列表节点的稳定 class 或 id。items.sort()处理了列表被倒序排列的情况——有些站会把最新条目插到列表中间而不是顶部,排序后只要集合没变就不报警。当然如果业务需要“顺序变了也通知”,可以去掉排序,看具体诉求。
3.3 通知层:邮件 / 企业微信机器人 / 本地日志,三选一怎么接
通知是可插拔的。最小版本先实现一个控制台打印和文件追加,确认检测逻辑没问题后再接真实渠道。控制台通知的代码很简单:指纹变化时,打印页码变化前的标题列表差异。为了后面扩展,我习惯把通知抽象成接口。
import smtplib from email.message import EmailMessage class Notifier: """通知基类,子类实现send方法""" def send(self, title, content): raise NotImplementedError class LogNotifier(Notifier): """本地日志通知,适合调试和初期验证""" def __init__(self, log_path): self.log_path = log_path def send(self, title, content): with open(self.log_path, 'a', encoding='utf-8') as f: f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {title}\\n{content}\\n") class MailNotifier(Notifier): """邮件通知,需要配置SMTP账户信息""" def __init__(self, smtp_host, smtp_port, username, password, to_addr): self.smtp_host = smtp_host self.smtp_port = smtp_port self.username = username self.password = password self.to_addr = to_addr def send(self, title, content): msg = EmailMessage() msg['Subject'] = title msg['From'] = self.username msg['To'] = self.to_addr msg.set_content(content) with smtplib.SMTP(self.smtp_host, self.smtp_port) as server: server.starttls() server.login(self.username, self.password) server.send_message(msg)调用端不需要感知具体是哪个 Notifier,统一notifier.send("资讯更新", "检测到3条新链接")。这样以后接 IM 机器人,只改一行配置。注意邮件通知需要保证 SMTP 密码不硬编码在代码里——从环境变量读取,避免泄露。
3.4 最小可用 crontab 调度:每 10 分钟巡检一次的完整脚本
把上面几块串起来,就是一个完整的单轮巡检函数。每次巡检执行:抓 URL → 提取链接 → 计算指纹 → 数据库查旧指纹 → 不同则插入新记录并通知。数据库表结构放在下一章,这里先用内存字典模拟状态,验证逻辑链路。
import json, time # 用一个JSON文件暂存上次指纹,真实场景换成SQLite STATE_FILE = '/tmp/sitemonitor_state.json' URL = 'https://example.com/news' XPATH = '//ul[@class="news-list"]//a' def load_state(): try: with open(STATE_FILE, 'r', encoding='utf-8') as f: return json.load(f) except FileNotFoundError: return {} def save_state(state): with open(STATE_FILE, 'w', encoding='utf-8') as f: json.dump(state, f, ensure_ascii=False, indent=2) def run_once(): state = load_state() old_fp = state.get('fingerprint') html = fetch_page(URL) if html is None: print("抓取失败,本轮跳过") return raw_content = extract_links(html, XPATH) fp = make_fingerprint(raw_content) if old_fp is None: # 第一次运行没有基准,只保存不通知 print("首次初始化,记录指纹") elif fp != old_fp: # 指纹变了,发通知并更新状态 print(f"检测到页面变更!旧指纹: {old_fp[:16]} 新指纹: {fp[:16]}") # notifier.send("资讯更新", "链接列表发生变化") else: print("无变化") state['fingerprint'] = fp state['last_check'] = time.time() save_state(state) if __name__ == '__main__': run_once()这个脚本本身不包含调度,而是作为单次任务交给 crontab。调度配置写在系统 crontab:*/10 * * * * cd /data/sitemonitor && /usr/bin/python3 monitor.py >> /var/log/sitemonitor.log 2>&1。每 10 分钟执行一次。第一次跑会全量初始化,当天后续每轮只算一次哈希和一次数据库查询,负载很低。参数说明:XPath 变了或 URL 变了,不需要改代码,只要改脚本顶部的配置变量。
4. 把监控从玩具变成工具:持久化、断点续抓与前端看板
4.1 用 SQLite 记录每次快照,做到可回溯
内存字典适合验证,但一旦程序重启就丢失历史。SQLite 最大的好处是单文件、零配置、事务可靠。SiteMonitor 需要两张表:snapshots记录每次抓取的指纹和概要,changes记录两次快照之间发生的差异。这样你不仅能收到通知,还能回去查“昨天下午那次更新具体多了哪些链接”。
CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, fingerprint TEXT NOT NULL, content_preview TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_id INTEGER, url TEXT NOT NULL, diff_summary TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) );content_preview存的是提取出来的链接列表的截断文本,用来快速回复“到底哪几条是新加的”。diff_summary可以存新增链接的标题集合。频繁写库不用担心性能,SQLite 单进程写事务比很多人想象中快,每秒几千次也不成问题。要注意的是给url + created_at建索引,否则几个月后查询历史会很慢。
4.2 增量抓取与断点续抓:只在站点变化时才重新解析
有些站点资讯列表很长,每次全量解析几百个链接没问题,但如果你监控的是分页列表,或者资讯正文页,每次都全量抓取很浪费。SiteMonitor 常见的优化是分两个阶段:先抓列表页,算出指纹;只有指纹变化了,才重新解析并提取新增链接;如果没变化,直接跳到下一个目标。
断点续抓要处理的是抓取中断问题。比如网络超时只抓到半个页面,或者解析抛异常。我习惯的做法是在每个步骤前写检查点:抓取前记录status=pending,抓完更新status=done。下次运行时发现pending状态,就重新抓取而不是沿用旧数据。SQLite 表里可以加一个status字段。这是为了避免“指纹暂时为空被误当成页面清空”的低级错误。
另一个细节是重试机制。对于偶发超时,常见的做法是连续重试 3 次,每次间隔指数退避(1 秒、4 秒、9 秒)。只有 3 次都失败才放弃本轮,并使用上一次成功的快照作为基准继续。这样既不会因为一次抖动就错失监控,也不会在网络故障时产生错误通知。
4.3 一个 100 行 Flask 看板:查看变更历史与当前状态
有了 SQLite,就可以做一个极简看板,用 Flask 提供两个页面:当前监控列表和历史变更记录。这个看板不追求花哨,只解决“通知来了但我不记得上次改了什么”的问题。用flask读取snapshots和changes表,在模板里按时间倒序展示。
from flask import Flask, render_template, request import sqlite3 app = Flask(__name__) DB_PATH = '/data/sitemonitor/sitemonitor.db' def query_db(q, args=()): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cur = conn.execute(q, args) rows = cur.fetchall() conn.close() return rows @app.route('/') def index(): # 展示所有监控URL当前状态 rows = query_db(''' SELECT url, fingerprint, MAX(created_at) AS last_check FROM snapshots GROUP BY url ''') return render_template('index.html', rows=rows) @app.route('/history') def history(): page = request.args.get('page', 1, type=int) per_page = 20 rows = query_db(''' SELECT * FROM changes ORDER BY created_at DESC LIMIT ? OFFSET ? ''', (per_page, (page-1)*per_page)) return render_template('history.html', rows=rows)这里需要注意GROUP BY url与MAX(created_at)的配合:标准 SQL 里fingerprint不保证取到最大时间对应的那一行,SQLite 实际会返回最先遇到的非聚合字段。更稳妥的写法是用子查询,或者直接按id DESC取每组最新一条。看板本身不承担监控逻辑,只读库,所以即使看板挂了也不影响采集进程。
5. SiteMonitor 落地避坑:5 个真实翻车场景与排查思路
5.1 现象:明明每 10 分钟只访问一次,目标站却封了 IP
有人配置了 SiteMonitor 监控某个行业资讯站,频率已经很保守,但运行两天后发现 IP 被封锁,所有请求都返回 403。原因不是请求频率,而是请求特征太明显。requests默认的User-Agent是python-requests/x.x.x,目标站的防护系统看到这个 UA 直接判定为爬虫。另一个隐藏原因是日志里每次请求都会带Accept-Encoding: gzip,但缺少Accept-Language和其他浏览器头,行为指纹不完整。解决方式是在fetch_page里设置一组完整的请求头,最好从浏览器复制真实请求头的User-Agent、Accept、Accept-Language、Referer。注意不要过度伪装:同一个 UA 每次都不变反而可疑,可以准备两三个 UA 轮换。
5.2 现象:抓下来的页面中文全是乱码,指纹也因此每天疯狂变化
某监控目标是一个老牌政策公告站,页面是 GBK 编码,但 HTTP 响应头里没有显式charset,requests默认按ISO-8859-1解码,导致response.text里全是乱码。乱码文本的哈希值每次可能不同,因为解码结果受字节流微小差异影响,于是每天收到几十条假更新通知。排查时先打印resp.encoding和resp.apparent_encoding,如果apparent_encoding能正确识别为GBK,就修正编码。我的习惯是在解析前强制检查:如果页面标题里的中文字符出现替换字符(�),就用resp.content.decode('gbk', errors='ignore')重新解码。永远不要相信 HTTP 头里的 charset,以实际内容为准。
5.3 现象:requests 拿到的 HTML 没有资讯列表,但是浏览器里明明能看到
这是动态渲染的典型特征。目标页面用 Vue 或 React 在客户端加载数据,初始 HTML 里只有一个<div id="app">,真正的资讯列表来自后续的 Ajax 请求。遇到这种情况要先在浏览器开发者工具里的 Network 面板找 XHR 请求,看看资讯数据是不是从某个 JSON 接口返回的。如果有,SiteMonitor 可以直接监控那个 JSON 接口,抓取和解析都更简单。如果没有独立接口,只能引入无头浏览器。常见做法是用playwright调起 Chromium 渲染完成后取 HTML,但代价是内存占用高,每分钟巡检一次可能让机器吃紧。我一般会优先找接口,实在找不到再上无头浏览器,同时把巡检间隔拉长到 15 分钟以上。
5.4 现象:一条更新通知被发送了三遍,记录里又没有任何重复
通知重复的根源不在通知层,而在状态保存的时机。我最初写的脚本是“先发通知,再存指纹”,结果进程在发完通知后、执行save_state前崩溃了,下一轮巡检发现指纹还是旧的,又触发了一次通知。解决方法是把状态更新放到通知之前的同一个事务里:先保存新指纹,再发通知。更稳妥的是给通知本身加一个去重表,记录上一次通知的指纹和通知时间,同一个指纹在 5 分钟内不重复发送。另外,如果任务并发执行,比如 crontab 和手动脚本同时跑了一轮,也会存在竞争条件。可以用一个文件锁或数据库锁保证同时只有一个巡检进程在运行。
5.5 现象:crontab 定时任务每天“晚了一小时”才执行
SiteMonitor 部署在一台 UTC 时区的服务器上,crontab 里写的是30 * * * *,本来应该每小时的 30 分执行,实际却在系统本地时区的 30 分执行。如果没有注意TZ=Asia/Shanghai和 crontab 的环境变量,就会出现“差 8 小时”的错觉。排查方法是先运行date看当前时区,再看/etc/crontab和用户 crontab 的时区定义。解决方式是不要在 crontab 里依赖时区,统一在 Python 脚本内部用datetime.now(timezone.utc)获取绝对时间,并让告警消息里的时间戳带上时区信息。另一个常见坑是 crontab 里的PATH不包含 Python 命令路径,导致脚本报python3: command not found。在 crontab 顶部显式设置PATH=/usr/bin:/bin:/usr/local/bin,或者在脚本首行写绝对路径的 shebang。
6. 进阶用法:把 SiteMonitor 的误报率降下来,并验证监控本身
6.1 用相似度阈值过滤“假更新”:编辑距离加权方案
哈希指纹对“改了一个字”和“重排整个列表”一视同仁,都会触发更新。但实际业务里,列表里偶尔插入一条无关紧要的推荐位链接,或者把某条标题从“【重要】xxx”改成“xxx”,并不值得发通知。常见做法是把哈希从“完全相等”升华为“相似度”。我试过用difflib.SequenceMatcher计算新旧链接列表的相似度,低于一定阈值才通知。比如相似度 0.9 以上说明只是微调,忽略;0.5 以下说明大改动,立即告警。对链接文本做归一化处理也很关键:去掉首尾空格和不可见字符,统一小写,能消掉一半假更新。
6.2 自监控与验证:用测试页确认告警链路没哑火
监控工具最怕的不是误报,而是漏报之后你还不知道。SiteMonitor 可以给自己配一个“心跳监控”:每轮巡检结束,把可观测指标推到本地状态文件,再单独有一个脚本检查这个文件是否更新。如果超过两倍的巡检周期没有更新,说明主监控卡死或进程退出,需要发一条“SiteMonitor 自身异常”的告警。我在实际部署中还会准备一个测试 URL,里面放一个随机数变动点,每轮手动改一次,确保通知链路是通的。这个测试 URL 不参与真实监控,纯粹是用来验证整个流水线的“体检信号”。养成这些习惯后,SiteMonitor 才算真正从“能跑的脚本”变成“敢托付的监控工具”。希望这篇笔记里的方案和坑位能帮你在同样的路线上少走几圈。
本文还有配套的精品资源,点击获取