内容分四块: 1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。 2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。 3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。 4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。 .gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
99 lines
5.1 KiB
Python
99 lines
5.1 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""无头 Chrome 取证小工具(③段 3b/3d 用)。
|
||
|
||
--mode dom : 取渲染后的 DOM(自检输出、数值取证都从这里读)
|
||
--mode shot : 截图(视口帧或整页长图)
|
||
|
||
用法示例:
|
||
python _tools/cap.py --mode dom --route selftest --out _scratch/selftest.html
|
||
python _tools/cap.py --mode shot --route p/p1 --viewport 1440x900 --out _gate_shots/P1-1440.png
|
||
|
||
⚠️ 本机实测(Chrome 154 · headless=new)——**两条通道对 --window-size 的解释不一样**,
|
||
同一台机器、同一个窗口尺寸,拿到的是两个不同视口(探针页实测记录见 3b §五):
|
||
|
||
--window-size=656,995 ─┬─ --dump-dom ⇒ 视口 640 × 900 (mq<=640 true)
|
||
└─ --screenshot ⇒ 视口 656 × 995 (mq<=640 false)
|
||
|
||
即:dump-dom 通道会从窗口里扣掉滚动条与标题栏(16 / 95),截图通道不扣。
|
||
⇒ 想拿到「视口 W × H」,dom 模式要传 (W+16, H+95),shot 模式**原样传 (W, H)**。
|
||
⚠️ 视口宽度下限 500(窗口最小 516):`--window-size=390,844` 拿到的仍是 500×844。
|
||
要 390px 视口,本机 CLI 做不到(需要 CDP 的 setDeviceMetricsOverride),
|
||
所以窄视口只测到 500×844 —— 报告里⛔ 不许写成 390。
|
||
"""
|
||
import argparse, os, subprocess, sys
|
||
from urllib.parse import quote
|
||
|
||
CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
PROTO = os.path.abspath(os.path.join(HERE, "..", "mcn-workbench.html"))
|
||
FRAME_W, FRAME_H = 16, 95 # window-size 与视口的实测差值
|
||
MIN_VP_W = 500 # 视口宽度下限(Chrome 窗口最小 516)
|
||
|
||
|
||
def url(route, proto=None):
|
||
# Windows 盘符路径 -> file:///E:/...
|
||
p = (proto or PROTO).replace("\\", "/")
|
||
# ⚠️ 路由不带前导 #/ :本机 bash 是 Git Bash(MSYS),会把 "#/xxx" 当成路径改写成
|
||
# "#C:/.../PortableGit/xxx"(实测踩过)⇒ 这里由脚本自己拼 "#/"。
|
||
h = route.strip().lstrip("#").lstrip("/")
|
||
return "file:///" + quote(p, safe="/:") + ("#/" + h if h else "")
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--mode", choices=["dom", "shot"], required=True)
|
||
ap.add_argument("--route", default="p/p1/ok", help="例:selftest / p/p4/empty(⛔ 不带 #/)")
|
||
ap.add_argument("--hash", default=None, help="兼容旧参数(不带 #/)")
|
||
ap.add_argument("--viewport", default="1440x900", help="目标 CSS 视口,例 1440x900 / 500x844")
|
||
ap.add_argument("--proto", default=None, help="要渲染的文件(默认本目录的 mcn-workbench.html;变异对照用)")
|
||
ap.add_argument("--out", required=True)
|
||
ap.add_argument("--budget", default="6000")
|
||
a = ap.parse_args()
|
||
route = a.hash or a.route
|
||
|
||
vw, vh = [int(x) for x in a.viewport.lower().replace(",", "x").split("x")]
|
||
vw = max(vw, MIN_VP_W)
|
||
# 🔴 dom 与 shot 两条通道对 --window-size 的解释不同(见文件头实测):截图通道原样等于视口,
|
||
# dump-dom 通道要补回被扣掉的 16 / 95。这里分开算,⛔ 别再用同一个偏移量。
|
||
win = ("%d,%d" % (vw + FRAME_W, vh + FRAME_H)) if a.mode == "dom" else ("%d,%d" % (vw, vh))
|
||
|
||
out = os.path.abspath(a.out)
|
||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||
cmd = [CHROME, "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars",
|
||
"--window-size=" + win, "--virtual-time-budget=" + a.budget,
|
||
"--run-all-compositor-stages-before-draw"]
|
||
if a.mode == "shot":
|
||
cmd += ["--screenshot=" + out]
|
||
else:
|
||
cmd += ["--dump-dom"]
|
||
cmd += [url(route, a.proto)]
|
||
r = subprocess.run(cmd, capture_output=True)
|
||
if a.mode == "dom":
|
||
with open(out, "wb") as f:
|
||
f.write(r.stdout)
|
||
sys.stderr.write("viewport %dx%d → window %s | dom bytes=%d\n" % (vw, vh, win, len(r.stdout)))
|
||
else:
|
||
# 🔴 截图出来必须**当面对尺寸**:这条通道曾经因为偏移量算错,把 1456×995 当成
|
||
# 「1440×900」交付出去(同一份报告里 dump-dom 又按 640 说话 ⇒ 两份证据互相打架)。
|
||
# 现在直接从 PNG 的 IHDR 里读真实像素,跟目标视口比,不等就报红。
|
||
if not os.path.exists(out):
|
||
sys.stderr.write("shot FAILED\n")
|
||
else:
|
||
with open(out, "rb") as f:
|
||
head = f.read(24)
|
||
pw = int.from_bytes(head[16:20], "big")
|
||
ph = int.from_bytes(head[20:24], "big")
|
||
if (pw, ph) == (vw, vh):
|
||
sys.stderr.write("shot ok: %s(视口 %dx%d,PNG 实测 %dx%d ✓)\n" % (out, vw, vh, pw, ph))
|
||
else:
|
||
sys.stderr.write("shot ⛔ 尺寸不符:目标视口 %dx%d,PNG 实际 %dx%d ⇒ 别拿它当 %dx%d 的证据\n"
|
||
% (vw, vh, pw, ph, vw, vh))
|
||
err = r.stderr.decode("utf-8", "replace")
|
||
if "ERROR" in err.upper():
|
||
sys.stderr.write(err[-1500:] + "\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|