feat(scripts): 添加 PEP 723 脚本元数据并修复 HTTP/2 伪头过滤问题

- 为 build_studio_urls.py 和 lookup_groups.py 添加 PEP 723 依赖声明
- 修复 HTTP/2 伪头导致 requests 抛 InvalidHeader 的问题
- 添加 CDP 端口连通性预检查及启动指引
This commit is contained in:
2026-08-27 13:33:34 +08:00
parent b8bd749f43
commit 9020752eed
23 changed files with 885 additions and 294 deletions

View File

@@ -1,4 +1,13 @@
# -*- coding: utf-8 -*-
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "playwright>=1.40",
# "requests>=2.31",
# "openpyxl>=3.1",
# "pandas>=2.0",
# ]
# ///
r"""
YouTube Studio 内容管理器「群组名 -> entity_idgroupId」批量解析脚本
Playwright 自动捕获鉴权套件 + requests 回放查询 + Excel/CSV 输出)。
@@ -88,6 +97,10 @@ HTTP_HINTS = {
429: "(限流:调低 --max-workers 或稍后重试)",
}
# HTTP/2 伪头(:authority/:method/:path/:schemerequests 作为普通头发送会抛
# InvalidHeader。捕获时即过滤不让伪头流入 bundles.json。
PSEUDO_HEADER_RE = re.compile(r"^:")
REQUEST_TIMEOUT = 30
MANUAL_WAIT_SECONDS = 180
@@ -256,8 +269,10 @@ def search(bundle: dict, name: str, session=None) -> dict:
session 可注入测试替身(.post(url, json=..., headers=..., timeout=...))。
"""
# 双保险:捕获时已滤伪头,这里再滤一次兜住手工维护的旧 bundles.json
headers = {k: v for k, v in (bundle.get("headers") or {}).items()
if str(k).lower() not in STRIP_HEADERS}
if str(k).lower() not in STRIP_HEADERS
and not PSEUDO_HEADER_RE.match(str(k))}
body = build_body(bundle.get("bodyTemplate") or {}, name)
url = bundle.get("url") or ENDPOINT
try:
@@ -386,6 +401,26 @@ OWNER_DISPLAY_SELECTORS = (
)
def _assert_cdp_reachable(cdp_url):
"""CDP 附加前先探测调试端口,失败时给出可执行的启动指引(区别于端口通但被吞)。"""
base = (cdp_url or "").rstrip("/")
try:
import urllib.request
with urllib.request.urlopen(f"{base}/json/version", timeout=3):
return
except Exception:
pass
raise SystemExit(
f"[!] CDP 端口不可达: {base}\n"
" 先确认浏览器已带调试端口参数启动:\n"
' chrome.exe --remote-debugging-port=9222 '
'(msedge.exe 同理,或 chrome.exe --remote-debugging-port=9222 --user-data-dir="C:/yts-cdp")\n'
" 若已带参数仍连不上:有 Chrome/Edge 残留进程占用了默认 profile"
"新实例的调试端口参数会被吞掉。请先在任务管理器彻底退出所有 Chrome/Edge 进程,"
'再重启;或改用独立 user-data-dir 启动(如 --user-data-dir="C:/yts-cdp")。')
def _launch_page(p, user_data_dir, channel, cdp_url):
"""按 interceptor 同款三种方式拿到 (browser, context, page)。"""
if cdp_url:
@@ -496,6 +531,12 @@ def _capture_bundle(page, url, wait_seconds, owner_display=""):
headers = dict(req.all_headers())
except Exception: # noqa: BLE001
headers = dict(req.headers)
# 过滤 HTTP/2 伪头,防止回放时 requests 抛 InvalidHeader
pseudo = sorted(k for k in headers if PSEUDO_HEADER_RE.match(k))
if pseudo:
print(f"[捕获] 已过滤 HTTP/2 伪头 {len(pseudo)} 个: {', '.join(pseudo)}",
file=sys.stderr)
headers = {k: v for k, v in headers.items() if not PSEUDO_HEADER_RE.match(k)}
try:
body = json.loads(req.post_data or "{}")
except Exception: # noqa: BLE001
@@ -535,6 +576,8 @@ def capture_bundles(urls, user_data_dir=None, channel=None, cdp_url=None,
mode = "CDP 附加" if cdp_url else ("用户数据目录" if user_data_dir else "全新会话(大概率未登录)")
print(f"[捕获] 浏览器会话:{mode}")
if cdp_url:
_assert_cdp_reachable(cdp_url)
bundles = []
with sync_playwright() as p:
@@ -545,7 +588,8 @@ def capture_bundles(urls, user_data_dir=None, channel=None, cdp_url=None,
except Exception as e: # noqa: BLE001
raise SystemExit(
f"[!] 启动/连接浏览器失败: {e}\n"
" 方式 A 需先关闭对应浏览器;方式 B 先以调试端口启动:\n"
" 方式 A 需先关闭对应浏览器(有残留进程会报目录被占用);"
"方式 B 先以调试端口启动:\n"
" chrome.exe --remote-debugging-port=9222")
try:
for i, url in enumerate(urls, 1):