Files
sbnews/docker/crawler/src/parser/nuxt_parser.py
T
2026-06-12 22:27:18 +08:00

180 lines
5.2 KiB
Python

"""
从懂球帝 liveDetail 页面的 window.__NUXT__ IIFE 中解析比赛详情数据
"""
import re
from typing import Optional, Dict, Any
def parse_js_args(args_str: str) -> list:
"""解析 JS IIFE 的实参列表,返回 Python 值列表"""
results = []
i = 0
n = len(args_str)
while i < n:
c = args_str[i]
if c in (' ', '\n', '\r', '\t'):
i += 1
continue
if c == ',':
i += 1
continue
if c == '"':
j = i + 1
while j < n:
if args_str[j] == '\\':
j += 2
continue
if args_str[j] == '"':
break
j += 1
raw = args_str[i:j+1]
inner = raw[1:-1]
if '\\u' in inner:
val = inner.encode('utf-8').decode('unicode_escape')
else:
val = inner.replace('\\n', '\n').replace('\\t', '\t').replace('\\"', '"').replace('\\\\', '\\')
results.append(val)
i = j + 1
continue
if args_str[i:i+4] == 'true':
results.append(True)
i += 4
continue
if args_str[i:i+5] == 'false':
results.append(False)
i += 5
continue
if args_str[i:i+4] == 'null':
results.append(None)
i += 4
continue
if args_str[i:i+6] == 'void 0':
results.append(None)
i += 6
continue
if c in '0123456789.-':
j = i + 1
while j < n and args_str[j] in '0123456789.eE+-':
j += 1
num_str = args_str[i:j]
try:
val = int(num_str)
except ValueError:
val = float(num_str)
results.append(val)
i = j
continue
if c == '{':
depth = 1
j = i + 1
while j < n and depth > 0:
if args_str[j] == '{':
depth += 1
elif args_str[j] == '}':
depth -= 1
elif args_str[j] == '"':
j += 1
while j < n and args_str[j] != '"':
if args_str[j] == '\\':
j += 1
j += 1
j += 1
results.append(args_str[i:j])
i = j
continue
if c == '[':
depth = 1
j = i + 1
while j < n and depth > 0:
if args_str[j] == '[':
depth += 1
elif args_str[j] == ']':
depth -= 1
elif args_str[j] == '"':
j += 1
while j < n and args_str[j] != '"':
if args_str[j] == '\\':
j += 1
j += 1
j += 1
results.append(args_str[i:j])
i = j
continue
j = i
while j < n and args_str[j] not in ',)]}':
j += 1
token = args_str[i:j].strip()
results.append(token)
i = j
return results
def _decode_str(val_expr: str) -> str:
"""解码 JS 字符串字面量(去掉引号)"""
inner = val_expr[1:-1]
if '\\u' in inner:
try:
return inner.encode('utf-8').decode('unicode_escape')
except Exception:
return inner
return inner.replace('\\n', '\n').replace('\\t', '\t').replace('\\"', '"').replace('\\\\', '\\')
def _resolve_value(val_expr: str, var_map: dict):
"""将 JS 赋值右侧表达式解析为 Python 值"""
if val_expr in var_map:
return var_map[val_expr]
if val_expr.startswith('"') and val_expr.endswith('"'):
return _decode_str(val_expr)
if val_expr == 'true':
return True
if val_expr == 'false':
return False
if val_expr == 'null':
return None
try:
return int(val_expr)
except ValueError:
pass
try:
return float(val_expr)
except ValueError:
pass
return val_expr
def parse_nuxt_match_sample(html: str) -> Optional[Dict[str, Any]]:
"""从 liveDetail HTML 中解析 matchSample 对象"""
pattern = r'window\.__NUXT__\s*=\s*\(function\(([^)]+)\)\s*\{(.+)\}\((.+)\)\);'
m = re.search(pattern, html, re.DOTALL)
if not m:
return None
params = [p.strip() for p in m.group(1).split(',')]
body = m.group(2)
args_str = m.group(3)
args = parse_js_args(args_str)
var_map = {}
for i, p in enumerate(params):
if i < len(args):
var_map[p] = args[i]
ms_var_match = re.search(r'(\w+)\.match_id\s*=\s*(\w+)', body)
if not ms_var_match:
return None
ms_var = ms_var_match.group(1)
assign_pattern = re.compile(rf'\b{re.escape(ms_var)}\.(\w+)\s*=\s*(.+?)\s*;', re.MULTILINE)
match_sample = {}
for am in assign_pattern.finditer(body):
field = am.group(1)
val_expr = am.group(2).strip()
match_sample[field] = _resolve_value(val_expr, var_map)
return match_sample if match_sample else None