1. 项目概述当数据被“画”出来时做爬虫的朋友尤其是盯上电商数据的对“字体加密”这个词应该不陌生。这玩意儿就像商家给关键数据比如价格、销量穿上了一件“皇帝的新衣”——在你浏览器里看着是清清楚楚的数字“123”等你用代码去抓取网页源码时看到的却是一堆乱码或者奇怪的字符。拼多多作为国内电商巨头之一其反爬策略里字体加密是相当经典且棘手的一环。它本质上是一种“动态字体映射”技术服务器每次返回的网页中关键数字或文字对应的并不是标准的Unicode字符而是自定义字体文件通常是woff或woff2格式中的某个字形。这个映射关系每次请求都可能发生变化。所以你直接拿到的HTML源码里的“”在浏览器里能正确渲染成“123”是因为浏览器加载了同时下发的那个唯一的字体文件并完成了“乱码-字形-视觉数字”的转换。而你的爬虫如果傻乎乎地直接去解析HTML文本得到的就是一堆天书。这个项目的核心就是逆向这个“乱码到真实数据”的映射过程用Python自动化地破解这套防御稳定地拿到我们想要的商品价格、销量等信息。这不仅仅是写几行正则表达式就能搞定的事它涉及网络请求分析、字体文件处理、编码映射还原等一系列典型的Web逆向工程技能。2. 核心思路拆解从乱码到明文的逆向之旅破解字体加密听起来高深但拆解开来核心逻辑是清晰且通用的。整个过程可以看作一个“侦察-解码-映射”的流水线。2.1 逆向分析的基本逻辑首先我们必须理解浏览器能正确显示而我们的脚本不能的原因。浏览器执行了一个完整的渲染流水线下载HTML - 解析DOM - 下载CSS/字体等资源 - 应用样式包括字体- 绘制字形到屏幕。我们的爬虫通常只做了第一步顶多到第二步。字体加密就是利用了“绘制字形”这个环节对资源的依赖。因此逆向的核心思路就是模拟浏览器获取字体文件并建立映射关系的过程。具体来说获取加密文本从目标网页的HTML源码中定位到被加密的数据块提取出那些“乱码”字符通常表现为#x开头的HTML实体或直接是\ue001这样的Unicode私有区域字符。获取字体文件从网页的响应中通常在CSSfont-face定义里或网络请求中找到字体文件的URL并下载下来。这个字体文件.woff/.woff2是解密的关键。解析字体文件使用专门的库如fontTools加载字体文件分析其内部结构。我们需要找到两个核心映射CMAP表这是字符编码到字形索引glyph index的映射。网页源码中的“乱码”如\ue001对应的编码在这里会指向字体文件内部的第几个字形。字形轮廓数据每个字形索引对应一个具体的矢量图形描述轮廓。但我们需要的是这个图形代表的“含义”比如数字“1”。建立映射关系这是最具技巧性的一步。我们需要将“字形索引”或“字形特征”与真实的数字/文字对应起来。常见方法有基准映射法寻找一个“已知点”。例如在网页的某个角落如页码、“共XX页”可能会存在未加密或可推测的数字。用这个已知的真实数字去反推其对应的加密字符和字形从而建立一个基准映射。字形坐标/特征比对法提取每个字形轮廓的关键坐标点如 bounding box 的宽高、特定笔画的相对位置为每个数字0-9建立一套“特征指纹”。然后将未知字形的特征与这套指纹库进行比对找出最匹配的那个数字。OCR辅助法将字体文件中的字形渲染成图片然后用OCR光学字符识别库去识别。这种方法相对“重”但在某些复杂或自定义字形情况下可能更通用。应用映射解密一旦建立了“加密字符 - 真实字符”的映射字典就可以遍历抓取到的加密文本将其逐一替换还原为明文数据。2.2 针对拼多多的策略考量拼多多的字体加密有其自身特点在制定策略时需要特别注意动态性字体文件URL和字符映射关系很可能不是固定的可能随着会话、时间甚至商品页面而变化。这意味着不能使用硬编码的映射字典必须为每一次或每一批次的请求动态生成映射。字体类型拼多多主要使用woff或woff2格式的Web字体。fontTools的TTFont对象可以很好地处理这两种格式。加密范围通常只对核心商业数据价格、销量、库存进行加密。像商品标题、描述等文本内容一般不受影响。这为我们定位加密数据提供了线索。反爬升级平台的反爬策略会持续升级。可能今天有效的特征提取方法明天就因为字形微调而失效。因此代码需要具备一定的鲁棒性和适应性优先选择相对稳定的特征如字形轮廓的标准化坐标。注意本文讨论的技术方法仅用于学习Web逆向技术和反爬机制原理旨在提升开发者的技术能力。任何技术的应用都必须严格遵守目标网站的服务条款与robots.txt协议尊重网站的数据权益不得用于未经授权的数据爬取、商业牟利或任何干扰网站正常运营的行为。3. 工具与环境准备工欲善其事必先利其器。开始逆向之前我们需要搭建一个合适的Python环境并准备好核心库。3.1 Python环境与核心库建议使用Python 3.7及以上版本。我们将主要依赖以下库可以通过pip安装pip install requests fonttools Pillowrequests用于发送HTTP请求获取网页HTML和字体文件。这是网络交互的基础。fonttools这是处理字体文件的瑞士军刀。它能够解析woff、woff2、ttf、otf等多种字体格式让我们可以访问字体内部的CMAP表、字形轮廓等关键数据。本项目将重度依赖它。Pillow(PIL)Python图像处理库。如果采用将字形渲染成图片再进行特征分析或OCR的路线Pillow是必不可少的。可选库lxml/BeautifulSoup4用于高效解析HTML定位加密数据所在的选择器。lxml解析速度更快BeautifulSoup4的API对新手更友好。选择其中一个即可。pip install lxml beautifulsoup4numpy如果进行复杂的字形坐标特征计算如计算重心、矩特征numpy可以提供高效的数组运算支持。pip install numpypytesseract如果你决定采用OCR辅助法需要安装Tesseract-OCR引擎及其Python封装。这条路配置稍复杂且依赖外部程序不是首选但可作为备选方案。3.2 开发工具与调试技巧浏览器开发者工具这是逆向工程师的“眼睛”。主要用到两个面板Network (网络)刷新目标页面如拼多多商品页记录所有网络请求。重点关注类型为font的请求woff/woff2这就是字体文件。同时查看包含商品信息的XHR/Fetch请求有时数据可能通过接口返回并自带加密。Elements (元素)在页面中右键点击一个被加密的价格数字选择“检查”。在Elements面板中你会看到该数字对应的HTML元素其textContent或innerHTML很可能就是加密后的字符实体如。记下这个元素的选择器路径如.J-price用于后续代码定位。字体查看工具虽然fontTools可以在代码中分析但有一个可视化工具会极大帮助理解。推荐使用在线工具如FontDrop!或本地软件FontForge。你可以把下载的.woff文件拖进去直观地看到每个字符编码通常是uniE001这样的格式对应的字形长什么样快速验证你的映射是否正确。代码编辑器/IDEVSCode、PyCharm等均可。建议配置好Python调试器方便在解析字体、建立映射的关键步骤设置断点查看变量状态。实操心得在开始写代码前强烈建议手动完成一次“破解流程”。即从Network面板复制字体URL用浏览器下载从Elements面板复制一段加密文本用字体查看工具打开字体手动找到加密字符对应的字形并尝试猜测它是哪个数字。这个手动过程能让你对整体流程和可能遇到的坑有最直接的感受。4. 实战步骤详解一步步构建破解器现在我们进入核心的代码实现环节。我们将按照“获取数据 - 解析字体 - 建立映射 - 解密应用”的流程构建一个完整的破解脚本。4.1 第一步获取加密页面与字体文件首先我们需要模拟浏览器获取包含加密数据的HTML和关键的字体文件。import requests import re from fontTools.ttLib import TTFont from io import BytesIO def fetch_page_and_font(url, headers): 获取目标页面HTML并从中提取字体文件URL并下载。 Args: url: 目标商品页URL headers: 请求头需包含User-Agent等模拟浏览器 Returns: html_text: 网页HTML文本 font_data: 字体文件的二进制数据 (BytesIO对象或bytes) font_url: 字体文件的URL用于调试 # 1. 获取页面HTML resp requests.get(url, headersheaders) resp.raise_for_status() html_text resp.text # 2. 从HTML或CSS中查找字体文件URL # 常见模式在style标签内或外链CSS中定义 font-face font_url_pattern re.compile(rsrc:\s*url\((.*?\.woff2?)\)) # 在HTML中搜索字体URL font_match font_url_pattern.search(html_text) if not font_match: # 有时字体URL在JS动态加载或Base64内联需要更复杂的解析这里先处理最常见情况 print(未在HTML中找到标准字体URL可能需要检查网络请求或JS生成。) # 可以尝试从Network记录中硬编码一个已知的字体URL模式进行匹配 # 例如拼多多的字体URL可能包含特定路径 pdd_font_pattern re.compile(r//(.*?\.woff2?)) all_urls pdd_font_pattern.findall(html_text) if all_urls: font_url_relative all_urls[0] # 取第一个实际可能需要更精确的过滤 font_url fhttps:{font_url_relative} if font_url_relative.startswith(//) else font_url_relative else: raise ValueError(无法定位字体文件URL) else: font_url font_match.group(1) if font_url.startswith(//): font_url https: font_url # 3. 下载字体文件 font_resp requests.get(font_url, headersheaders) font_data BytesIO(font_resp.content) # 转为BytesIO方便fontTools直接读取 print(f页面获取成功字体文件URL: {font_url}) return html_text, font_data, font_url # 使用示例 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } target_url https://yangkeduo.com/goods.html?goods_idxxxxxxxx # 替换为实际商品ID html, font_io, font_url fetch_page_and_font(target_url, headers)注意事项请求头务必设置合理的User-Agent模仿真实浏览器否则可能直接被拒绝访问或返回不同的内容。字体URL的多样性正则表达式可能无法覆盖所有情况。字体可能通过JavaScript动态加载或者以Base64格式内嵌在CSS中形如src: url(data:font/woff2;base64,...)。如果上述简单正则匹配失败你需要深入分析Network中实际的字体请求或者使用BeautifulSoup更精细地解析style标签和font-face规则。错误处理网络请求和解析都可能失败在生产环境中需要添加try...except和重试机制。4.2 第二步解析字体文件与提取字形信息拿到字体文件font_io后我们用fontTools打开它开始探查内部结构。def parse_font(font_io): 解析字体文件提取关键信息。 Args: font_io: 字体文件的BytesIO对象或文件路径 Returns: font: TTFont对象 cmap: 编码到字形索引的映射字典 glyph_order: 字形名称列表 font TTFont(font_io) # 获取CMAP表这是编码(unicode) - 字形名称(glyph name)的映射 # cmap 表可能有多个子表通常我们取第一个或用于Unicode的 cmap_table font[cmap] cmap None for table in cmap_table.tables: if table.format 4 or table.format 12: # 常用格式 cmap table.cmap break if cmap is None: raise ValueError(无法从字体中找到有效的cmap表) # 获取字形名称顺序列表 glyph_order font.getGlyphOrder() # 打印一些基本信息用于调试 print(f字体包含字形数量: {len(glyph_order)}) print(fCMAP映射条目示例 (前5个): {list(cmap.items())[:5]}) # 注意cmap映射的是 Unicode码点 - 字形名称如 {100001: uniE001} # 网页中的 #xe001; 或 \ue001 对应的就是 unicode 0xE001 (即十进制的 100001) return font, cmap, glyph_order # 使用示例 font_obj, encoding_map, glyph_names parse_font(font_io)现在我们有了encoding_map。假设网页中的加密字符是其HTML实体为#xe001;Unicode码点为0xE001那么encoding_map.get(0xE001)就会返回一个字形名称例如uniE001。这个名称对应glyph_names列表中的一个索引。关键点网页中的加密字符如是一个Unicode字符它位于“私有使用区”。fontTools帮我们找到了这个字符在字体文件中对应的具体字形名称。接下来我们需要知道这个字形uniE001画出来到底是数字“1”还是“2”。4.3 第三步建立映射关系核心算法这是最具挑战性的一步。我们需要将字形与真实数字对应起来。这里详细介绍两种最实用的方法基准映射法和字形特征比对法。4.3.1 方法一基准映射法找参照物这个方法的核心思想是“以己之矛攻己之盾”。在网页上并非所有数字都被加密。例如“共20页”中的“20”或者商品评分“4.8”中的“4”和“8”有时可能是明文或者可以通过其他方式推断。我们可以利用这些“已知点”来建立映射。步骤定位已知明文数字在HTML中找到包含已知数字的文本节点。可能需要结合多个选择器或正则表达式。获取其加密形态获取该文本节点下的所有子文本节点或字符分析其中哪些字符是加密的通常是#x格式或私有Unicode字符。建立映射字典将已知的真实数字如‘2’, ‘0’与找到的加密字符如\ue00A,\ue00B一一配对。通过字形名称关联利用上一步的encoding_map找到加密字符对应的字形名称如uniE00A。那么我们就知道了字形uniE00A代表数字‘2’。推广映射有了一个或多个基准映射后理论上就可以解密其他使用同一字体的加密文本了。因为同一个字体文件中同一个字形名称对应的形状是固定的。def build_mapping_by_reference(font_obj, cmap, html, known_text_selector, known_real_text): 通过已知的明文文本来构建映射。 Args: font_obj: TTFont对象 cmap: 编码映射字典 html: 网页HTML known_text_selector: 用于定位已知明文文本的CSS选择器或XPath known_real_text: 已知的真实文本如“共20页” Returns: dict: 字形名称 - 真实字符 的映射 from lxml import etree import re tree etree.HTML(html) # 假设我们通过选择器找到了那个包含已知明文和加密字符的元素 element tree.xpath(known_text_selector)[0] # 请替换为实际可用的选择器 element_html etree.tostring(element, encodingunicode) # 我们需要从这个元素的HTML或文本中提取出加密字符序列。 # 已知明文“20”但元素内可能是“span共/spani/ispan页/span” # 我们需要找到i标签内的加密字符。 # 这里简化处理假设已知文本所在的元素内加密字符是连续的并且顺序与已知明文对应。 # 提取私有区字符 (例如 \ue001 格式) encrypted_chars re.findall(r[\ue000-\uf8ff], element_html) # 匹配私有使用区字符 if len(encrypted_chars) ! len(known_real_text): print(f警告加密字符数量({len(encrypted_chars)})与已知文本长度({len(known_real_text)})不符) # 可能需要更精细的解析比如按文本节点拆分 return {} glyph_to_real {} for enc_char, real_char in zip(encrypted_chars, known_real_text): unicode_code ord(enc_char) # 获取Unicode码点如 0xe001 glyph_name cmap.get(unicode_code) if glyph_name: glyph_to_real[glyph_name] real_char print(f映射建立: 字符{enc_char} (U{unicode_code:04X}) - 字形 {glyph_name} - 真实字符 {real_char}) else: print(f警告字符{enc_char}未在字体CMAP中找到映射) return glyph_to_real此方法的局限性非常依赖于页面上存在稳定、可定位的已知明文参照物。如果参照物消失或也变为加密此方法失效。且需要人工介入分析页面结构自动化程度较低。4.3.2 方法二字形特征比对法自动化推荐这是更通用和自动化的方法。我们利用数字0-9的字形在视觉上的结构性差异为每个数字定义一个“特征向量”然后将未知字形的特征与这个标准库进行比对。特征选择字形轮廓由一系列坐标点构成。一个简单而有效的特征是字形轮廓的边界框bounding box以及其标准化坐标。我们可以将每个字形渲染到一个固定的坐标系比如1000x1000的单位正方形内然后提取其轮廓上所有点的坐标计算一些统计特征如重心、宽高比、特定区域的点密度等。但更简单直接的是使用fontTools提供的glyf表数据。这里介绍一种基于字形轮廓XML表示和关键点坐标的简化特征提取方法def extract_glyph_features(font_obj, glyph_name): 提取指定字形的特征。 这里使用一种简化特征字形轮廓的‘边界框’和‘笔划端点’的标准化坐标。 Args: font_obj: TTFont对象 glyph_name: 字形名称如 uniE001 Returns: dict: 包含特征信息的字典 from fontTools.pens.recordingPen import RecordingPen import math glyph_set font_obj.getGlyphSet() glyph glyph_set[glyph_name] # 使用RecordingPen记录绘制指令 pen RecordingPen() glyph.draw(pen) # 获取绘制指令序列 # 指令包括 moveTo, lineTo, curveTo, closePath 等附带坐标点 # 我们可以从这些点中提取特征 all_points [] for cmd, pts in pen.value: all_points.extend(pts) # pts是点元组的列表如 [(x1,y1), (x2,y2)] if not all_points: return {width: 0, height: 0, points: []} xs, ys zip(*all_points) if all_points else ([], []) x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) width x_max - x_min height y_max - y_min # 计算所有点的重心质心 centroid_x sum(xs) / len(xs) centroid_y sum(ys) / len(ys) # 将坐标标准化到[0, 1]范围相对于边界框 # 这有助于消除字体大小和位置的影响 normalized_points [] if width 0 and height 0: for x, y in all_points: nx (x - x_min) / width ny (y - y_min) / height normalized_points.append((nx, ny)) # 特征字典 features { glyph_name: glyph_name, bbox: (x_min, y_min, x_max, y_max), width: width, height: height, aspect_ratio: width / height if height ! 0 else 0, centroid: (centroid_x, centroid_y), num_points: len(all_points), normalized_points: normalized_points[:50], # 取前50个点作为特征可根据需要调整 } return features def create_digit_template_features(): 创建一个标准数字0-9的特征模板库。 注意这个模板库是基于一种‘标准’字体如Arial预先计算好的。 对于拼多多其自定义字体的数字形状可能与标准字体有差异但结构如0是封闭圆圈1是单竖线应大体一致。 在实际项目中你可能需要手动查看一次拼多多的字体确认0-9对应的字形名称然后为这些名称计算特征作为本次会话的模板。 这里我们假设我们已经通过某种方式比如第一次手动或基准法知道了本次字体文件中0-9对应的字形名称。 这是一个需要初始化的步骤。 # 这是一个示例结构。实际应用中你需要先运行一次通过基准法或人工识别得到本次字体中0-9的字形名。 # 例如{0: uniE010, 1: uniE001, 2: uniE002, ...} # 然后为这些字形名计算特征存入模板库。 template_lib {} # 假设我们已经有了 digit_glyph_mapping # for real_digit, glyph_name in digit_glyph_mapping.items(): # features extract_glyph_features(font_obj, glyph_name) # template_lib[real_digit] features return template_lib def match_glyph_to_digit(features, template_lib): 将未知字形的特征与模板库进行匹配找出最相似的数字。 使用简单的欧氏距离比较特征向量。 Args: features: 未知字形的特征字典 template_lib: 数字模板特征库 Returns: str: 匹配的数字字符如 5如果匹配失败返回 None if not template_lib: return None best_match None min_distance float(inf) # 这里我们使用一个简化的距离计算比较标准化点的坐标取前N个点 # 更复杂的可以结合宽高比、重心等多个特征 unknown_points features.get(normalized_points, []) if not unknown_points: return None for digit, temp_features in template_lib.items(): temp_points temp_features.get(normalized_points, []) if len(temp_points) ! len(unknown_points): # 点数不同可以填充或截断这里简单跳过或处理 continue # 计算点对之间的欧氏距离和 distance 0 for (ux, uy), (tx, ty) in zip(unknown_points, temp_points): distance math.sqrt((ux - tx)**2 (uy - ty)**2) if distance min_distance: min_distance distance best_match digit # 可以设置一个距离阈值超过则认为匹配失败 threshold 5.0 # 这个阈值需要根据实际情况调整 if min_distance threshold: return best_match else: return None实操心得特征比对法的关键在于特征的选择和距离度量。上述基于轮廓点的方法是一种思路但在实际中拼多多的数字字体可能设计得比较独特直接与标准字体比对效果可能不佳。更稳健的做法是首次请求时结合基准映射法哪怕只找到一个已知数字或人工干预识别出本次字体文件中10个数字0-9对应的字形名然后为这10个字形计算特征作为本次会话的模板库。这样模板和待匹配的字形来自同一个字体文件匹配准确率会极高。之后对同一会话中其他使用该字体的加密文本就可以用这个模板库进行匹配了。4.4 第四步整合与解密应用有了映射关系无论是基准法得到的glyph_to_real字典还是特征法得到的template_lib和匹配函数我们就可以解密页面中的任何加密文本了。def decrypt_text(encrypted_text, cmap, mapping_dict): 根据映射字典解密一段加密文本。 Args: encrypted_text: 加密的文本字符串如 cmap: 编码-字形名的映射 mapping_dict: 字形名-真实字符的映射 Returns: str: 解密后的明文 decrypted_chars [] for char in encrypted_text: unicode_val ord(char) glyph_name cmap.get(unicode_val) if glyph_name and glyph_name in mapping_dict: decrypted_chars.append(mapping_dict[glyph_name]) else: # 如果映射中找不到保留原字符可能是未加密的标点或文字 decrypted_chars.append(char) return .join(decrypted_chars) def extract_and_decrypt_price(html, font_obj, cmap, mapping_dict): 从HTML中提取加密的价格元素并解密。 Args: html: 网页HTML font_obj: TTFont对象可能用于特征匹配法此处备用 cmap: 编码映射 mapping_dict: 字形名-真实字符的映射 Returns: str: 解密后的价格字符串如 129.00 from lxml import etree import re tree etree.HTML(html) # 假设价格元素的CSS类为 .J-price 实际需要通过分析页面确定 price_elements tree.xpath(//*[contains(class, J-price)]//text()) if not price_elements: # 尝试其他常见选择器 price_elements tree.xpath(//*[contains(class, price)]//text()) encrypted_price_str .join(price_elements).strip() # 清理可能的空白和换行 encrypted_price_str re.sub(r\s, , encrypted_price_str) print(f提取到的加密价格文本: {encrypted_price_str}) decrypted_price decrypt_text(encrypted_price_str, cmap, mapping_dict) print(f解密后的价格: {decrypted_price}) return decrypted_price # 主流程整合示例 def main(): # 1. 获取页面和字体 html, font_io, _ fetch_page_and_font(target_url, headers) # 2. 解析字体 font_obj, cmap, glyph_names parse_font(font_io) # 3. 建立映射这里演示基准法假设我们通过分析页面找到了已知参照 # 假设我们知道页面中“销量200件”的“200”是明文且其加密字符对应字形为 uniE100, uniE101, uniE102 # 我们需要手动或通过更智能的解析得到这个对应关系。这里简化假设我们已经有了。 # 例如通过人工查看字体工具和页面我们确定了 manual_mapping { uniE100: 2, uniE101: 0, uniE102: 0, # ... 继续添加其他通过同样方式或特征匹配法得到的映射 } # 如果是特征匹配法则需要先构建模板库 # 假设我们已经通过某种方式得到了0-9的字形名列表 digit_glyph_names [uniE100, uniE101, uniE102, uniE103, ...] # 0,1,2,3... template_lib {} for idx, glyph_name in enumerate(digit_glyph_names): features extract_glyph_features(font_obj, glyph_name) template_lib[str(idx)] features # 数字字符作为key # 4. 对于未知字形使用特征匹配法补充映射 full_mapping manual_mapping.copy() # 从基准映射开始 for glyph_name in glyph_names: if glyph_name.startswith(uni) and glyph_name not in full_mapping: features extract_glyph_features(font_obj, glyph_name) matched_digit match_glyph_to_digit(features, template_lib) if matched_digit: full_mapping[glyph_name] matched_digit print(f最终建立的映射字典部分: {list(full_mapping.items())[:10]}) # 5. 解密目标数据 price extract_and_decrypt_price(html, font_obj, cmap, full_mapping) print(f商品价格: {price}) if __name__ __main__: main()5. 常见问题与排查技巧实录在实际操作中你会遇到各种各样的问题。下面记录了一些典型场景和解决思路。5.1 字体文件获取失败或格式异常问题正则匹配不到字体URL或者下载的字体文件fontTools无法打开。排查检查Network请求确保在开发者工具的Network面板中能看到字体请求过滤font类型。如果看不到可能是字体通过JS动态加载或使用了data:URI内联。内联字体在HTML或CSS中搜索data:font/woff2;base64或data:application/font-woff;base64。如果找到这是Base64编码的字体数据需要用base64.b64decode()解码后再传递给TTFont。字体格式确认文件魔数。.woff文件头是wOFF.woff2是wOF2。如果文件损坏可能下载不完整需要检查请求头或会话状态如Cookie。5.2 CMAP映射为空或找不到对应字形问题cmap表为空或者网页中的加密字符如\ue001在cmap中找不到对应字形名。排查字符编码确认确保你从HTML中提取的“加密字符”是正确的。有时它可能被HTML实体转义如#xe001;你需要先将其转换为实际的Unicode字符chr(0xe001)。检查CMAP子表font[cmap].tables可能包含多个子表对应不同的平台和编码。确保你遍历的是正确的子表通常是format4或format12platformID3或0platEncID1或10对应Unicode。可以打印所有子表信息来检查。字体子集可能字体只包含了页面用到的部分字符而你的cmap查找方式有误。尝试直接遍历font[cmap].tables的所有cmap属性。5.3 映射建立不准确或解密结果乱码问题成功建立了映射但解密出来的数字是错的比如价格“129”解成了“753”。排查基准映射错误检查你用作基准的“已知明文”是否真的未被加密或者其顺序是否与加密字符严格对应。页面DOM结构可能很复杂文本节点可能被拆分。特征匹配失效如果你使用特征匹配法可能是特征提取或距离度量不够鲁棒。尝试增加特征维度除了轮廓点加入字形宽度、高度、重心、宽高比等。使用更稳定的特征计算字形在若干等分网格内的笔画存在性将Bounding Box划分为NxN的网格判断每个网格内是否有轮廓点形成一个二进制特征向量。人工校验模板库首次运行时务必人工核对自动或半自动生成的0-9模板是否正确。可以先将所有字形保存为图片查看。字体动态变化最棘手的情况。拼多多可能频繁更换字体文件或字形坐标导致之前生成的映射失效。解决方案会话级缓存在一次爬虫会话中对同一域名只获取并解析一次字体后续沿用映射。但不同商品页可能用不同字体需要根据字体URL或文件哈希进行缓存。映射自验证设计一种校验机制。例如解密后的价格应该符合某些规则如包含小数点、是合理范围内的数字。如果解密结果明显不符合规则如出现字母则触发重新获取字体和建立映射的流程。降级策略如果自动破解连续失败可以记录日志并报警转为人工介入分析新字体。5.4 性能优化与工程化考虑当需要大规模、持续爬取时效率很重要。字体缓存将字体文件的URL及其对应的映射字典缓存起来例如使用redis或本地文件。键可以是字体文件的MD5哈希值或URL本身。下次遇到相同字体时直接使用缓存映射避免重复解析和特征计算。并发请求如果爬取多个页面注意字体文件请求也可能被限速。合理安排并发并考虑复用HTTP连接requests.Session。错误重试与降级网络请求、字体解析、映射匹配都可能失败。代码中应有完善的try-except和重试逻辑。对于非关键数据可以考虑降级如记录原始加密文本稍后人工处理。日志记录详细记录每一步的操作特别是字体URL、映射关系、解密前后的文本。这对于调试和监控系统状态至关重要。最后的小技巧有时候最简单的办法可能最有效。如果目标网站的反爬不是极端复杂可以尝试直接使用Selenium或Playwright等浏览器自动化工具让真实的浏览器引擎去渲染页面然后直接从DOM中获取渲染后的文本。这相当于把解密工作交给了浏览器本身省去了逆向字体的过程。当然这种方案资源消耗大、速度慢适用于小规模或对稳定性要求极高的场景。选择哪种方案需要在开发成本、维护成本和运行效率之间做权衡。破解字体加密是一个典型的“道高一尺魔高一丈”的过程。核心在于深刻理解其原理并构建一个灵活、可适应变化的自动化流程。本文提供的指南和代码框架是一个坚实的起点但面对具体的目标网站仍需你耐心分析、不断调试和优化。记住逆向工程没有银弹清晰的思路和细致的观察往往比复杂的代码更重要。
Python逆向破解电商字体加密:从原理到实战的完整指南
1. 项目概述当数据被“画”出来时做爬虫的朋友尤其是盯上电商数据的对“字体加密”这个词应该不陌生。这玩意儿就像商家给关键数据比如价格、销量穿上了一件“皇帝的新衣”——在你浏览器里看着是清清楚楚的数字“123”等你用代码去抓取网页源码时看到的却是一堆乱码或者奇怪的字符。拼多多作为国内电商巨头之一其反爬策略里字体加密是相当经典且棘手的一环。它本质上是一种“动态字体映射”技术服务器每次返回的网页中关键数字或文字对应的并不是标准的Unicode字符而是自定义字体文件通常是woff或woff2格式中的某个字形。这个映射关系每次请求都可能发生变化。所以你直接拿到的HTML源码里的“”在浏览器里能正确渲染成“123”是因为浏览器加载了同时下发的那个唯一的字体文件并完成了“乱码-字形-视觉数字”的转换。而你的爬虫如果傻乎乎地直接去解析HTML文本得到的就是一堆天书。这个项目的核心就是逆向这个“乱码到真实数据”的映射过程用Python自动化地破解这套防御稳定地拿到我们想要的商品价格、销量等信息。这不仅仅是写几行正则表达式就能搞定的事它涉及网络请求分析、字体文件处理、编码映射还原等一系列典型的Web逆向工程技能。2. 核心思路拆解从乱码到明文的逆向之旅破解字体加密听起来高深但拆解开来核心逻辑是清晰且通用的。整个过程可以看作一个“侦察-解码-映射”的流水线。2.1 逆向分析的基本逻辑首先我们必须理解浏览器能正确显示而我们的脚本不能的原因。浏览器执行了一个完整的渲染流水线下载HTML - 解析DOM - 下载CSS/字体等资源 - 应用样式包括字体- 绘制字形到屏幕。我们的爬虫通常只做了第一步顶多到第二步。字体加密就是利用了“绘制字形”这个环节对资源的依赖。因此逆向的核心思路就是模拟浏览器获取字体文件并建立映射关系的过程。具体来说获取加密文本从目标网页的HTML源码中定位到被加密的数据块提取出那些“乱码”字符通常表现为#x开头的HTML实体或直接是\ue001这样的Unicode私有区域字符。获取字体文件从网页的响应中通常在CSSfont-face定义里或网络请求中找到字体文件的URL并下载下来。这个字体文件.woff/.woff2是解密的关键。解析字体文件使用专门的库如fontTools加载字体文件分析其内部结构。我们需要找到两个核心映射CMAP表这是字符编码到字形索引glyph index的映射。网页源码中的“乱码”如\ue001对应的编码在这里会指向字体文件内部的第几个字形。字形轮廓数据每个字形索引对应一个具体的矢量图形描述轮廓。但我们需要的是这个图形代表的“含义”比如数字“1”。建立映射关系这是最具技巧性的一步。我们需要将“字形索引”或“字形特征”与真实的数字/文字对应起来。常见方法有基准映射法寻找一个“已知点”。例如在网页的某个角落如页码、“共XX页”可能会存在未加密或可推测的数字。用这个已知的真实数字去反推其对应的加密字符和字形从而建立一个基准映射。字形坐标/特征比对法提取每个字形轮廓的关键坐标点如 bounding box 的宽高、特定笔画的相对位置为每个数字0-9建立一套“特征指纹”。然后将未知字形的特征与这套指纹库进行比对找出最匹配的那个数字。OCR辅助法将字体文件中的字形渲染成图片然后用OCR光学字符识别库去识别。这种方法相对“重”但在某些复杂或自定义字形情况下可能更通用。应用映射解密一旦建立了“加密字符 - 真实字符”的映射字典就可以遍历抓取到的加密文本将其逐一替换还原为明文数据。2.2 针对拼多多的策略考量拼多多的字体加密有其自身特点在制定策略时需要特别注意动态性字体文件URL和字符映射关系很可能不是固定的可能随着会话、时间甚至商品页面而变化。这意味着不能使用硬编码的映射字典必须为每一次或每一批次的请求动态生成映射。字体类型拼多多主要使用woff或woff2格式的Web字体。fontTools的TTFont对象可以很好地处理这两种格式。加密范围通常只对核心商业数据价格、销量、库存进行加密。像商品标题、描述等文本内容一般不受影响。这为我们定位加密数据提供了线索。反爬升级平台的反爬策略会持续升级。可能今天有效的特征提取方法明天就因为字形微调而失效。因此代码需要具备一定的鲁棒性和适应性优先选择相对稳定的特征如字形轮廓的标准化坐标。注意本文讨论的技术方法仅用于学习Web逆向技术和反爬机制原理旨在提升开发者的技术能力。任何技术的应用都必须严格遵守目标网站的服务条款与robots.txt协议尊重网站的数据权益不得用于未经授权的数据爬取、商业牟利或任何干扰网站正常运营的行为。3. 工具与环境准备工欲善其事必先利其器。开始逆向之前我们需要搭建一个合适的Python环境并准备好核心库。3.1 Python环境与核心库建议使用Python 3.7及以上版本。我们将主要依赖以下库可以通过pip安装pip install requests fonttools Pillowrequests用于发送HTTP请求获取网页HTML和字体文件。这是网络交互的基础。fonttools这是处理字体文件的瑞士军刀。它能够解析woff、woff2、ttf、otf等多种字体格式让我们可以访问字体内部的CMAP表、字形轮廓等关键数据。本项目将重度依赖它。Pillow(PIL)Python图像处理库。如果采用将字形渲染成图片再进行特征分析或OCR的路线Pillow是必不可少的。可选库lxml/BeautifulSoup4用于高效解析HTML定位加密数据所在的选择器。lxml解析速度更快BeautifulSoup4的API对新手更友好。选择其中一个即可。pip install lxml beautifulsoup4numpy如果进行复杂的字形坐标特征计算如计算重心、矩特征numpy可以提供高效的数组运算支持。pip install numpypytesseract如果你决定采用OCR辅助法需要安装Tesseract-OCR引擎及其Python封装。这条路配置稍复杂且依赖外部程序不是首选但可作为备选方案。3.2 开发工具与调试技巧浏览器开发者工具这是逆向工程师的“眼睛”。主要用到两个面板Network (网络)刷新目标页面如拼多多商品页记录所有网络请求。重点关注类型为font的请求woff/woff2这就是字体文件。同时查看包含商品信息的XHR/Fetch请求有时数据可能通过接口返回并自带加密。Elements (元素)在页面中右键点击一个被加密的价格数字选择“检查”。在Elements面板中你会看到该数字对应的HTML元素其textContent或innerHTML很可能就是加密后的字符实体如。记下这个元素的选择器路径如.J-price用于后续代码定位。字体查看工具虽然fontTools可以在代码中分析但有一个可视化工具会极大帮助理解。推荐使用在线工具如FontDrop!或本地软件FontForge。你可以把下载的.woff文件拖进去直观地看到每个字符编码通常是uniE001这样的格式对应的字形长什么样快速验证你的映射是否正确。代码编辑器/IDEVSCode、PyCharm等均可。建议配置好Python调试器方便在解析字体、建立映射的关键步骤设置断点查看变量状态。实操心得在开始写代码前强烈建议手动完成一次“破解流程”。即从Network面板复制字体URL用浏览器下载从Elements面板复制一段加密文本用字体查看工具打开字体手动找到加密字符对应的字形并尝试猜测它是哪个数字。这个手动过程能让你对整体流程和可能遇到的坑有最直接的感受。4. 实战步骤详解一步步构建破解器现在我们进入核心的代码实现环节。我们将按照“获取数据 - 解析字体 - 建立映射 - 解密应用”的流程构建一个完整的破解脚本。4.1 第一步获取加密页面与字体文件首先我们需要模拟浏览器获取包含加密数据的HTML和关键的字体文件。import requests import re from fontTools.ttLib import TTFont from io import BytesIO def fetch_page_and_font(url, headers): 获取目标页面HTML并从中提取字体文件URL并下载。 Args: url: 目标商品页URL headers: 请求头需包含User-Agent等模拟浏览器 Returns: html_text: 网页HTML文本 font_data: 字体文件的二进制数据 (BytesIO对象或bytes) font_url: 字体文件的URL用于调试 # 1. 获取页面HTML resp requests.get(url, headersheaders) resp.raise_for_status() html_text resp.text # 2. 从HTML或CSS中查找字体文件URL # 常见模式在style标签内或外链CSS中定义 font-face font_url_pattern re.compile(rsrc:\s*url\((.*?\.woff2?)\)) # 在HTML中搜索字体URL font_match font_url_pattern.search(html_text) if not font_match: # 有时字体URL在JS动态加载或Base64内联需要更复杂的解析这里先处理最常见情况 print(未在HTML中找到标准字体URL可能需要检查网络请求或JS生成。) # 可以尝试从Network记录中硬编码一个已知的字体URL模式进行匹配 # 例如拼多多的字体URL可能包含特定路径 pdd_font_pattern re.compile(r//(.*?\.woff2?)) all_urls pdd_font_pattern.findall(html_text) if all_urls: font_url_relative all_urls[0] # 取第一个实际可能需要更精确的过滤 font_url fhttps:{font_url_relative} if font_url_relative.startswith(//) else font_url_relative else: raise ValueError(无法定位字体文件URL) else: font_url font_match.group(1) if font_url.startswith(//): font_url https: font_url # 3. 下载字体文件 font_resp requests.get(font_url, headersheaders) font_data BytesIO(font_resp.content) # 转为BytesIO方便fontTools直接读取 print(f页面获取成功字体文件URL: {font_url}) return html_text, font_data, font_url # 使用示例 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } target_url https://yangkeduo.com/goods.html?goods_idxxxxxxxx # 替换为实际商品ID html, font_io, font_url fetch_page_and_font(target_url, headers)注意事项请求头务必设置合理的User-Agent模仿真实浏览器否则可能直接被拒绝访问或返回不同的内容。字体URL的多样性正则表达式可能无法覆盖所有情况。字体可能通过JavaScript动态加载或者以Base64格式内嵌在CSS中形如src: url(data:font/woff2;base64,...)。如果上述简单正则匹配失败你需要深入分析Network中实际的字体请求或者使用BeautifulSoup更精细地解析style标签和font-face规则。错误处理网络请求和解析都可能失败在生产环境中需要添加try...except和重试机制。4.2 第二步解析字体文件与提取字形信息拿到字体文件font_io后我们用fontTools打开它开始探查内部结构。def parse_font(font_io): 解析字体文件提取关键信息。 Args: font_io: 字体文件的BytesIO对象或文件路径 Returns: font: TTFont对象 cmap: 编码到字形索引的映射字典 glyph_order: 字形名称列表 font TTFont(font_io) # 获取CMAP表这是编码(unicode) - 字形名称(glyph name)的映射 # cmap 表可能有多个子表通常我们取第一个或用于Unicode的 cmap_table font[cmap] cmap None for table in cmap_table.tables: if table.format 4 or table.format 12: # 常用格式 cmap table.cmap break if cmap is None: raise ValueError(无法从字体中找到有效的cmap表) # 获取字形名称顺序列表 glyph_order font.getGlyphOrder() # 打印一些基本信息用于调试 print(f字体包含字形数量: {len(glyph_order)}) print(fCMAP映射条目示例 (前5个): {list(cmap.items())[:5]}) # 注意cmap映射的是 Unicode码点 - 字形名称如 {100001: uniE001} # 网页中的 #xe001; 或 \ue001 对应的就是 unicode 0xE001 (即十进制的 100001) return font, cmap, glyph_order # 使用示例 font_obj, encoding_map, glyph_names parse_font(font_io)现在我们有了encoding_map。假设网页中的加密字符是其HTML实体为#xe001;Unicode码点为0xE001那么encoding_map.get(0xE001)就会返回一个字形名称例如uniE001。这个名称对应glyph_names列表中的一个索引。关键点网页中的加密字符如是一个Unicode字符它位于“私有使用区”。fontTools帮我们找到了这个字符在字体文件中对应的具体字形名称。接下来我们需要知道这个字形uniE001画出来到底是数字“1”还是“2”。4.3 第三步建立映射关系核心算法这是最具挑战性的一步。我们需要将字形与真实数字对应起来。这里详细介绍两种最实用的方法基准映射法和字形特征比对法。4.3.1 方法一基准映射法找参照物这个方法的核心思想是“以己之矛攻己之盾”。在网页上并非所有数字都被加密。例如“共20页”中的“20”或者商品评分“4.8”中的“4”和“8”有时可能是明文或者可以通过其他方式推断。我们可以利用这些“已知点”来建立映射。步骤定位已知明文数字在HTML中找到包含已知数字的文本节点。可能需要结合多个选择器或正则表达式。获取其加密形态获取该文本节点下的所有子文本节点或字符分析其中哪些字符是加密的通常是#x格式或私有Unicode字符。建立映射字典将已知的真实数字如‘2’, ‘0’与找到的加密字符如\ue00A,\ue00B一一配对。通过字形名称关联利用上一步的encoding_map找到加密字符对应的字形名称如uniE00A。那么我们就知道了字形uniE00A代表数字‘2’。推广映射有了一个或多个基准映射后理论上就可以解密其他使用同一字体的加密文本了。因为同一个字体文件中同一个字形名称对应的形状是固定的。def build_mapping_by_reference(font_obj, cmap, html, known_text_selector, known_real_text): 通过已知的明文文本来构建映射。 Args: font_obj: TTFont对象 cmap: 编码映射字典 html: 网页HTML known_text_selector: 用于定位已知明文文本的CSS选择器或XPath known_real_text: 已知的真实文本如“共20页” Returns: dict: 字形名称 - 真实字符 的映射 from lxml import etree import re tree etree.HTML(html) # 假设我们通过选择器找到了那个包含已知明文和加密字符的元素 element tree.xpath(known_text_selector)[0] # 请替换为实际可用的选择器 element_html etree.tostring(element, encodingunicode) # 我们需要从这个元素的HTML或文本中提取出加密字符序列。 # 已知明文“20”但元素内可能是“span共/spani/ispan页/span” # 我们需要找到i标签内的加密字符。 # 这里简化处理假设已知文本所在的元素内加密字符是连续的并且顺序与已知明文对应。 # 提取私有区字符 (例如 \ue001 格式) encrypted_chars re.findall(r[\ue000-\uf8ff], element_html) # 匹配私有使用区字符 if len(encrypted_chars) ! len(known_real_text): print(f警告加密字符数量({len(encrypted_chars)})与已知文本长度({len(known_real_text)})不符) # 可能需要更精细的解析比如按文本节点拆分 return {} glyph_to_real {} for enc_char, real_char in zip(encrypted_chars, known_real_text): unicode_code ord(enc_char) # 获取Unicode码点如 0xe001 glyph_name cmap.get(unicode_code) if glyph_name: glyph_to_real[glyph_name] real_char print(f映射建立: 字符{enc_char} (U{unicode_code:04X}) - 字形 {glyph_name} - 真实字符 {real_char}) else: print(f警告字符{enc_char}未在字体CMAP中找到映射) return glyph_to_real此方法的局限性非常依赖于页面上存在稳定、可定位的已知明文参照物。如果参照物消失或也变为加密此方法失效。且需要人工介入分析页面结构自动化程度较低。4.3.2 方法二字形特征比对法自动化推荐这是更通用和自动化的方法。我们利用数字0-9的字形在视觉上的结构性差异为每个数字定义一个“特征向量”然后将未知字形的特征与这个标准库进行比对。特征选择字形轮廓由一系列坐标点构成。一个简单而有效的特征是字形轮廓的边界框bounding box以及其标准化坐标。我们可以将每个字形渲染到一个固定的坐标系比如1000x1000的单位正方形内然后提取其轮廓上所有点的坐标计算一些统计特征如重心、宽高比、特定区域的点密度等。但更简单直接的是使用fontTools提供的glyf表数据。这里介绍一种基于字形轮廓XML表示和关键点坐标的简化特征提取方法def extract_glyph_features(font_obj, glyph_name): 提取指定字形的特征。 这里使用一种简化特征字形轮廓的‘边界框’和‘笔划端点’的标准化坐标。 Args: font_obj: TTFont对象 glyph_name: 字形名称如 uniE001 Returns: dict: 包含特征信息的字典 from fontTools.pens.recordingPen import RecordingPen import math glyph_set font_obj.getGlyphSet() glyph glyph_set[glyph_name] # 使用RecordingPen记录绘制指令 pen RecordingPen() glyph.draw(pen) # 获取绘制指令序列 # 指令包括 moveTo, lineTo, curveTo, closePath 等附带坐标点 # 我们可以从这些点中提取特征 all_points [] for cmd, pts in pen.value: all_points.extend(pts) # pts是点元组的列表如 [(x1,y1), (x2,y2)] if not all_points: return {width: 0, height: 0, points: []} xs, ys zip(*all_points) if all_points else ([], []) x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) width x_max - x_min height y_max - y_min # 计算所有点的重心质心 centroid_x sum(xs) / len(xs) centroid_y sum(ys) / len(ys) # 将坐标标准化到[0, 1]范围相对于边界框 # 这有助于消除字体大小和位置的影响 normalized_points [] if width 0 and height 0: for x, y in all_points: nx (x - x_min) / width ny (y - y_min) / height normalized_points.append((nx, ny)) # 特征字典 features { glyph_name: glyph_name, bbox: (x_min, y_min, x_max, y_max), width: width, height: height, aspect_ratio: width / height if height ! 0 else 0, centroid: (centroid_x, centroid_y), num_points: len(all_points), normalized_points: normalized_points[:50], # 取前50个点作为特征可根据需要调整 } return features def create_digit_template_features(): 创建一个标准数字0-9的特征模板库。 注意这个模板库是基于一种‘标准’字体如Arial预先计算好的。 对于拼多多其自定义字体的数字形状可能与标准字体有差异但结构如0是封闭圆圈1是单竖线应大体一致。 在实际项目中你可能需要手动查看一次拼多多的字体确认0-9对应的字形名称然后为这些名称计算特征作为本次会话的模板。 这里我们假设我们已经通过某种方式比如第一次手动或基准法知道了本次字体文件中0-9对应的字形名称。 这是一个需要初始化的步骤。 # 这是一个示例结构。实际应用中你需要先运行一次通过基准法或人工识别得到本次字体中0-9的字形名。 # 例如{0: uniE010, 1: uniE001, 2: uniE002, ...} # 然后为这些字形名计算特征存入模板库。 template_lib {} # 假设我们已经有了 digit_glyph_mapping # for real_digit, glyph_name in digit_glyph_mapping.items(): # features extract_glyph_features(font_obj, glyph_name) # template_lib[real_digit] features return template_lib def match_glyph_to_digit(features, template_lib): 将未知字形的特征与模板库进行匹配找出最相似的数字。 使用简单的欧氏距离比较特征向量。 Args: features: 未知字形的特征字典 template_lib: 数字模板特征库 Returns: str: 匹配的数字字符如 5如果匹配失败返回 None if not template_lib: return None best_match None min_distance float(inf) # 这里我们使用一个简化的距离计算比较标准化点的坐标取前N个点 # 更复杂的可以结合宽高比、重心等多个特征 unknown_points features.get(normalized_points, []) if not unknown_points: return None for digit, temp_features in template_lib.items(): temp_points temp_features.get(normalized_points, []) if len(temp_points) ! len(unknown_points): # 点数不同可以填充或截断这里简单跳过或处理 continue # 计算点对之间的欧氏距离和 distance 0 for (ux, uy), (tx, ty) in zip(unknown_points, temp_points): distance math.sqrt((ux - tx)**2 (uy - ty)**2) if distance min_distance: min_distance distance best_match digit # 可以设置一个距离阈值超过则认为匹配失败 threshold 5.0 # 这个阈值需要根据实际情况调整 if min_distance threshold: return best_match else: return None实操心得特征比对法的关键在于特征的选择和距离度量。上述基于轮廓点的方法是一种思路但在实际中拼多多的数字字体可能设计得比较独特直接与标准字体比对效果可能不佳。更稳健的做法是首次请求时结合基准映射法哪怕只找到一个已知数字或人工干预识别出本次字体文件中10个数字0-9对应的字形名然后为这10个字形计算特征作为本次会话的模板库。这样模板和待匹配的字形来自同一个字体文件匹配准确率会极高。之后对同一会话中其他使用该字体的加密文本就可以用这个模板库进行匹配了。4.4 第四步整合与解密应用有了映射关系无论是基准法得到的glyph_to_real字典还是特征法得到的template_lib和匹配函数我们就可以解密页面中的任何加密文本了。def decrypt_text(encrypted_text, cmap, mapping_dict): 根据映射字典解密一段加密文本。 Args: encrypted_text: 加密的文本字符串如 cmap: 编码-字形名的映射 mapping_dict: 字形名-真实字符的映射 Returns: str: 解密后的明文 decrypted_chars [] for char in encrypted_text: unicode_val ord(char) glyph_name cmap.get(unicode_val) if glyph_name and glyph_name in mapping_dict: decrypted_chars.append(mapping_dict[glyph_name]) else: # 如果映射中找不到保留原字符可能是未加密的标点或文字 decrypted_chars.append(char) return .join(decrypted_chars) def extract_and_decrypt_price(html, font_obj, cmap, mapping_dict): 从HTML中提取加密的价格元素并解密。 Args: html: 网页HTML font_obj: TTFont对象可能用于特征匹配法此处备用 cmap: 编码映射 mapping_dict: 字形名-真实字符的映射 Returns: str: 解密后的价格字符串如 129.00 from lxml import etree import re tree etree.HTML(html) # 假设价格元素的CSS类为 .J-price 实际需要通过分析页面确定 price_elements tree.xpath(//*[contains(class, J-price)]//text()) if not price_elements: # 尝试其他常见选择器 price_elements tree.xpath(//*[contains(class, price)]//text()) encrypted_price_str .join(price_elements).strip() # 清理可能的空白和换行 encrypted_price_str re.sub(r\s, , encrypted_price_str) print(f提取到的加密价格文本: {encrypted_price_str}) decrypted_price decrypt_text(encrypted_price_str, cmap, mapping_dict) print(f解密后的价格: {decrypted_price}) return decrypted_price # 主流程整合示例 def main(): # 1. 获取页面和字体 html, font_io, _ fetch_page_and_font(target_url, headers) # 2. 解析字体 font_obj, cmap, glyph_names parse_font(font_io) # 3. 建立映射这里演示基准法假设我们通过分析页面找到了已知参照 # 假设我们知道页面中“销量200件”的“200”是明文且其加密字符对应字形为 uniE100, uniE101, uniE102 # 我们需要手动或通过更智能的解析得到这个对应关系。这里简化假设我们已经有了。 # 例如通过人工查看字体工具和页面我们确定了 manual_mapping { uniE100: 2, uniE101: 0, uniE102: 0, # ... 继续添加其他通过同样方式或特征匹配法得到的映射 } # 如果是特征匹配法则需要先构建模板库 # 假设我们已经通过某种方式得到了0-9的字形名列表 digit_glyph_names [uniE100, uniE101, uniE102, uniE103, ...] # 0,1,2,3... template_lib {} for idx, glyph_name in enumerate(digit_glyph_names): features extract_glyph_features(font_obj, glyph_name) template_lib[str(idx)] features # 数字字符作为key # 4. 对于未知字形使用特征匹配法补充映射 full_mapping manual_mapping.copy() # 从基准映射开始 for glyph_name in glyph_names: if glyph_name.startswith(uni) and glyph_name not in full_mapping: features extract_glyph_features(font_obj, glyph_name) matched_digit match_glyph_to_digit(features, template_lib) if matched_digit: full_mapping[glyph_name] matched_digit print(f最终建立的映射字典部分: {list(full_mapping.items())[:10]}) # 5. 解密目标数据 price extract_and_decrypt_price(html, font_obj, cmap, full_mapping) print(f商品价格: {price}) if __name__ __main__: main()5. 常见问题与排查技巧实录在实际操作中你会遇到各种各样的问题。下面记录了一些典型场景和解决思路。5.1 字体文件获取失败或格式异常问题正则匹配不到字体URL或者下载的字体文件fontTools无法打开。排查检查Network请求确保在开发者工具的Network面板中能看到字体请求过滤font类型。如果看不到可能是字体通过JS动态加载或使用了data:URI内联。内联字体在HTML或CSS中搜索data:font/woff2;base64或data:application/font-woff;base64。如果找到这是Base64编码的字体数据需要用base64.b64decode()解码后再传递给TTFont。字体格式确认文件魔数。.woff文件头是wOFF.woff2是wOF2。如果文件损坏可能下载不完整需要检查请求头或会话状态如Cookie。5.2 CMAP映射为空或找不到对应字形问题cmap表为空或者网页中的加密字符如\ue001在cmap中找不到对应字形名。排查字符编码确认确保你从HTML中提取的“加密字符”是正确的。有时它可能被HTML实体转义如#xe001;你需要先将其转换为实际的Unicode字符chr(0xe001)。检查CMAP子表font[cmap].tables可能包含多个子表对应不同的平台和编码。确保你遍历的是正确的子表通常是format4或format12platformID3或0platEncID1或10对应Unicode。可以打印所有子表信息来检查。字体子集可能字体只包含了页面用到的部分字符而你的cmap查找方式有误。尝试直接遍历font[cmap].tables的所有cmap属性。5.3 映射建立不准确或解密结果乱码问题成功建立了映射但解密出来的数字是错的比如价格“129”解成了“753”。排查基准映射错误检查你用作基准的“已知明文”是否真的未被加密或者其顺序是否与加密字符严格对应。页面DOM结构可能很复杂文本节点可能被拆分。特征匹配失效如果你使用特征匹配法可能是特征提取或距离度量不够鲁棒。尝试增加特征维度除了轮廓点加入字形宽度、高度、重心、宽高比等。使用更稳定的特征计算字形在若干等分网格内的笔画存在性将Bounding Box划分为NxN的网格判断每个网格内是否有轮廓点形成一个二进制特征向量。人工校验模板库首次运行时务必人工核对自动或半自动生成的0-9模板是否正确。可以先将所有字形保存为图片查看。字体动态变化最棘手的情况。拼多多可能频繁更换字体文件或字形坐标导致之前生成的映射失效。解决方案会话级缓存在一次爬虫会话中对同一域名只获取并解析一次字体后续沿用映射。但不同商品页可能用不同字体需要根据字体URL或文件哈希进行缓存。映射自验证设计一种校验机制。例如解密后的价格应该符合某些规则如包含小数点、是合理范围内的数字。如果解密结果明显不符合规则如出现字母则触发重新获取字体和建立映射的流程。降级策略如果自动破解连续失败可以记录日志并报警转为人工介入分析新字体。5.4 性能优化与工程化考虑当需要大规模、持续爬取时效率很重要。字体缓存将字体文件的URL及其对应的映射字典缓存起来例如使用redis或本地文件。键可以是字体文件的MD5哈希值或URL本身。下次遇到相同字体时直接使用缓存映射避免重复解析和特征计算。并发请求如果爬取多个页面注意字体文件请求也可能被限速。合理安排并发并考虑复用HTTP连接requests.Session。错误重试与降级网络请求、字体解析、映射匹配都可能失败。代码中应有完善的try-except和重试逻辑。对于非关键数据可以考虑降级如记录原始加密文本稍后人工处理。日志记录详细记录每一步的操作特别是字体URL、映射关系、解密前后的文本。这对于调试和监控系统状态至关重要。最后的小技巧有时候最简单的办法可能最有效。如果目标网站的反爬不是极端复杂可以尝试直接使用Selenium或Playwright等浏览器自动化工具让真实的浏览器引擎去渲染页面然后直接从DOM中获取渲染后的文本。这相当于把解密工作交给了浏览器本身省去了逆向字体的过程。当然这种方案资源消耗大、速度慢适用于小规模或对稳定性要求极高的场景。选择哪种方案需要在开发成本、维护成本和运行效率之间做权衡。破解字体加密是一个典型的“道高一尺魔高一丈”的过程。核心在于深刻理解其原理并构建一个灵活、可适应变化的自动化流程。本文提供的指南和代码框架是一个坚实的起点但面对具体的目标网站仍需你耐心分析、不断调试和优化。记住逆向工程没有银弹清晰的思路和细致的观察往往比复杂的代码更重要。