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:
@@ -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_id(groupId)」批量解析脚本
|
||||
(Playwright 自动捕获鉴权套件 + requests 回放查询 + Excel/CSV 输出)。
|
||||
@@ -88,6 +97,10 @@ HTTP_HINTS = {
|
||||
429: "(限流:调低 --max-workers 或稍后重试)",
|
||||
}
|
||||
|
||||
# HTTP/2 伪头(:authority/:method/:path/:scheme):requests 作为普通头发送会抛
|
||||
# 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):
|
||||
|
||||
Reference in New Issue
Block a user