PDD 日期控件与指标解码
这个技能解决什么问题
拼多多商家后台的日期筛选不能用常规方式操作:
- 选项是 React 受控 radio,
element.click()不生效,必须用坐标真实点击 - 日历面板是浮层,点开才知道结构,且「商品数据」页两行的面板根本不是同一个组件
- 指标数字是私有区码位字体混淆(
spider-font),DOM 里不是123而是 PUA 字符,复制出来是空白
本技能把日期操作封装成可直接调用的函数,并内置混淆数字的解码器。这些坑都已固化在代码里。
> 混淆数字解码的完整逆向过程与验证见 references/spider-font.md(已解决,别再重新摸一遍)。
前置条件
- 调试版 Chrome 在跑(端口 9333)——见
chrome-automation-setup技能 - 页面停在
https://mms.pinduoduo.com/sycm/goods_effect且已登录 - 用
~/.workbuddy/bu.sh作为 browser-use 的入口(自动处理 PATH / 代理 / 空标签)
怎么用
~/.workbuddy/bu.sh <<'PY'
import os, pathlib
exec(open(pathlib.Path(os.path.expanduser("~/.workbuddy"))
/ "skills/pdd-date-picker/scripts/pdd_date_picker.py",
encoding="utf-8").read())
pdd_diag() # 排障首选:URL/前台/浮层/选项/面板 一次看全
pdd_demo() # 看各行选项与统计时间
pdd_pick("2026-08-10", "2026-08-20") # 商品概况:区间(返回 matched 校验落位)
pdd_pick("2026-08-05") # 商品概况:单日(内部点两次同一天)
pdd_pick("2026-08-25", row="商品明细") # 商品明细:单日
pdd_quick("7日") # 快捷项
pdd_quick("实时", row="商品明细")
PY
> 不需要自己先导航等就绪——pdd_pick / pdd_quick 内部会自动:
> 等「自定义」选项可点 → 把标签页/窗口置前 → 检测并关闭站内消息浮层。
> 但要注意它们返回的 matched 字段(见下)。
API
| 函数 | 作用 |
|---|---|
pdd_demo() |
自检:打印各行选项、选中态、统计时间 |
pdd_status() |
同上,返回 dict |
pdd_diag() |
排障首选:URL / 前台焦点 / 消息浮层 / 日期选项 / 面板状态 |
pdd_pick(start, end=None, row=0, confirm=True) |
选日期并提交,返回含 matched 落位校验 |
pdd_quick(label, row=0) |
快捷项:实时 / 昨日 / 7日 / 30日 / 周 / 月 |
pdd_open_picker(row=0) |
只打开面板(会先关掉已开的面板,避免操作错行) |
pdd_close() |
不提交,关掉面板 |
pdd_panel_state() |
面板状态:模式、显示月份、已选日期、确认按钮、可选日期 |
pdd_selectable() |
当前面板里各月「可选/不可选」的日期清单 |
pdd_row_subtitle(row=0) |
某行自己的「统计时间:…」文案(一页可能有多行) |
pdd_precheck() |
操作日期前的前置检查:置前 + 扫描并关闭消息浮层。这是唯一的浮层处理点——pdd_open_picker / pdd_quick / pdd_pick 入口已自动调用,无需手动加 |
pdd_wait_option(label, row=0) / pdd_wait_custom(row=0) |
等某个选项可点(导航后调用) |
pdd_wait_ready(page=None) |
等页面骨架就绪(卡片等);不是操作日期的前置条件。page 可传 goods_effect / stores_data / payment-bills / promotion,省略按 pdd_page_key() 自动判 |
pdd_wait_until(cond, timeout=30) |
通用条件轮询,返回 (耗时, 是否达成) |
pdd_page_key() |
当前页 key:goods_effect / stores_data / payment-bills / promotion / None |
pdd_overlay_guard(mode=None) |
查/改浮层防护范围:scoped(默认)/ always / off |
pdd_overlay_scan() |
扫出压住页面顶部的站内消息浮层 |
pdd_dismiss_overlay() |
关掉浮层,返回 none / closed / hidden / blocked |
pdd_pick 返回 {ok, applied, mode, row_label, subtitles, row_subtitle, matched},
其中 mode 是 range 或 single。
点击可靠性的三条护栏(已内建,别绕过)
「脚本点了没反应、但也不报错」有三个成因,都已在代码里处理。改代码时不要删:
| # | 成因 | 处理 | 状态 |
|---|---|---|---|
| ① | 站内消息浮层遮挡:右上角「新消息」不定时异步出现,盖住顶部日期选项带。只在「商品数据」「交易数据」两页出现;实测对账中心、推广报表不会出现 | 只在「打开日期面板之前」扫一次:pdd_open_picker / pdd_quick / pdd_pick 入口各调一次 pdd_precheck(),发现即关(点官方关闭图标,点不掉退回 display:none)。面板打开后的点击(选日历格/翻月/确认)一律不再判定——浮层只压住顶部选项带,够不到面板内部。范围限 OVERLAY_PAGES = (goods_effect, stores_data),其余页面直接跳过。要全开/全关:pdd_overlay_guard("always" / "off") 或环境变量 PDD_OVERLAY_GUARD;_click(guard=True) 是逐坐标判定的逃生开关(日常不用) |
✅ 已复现并修复,3 轮实测从 1/3 通过提升到 3/3;改为面板前置扫描后,1.4.1 实测一次真实浮层被 precheck 关掉、流程正常 |
| ② | 就绪判据不足:导航后只等「卡片出现」不够,日期选项渲染更晚,存在「行在了但选项没渲染全」的中间态 | pdd_wait_option(label) 直接等目标选项可点 |
✅ 已复现并修复 |
| ③ | 标签页/窗口未置前 | 每次点击前无条件 Page.bringToFront(~0.7ms) |
⚠️ 未复现。用临时标签页置后台做的对照实验里 CDP 点击依然送达(探针计数=1)。保留为廉价防护(窗口被完全遮住/最小化时可能不派发事件) |
> ③ 附带一个重要陷阱:Emulation.setFocusEmulationEnabled 一旦打开,
> document.hasFocus() 与 visibilityState 会被无条件伪造成 true/visible,
> 即使标签页真的在后台。所以不能用它们判断「要不要置前」,只能无条件置前。
> pdd_diag() 里报的这两个值也因此仅供参考。
⚠️ 落位校验:务必看 matched
平台存在数据边界回退:某些日期格子是可点的,但该日数据还没生成,页面会把
统计时间落到前一天。实测(2026-09-11 凌晨)对「商品明细」:
| 请求 | 该行实际显示 |
|---|---|
| 2026-09-07 | 统计时间:2026-09-07 ✅ |
| 2026-09-05 | 统计时间:2026-09-05 ✅ |
| 2026-09-10 | 统计时间:2026-09-09 ⚠️ 回退一天 |
所以 pdd_pick 提交后一定会读该行自己的「统计时间」与请求日期比对:
matched=True→ 日期确实落在你要的那天matched=False→ 打印告警,请用row_subtitle里的真实日期记账,
不要用你请求的日期(否则会静默算错)
这不是点击出错,所以不抛异常——但静默按错误日期记账是更坏的结果,别忽略这个字段。
两条最重要的规则
1. 两行的语义完全不同
| 行 | 面板 | 交互 |
|---|---|---|
| 商品概况 | 双月 544×321 | 点两次定起止 → 点「确认」提交 |
| 商品明细 | 单月 272×280 | 点一个日期立即生效并关闭,没有确认按钮,只支持单日 |
给「商品明细」传区间会被明确拒绝(并附上原因),这是设计行为,不是 bug。
2. 只有「商品明细」有范围限制
实测(当天 2026-09-10):商品明细可选范围仅 [今天-23, 今天-1]
——8月只能选 18~31,9月只能选 1~9,今天与未来一律禁用。
不要硬编码这条规则,用 pdd_selectable() 实时读 DOM。
其他必须知道的坑
- 按钮文字是「确认」不是「确定」——按文字找按钮时写错就找不到
- 日期要点两次才成为有效区间;只点一次时「确认」按钮 disabled,点了没反应
- 翻月箭头按 x 坐标取(最左=上月,最右=下月),不能按 header 下标取
——单月面板两个箭头在同一个 header 里,按下标取会让「下月」变成「上月」 - 翻月翻过界会把面板整个关掉,循环里每步都要检查面板是否还在
Escape关不掉面板,用pdd_close()(合成鼠标事件)- 坐标必须每次现场重算(
getBoundingClientRect()),滚动/重渲染会让旧坐标失效 - 找日期格要限定在正确的月份表格内并按
outOfMonth过滤
——同一个日号会在相邻月补位格里重复出现 - 星期表头是周一开头(一 二 三 四 五 六 日)
- 读结果按文本匹配(以「统计时间」开头),别依赖
goods-overview-new_subTitle__1-HfY
这类哈希类名 - 行名是子串匹配,
'概况'/'明细'/'商品概况'/'商品明细'/'交易概况'/'地区交易数据'
都能用。行名优先从短容器的标题识别(大容器 innerText 混着整页文案,在里面
做正则会把「商品明细」认成「刷新 商品概况」——踩过),认不出时按页面已知行序兜底 - 一页可能有多行各带统计时间。要判断「我这一行到底落在哪天」必须用
pdd_row_subtitle(row)取该行自己的文案,别用页面级的subtitles,会串行
遇到「点了没反应」,第一件事是跑 pdd_diag()——大多数情况一眼就能定位
(不在前台 / 有浮层 / 选项没渲染 / 面板还开着)。
完整 DOM 地图与逆向结论见 references/dom-notes.md。
指标数字解码(spider-font)
页面上的指标数字是 PUA 码位混淆,直接读 innerText 得到的是空白/乱码。
用配套解码器还原:
~/.workbuddy/bu.sh <<'PY'
import os, pathlib
R = pathlib.Path(os.path.expanduser("~/.workbuddy")) / "skills/pdd-date-picker"
exec(open(R/"scripts/pdd_date_picker.py", encoding="utf-8").read())
exec(open(R/"scripts/decode_spider.py", encoding="utf-8").read())
spider_ensure_map() # 现场识别,同一页面加载内复用
print(spider_decode("粘贴从页面取到的原始串"))
PY
| 函数 | 作用 |
|---|---|
spider_ensure_map() |
取映射;PUA 集合未变则复用,变了自动重建 |
spider_build_map() |
强制重新识别 |
spider_decode(text) |
还原含 PUA 的字符串(, . % 原样保留) |
spider_pua_list() / spider_map_key() / spider_font_url() |
排查用 |
四条必须记住的(都踩过):
- 映射每次页面加载都会重新随机生成(字体文件 hash 也换)。所以
绝不能硬编码映射、也绝不能做磁盘缓存——错了是静默错。
唯一可靠的复用依据是「当前页面 PUA 字符集合与建映射时一致」。 - 不是所有页面都混淆——实测「对账中心」是明文。先
spider_pua_list(),
返回空就说明本页无需解码,不要硬调spider_ensure_map()(会抛异常)。 - 重新加载/导航后必须重建映射。同一页面加载内可放心批量解码。
- spider-font 的数字字形 = Arial 的数字(重映射码位),这是选参考字体的依据。
> 完整逆向过程、三次映射对照、决定性验证见 references/spider-font.md。
其他页面的日期控件(同样已实测)
| 页面 | URL | 控件形态 | 操作要点 |
|---|---|---|---|
| 商品数据 | /sycm/goods_effect |
双行:概况=双月+确认;明细=单月点即生效 | 用 pdd_pick(),row 0 / 1 |
| 交易数据 | /sycm/stores_data(会跳 /operation) |
两组:交易概况(7 项含自定义,row 0)、地区交易数据(只有 昨日/7日/30日,row 1)。交易概况面板 = 双月 + 确认 |
直接用 pdd_pick() / pdd_quick(),row 0 = 交易概况 |
| 对账中心 | 入口 /cashier/finance/payment-bills,会 302 跳到 cashier.pinduoduo.com/main/bills?tab=4001…(最终地址里没有 payment-bills) |
日期+时间范围,输入框式 | 点输入框 → 点两次同一天 → 时间框已默认 00:00:00/23:59:59 → 确认。页面判据要按真实地址认,见 pdd_page_key() |
| 推广报表 | yingxiao.pinduoduo.com/goods/report/promotion/overView |
今日/昨日/近7日/近30日/近90日 + 自定义 | 切「表格」页签取按天数据;表头列序要现读,别记位置 |
跨页通用坑:
- 交易数据页的日期选项是另一套 class(
date-picker-group_*;商品数据页是
data-pick-group_*)。选择器已做联合匹配,所以rows()/pdd_pick()/pdd_quick()
在商品数据页与交易数据页都通用——不要按单个 class 硬写。 - 对账中心 / 推广报表的日期控件还不是通用面板,
pdd_pick()目前只接管
商品数据页与交易数据页;这两页要按references/dom-notes.md的手工流程走。 - 账务类型等勾选项是
label[class*="CBX_outerWrapper"],用data-checked读状态,
input.checked不可靠。坐标点击勾选,不要element.click()。 - 展开「高级选项」/切换页签后布局会变,之前记下的坐标全部作废,必须重新定位。
- 所有页面统一用「按文本找叶子节点 → 取
getBoundingClientRect()算中心 →click_at_xy()」。
安全边界
- 不读取、不存储、不外传任何账号、密码、Cookie 或登录态;登录由用户自己在浏览器里完成
- 不发起任何外部网络请求,只用 CDP 驱动本地已登录的浏览器
- 改日期筛选是只读查询,会触发一次真实的数据请求,可随时切回「实时」还原
- 本技能只碰日期控件,不碰任何写操作(改价、发货、编辑商品)
- 做完记得把筛选还原成用户原来的状态(通常默认是「实时」)