1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
| import re import ast from bs4 import BeautifulSoup
def compute_calc_value(expr: str, variables: dict = None) -> int: """ 计算 calc() 内部的表达式,例如 (3 * 3 * 5px) + 0px 支持 min/max,支持 0x / 0b 数字,自动去除 px 单位。 variables 用于替换 var(--name) """ if variables is None: variables = {} # 去掉 calc( 和末尾的 ) expr = expr.strip() if expr.startswith('calc('): expr = expr[5:-1].strip() # 递归替换 var(--name) var_pattern = r'var\(--([\w-]+)\)' def repl_var(m): name = m.group(1) if name in variables: return str(variables[name]) else: # 未解析到时保留原样,后续处理(实际上应在调用前解析好) return m.group(0) while re.search(var_pattern, expr): expr = re.sub(var_pattern, repl_var, expr) # 去掉所有 px 单位 expr = re.sub(r'px', '', expr) # 处理 min(...) 和 max(...),取第一个参数(因为参数相同) expr = re.sub(r'min\(([^,]+),[^)]+\)', r'\1', expr) expr = re.sub(r'max\(([^,]+),[^)]+\)', r'\1', expr) # 安全求值(数据可信) try: # 使用 ast.literal_eval 不安全,因为表达式含运算符,改用 eval # 限制全局/局部命名空间为空,避免危险调用 result = eval(expr, {'__builtins__': {}}, {}) return int(result) except Exception: # 降级:手动提取数字乘积(简单场景) nums = re.findall(r'(\d+)', expr) if nums: product = 1 for n in nums: product *= int(n) return product return 0
def parse_css_variables(style_text: str) -> dict: """ 从 <style>:root { ... }</style> 中解析所有 CSS 变量名及其数值 """ # 提取 :root 块内的内容 root_match = re.search(r':root\s*{([^}]*)}', style_text, re.DOTALL) if not root_match: return {} content = root_match.group(1) # 匹配所有变量定义: --name: value; pattern = r'--([\w-]+)\s*:\s*([^;]+);' defs = re.findall(pattern, content) var_values = {} # 先解析不依赖 var() 的定义(普通正变量) for name, raw_val in defs: if 'var(' not in raw_val: var_values[name] = compute_calc_value(raw_val, var_values) # 再解析依赖 var() 的定义(如负变量) for name, raw_val in defs: if name not in var_values: var_values[name] = compute_calc_value(raw_val, var_values) return var_values
def extract_digits_and_offsets(html: str, var_values: dict, char_width: int = 15): """ 遍历 HTML,提取所有数字字符及其最终 X 坐标。 返回列表 [(digit, x_final), ...] """ soup = BeautifulSoup(html, 'html.parser') # 移除 style 标签避免干扰遍历 for style in soup.find_all('style'): style.decompose() digits_info = [] # 元素: (digit, offset_px, initial_index) index = 0
def process_text(text): nonlocal index # 提取所有数字字符(仅保留 0-9) digits = re.findall(r'\d', text) for d in digits: # 纯文本无偏移,初始坐标 = index * char_width,偏移 0 digits_info.append((d, 0, index)) index += 1
def process_tag(tag): nonlocal index if tag.name == 'span': # 提取数字 text = tag.get_text(strip=True) if not text: return # 只取第一个数字(span 中一般仅一个数字) digit = re.search(r'\d', text) if not digit: return digit = digit.group() # 解析偏移样式 style_attr = tag.get('style', '') offset = 0 # left: var(--xxx) left_match = re.search(r'left:\s*var\(--([\w-]+)\)', style_attr) if left_match: var_name = left_match.group(1) offset = var_values.get(var_name, 0) else: right_match = re.search(r'right:\s*var\(--([\w-]+)\)', style_attr) if right_match: var_name = right_match.group(1) offset = -var_values.get(var_name, 0) digits_info.append((digit, offset, index)) index += 1 else: # 其他标签,递归处理子节点 for child in tag.children: if child.name is None: # NavigableString process_text(str(child)) else: process_tag(child)
# 从 body 开始遍历,如果没有 body 则遍历顶层 body = soup.body if body: for child in body.children: if child.name is None: process_text(str(child)) else: process_tag(child) else: # 直接遍历 soup 顶层 for child in soup.children: if child.name is None: process_text(str(child)) else: process_tag(child)
# 计算最终 X 坐标并排序 items = [] for digit, offset, idx in digits_info: x = idx * char_width + offset items.append((x, digit)) items.sort(key=lambda t: t[0]) return ''.join(d[1] for d in items)
def restore_display_html(display_html: str) -> str: """还原单个 display_html 中的数字""" # 提取 style 部分(可能存在多个 style,取第一个包含 :root 的) style_match = re.search(r'<style>(.*?)</style>', display_html, re.DOTALL) var_values = {} if style_match: style_text = style_match.group(1) var_values = parse_css_variables(style_text) # 去掉原 style 标签后的 html html_no_style = re.sub(r'<style>.*?</style>', '', display_html, flags=re.DOTALL) result = extract_digits_and_offsets(html_no_style, var_values, char_width=15) return result
|