基于 Flask 的「自动任务控制台」设计与踩坑实录

基于 Flask 的「自动任务控制台」设计与踩坑实录

------ Python 常驻调度 + IIS/wfastcgi 部署 + 云端 MySQL 的一体化实践

本文是对一个已稳定运行的生产级项目 webconsole(自动任务控制台) 的完整复盘:

一个 Flask 单体应用,同时承担 Web 控制台 与 5 个常驻采集/自动化任务 的调度中心,

部署在 Windows IIS + wfastcgi 上,数据落地云端 MySQL。

文章包含完整的设计思路、模块拆解,以及从开发到上线踩过的 12 个真实的坑(含排查过程与修复方案),希望对做同类系统的朋友有所帮助。


一、项目概述

1.1 这个系统做什么

自动任务控制台是一个「Web 界面 + 后台任务调度」二合一的单体应用,管理 5 个互不相关的自动化任务:

# 任务 功能 调度模式
1 程序员客栈 · 自动接单 轮询外包平台待办邀请,按策略(预算/关键词/周期)自动接单 7×24 常驻(默认 180s 巡检)
2 雪球 · 调仓数据采集 抓取自选/大V组合持仓快照,对比生成调仓记录,异动邮件提醒 交易日盘中轮询 + 收盘全量
3 涨停数据 · 采集 开盘啦涨停板池 + 涨停原因,经 C# WebAPI 落云端 每交易日 15:30 一次
4 财经舆情 · 多源采集 8 路财经资讯源采集 → SimHash 雷同检测 + 股票命中 → 邮件预警 7×24 常驻(30s 轮询)
5 中标线索库 · 定期采集 政采网站点信息化项目中标/成交公告采集入库 每 3 天 09:00

每个任务都有对应的可视化页面:实时状态卡、数据浏览、常用参数在线调整、手动触发。

1.2 技术选型

层 选型 理由
Web 框架 Flask + Jinja2 轻量单体,模板服务端渲染,无前端构建链
调度 自研常驻线程(非 APScheduler/Celery) 任务本质是「轮询 + 幂等」,常驻线程最简单可靠,见 §4.1
数据库 MySQL 8.0(云端) 与既有 C# 股票系统共用同一个 stock 库,实现跨语言数据共享
部署 Windows IIS + wfastcgi 服务器上已有多个 IIS 站点,统一运维
采集 requests + BeautifulSoup + Playwright 静态页/API 用 requests,需登录态的用本机浏览器 CDP
分析 jieba + TF-IDF + KMeans + SimHash 资讯话题聚类与雷同检测

一个值得强调的决策:不用 Celery/Redis,不用 APScheduler 。

5 个任务的调度逻辑差异很大(7×24 巡检 / 交易日时点 / 每 N 天),但共同点是「到点检查 → 幂等执行 → 写日志」。

每个模块一个 daemon 线程 + sys_config 开关,约 200 行代码搞定全部调度,且天然与 Flask 同进程,页面能直接读线程内部状态。


二、总体架构

复制代码
                        ┌────────────────────────────────────────────┐
                        │              Windows IIS (8015)             │
                        │   站点 webconsole → 物理路径(Junction)       │
                        │   应用池 news (wfastcgi, maxInstances=1)    │
                        └──────────────────┬─────────────────────────┘
                                           │ FastCGI
                        ┌──────────────────▼─────────────────────────┐
                        │              Flask app (app.py)             │
                        │  ┌─────────────┐  ┌──────────────────────┐ │
                        │  │ 11 个页面    │  │ 30+ REST API         │ │
                        │  │ /jobs /news... │  │ /api/news/* /api/... │ │
                        │  └─────────────┘  └──────────────────────┘ │
                        │  ┌──────────────────────────────────────┐  │
                        │  │  jobs_registry.build_scheduler()      │  │
                        │  │  (模块级挂载!见踩坑 #1)             │  │
                        │  └──────┬──────┬──────┬──────┬─────────┘  │
                        │   ┌──────▼─┐ ┌──▼───┐ ┌▼─────┐ ┌▼──────┐┌──▼──────┐
                        │   │cxy-mon │ │xq-mon│ │zt-mon│ │news-mon││bid-mon │
                        │   └────┬───┘ └──┬───┘ └┬─────┘ └┬──────┘└──┬──────┘
                        └────────┼────────┼──────┼────────┼──────────┼─────┘
                                 ▼        ▼      ▼        ▼          ▼
                     ┌───────────────────────────────────────────────────┐
                     │  services/ 层:storage(统一DB) · news_analyzer      │
                     │  (SimHash/命中/邮件) · xueqiu_cookie_capture        │
                     └──────┬──────────────┬──────────────┬──────────────┘
                            ▼              ▼              ▼
                     云端 MySQL stock 库   C# WebAPI      SMTP(163)
                     (共用原系统表)        (涨停数据落库)  (预警邮件)

关键设计原则:

  1. 单进程单调度 :FastCGI maxInstances=1,保证调度线程全应用只有一份(双实例会重复采集);
  2. 页面读线程、不抢任务 :页面只读各模块 state_view()(内存态)+ job_run_log(库态),手动触发走 API 队列;
  3. 存储统一收口 :所有 DB 访问经 services/storage.py,线程本地连接,禁止各模块自建连接;
  4. 敏感信息零落码 :SMTP 授权码/DB 密码走环境变量或本机密钥文件,config.py 只放非敏感默认值。

三、目录结构

复制代码
webconsole/
├── app.py                    # Flask 入口:11 页面 + 30+ API + 调度器模块级挂载
├── jobs_registry.py          # 任务注册表:JOBS 定义 / job_view 视图模型 / build_scheduler
├── config.py                 # 全部默认配置(无密钥):CXY/XUEQIU/ZT/NEWS/BID + 设置页表单定义
├── smoke_test.py             # 冒烟测试(WEBCONSOLE_NO_NETWORK=1 全链路离线)
├── web.config                # IIS wfastcgi 配置
├── services/
│   ├── storage.py            # 唯一 DB 层:线程本地连接 / 建表 / 幂等 upsert / TTL 缓存 / 清理
│   ├── cxy_agent.py          # 任务1:客栈自动接单(巡检→策略→接单)
│   ├── xueqiu_agent.py       # 任务2:雪球调仓(盘中轮询+收盘全量+异动邮件)
│   ├── xueqiu_cookie_capture.py  # Playwright 浏览器登录采集 Cookie(CDP)
│   ├── zt_agent.py           # 任务3:涨停采集(开盘啦 API → WebAPI 保存)
│   ├── news_collector.py     # 任务4a:8 路资讯采集 + 常驻调度
│   ├── news_analyzer.py      # 任务4b:SimHash 雷同 + 股票命中 + 邮件预警
│   └── bid_leads_agent.py    # 任务5:中标线索(4 类站点解析器)
└── templates/                # base + 12 个业务页面 + 404 + 公共参数卡片

services 层的模块规范(每个任务 agent 都遵守):

python 复制代码
SERVICE_NAME = "xxx"          # 与 JOBS 条目的 service 字段严格一致(见踩坑 #3)
JOB_ID      = "xxx"           # job_run_log 的任务标识

def start_monitor(): ...      # 幂等启动 daemon 线程(已在跑则直接返回)
def monitor_alive() -> bool:  # 线程存活探测
def state_view() -> dict:     # 页面读取的内存态快照(含 monitor_alive/last_heartbeat)
def run_once(): ...           # 手动触发入口(页面按钮调用)

这套约定让 jobs_registry 可以用纯字符串约定把注册表、页面状态、手动触发三者串起来,新增任务只需「写一个 agent + JOBS 加一条」。


四、核心设计

4.1 调度中心 jobs_registry.py

这是全系统最核心的一个文件,三个职责:

(1)JOBS 注册表------任务元数据的唯一事实来源:

python 复制代码
JOBS = [
    {
        "id": "zt_collect",            # 任务标识(job_run_log 外键语义)
        "order": 3,                    # 页面排序
        "name": "涨停数据 · 采集",
        "url": "/zt",                  # 模块页地址
        "schedule_mode": SCHEDULE_TRADING,   # 24x7 / trading / every_n_days
        "cron_text": "30 15 * * 1-5",  # 展示用 cron 文案
        "service": "services.zt_agent",      # → agent 模块(字符串约定!)
        "enabled": True,
        ...
    },
    ...
]

(2)job_view 视图模型------把静态注册条目渲染成带实时状态的完整视图:

python 复制代码
def job_view(job, now=None):
    v = dict(job)
    if job["service"] == "services.zt_agent":
        # 导入对应 agent,读取:DB 指标(metrics_of)、启停开关(get_config)、
        # 线程存活(state_view()["monitor_alive"])、最近运行(last_run_of)、
        # 实时生效参数(effective_collect_time) → 动态改写 schedule_text / next_run
        ...
    # 状态机:enabled=False→paused;真实任务 state 跟随 monitor_alive
    if job["service"] in (...) and v.get("enabled"):
        v["state"] = "running" if v.get("monitor_alive") else "idle"

这样页面上的「运行中 / 待触发 / 已暂停 / 下次执行 xx:xx」全部是真实状态,而非静态文案。

(3)build_scheduler()------调度挂载点,带三重守卫:

python 复制代码
def build_scheduler():
    try:
        # 守卫1:冒烟测试离线模式不拉线程
        if os.environ.get("WEBCONSOLE_NO_NETWORK") == "1":
            return None
        # 守卫2:Werkzeug 自动重载时父进程不启动(避免双份线程)
        if os.environ.get("WEB_RELOAD") == "1" and os.environ.get("WERKZEUG_RUN_MAIN") != "true":
            return None
        # 守卫3:受 sys_config 启停开关控制
        if storage.get_config("zt_enabled", "1") == "1":
            zt_agent.start_monitor()
        ...
    except Exception:
        return None

(4)调度算法 :不用 cron 库,每种模式一个 next_run_of 计算:

  • 24x7:now + interval(下次轮询时刻);
  • trading:下一个交易日的 HH:MM(先调交易日服务接口,失败回退「排除周末」);
  • every_n_days:上次运行日 + N 天 的 run_time,sys_config 记录上次运行日防重。

4.2 数据库层 services/storage.py

python 复制代码
MYSQL_CONFIG = { "host": "<云端IP>", "port": 3306, "database": "stock", ... }

_CONN = threading.local()      # 线程本地连接:每个线程独享,避免 MySQL 协议线程竞争
_CFG_CACHE: dict | None = None # sys_config 3 秒 TTL 缓存
_CFG_LOADED_AT = 0.0
_CFG_LOCK = threading.Lock()

三个关键设计:

(1)线程本地连接 。Flask 请求线程和 5 个调度线程并发访问 DB,MySQL 连接不是线程安全的。用 threading.local() 让每线程第一次访问时建连、之后复用,ping(reconnect=True) 处理断线。

(2)sys_config 读取带 3 秒 TTL 缓存(写穿透) 。远程 MySQL 单次往返实测 40~60ms,而 get_config 在调度循环里每轮都要调(启停开关、间隔参数......)。设计为:

python 复制代码
def get_config(key, default=""):
    global _CFG_CACHE, _CFG_LOADED_AT
    # 3 秒内直接读内存;过期则整表拉回内存(一张小表,一次往返换 N 次查询)
    # 查无此键 → 用哨兵 _CFG_MISS 记入缓存,避免"每次都 miss 都打 DB"
def set_config(key, value):
    # 写库 + 同步更新内存缓存(写穿透),保证本进程立即可见

界面保存参数后下一轮调度即生效、无需重启进程,就是靠这张表 + 这套缓存。

(3)幂等写入 。采集任务的黄金法则是「重复执行不能产生脏数据」,各表分别用:

表 幂等机制
bid_announcements url 唯一索引 + INSERT IGNORE(天然防重)
xq_rebalancings uid(雪球原始调仓ID)唯一索引 + upsert
zt_pool(WebAPI 端) 同日重复提交,C# 端按 code 跳过已存在
news_articles (source, title_hash) 唯一键,插入返回 (inserted, skipped)
cxy_orders order_no 判重后插入

4.3 配置热更新体系(三层)

复制代码
config.py(代码默认值,含全部字段的取值范围/hint)
     │  覆盖
     ▼
sys_config 表(界面「常用参数」保存,各模块 effective_xxx() 读取)
     │  约束
     ▼
agent.effective_xxx()   # 统一入口:sys_config 优先 → 非法值回退 config 默认 → min/max 夹紧

config.py 里的 SETTINGS_SECTIONS / MODULE_SETTINGS 用声明式表单定义 (key/label/type/min/max/hint),系统设置页和各模块页的参数卡片直接由这份数据渲染,保存统一走 POST /api/settings 写 sys_config。新增一个可调参数 = 配置里加一个 dict,前端零改动。

4.4 邮件预警共用通道

所有邮件(雪球仓位异动、资讯雷同、股票命中、中标新线索)走同一个通道:

  • 开关与收件人在「系统设置 → 通知与预警」维护(sys_config.mail_enabled / mail_recipients);
  • SMTP 授权码从环境变量 SMTP_AUTH_CODE 读取,回退本机 smtp_auth_code.txt,绝不进代码、不进界面;
  • 各任务只管调用 news_analyzer.send_alert(...),发送结果按约定记 job_run_log(成功=INFO / 因开关关闭跳过=WARNING / 失败=ERROR)。

4.5 财经舆情分析管线(最复杂的一条数据流)

复制代码
8 路采集源(央视API/tushare三路/新浪/同花顺/金融街/东财7×24)
   │  每 poll_seconds 醒一次,按各源 interval 判断到期,只采到期源
   ▼
news_articles((source,title_hash) 去重,SimHash(64bit) + 标题MD5 落列)
   │  每轮末尾自动分析
   ├─► 雷同检测:24h 窗口内 SimHash 海明距离 ≤3 聚类 → 命中来源 ≥3 家 → 邮件预警
   │            (已预警分组打 alerted 标记,不重复提醒)
   ├─► 标题命中:央视栏目文本 × 全A股 stock_basic / 基金 fund_basic 精确匹配 → 预警
   └─► 热门话题:近 3 天资讯 jieba 分词 + TF-IDF + KMeans(k=3) 聚类(页面展示)

性能关键 :话题聚类不能在页面请求线程里做(实测 jieba+KMeans 一跑 2~7 秒,页面直接卡死,见踩坑 #6),采用「缓存过期立即返回旧值 + 后台线程重算」的最终一致模型。

4.6 数据清理

CLEANUP_SCOPE 声明清范围(表 → 中文名 → 时间判定列),界面预览条数 → 确认后真实 DELETE,范围外的表(如 WebAPI 端的涨停池)不碰。清理动作记 WARNING 级任务日志,可审计。


五、Web 层设计

5.1 页面与 API

页面 内容 主要数据来源
/ 总览 五任务状态卡 + 各模块统计 all_job_views() + 各模块 stats
/jobs 任务调度 五任务运行态/下次执行/手动触发/启停 jobs_registry
/cxy /xueqiu /zt /news /bids 各模块数据浏览 + 常用参数卡 agent + storage
/xq-records 雪球调仓记录(客户查看页,含共振分析) storage 分页查询
/logs /settings 运行日志 / 系统设置 job_run_log / sys_config

API 约定:页面数据接口 GET /api/<module>/* 返回 JSON,前端 5s 轮询(间隔可在设置页调);动作接口 POST(手动运行、启停、保存配置、清理执行)。

5.2 读路径优化

远程库往返 40~60ms 的现实下,几条硬性规矩:

  • 统计用条件聚合合并 :一个页面要 5 个 COUNT 时,合并成一条 SUM(CASE WHEN ...);
  • get_config 走 TTL 缓存(见 §4.3);
  • 列表查询强制 LIMIT,默认 page_size=50 可调;
  • 重计算放后台(话题聚类模型,见 §4.5)。

六、部署方案(Windows IIS + wfastcgi)

复制代码
安装:Python 3.11 (venv) → pip install wfastcgi → wfastcgi-enable
站点:物理路径指向 d:\产品\MIS\weike\webconsole
      (D:\wwwroot\webconsole 做成 Junction 指向工作目录,两边同一份文件)
应用池:无托管代码 + FastCGI maxInstances=1(单调度进程的关键!)
web.config:
  handler:*.py → wfastcgi.py
  环境变量:WSGI_HANDLER=app.app / PYTHONPATH=<站点根> / WSGI_LOG=<日志文件>

webconsole 的 web.config 核心内容:

xml 复制代码
<configuration>
  <system.webServer>
    <handlers>
      <add name="Python FastCGI"
           path="*" verb="*"
           modules="FastCgiModule"
           scriptProcessor="<python路径>\wfastcgi.py"
           resourceType="Unspecified" requireAccess="Script" />
    </handlers>
    <httpErrors existingResponse="PassThrough" />  <!-- 保留 Flask 错误正文 -->
  </system.webServer>
</configuration>

FastCGI 的 maxInstances 必须设为 1 :IIS 默认按负载拉多个 python 进程,每个进程都会执行 from app import app → 每个进程都挂一套调度线程 → 重复采集。单实例后,Flask 请求与 5 个调度线程同进程,页面读状态零开销。


七、踩坑心得(重点)

以下 12 个坑全部来自真实排查,按「现象 → 根因 → 修复 → 启示」组织。

坑 #1:调度器在开发机上好好的,部署到 IIS 一个任务都不跑

  • 现象 :本地 py -3 app.py 五个任务全部正常;部署到 IIS 后页面能开,但所有任务「尚未运行」,采集数据一条不进。

  • 根因 :调度器挂载写在 if __name__ == "__main__": 里。开发模式直接运行会执行;而 IIS/wfastcgi 是 from app import app 模块导入,__main__ 块永远不执行。

  • 修复 :挂载提升到模块级,from app import app 必然执行:

    python 复制代码
    SCHEDULER = jobs_registry.build_scheduler()   # 模块级,__main__ 之外
  • 启示 :「在我机器上能跑」的最大来源之一就是 __main__ 里的初始化逻辑。凡是 wsgi/gunicorn/uvicorn 导入式部署,初始化必须放模块级(或应用工厂)。

坑 #2:四个任务线程明明活着,页面却显示「待触发」------UnboundLocalError 被静默吞掉

这是排查成本最高的一个坑,值得一整节。

  • 现象 :修复 #1 后 DB 日志证明线程在持续采集,但 /jobs 页面仍显示 3 个任务「待触发/尚未运行」;job_view 里的异常插桩写日志,DB 里却一条 WARNING 都没有。

  • 排查 :做了个临时调试路由,把 worker 进程内部的模块身份、线程清单、甚至 job_view.__code__.co_consts(已加载代码对象的字符串常量,用于排除「进程加载了旧版代码」)全部吐出来。最后把 except 的插桩从「写 DB 日志」改成「直接把异常写进返回值」,异常现形:

    复制代码
    UnboundLocalError: cannot access local variable 'storage'
    where it is not associated with a value
  • 根因 :job_view 函数里 cxy/xueqiu 两个分支写了局部 import services.storage as storage。Python 的作用域规则:函数内任何位置对名字赋值(import 也算赋值),该名字就是整个函数的局部变量 。于是模块顶部的 import services.storage as storage 全局名被屏蔽;当请求进入 zt/news/bid 分支(不执行 cxy 分支的 import)时,局部 storage 未赋值 → 直接 UnboundLocalError。而 except 里的兜底逻辑 storage.add_log(...) 用的还是这个 storage → 兜底也炸 → 双重静默。

  • 修复:删掉分支内的局部 import,统一用模块顶部全局 import。

  • 启示:

    1. 函数内不要重复 import 顶部已有的同名模块------这是 Python 版的「变量提升陷阱」;
    2. except 里写日志的兜底代码,本身也可能抛异常 。关键路径的调试插桩应该把异常写进不依赖外部资源的地方(比如直接塞进返回值),否则「插了桩也没有日志」会把你引向完全错误的方向。

坑 #3:注册表字符串与分支判断不一致,雪球任务状态永远是「待触发」

  • 现象:雪球条目配置齐全、线程正常,页面状态就是不对。
  • 根因 :JOBS 条目写的是 "service": "services.xueqiu_collector",而 job_view 分支判断的是 services.xueqiu_agent。字符串约定类架构的典型故障------约定靠人肉维护,改文件名/重构时必然漂移。
  • 修复 :统一为 services.xueqiu_agent。
  • 启示 :用「字符串 → 模块」映射时,最好加一条启动期自检(如 assert importlib.import_module(service)),把约定破裂从运行期静默失败提前到启动期报错。

坑 #4:新建的专用应用池一挂站点就 500,进程秒退

  • 现象 :为隔离性新建应用池「webconsole」,切过去后站点 500、响应空 body。FRT(失败请求跟踪)拿到 FASTCGI_UNEXPECTED_EXIT------python 进程启动即退,无任何 stderr。
  • 排查:排除了池配置差异、ACL 权限、AppLocker/IFEO、WAS 重启、池重建、loadUserProfile......均无效;最终回退到既有的「news」池立即恢复。
  • 启示 :
    1. wfastcgi 的 stderrMode=ReturnStdErrIn500 下,500 空 body 基本等于「python 根本没启动起来」,先看 FRT 再猜;
    2. IIS 匿名账号(虚拟账号)+ 特殊目录(Junction)组合会有玄学问题,生产上优先复用已验证的应用池,别为洁癖新建;
    3. 事件查看器 → WER 报告能发现底层运行库问题 (本机曾查到 194 条 msvcp140.dll 崩溃记录,运行库版本过老)。

坑 #5:改了模板不生效------Jinja 缓存

  • 现象 :修改 .html 模板刷新页面无变化,本地开发却即时生效。
  • 根因 :wfastcgi 下 app.debug=False,Jinja2 模板缓存不会自动失效 。代码文件 .py 变更 wfastcgi 会自动重启进程,但模板变更不会。
  • 修复 :改模板后 Restart-WebAppPool <池名>。
  • 启示 :把「改模板 → 重启池」写进部署习惯,或者开发期开 TEMPLATES_AUTO_RELOAD。

坑 #6:/news 页面动辄 5 秒------重计算跑在了请求线程里

  • 现象:资讯页首屏极慢,CPU 尖峰。
  • 根因:页面请求线程内同步执行 jieba 分词 + TF-IDF + KMeans 话题聚类,实测 2~7 秒。
  • 修复 :hot_topics 改为最终一致模型------缓存过期时立即返回上次结果,同时后台线程重算,请求线程永远不做重计算。
  • 启示:Web 请求线程里只允许 O(数据库索引查询) 级别的工作;任何秒级计算都该后台化 + 返回旧值。

坑 #7:远程 MySQL 每条 SQL 都是 50ms------读路径的往返预算

  • 现象:页面统计卡一堆数字,加载明显慢。
  • 根因 :远程库单次往返 40~60ms,页面读路径每多一条 SQL 就多 50ms,5 个 COUNT 就是 250ms。
  • 修复 :统计合并为条件聚合一条 SQL;get_config 3 秒 TTL 缓存(写穿透);列表强制 LIMIT。
  • 启示 :远程数据库场景下,性能预算的单位是「往返次数」而不是「SQL 复杂度」。一条复杂的条件聚合,快于五条简单 COUNT。

坑 #8:自建表导致与老系统数据隔离

  • 根因 :最初为雪球模块新建了 wc_ 前缀的独立表,结果老 C# 系统看不到新数据,两边各玩各的。
  • 修复 :直接使用老系统既有表 (xq_portfolios/xq_rebalancings),按原表结构派生缺失列(如 rebalanced_at=created_at),写路径满足老表的外键约束(组合不存在先按 Symbol 建组合)。
  • 启示 :多系统共享库时,表的所有权约定优先于工程洁癖。新写方要适配老表结构 + 用唯一索引 upsert 保证不污染老数据。

登录态判定看似简单,实际连踩三坑:

  1. 匿名访客也带 token :未登录浏览器同样会拿到 xq_a_token/xq_id_token(JWT 里 uid=-1),只有 Cookie 中 xq_is_login=1 才是真实登录标记;
  2. 同名 Cookie 双份 :Cookie 串里同名键可能出现两次,逐条判定时「任一条 xq_is_login==1 即登录」,避免被重复键遮蔽漏检;
  3. 未验证不落盘 :手工粘贴的 Cookie 可能是复制的旧请求、或粘贴后账号又在别处登录导致作废------必须先在线 verify_cookie() 复核通过,才允许覆盖 sys_config 里已有的值。

另附 Cookie 采集的实现经验:用 Playwright channel="msedge" (直接驱动本机 Edge,无需下载浏览器),后台线程起 CDP 连接;无控制台环境(IIS)下 Playwright 子进程会调 sys.stderr.fileno() 而抛 io.UnsupportedOperation,需在启动前把 stdout/stderr(含 __stdout__)替换为 devnull 真实句柄;单次 CDP 抖动只记 WARNING,连续 5 次失败才判定浏览器被关------单次异常就降级会让采集功能脆得不可用。

坑 #10:政府采购网站点的四类风控/脏数据

政采类站点是采集工程的重灾区,四个教训:

  1. 导航链接混入列表 :校园门户「统一身份认证平台」指向内网 /user/login,每页误抓 1 条 → 3 次重试 × 25s 超时,一轮采集被拖爆。修复:_is_junk_url() 按 login|sso|\.css|\.js 正则直接丢弃;
  2. 风控限流伪装成故障 :中国政府采购网搜索接口对云/机房 IP 直接回 HTTP 500 或「频繁访问」限流页------这是环境问题不是代码问题 ,排查时先看 请求重试耗尽 的 WARNING 日志,别急着改代码;界面提供「启用数据源」开关,把不稳定源临时摘除;
  3. SPA 站点没有直链正文:湖南省政采网列表是 JSON 接口(POST 分页),正文在 SPA 壳页里,入库 URL 需要拼「跳转页地址」(articleId 整体 URL 编码后 base64),否则点开是空壳;
  4. 正文缺字段时的兜底 :采购公告/意向类正文常没有「地址」「中标商」,is_local(本地/外地打标)必须加标题兜底(标题命中本地地名关键词即置位),否则整县线索全被误标为外地。

坑 #11:时间显示的「历史日志误读」问题

  • 现象 :状态卡只显示 09:55:53,隔天看日志会把昨天的记录当成今天的。
  • 修复 :展示时间非当天自动补日期前缀 (09-16 09:55:53),当天才只显示时分秒。
  • 启示 :时间展示的完整律:「凡是可能被隔天回看的界面,一律带日期」。

坑 #12:演示模式残留的兜底分支

  • 现象 :早期为原型写了 DEMO_MODE 的 mock 数据兜底,真实化之后个别页面仍会偶发落到演示数据,且极难复现。
  • 修复 :彻底退出演示模式后,删掉所有 mock 兜底分支而不是留着「以防万一」。
  • 启示 :死代码不可怕,可怕的「偶发生效的兜底」------它让真实的异常被假数据掩盖。

八、总结:可复用的经验清单

架构层面

  1. 中小规模「Web 控制台 + 定时任务」用单体 Flask + daemon 线程完全够用,别上分布式;
  2. 任务模块遵守统一接口约定(start_monitor / monitor_alive / state_view / run_once),注册表即可驱动页面与调度;
  3. 多系统共用库时直接写老系统既有表,靠唯一索引 + upsert 保证幂等,比新建隔离表再同步划算得多。

可靠性层面

  1. 调度初始化必须放模块级 (导入式部署不执行 __main__);
  2. FastCGI/IIS 多进程会复制调度线程,maxInstances=1 或改用进程内单例锁;
  3. 关键路径的 except 兜底要「不依赖外部资源」------写日志本身可能就是抛异常的那行;
  4. 登录态/Cookie 类凭证:在线验证通过才落库 ,判定标记用业务语义字段(如 is_login)而非 token 存在性。

性能层面

  1. 远程数据库的性能单位是往返次数:条件聚合合并 COUNT、小表 TTL 缓存(写穿透)、强制 LIMIT;
  2. 秒级计算(分词/聚类/大循环)一律后台线程 + 「返回旧值 + 后台重算」的最终一致模型。

运维层面

  1. wfastcgi 三件套要烂熟:__main__ 不执行、模板改后重启池、500 空 body = python 没起来(看 FRT);
  2. 采集工程的异常十有八九是环境风控而非代码 bug------先把「重试耗尽」这类关键 WARNING 落到日志页,再谈排查;
  3. 界面时间一律带日期;演示/mock 兜底代码上线前删除干净。

本文基于真实项目复盘撰写,涉及的服务器地址、账号、密钥均已脱敏。欢迎在评论区交流你的定时任务系统设计经验。

相关推荐
Bazingga1 小时前
中文提问,能检到英文文档吗?——一次跨语言RAG检索五语种实测
后端
对象存储与RustFS1 小时前
用 warp 压测 S3 兼容存储,十分钟跑出一份可复现的基准报告
后端·云原生·性能优化
吴佳浩 Alben1 小时前
一文讲透推理引擎性能指标:TTFT、TPOT、KV命中率、并发,到底对应 vLLM / SGLang 的哪个参数?
人工智能·python·语言模型·架构·ai编程·vllm·sglang
用户298698530141 小时前
Python 实现 Word 转 XPS、PostScript 与 OFD 格式
后端·python·api
武子康1 小时前
PUBACK 已到,灯还没亮:Agent 设备指令何时算完成?
人工智能·后端·agent
Patrick在香港1 小时前
你写的「比较 token」函数,正在用 36 纳秒泄露密码:时序攻击实测
python·安全·标准库·时序攻击·常量时间
蜗牛互联网1 小时前
Python Agent 工具调用:审批令牌、参数白名单和累计预算
java·开发语言·人工智能·windows·后端·python
环境栈笔记1 小时前
指纹浏览器检查代理失败怎么排查?先分清代理、网络和检测服务
前端·人工智能·后端·自动化
武子康2 小时前
ESP32 唤醒词标称 98%,为什么还要自己做验收?
人工智能·后端·agent