180 lines
5.2 KiB
Python
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
|