尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实战:破解字体反爬虫,从乱码到可读文本的完整方案

Python实战:破解字体反爬虫,从乱码到可读文本的完整方案 1. 项目缘起当小说网站的文字变成“乱码”最近在尝试抓取某个小说网站的内容时遇到了一个挺有意思的“小麻烦”。我像往常一样用requests库发起请求用BeautifulSoup解析HTML本以为能轻松拿到干净的文本。结果一看返回的HTML小说正文部分显示的并不是“第一章 重生归来”这样的文字而是一堆类似“”的奇怪字符或者是一些看似正常但完全对不上的“乱码”。这显然不是简单的编码问题。检查网页源代码发现这些“乱码”字符在HTML里是正常的Unicode字符比如但浏览器里显示出来的却是正确的汉字。稍微研究一下就发现页面的head里引入了一个或多个.woff或.ttf格式的字体文件同时正文中的文字被包裹在带有特定class的span标签里。这就是典型的字体反爬手段。网站开发者自定义了一套字体文件将标准的Unicode字符映射到了他们自己设计的字形上。在HTML源码里你看到的是Unicode字符A比如但通过他们自定义的字体渲染后在浏览器里显示出来的却是字形B比如“第”字。爬虫如果直接提取Unicode字符得到的就是一堆“天书”而人眼通过浏览器借助自定义字体看到的是正确的文本。这个反爬策略的核心在于信息的不对称服务器同时提供了“加密”的文本Unicode码和“解密”的钥匙自定义字体浏览器能自动完成解密渲染而常规爬虫只会傻傻地拿走“加密”文本。今天我们就来彻底拆解这个“锁”并打造一把通用的“钥匙”。2. 字体反爬的原理与核心战场CSS与字体文件要破解字体反爬首先得明白它到底是怎么工作的。这涉及到前端三剑客中的两位CSS和字体Font而JavaScriptJS有时也会参与其中负责动态加载或混淆。2.1 自定义字体的运作机制在CSS中我们使用font-face规则来定义自定义字体。这是整个反爬体系的声明式起点。font-face { font-family: MySecretFont; /* 给这个自定义字体起个名字 */ src: url(path/to/secret-font.woff) format(woff); /* 字体文件的来源和格式 */ }然后在需要“加密”的文字元素上应用这个字体span classtext-content stylefont-family: MySecretFont, sans-serif;/span浏览器在解析到这段HTML和CSS后会执行以下操作下载secret-font.woff字体文件。识别到span内的文本内容为Unicode字符UE000,UE001,UE002这里是UE000的HTML十进制表示。在secret-font.woff字体文件中查找这些Unicode码点对应的字形glyph信息。将找到的字形可能是“第”、“一”、“章”的轮廓渲染到屏幕上。对于爬虫来说如果不处理字体第2步就是终点我们拿到的是[\ue000, \ue001, \ue002]。对于浏览器和人类读者经过第3、4步看到的是“第一章”。2.2 字体文件格式与内容解析常见的网络字体格式有WOFF(Web Open Font Format)、WOFF2、TTF(TrueType Font)、EOT等。它们本质上都是容器包含了字符映射表和字形轮廓数据。CMAP表Character to Glyph Mapping这是最关键的一张表。它定义了Unicode码点Code Point到字形索引Glyph ID的映射关系。在反爬场景中网站会修改这个映射表。例如原本UE000可能映射到字形ID 100对应汉字“第”但网站把它改成了映射到字形ID 200对应汉字“一”或者映射到一个完全无意义的图形。GLYF表或其他轮廓数据存储了每个字形ID对应的实际绘制轮廓信息点、线、曲线。网站有时也会修改这些轮廓但成本较高更常见的是修改CMAP表。我们的核心任务就是解析出目标字体文件中当前有效的Unicode码点 - 字形ID - 实际代表汉字的完整映射关系。2.3 动态化与混淆JS的介入为了增加难度网站可能会升级策略动态字体每次访问页面服务器生成的字体文件URL可能不同通过添加时间戳或哈希值或者字体文件内容CMAP映射本身定期变化。这需要爬虫具备实时解析字体的能力而不是使用一次性的静态映射。字体分割将一套完整的映射拆分成多个字体文件页面中不同的部分使用不同的字体。这需要爬虫能关联不同的class和对应的字体文件。JS动态加载字体文件不是写在CSS里而是通过JavaScript动态创建link或font-face规则并插入到DOM中。爬虫需要能够执行JS或模拟这一行为才能获取到字体URL。Base64内嵌字体文件不再是一个独立的URL而是以Base64编码的形式直接内嵌在CSS或JS代码中。这要求爬虫能从代码中提取并解码这段数据。这些进阶手段将反爬的战场从单纯的CSS/字体分析部分转移到了JS逆向和动态请求处理上。但无论前端如何变化最终都要落脚到对字体文件本身的解析上。3. 实战破解从获取字体到建立映射理论清晰后我们进入实战环节。我们将使用Python作为主要工具因为它拥有强大的网页抓取和字体处理库。整个流程可以概括为抓取页面 - 提取字体 - 解析字体 - 构建映射 - 还原文本。3.1 环境准备与工具选型首先确保你的Python环境已经安装了以下库pip install requests beautifulsoup4 fonttoolsrequests用于发起网络请求获取HTML和字体文件。beautifulsoup4用于解析HTML定位加密文本和字体链接。fonttools这是一个功能极其强大的字体处理库我们可以用它来解析TTF/WOFF等字体文件读取其内部的CMAP等表数据。这是本次破解的核心武器。为什么选择fonttools而不是其他因为它专业、底层能提供最直接的字体表数据访问。其他一些教程可能用pyttx或TTXfonttools的一部分将字体转成XML分析但直接使用fonttools的API更灵活适合编程处理。3.2 步骤一获取页面与初步分析假设目标网址是https://www.example-novel.com/chapter/1。import requests from bs4 import BeautifulSoup url https://www.example-novel.com/chapter/1 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } response requests.get(url, headersheaders) html response.content soup BeautifulSoup(html, html.parser)接下来我们需要做两件关键事找到加密文本的容器通常小说正文会放在一个具有特定id或class的div里里面的span标签包含着乱码。# 假设正文在 div idchapter-content content_div soup.find(div, idchapter-content) # 或者通过class寻找注意可能有多个class用css选择器更稳妥 # content_div soup.select_one(.chapter-text .content)找到自定义字体在head的style标签内或者外链的CSS文件中查找font-face规则。# 查找所有style标签 for style in soup.find_all(style): if font-face in style.text: # 这里可以简单用正则提取字体URL import re font_urls re.findall(rurl\([\]?(.*?\.woff2?)[\]?\), style.text) print(f找到内联字体定义: {font_urls}) # 查找所有link标签获取外链CSS css_links [] for link in soup.find_all(link, relstylesheet): if link.get(href): css_links.append(link[href]) # 需要进一步下载并解析这些CSS文件寻找font-face注意字体URL可能是相对路径如/fonts/secret.woff需要根据当前页面URL将其补全为绝对路径。urllib.parse.urljoin(base_url, font_url)是处理这个的好帮手。3.3 步骤二下载并解析字体文件获取到字体文件的URL后下载它并用fonttools打开。from fontTools.ttLib import TTFont import io # 假设我们找到了一个字体URL font_url https://www.example-novel.com/static/fonts/secret.woff font_data requests.get(font_url, headersheaders).content # 将二进制数据加载为TTFont对象 # 使用 io.BytesIO 将 bytes 转换为文件类对象 font TTFont(io.BytesIO(font_data))现在font对象包含了字体所有的信息。我们最关心的是cmap表。# 获取cmap表 cmap font.getBestCmap() # 这是一个字典 print(cmap)cmap字典的格式通常是{unicode_code_point: glyph_id}。例如输出可能是{60928: 1, 60929: 2, 60930: 3, ...}。这里的键60928就是Unicode码点十进制对应HTML中的#60928;十进制或十六进制\ue000。值1、2、3是字形ID。但问题来了字形ID1对应哪个汉字我们不知道。字体文件只存储了字形轮廓没有存储这个轮廓代表什么字符的语义信息。3.4 步骤三建立映射关系——寻找“密码本”这是破解字体反爬最核心、也最具技巧性的一步。我们需要找到一个参照物来建立字形ID - 真实汉字的映射。常见方法有方法A与已知字体对比推荐这是最可靠的方法。观察发现很多小说网站使用的自定义字体其字形轮廓本身是标准的汉字形状只是被重新映射了Unicode码点。我们可以找一个标准的、公开的字体如“宋体”、“微软雅黑”的某个常用版本作为基准。从目标字体中提取每个字形ID的轮廓数据或特征例如字形名称、轮廓点坐标的哈希值。从基准字体中提取常用汉字如3500个常用字的字形特征。进行匹配。如果目标字体的字形ID 100的轮廓特征与基准字体中“第”字的轮廓特征高度相似或完全一致我们就可以认为glyph_id 100 “第”。fonttools提供了比较字形的方法但完全自动化的高精度匹配比较复杂。一个更实用的技巧是网站为了省事经常只是简单地对Unicode码点做了偏移或替换而字形顺序本身是标准的。也就是说字形ID 1可能对应“一”ID 2对应“丁”ID 3对应“七”……这个顺序是固定的通常是按汉字编码顺序如GB2312。如果我们能确定目标字体中任意一个字形ID对应的真实汉字就能推算出整个顺序。如何确定第一个字有时可以通过页面上的固定不变的非加密文字来推断。例如章节标题“第一章”可能是加密的但网站标题、导航栏的“首页”、“书架”可能没有加密。如果这些非加密文字也使用了相同的字体class但它们显示正常说明它们的Unicode码点没有被重映射或者映射到了正确的位置。通过对比这些文字的Unicode码点和它们在标准字体中的字形ID可能找到突破口。方法BOCR识别备用方案如果对比法失效可以考虑“暴力”破解用程序将字体中的每个字形渲染成图片然后用OCR光学字符识别技术识别图片中的汉字。使用fontTools.pens.boundsPen计算字形边界然后使用reportLab或PILPillow库将字形绘制到图像上。使用OCR引擎如pytesseract需要安装Tesseract-OCR识别图像。 这种方法准确率受字体复杂度、OCR引擎训练程度影响较大且速度较慢通常作为最后的手段。方法C人工干预与机器学习对于长期抓取且字体变化不频繁的网站可以建立一个小型映射库。首次遇到新字体时通过半自动方式如人工识别少量关键字形建立部分映射然后利用字形轮廓的向量特征训练一个简单的分类模型来预测其他字形的汉字。这需要一定的数据积累和ML知识。在实际操作中对于大多数小说网站方法A与已知字体对比结合一些启发式规则如观察字形ID顺序往往是最有效的。我们可以利用fonttools的ttx工具将字体导出为XML直观地查看字形名称和轮廓引用有时能发现规律。3.5 步骤四应用映射还原文本一旦我们成功构建了映射字典比如mapping {60928: ‘第‘ 60929: ‘一‘ 60930: ‘章‘ …}还原文本就变得非常简单# 假设我们从HTML中提取到了加密文本的Unicode码点列表 # 例如从 span/span 提取出 code_points [‘\ue000‘ ‘\ue001‘ ‘\ue002‘] # 或者直接拿到字符串 encrypted_text ‘‘ encrypted_text ‘‘ decoded_text ‘’ for char in encrypted_text: # 获取字符的Unicode码点十进制 code_point ord(char) # 从映射字典中查找对应的真实汉字如果找不到则保留原字符 decoded_text mapping.get(code_point char) print(decoded_text) # 输出第一章对于整个章节内容我们需要先提取所有包含加密字符的HTML片段然后应用上述替换逻辑。4. 深入应对动态字体与高级混淆策略如果网站采用了更高级的策略我们的爬虫也需要升级。4.1 处理动态字体URL或内容如果字体URL每次都会变我们需要在每次抓取页面时重新执行步骤一和步骤二动态获取最新的字体并解析映射。这意味着不能缓存映射关系或者缓存的映射需要设置较短的过期时间。如果字体文件内容CMAP定期变化但字形轮廓顺序不变我们可以尝试只解析一次字形顺序然后每次只获取字体文件并读取其CMAP表将新的CMAP映射应用到已知的字形顺序上。这需要对字体文件格式有更深的理解以分离不变的部分轮廓和变化的部分映射。4.2 处理多字体文件与CSS类关联页面可能使用多个字体文件不同的class使用不同的字体。style font-face { font-family: ‘FontA‘; src: url(‘font_a.woff‘); } font-face { font-family: ‘FontB‘; src: url(‘font_b.woff‘); } .secret-a { font-family: ‘FontA‘; } .secret-b { font-family: ‘FontB‘; } /style pspan class“secret-a“/span和span class“secret-b“/span/p我们的爬虫需要建立class到font-family名的映射通过解析CSS。建立font-family名到具体字体文件URL及解析后映射字典的映射。在替换文本时根据每个span标签的class选择正确的映射字典进行替换。4.3 处理Base64内嵌字体当字体以Base64形式内嵌时我们需要从CSS或JS代码中将其提取并解码。font-face { font-family: ‘MyFont‘; src: url(‘data:font/woff2;base64,d09GRgABAAAAA...‘) format(‘woff2‘); }提取和解码的Python代码import re import base64 css_content ‘...‘ # 获取到的CSS字符串 # 使用正则匹配Base64数据 pattern r‘url\(data:font/\w;base64,([a-zA-Z0-9/])\)‘ match re.search(pattern css_content) if match: base64_data match.group(1) # 解码Base64得到字体二进制数据 font_data base64.b64decode(base64_data) # 后续解析步骤与之前相同 font TTFont(io.BytesIO(font_data))4.4 处理JavaScript动态加载如果字体是由JS动态加载的情况就复杂得多。你可能需要分析JS代码找到负责加载字体的JS函数看它是如何生成字体URL或Base64数据的。可能涉及对参数的计算或对某个API的调用。模拟JS执行使用如execjs、PyExecJS库或者更强大的Selenium、Playwright等浏览器自动化工具来执行这段JS代码从而获得字体数据。Selenium等工具能完整模拟浏览器环境包括执行所有JS和下载所有资源但代价是速度慢、资源消耗大。直接请求API如果JS是通过调用某个API接口来获取字体信息你可以尝试直接模拟这个API请求。这需要抓包分析使用浏览器开发者工具的Network面板。5. 经验总结与避坑指南在多次实战中我积累了一些宝贵的经验和容易踩的坑1. 字体映射的稳定性是关键不要假设一次破解的映射永远有效。务必在爬虫中设计验证机制。例如在还原文本后检查是否出现了大量非常用汉字或乱码这可能是映射失效的标志。可以设置一个定时任务定期如每天用已知的测试页面验证映射的正确性。2. 注意编码与码点表示Python中ord(‘‘)得到的是十进制的Unicode码点。在HTML中是十进制表示是十六进制表示。在匹配和替换时要确保码点格式的一致性。我习惯在内部全部使用十进制整数进行处理。3. 字体文件可能有多个cmap表一个字体文件可以包含多个cmap子表用于兼容不同的平台如Unicode Windows Mac。font.getBestCmap()通常会返回最合适的那个但有时可能需要检查font[‘cmap‘].tables来手动选择。如果发现映射不对可以遍历这些表看看。4. 字形轮廓对比的精度问题使用轮廓对比法时要注意不同字体对同一个汉字的描绘可能有细微差别。比较轮廓的哈希值如将轮廓点坐标序列化成字符串后取MD5可能因为精度问题导致匹配失败。更稳健的方法是计算轮廓的某些几何特征如宽高比、重心位置进行模糊匹配或者使用专业的图形匹配算法但这会大大增加复杂度。对于小说网站字形通常比较简单直接精确匹配成功率较高。5. 法律与道德边界在实施任何爬虫之前请务必检查robots.txt尊重网站所有者设置的爬虫协议。控制请求频率添加合理的延时如time.sleep(1)避免对目标服务器造成压力否则可能导致IP被封。明确数据用途仅将数据用于个人学习、研究或符合网站条款的用途。大规模复制并商用网站内容可能涉及侵权。字体反爬是一场围绕“信息表达权”的攻防战。作为爬虫开发者理解其原理不仅是为了“破解”更是为了深入理解Web技术栈如何协同工作。这套从分析到破解的完整思路不仅适用于小说网站对于任何使用自定义字体来保护文本内容的场景如某些电商网站的价格、票务网站的信息等都具有参考价值。核心永远是找到加密规则字体映射并找到或构造出解密密钥字形到汉字的对应关系。
返回列表