From cf1e5908a324e13b1dbd571a27748e644003afd1 Mon Sep 17 00:00:00 2001 From: chenshuobo <1119780489@qq.com> Date: Wed, 10 Jun 2026 16:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BE=8E=E5=9B=A2=E5=88=B8=20ETL:=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E9=94=81=E6=94=B9=E7=94=A8=20/tmp(=E6=91=86=E8=84=B1?= =?UTF-8?q?=20data/=20=E7=9A=84=20root=20=E6=9D=83=E9=99=90=E4=BE=9D?= =?UTF-8?q?=E8=B5=96)+=20=E6=96=B0=E5=A2=9E=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=BF=90=E7=BB=B4=E6=89=8B=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 锁文件默认改到系统临时目录(任何账号可写),支持 MEITUAN_ETL_LOCK 覆盖;修无 sudo 部署时 data/ 属 root → 每轮 PermissionError、cron 抓不进数据的问题 - 新增 deploy/meituan-etl.md:启动/关闭/终止/代码位置/健康检查/调频率/根因背景 --- deploy/meituan-etl.md | 90 +++++++++++++++++++++++++++++++++ scripts/pull_meituan_coupons.py | 6 ++- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 deploy/meituan-etl.md diff --git a/deploy/meituan-etl.md b/deploy/meituan-etl.md new file mode 100644 index 0000000..977e6c7 --- /dev/null +++ b/deploy/meituan-etl.md @@ -0,0 +1,90 @@ +# 美团券 ETL 定时任务 — 运维手册 + +> 对象:维护「美团 CPS 券抓取入库」这套定时任务的同事。 +> 🔒 登录信息(服务器 IP / 账号密码 / DB 密码)见**私密交接清单**,**不入库**。 +> 运行账号 `cps` **无 sudo**,本手册所有操作都不需要 root。 + +## 它是什么 +每小时全量抓美团 CPS 券(北京)→ upsert 进 PostgreSQL 的 `meituan_coupon` 表,作为 App「销量最高 / 智能推荐」两个 tab 的数据源。一轮约 140 秒、~2700 条;靠 `cps` 用户的 crontab 常驻,关终端也照跑。 + +## 代码 / 文件在哪(都在生产服务器上) +| 项 | 路径 | +|---|---| +| 代码目录 | `/opt/shaguabijia-app-server` | +| 抓取脚本 | `scripts/pull_meituan_coupons.py` | +| Python(venv) | `/opt/shaguabijia-app-server/.venv/bin/python` | +| 配置(美团凭证 / DB 连接串) | `/opt/shaguabijia-app-server/.env`(`MT_CPS_*` / `DATABASE_URL`)| +| 日志 | `/home/cps/meituan_etl.log` | +| 运行锁 | `/tmp/meituan_etl.lock`(根治版)或 `~/data/.meituan_etl.lock`(过渡版,见文末)| +| 定时配置 | `cps` 用户 crontab(`crontab -l`)| +| 入库表 | PostgreSQL `shaguabijia` 库的 `meituan_coupon` | + +## 怎么看在不在跑 / 健康 +```bash +crontab -l # 看定时任务(应有每小时第 17 分那条) +tail -n 20 ~/meituan_etl.log # 看最近几轮:本轮完成 / 入库条数 / 有无报错 +# 查库存量与新鲜度(走 .env 连接串,不用输 DB 密码): +cd /opt/shaguabijia-app-server && .venv/bin/python -c "from sqlalchemy import text; from app.db.session import engine; print(engine.connect().execute(text('select count(*) total, count(*) filter (where sale_volume_num is not null) sales, max(last_seen) last_run from meituan_coupon')).one())" +``` +**健康标准**:`last_run` 在最近 1–2 小时内、`total` 两三千、日志最近一轮是 `本轮完成`(不是 `Permission denied` / Traceback)。 + +## 怎么启动 +定时任务已在 `cps` 的 crontab 里常驻,**随系统 cron 服务自动运行,无需手动启动**。若 crontab 被清空、要重新装回:`crontab -e`,粘下面这行存盘。 + +```cron +# 根治版(锁文件已改到 /tmp 后用这条,推荐): +17 * * * * cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1 +``` + +**手动立即跑一轮**(不等下一个整点,比如刚发现库空): +```bash +cd /opt/shaguabijia-app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 +``` +前台跑、约 140 秒,出现 `本轮完成` 即成功。 + +## 怎么关闭(停用定时任务) +```bash +crontab -e +``` +在那一行最前面加 `#` 注释掉(**推荐**,保留备查),存盘即停;要彻底删就删那一行。 +> ⚠️ 别用 `crontab -r`——它会**清空你所有 cron**,不只这一条。 + +## 怎么终止(杀掉正在跑的那一轮) +```bash +pgrep -af pull_meituan_coupons # 查在跑的进程和 PID +pkill -f pull_meituan_coupons # 杀掉它(或 kill ) +rm -f /tmp/meituan_etl.lock ~/data/.meituan_etl.lock # 清掉残留锁,否则下一轮可能"跳过本轮" +``` +> 脚本有 **30 分钟陈旧锁自动接管**机制:即使没手动清锁,30 分钟后下一轮也会自愈;急着立刻重跑才需手动 `rm` 锁。 + +## 调整频率 +`crontab -e` 改那行最前面的 cron 表达式: +| 频率 | 表达式 | +|---|---| +| 每小时第 17 分(当前) | `17 * * * *` | +| 每 30 分钟 | `*/30 * * * *` | +| 每 2 小时 | `17 */2 * * *` | +> ⚠️ 别调太密:一轮 ~140 秒,且要守美团限流(脚本已配速到 ~2–3 req/s,远低于 ~15 req/s 红线;402 内置退避重试)。**每小时一轮足够**,勿并行、勿调小页间 sleep。 + +## 脚本参数 +- `--once`:跑一轮(定时任务用这个) +- `--loop --interval 600`:常驻循环,每 600 秒一轮(调试用) +- `--prune-hours N`:清理超过 N 小时没再出现的旧券(默认 24;`0`=不清) +- 锁文件位置可用环境变量 `MEITUAN_ETL_LOCK=/path/to.lock` 覆盖 +> 详见脚本头部注释:`head -30 scripts/pull_meituan_coupons.py` + +## 已知问题与根治(运行锁权限) +- **现象**:cron 报 `PermissionError: [Errno 13] ... 'data/.meituan_etl.lock'`,每轮抓不进数据、表为空。 +- **根因**:旧版脚本把运行锁建在代码目录的 `data/` 下,而 `data/` 属 root、`cps` 不可写(交接清单里"已给写权限"实际不成立)。 +- **根治(已在代码层修复)**:脚本把锁默认改到 `/tmp/meituan_etl.lock`(任何账号可写),并支持 `MEITUAN_ETL_LOCK` 覆盖。**部署该版本后,用上面「根治版」那条干净 crontab 即可。** +- **过渡写法(根治版部署前的临时方案)**:crontab 里 `cd /home/cps` 把锁落到家目录、再用 `PYTHONPATH` 让代码仍能 import: + ```cron + 17 * * * * cd /home/cps && PYTHONPATH=/opt/shaguabijia-app-server /opt/shaguabijia-app-server/.venv/bin/python -m scripts.pull_meituan_coupons --once --prune-hours 24 >> /home/cps/meituan_etl.log 2>&1 + ``` + 根治版上线后,把这条换回上面的「根治版」标准写法即可。 + +## 注意事项 +- **只抓北京**:脚本里 `city_id` 硬编码,扩城市需改脚本。 +- **表行数 > 单轮抓取数**属正常:美团 `product_view_sign` 会轮换,跨轮累积;查询侧有 `DISTINCT ON(dedup_key)` 按「品牌\|名\|价」去重,用户看到的不重复;旧券 24h 后被 prune 清掉。 +- **别和 systemd 双跑**:仓库 `deploy/meituan-etl.{service,timer}` 是**未来由 root 纳入 systemd 正规部署**的备选,和本 user cron **二选一、别同时开**(否则重复抓、浪费限流额度)。当前生产用 user cron。 +- **改脚本 / 改部署**:走 git + PR,让有 root 的人 `/opt/deploy.sh` 部署(`git reset` + `alembic upgrade` + 重启);`cps` 账号改不了 `/opt` 下的代码。 diff --git a/scripts/pull_meituan_coupons.py b/scripts/pull_meituan_coupons.py index 8d0de94..aaefdc2 100644 --- a/scripts/pull_meituan_coupons.py +++ b/scripts/pull_meituan_coupons.py @@ -22,6 +22,7 @@ import hashlib import os import re import sys +import tempfile import time from datetime import datetime, timedelta, timezone @@ -44,7 +45,10 @@ PAGE_SIZE = 20 MAX_PAGES = 80 # 单路安全上限(搜索 ~52 页、供给 ~70 页) PAGE_SLEEP = 0.35 # 页间配速,缓解 402 RETRY = 7 -LOCK_FILE = "data/.meituan_etl.lock" +# 运行锁默认放系统临时目录(任何账号可写),不依赖代码目录 data/ 的写权限—— +# 无 sudo 部署时 data/ 常属 root,cps 写不了会导致每轮 PermissionError、cron 抓不进数据。 +# 需要指定位置时用环境变量 MEITUAN_ETL_LOCK 覆盖。 +LOCK_FILE = os.environ.get("MEITUAN_ETL_LOCK") or os.path.join(tempfile.gettempdir(), "meituan_etl.lock") LOCK_STALE_SEC = 30 * 60 # 锁超过 30min 视为陈旧(进程异常退出残留),自动接管 SOURCES = [