news 2026/9/1 4:43:16

Python实现网页状态监控:以FansToys新品预告为例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实现网页状态监控:以FansToys新品预告为例

最近很多人应该都在关注 FansToys 的新品动态,尤其是那条COMING SOON的 FT-63 TURBO 预告。对于收藏玩家来说,新品从预告到开放预订、再到发售,中间的状态变化非常关键,但信息分散在官网、论坛、社交平台等多个渠道。与其每天手动刷新页面,不如自己写一个轻量级的“新品状态监控”脚本,让程序替我们盯住页面变化。

本文就以 FansToys FT-63 TURBO 的预告信息为案例,完整拆解一个基于 Python 的新品状态监控工具。你不需要很强的爬虫基础,跟着文章把环境、代码、定时任务配好,就能把“COMING SOON”变成自动通知。

1. 背景与核心概念

1.1 新品预告信息为什么难跟踪

像 FansToys 这类第三方玩具品牌,新品发布通常不会只在一个渠道公告。官网会有产品页,社交媒体会发图,第三方代理商店铺会提前挂出预订链接,论坛里还会有玩家讨论帖。不同渠道的信息更新节奏不同,状态描述也不统一。

常见状态词大概有这么几类:

  • COMING SOON:预告,还没有开放预订
  • PO/PRE-ORDER:已经开放预订
  • IN STOCK:现货
  • SOLD OUT:售罄

如果只关注一个型号,比如 FT-63 TURBO,手动刷新几天还能接受。但如果同时关注十几个型号,还要对比不同渠道的价格和状态,手工操作就容易遗漏关键节点。尤其是“开放预订”这个节点,很多人因为没有第一时间收到消息,错过了首发价。

1.2 监控工具的通用模型

这类状态监控本质上是一个“网页内容变化检测”问题。通用流程可以拆成五步:

  1. 抓取目标页面 HTML
  2. 抽取页面文本内容
  3. 匹配关键词,判断当前状态
  4. 与上次状态比对,判断是否变化
  5. 状态变化时发送通知

对应到技术方案,就是 Python 加四个基础组件:

  • requests:抓取页面
  • BeautifulSoup:解析 HTML 文本
  • SQLite:持久化历史状态
  • SMTP/ 消息推送:发送通知

这套方案的优点是轻量、好理解、便于扩展。不需要引入 Scrapy、Celery 这类重型框架,单文件脚本就能跑起来。

1.3 本文示例边界

需要提前说明的是,本文重点是“状态监控”的工程实现,而不是对 FT-63 TURBO 这款产品本身的猜测和讨论。玩具的具体角色、尺寸、价格、发售日期,请以 FansToys 官方渠道发布的信息为准。示例代码中的 URL 是占位符,实际使用时替换成你要监控的页面地址即可。

2. 环境准备与版本说明

2.1 运行环境

本文示例使用 Python 3.9+,操作系统可以是 Windows、Linux 或 macOS。代码中用到的库都是跨平台的,不需要额外处理系统差异。

主要依赖如下:

依赖用途
requests发送 HTTP 请求,抓取页面
beautifulsoup4解析 HTML
lxmlBeautifulSoup 的 HTML 解析器
schedule 或 cron定时执行(本文使用系统 crontab 示例)

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。建议在虚拟环境中安装依赖,避免污染全局 Python 环境。

2.2 安装依赖

先创建一个项目目录:

mkdir ft63-tracker cd ft63-tracker

创建虚拟环境并激活:

python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate

然后安装依赖:

pip install requests beautifulsoup4 lxml

为了便于复现,可以生成依赖清单:

pip freeeze > requirements.txt

2.3 项目目录结构

项目文件组织如下:

ft63-tracker/ ├── config.py # 全局配置,网址、关键词、通知参数 ├── db.py # SQLite 初始化与读写 ├── notify.py # 通知模块 ├── tracker.py # 主程序 ├── sample.html # 本地模拟页面,用于测试解析逻辑 └── requirements.txt # 依赖清单

如果是在正式项目中,可以把配置和密钥放到环境变量或独立的配置中心。这里为了演示直观,先把配置集中到config.py,后面章节会单独讲如何安全地管理密钥。

3. 核心实现原理

3.1 页面抓取与请求头

requests库发送 GET 请求很简单,但直接裸请求容易碰到两个问题:

  • 部分站点会拦截没有 User-Agent 的请求
  • 请求超时时间不设置,程序可能长时间卡住

所以抓取函数需要带 User-Agent 和超时时间,同时做状态码检查。

def fetch_page(url): headers = {"User-Agent": USER_AGENT} resp = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT) resp.raise_for_status() if not resp.encoding or resp.encoding.lower() == "iso-8859-1": resp.encoding = resp.apparent_encoding return resp.text

这里有个细节:resp.encoding如果识别错误,中文页面很容易乱码。通过apparent_encoding从内容中推断编码,能解决大部分乱码情况。

3.2 HTML 文本抽取

拿到 HTML 后,不能直接做关键词匹配。HTML 里包含大量标签、脚本和样式,比如:

<p><strong>FT-63 TURBO</strong> is <span>COMING SOON</span>!</p>

直接搜文本会匹配到标签属性,也容易被内嵌脚本干扰。用 BeautifulSoup 把文本抽出来再匹配,会更稳。

def extract_text(html): soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "noscript"]): tag.decompose() return " ".join(soup.stripped_strings)

stripped_strings会去掉首尾空白,拼接后的文本干净很多。实际项目中还可以根据页面结构缩小查找范围,比如只解析main容器,减少干扰。

3.3 关键词与状态判定

状态判定逻辑可以做成一个独立函数:把文本转成大写,然后按优先级检查关键词。

def detect_status(text): upper_text = text.upper() for keyword in KEYWORDS: if keyword.upper() in upper_text: return keyword.upper() return "UNKNOWN"

关键词的顺序很关键。COMING SOONPRE-ORDER如果同时出现在页面里,说明商品已经进入了下一阶段,应该优先匹配“更靠后”的状态。所以KEYWORDS列表要按状态演进顺序排列。

3.4 状态持久化与去重

如果每次都把状态写入数据库,会产生大量无意义数据。更合理的做法是只保存“状态变化记录”和“最近一次确认时间”。

SQLite 表结构如下:

CREATE TABLE IF NOT EXISTS item_status ( url TEXT PRIMARY KEY, keyword TEXT NOT NULL, status TEXT NOT NULL, first_seen TEXT NOT NULL, last_seen TEXT NOT NULL, update_count INTEGER DEFAULT 1 )
  • url:页面地址,作为唯一标识
  • keyword:匹配到的关键词
  • status:归一化后的状态
  • first_seen:第一次发现该 URL 的时间
  • last_seen:最近一次检查的时间
  • update_count:状态变化次数

状态变化时更新状态和时间戳;状态没变化时只更新last_seen,用于判断页面是否还可达。

3.5 通知机制

通知是整套工具真正“有用”的地方。最简单的通知方式是打印日志,但这样还是需要人盯着终端。更实用的做法是接入邮件或消息推送。

邮件方案可以在 Python 标准库smtplib基础上实现,不依赖第三方平台,适合自用。如果追求微信推送,可以接入 Server酱,原理也是发一个 HTTP 请求。代码里可以通过一个配置开关切换。

4. 完整实战案例

4.1 编写配置模块 config.py

首先创建config.py,把所有可调整的参数集中管理。

import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") DB_PATH = os.path.join(DATA_DIR, "tracker.db") # 替换成你要监控的实际页面地址 TARGET_URLS = [ "https://www.example.com/fanstoys/ft-63-turbo", ] # 状态关键词,按状态演进顺序排列 KEYWORDS = ["COMING SOON", "PRE-ORDER", "AVAILABLE", "SOLD OUT"] # 品牌与型号关键字,用于二次确认是否与目标产品相关 BRAND_KEYWORDS = ["FT-63", "TURBO", "FANS TOYS", "FANSTOYS"] USER_AGENT = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ) REQUEST_TIMEOUT = 10 # 通知开关 NOTIFY_ENABLED = False # 邮件通知参数 MAIL_HOST = "smtp.example.com" MAIL_PORT = 465 MAIL_USER = "" MAIL_PASSWORD = "" MAIL_TO = ""

BRAND_KEYWORDS的作用是防止误判。有些页面可能会把COMING SOON当通用文案放在网页底部,如果不做二次确认,就很容易误报。

4.2 编写数据库模块 db.py

db.py负责数据库初始化和状态读写。

import os import sqlite3 from datetime import datetime from config import DATA_DIR, DB_PATH def get_connection(): os.makedirs(DATA_DIR, exist_ok=True) return sqlite3.connect(DB_PATH) def init_db(): conn = get_connection() conn.execute(""" CREATE TABLE IF NOT EXISTS item_status ( url TEXT PRIMARY KEY, keyword TEXT NOT NULL, status TEXT NOT NULL, first_seen TEXT NOT NULL, last_seen TEXT NOT NULL, update_count INTEGER DEFAULT 1 ) """) conn.commit() conn.close() def upsert_record(conn, url, status, keyword): now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") row = conn.execute( "SELECT status, update_count FROM item_status WHERE url = ?", (url,), ).fetchone() if row is None: conn.execute( """ INSERT INTO item_status (url, keyword, status, first_seen, last_seen, update_count) VALUES (?, ?, ?, ?, ?, 1) """, (url, keyword, status, now, now), ) conn.commit() return True, None old_status = row[0] if old_status != status: conn.execute( """ UPDATE item_status SET status = ?, keyword = ?, last_seen = ?, update_count = update_count + 1 WHERE url = ? """, (status, keyword, now, url), ) conn.commit() return True, old_status conn.execute( "UPDATE item_status SET last_seen = ? WHERE url = ?", (now, url), ) conn.commit() return False, old_status

upsert_record返回两个值:第一个表示状态是否变化,第二个是旧状态。主程序可以根据返回值决定是否发通知。

4.3 编写通知模块 notify.py

邮件通知模块如下:

import smtplib from email.mime.text import MIMEText from config import ( MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_TO, MAIL_USER, NOTIFY_ENABLED, ) def send_mail(subject, content): if not NOTIFY_ENABLED: return False if not (MAIL_USER and MAIL_PASSWORD and MAIL_TO): return False msg = MIMEText(content, "plain", "utf-8") msg["Subject"] = subject msg["From"] = MAIL_USER msg["To"] = MAIL_TO try: with smtplib.SMTP_SSL(MAIL_HOST, MAIL_PORT) as server: server.login(MAIL_USER, MAIL_PASSWORD) server.sendmail(MAIL_USER, [MAIL_TO], msg.as_string()) return True except Exception as exc: print(f"[notify] 邮件发送失败: {exc}") return False

如果使用 QQ 邮箱或 163 邮箱,MAIL_PASSWORD填的是授权码,不是登录密码。这是新手最容易踩的坑。

4.4 编写抓取与解析逻辑

把抓取、文本抽取、状态判断整合到tracker.py中。为了便于测试,将核心逻辑拆成独立函数。

import argparse from datetime import datetime import requests from bs4 import BeautifulSoup from config import ( BRAND_KEYWORDS, KEYWORDS, REQUEST_TIMEOUT, TARGET_URLS, USER_AGENT, ) from db import get_connection, init_db, upsert_record from notify import send_mail def fetch_page(url): headers = {"User-Agent": USER_AGENT} resp = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT) resp.raise_for_status() # 解决中文页面乱码问题 if not resp.encoding or resp.encoding.lower() == "iso-8859-1": resp.encoding = resp.apparent_encoding return resp.text def extract_text(html): soup = BeautifulSoup(html, "html.parser") # 移除脚本和样式,避免干扰文本匹配 for tag in soup(["script", "style", "noscript"]): tag.decompose() return " ".join(soup.stripped_strings) def detect_status(text): upper_text = text.upper() for keyword in KEYWORDS: if keyword.upper() in upper_text: return keyword.upper() return "UNKNOWN" def is_brand_related(text): upper_text = text.upper() return any(keyword in upper_text for keyword in BRAND_KEYWORDS)

这里把detect_statusextract_text拆开,方便后面写本地测试。

4.5 编写主程序入口

主程序逻辑就是前面说的五步流程。

def process_url(conn, url): print(f"[tracker] 开始检查: {url}") try: html = fetch_page(url) text = extract_text(html) except Exception as exc: print(f"[tracker] 抓取失败: {exc}") return # 二次确认页面与目标产品相关 if not is_brand_related(text): print("[tracker] 页面未匹配到品牌或型号关键字,跳过") return status = detect_status(text) changed, old_status = upsert_record(conn, url, status, status) print(f"[tracker] 当前状态: {status}") if changed: subject = f"FT-63 TURBO 状态变化: {old_status or '新记录'} -> {status}" content = ( f"检测到 {url} 的状态变为 {status}\n" f"时间: {datetime.now()}" ) send_mail(subject, content) print(f"[tracker] 状态变化,已发送通知: {old_status or '新记录'} -> {status}") else: print("[tracker] 状态未变化") def main(): parser = argparse.ArgumentParser(description="FT-63 TURBO 新品状态监控") parser.add_argument("--once", action="store_true", help="只执行一次") args = parser.parse_args() init_db() conn = get_connection() for url in TARGET_URLS: process_url(conn, url) conn.close() if args.once: return if __name__ == "__main__": main()

整个程序的核心思想是“无状态检查”:每次执行都重新抓取、重新判定,然后和数据库中的历史状态比对。这样即使上次运行崩溃了,下一次运行也能从数据库恢复状态,不会漏报。

4.6 本地模拟测试

直接运行脚本去抓真实网站,可能因为网络不稳定或页面结构问题导致排查困难。建议先用本地 HTML 文件验证解析逻辑。

创建sample.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>FansToys FT-63 TURBO Pre-Order</title> <style> .price { color: red; } </style> </head> <body> <h1>FansToys FT-63 TURBO</h1> <p>New item is <strong>COMING SOON</strong>!</p> <p>Please stay tuned.</p> </body> </html>

然后在 Python 交互环境里测试:

from tracker import extract_text, detect_status, is_brand_related html = open("sample.html", encoding="utf-8").read() text = extract_text(html) print(text) print(is_brand_related(text)) print(detect_status(text))

预期输出类似:

FansToys FT-63 TURBO Pre-Order New item is COMING SOON! Please stay tuned. True COMING SOON

这里有个常见问题:title里面出现了Pre-Order,正文里是COMING SOON。因为KEYWORDS列表里COMING SOON排在前面,程序会输出COMING SOON。如果实际页面里已经有明确的开放预订按钮,关键字顺序应该调整,或者只匹配页面指定区域。

4.7 配置定时任务

脚本本身支持手动执行:

python tracker.py --once

要让它定时运行,Linux 环境可以用crontab

crontab -e

加入一行,每 30 分钟执行一次:

*/30 * * * * cd /path/to/ft63-tracker && /path/to/venv/bin/python tracker.py --once >> logs/tracker.log 2>&1

Windows 环境下,可以使用“任务计划程序”,创建基本任务时选择“每天”,并在“操作”中设置程序为python.exe,参数为脚本路径。注意 Python 解释器建议使用绝对路径,避免环境变量问题。

4.8 运行结果解读

第一次运行时,数据库没有记录,程序会输出:

[tracker] 开始检查: https://www.example.com/fanstoys/ft-63-turbo [tracker] 当前状态: COMING SOON [tracker] 状态变化,已发送通知: 新记录 -> COMING SOON

之后只要页面还是COMING SOON,输出会变成:

[tracker] 当前状态: COMING SOON [tracker] 状态未变化

当页面更新为“开放预订”时,输出变为:

[tracker] 状态变化,已发送通知: COMING SOON -> PRE-ORDER

查看数据库历史记录:

sqlite3 data/tracker.db "SELECT * FROM item_status;"

可以看到update_count字段累计了状态变化次数,last_seen记录了最近一次检查时间。

5. 常见问题与排查思路

问题现象常见原因解决思路
页面返回 403请求被站点反爬拦截配置浏览器 User-Agent,降低请求频率,不要短时间大量抓取
中文内容乱码页面编码识别错误检查resp.encoding,使用apparent_encoding推断
一直处于UNKNOWN关键词不匹配或页面结构变化打印抽取后的文本内容,人工确认实际文案
邮件通知收不到SMTP 授权码错误或端口不对检查授权码,确认 SSL 端口,查看发送日志
数据库文件被锁多进程同时写入 SQLite使用单进程定时任务,或开启 WAL 模式

最容易忽视的问题是“页面结构变化”。第三方店铺的页面改版后,关键词可能还在,但文本被拆分到了不同节点。比如原来是COMING SOON,改版后变成了COMING</span><span>SOON,直接字符串匹配可能就失效了。这时可以把文本中的空白符统一替换为空格再匹配。

import re text = re.sub(r"\s+", " ", text)

如果页面结构经常变化,建议把抽取到的文本快照保存到日志或数据库,方便定位问题。

6. 最佳实践与工程建议

6.1 控制抓取频率,遵守站点规则

监控工具的本质是轻量轮询,不是爬虫系统。对目标站点要保持礼貌:

  • 单个站点的抓取间隔不要低于 10 分钟
  • 先确认robots.txt允许抓取
  • 只抓取公开页面,不登录、不绕过验证码
  • 如果页面提供 RSS 或官方通知接口,优先使用

脚本里可以通过time.sleep()控制请求间隔,也可以更进一步,把不同站点的抓取间隔配置化。

6.2 配置与密钥管理

config.py里的邮件密码、推送 Key 都是敏感信息。如果项目要提交到 Git,建议:

  • 使用.gitignore忽略config.py或使用config.local.py
  • 密钥通过环境变量注入
  • 示例配置只保留占位符

一个简单做法是创建config.example.py,提交到仓库,真实config.py留在本机。

6.3 用状态机管理关键词

前面的KEYWORDS列表本质上是状态机。

UNKNOWN -> COMING SOON -> PRE-ORDER -> AVAILABLE -> SOLD OUT

用列表顺序表达状态优先级,代码简单,适合这种轻量场景。如果状态变多、逻辑变复杂,建议引入枚举类,把状态迁移规则单独抽出来,减少硬编码。

6.4 日志与异常处理

定时任务最怕“静默失败”。程序异常如果不打印或记日志,你可能根本不知道监控已经停了。推荐做法:

  • 控制台输出统一格式,方便调试
  • 定时任务把输出重定向到日志文件
  • 抓取异常单独记录,连续失败多次可以告警
logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", )

6.5 安全与合规边界

本文的所有代码都是基于“公开页面信息”做的状态检测。实际操作中需要注意:

  • 不抓取需要登录后才能看到的非公开信息
  • 不通过技术手段绕过访问控制
  • 不加高并发压力测试目标站点
  • 抓取的数据仅用于个人学习或合法用途,不批量搬运、不用于商业化牟利

如果对某类数据的抓取行为是否合规不确定,先联系站点运营方确认,或者直接放弃该数据源。

6.6 通知去重与升级

状态监控最怕“重复轰炸”。当前代码在状态没变化时不会发通知,只有在状态变化时发一次,已经实现了基本的去重。

但如果页面在某段时间内反复横跳,比如一会儿COMING SOON,一会儿PRE-ORDER,数据库会记录多次变化,通知也会多次发送。可以在notify.py里加上“冷静期”,比如同一 URL 1 小时内最多发送一次通知。

7. 总结与后续扩展

本文以 FansToys FT-63 TURBO 的预告信息为切入点,实现了一个完整的新品状态监控工具。核心流程是抓取页面、抽取文本、匹配关键词、持久化状态、变化时通知,这套思路不仅适用于玩具新品,也适用于任何需要监控网页状态变化的场景。

如果你只是关注一个型号,可以先把TARGET_URLSBRAND_KEYWORDS改成你的目标页面,跑几天看看效果。再往后,可以尝试几个扩展方向:

  • 把 SQLite 换成 PostgreSQL,支持多用户协同
  • 增加一个简单的 Web 展示页面,用图表展示状态变化历史
  • 接入企业微信或钉钉机器人,把通知发到团队群
  • 用 Docker 打包部署,放到服务器上稳定运行
  • 把关键词匹配改成更智能的语义判断,减少误报

动手改一版属于你自己的监控工具,比单纯收藏这篇文章更有价值。如果运行过程中遇到什么问题,欢迎对照常见问题清单排查,也建议把页面抽取的文本先打印出来看看,很多问题都会一目了然。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 4:42:50

YOLOv8+PyQt5:构建工地安全帽检测系统的完整实践

前阵子帮一个安全信息化小组做技术选型&#xff0c;聊到工地安全帽检测。对方一开始的需求很直接&#xff1a;能不能用摄像头自动识别工人有没有戴安全帽&#xff0c;有违规就报警。讨论下来发现&#xff0c;问题根本不在“识别”本身&#xff0c;而在识别之后怎么形成证据、怎…

作者头像 李华
网站建设 2026/9/1 4:42:03

MPX实战全解析:暗区突围冲锋枪改装与出金路线指南

之前在《暗区突围》里反复用 MPX 打电视台和军港&#xff0c;总觉得这把冲锋枪有点“闹脾气”&#xff1a;明明射速快、后坐力低&#xff0c;近距离遇敌却经常打不出该有的压制力。后来把改装思路、交战距离和出金路线整体梳理了一遍&#xff0c;才发现问题大多出在对枪械定位的…

作者头像 李华
网站建设 2026/9/1 4:41:57

从运镜教程到Skill:AI视频创作的可复用工作流

花了85分钟看完一套AI电影的运镜教程&#xff0c;又花了10分钟重新翻看关键片段&#xff0c;最后做了一个Skill。很多人学完之后最真实的感受是&#xff1a;看的时候什么都懂&#xff0c;推到AI视频工具里写提示词时还是什么都不会。镜头型号记了一堆&#xff0c;情绪节奏背了几…

作者头像 李华
网站建设 2026/9/1 4:41:30

基于python大数据的b站数据分析可视化系统

1、研究背景虽说处于数字化时代背景里, 然而视频内容平台, 特别是像B站这般的弹幕视频分享网站, 已然变成年轻人获取信息以及娱乐的关键渠道。伴随用户基数持续增长且内容创作活跃起来, B站积攒了海量的用户行为数据与视频内容数据。这些数据不但包含了用户偏好、内容趋势等珍贵…

作者头像 李华
网站建设 2026/9/1 4:40:34

Qt 主窗口开发三件套:菜单栏、工具栏、状态栏与停靠窗口实战

Qt 主窗口开发三件套&#xff1a;菜单栏、工具栏、状态栏与停靠窗口实战Qt 主窗口开发三件套&#xff1a;菜单栏、工具栏、状态栏与停靠窗口实战前言1. 为什么 Qt 很适合做传统桌面程序2. QMenuBar&#xff1a;菜单栏是主窗口的骨架2.1 给菜单设置快捷方式2.2 给action(动作)设…

作者头像 李华