反爬加密字体替换机制解析

加密字体替换是网站反爬虫的常用技术之一,其核心是通过自定义字体文件对关键数据(如数字、文字)进行动态渲染,使源码中显示的字符与用户实际看到的内容不一致。下面从技术原理、实现类型和破解方法三个方向展开分析,并提供丰富的代码示例。

一、技术原理与核心逻辑

(一)字体映射机制

网站通过自定义字体文件(如.woff、.ttf)定义字符的渲染规则。

# 示例:字体文件定义的映射关系
unicode_map = {
    '\ue000': '0',
    '\ue001': '1',
    '\ue002': '2',
    # ...
    '\ue009': '9'
}
  • 源码中的 &#x518b(Unicode码)可能被映射为实际显示的字符“地”。

  • 数字“1”在字体文件中可能被编码为特殊符号(如“”),需通过字形匹配还原。

(二)动态更新策略

部分网站(如58同城、起点中文网)每次请求会生成新的字体文件,导致映射关系动态变化。

# 模拟动态字体文件获取
import requests
from io import BytesIO
from fontTools.ttLib import TTFont

def download_font(url):
    """下载字体文件"""
    response = requests.get(url)
    return TTFont(BytesIO(response.content))

# 使用示例:在每次请求时重新下载并解析字体文件
font_url = "https://example.com/fonts/dynamic_font.woff"
font = download_font(font_url)
cmap = font.getBestCmap()
  • 同一数字“5”在不同会话中可能对应不同的Unicode码。

(三)反OCR设计

通过扭曲字形、添加噪点或粘连字符,增加OCR识别难度。

# 示例:扭曲字形的特征
from PIL import Image, ImageDraw

def draw_distorted_digit(digit, distortion_factor=0.5):
    """绘制扭曲的数字图像"""
    image = Image.new('L', (50, 50))
    draw = ImageDraw.Draw(image)
    # 根据数字生成基本形状
    if digit == '3':
        draw.polygon([(10,10), (40,10), (40,40), (10,40)], fill=255)
        # 添加扭曲效果
        draw.arc((15,15,35,35), start=0, end=270, fill=0)
    elif digit == '8':
        draw.ellipse((10,10,40,40), fill=255)
        draw.ellipse((15,15,35,35), fill=0)
    # 更多数字扭曲逻辑...
    return image

# 生成扭曲的“3”和“8”用于演示
image_3 = draw_distorted_digit('3')
image_8 = draw_distorted_digit('8')
  • 字符“3”和“8”可能设计为相似轮廓,干扰传统OCR工具。

二、加密字体替换的常见类型

(一)动态字体加密

特点:每次访问生成不同的字体文件,映射关系不固定。

案例:律师执业诚信平台(credit.acla.org.cn)的字体文件会动态更新,需实时解析。

# 动态字体解析流程
from selenium import webdriver
import time

def get_dynamic_font_mapping():
    """获取动态字体映射"""
    driver = webdriver.Chrome()
    driver.get("https://credit.acla.org.cn")
    time.sleep(2)  # 等待字体加载
    
    # 提取字体文件URL(通常在页面的CSS中)
    font_urls = []
    for style in driver.find_elements_by_tag_name('style'):
        if 'font-face' in style.get_attribute('innerText'):
            # 提取字体文件URL的正则表达式
            import re
            matches = re.findall(r'url\((.*?)\)', style.get_attribute('innerText'))
            for url in matches:
                if url.endswith(('.woff', '.ttf')):
                    font_urls.append(url)
    
    # 下载并解析字体文件
    mappings = {}
    for font_url in font_urls:
        font = download_font(font_url)
        cmap = font.getBestCmap()
        # 构建映射关系
        for unicode_char, glyph_name in cmap.items():
            # 假设数字映射到特定前缀的字形名称
            if glyph_name.startswith('num_'):
                actual_digit = glyph_name.split('_')[1]
                mappings[chr(unicode_char)] = actual_digit
    
    driver.quit()
    return mappings

# 使用示例
dynamic_mapping = get_dynamic_font_mapping()
print(dynamic_mapping)

(二)静态字体加密

特点:字体文件固定,但字符与实际显示内容不一致。

案例:黄页网站(huangye88.com)使用静态加密,通过解析.woff文件可建立永久映射表。

# 静态字体解析
from fontTools.ttLib import TTFont

def build_static_mapping(font_path):
    """构建静态字体映射表"""
    font = TTFont(font_path)
    cmap = font.getBestCmap()
    mapping = {}
    
    # 假设数字映射到特定前缀的字形名称
    for unicode_char, glyph_name in cmap.items():
        if glyph_name.startswith('num_'):
            actual_digit = glyph_name.split('_')[1]
            mapping[chr(unicode_char)] = actual_digit
    
    return mapping

# 使用示例
static_mapping = build_static_mapping('huangye88_static.woff')
print(static_mapping)

(三)Base64内嵌字体

特点:字体文件以Base64编码形式嵌入CSS或JavaScript中。

破解步骤:提取Base64字符串 → 解码为二进制文件 → 解析字体映射关系。

# 提取并解析Base64内嵌字体
import base64
from io import BytesIO

def extract_base64_font(css_content):
    """从CSS中提取Base64字体"""
    import re
    matches = re.findall(r'data:font/woff;base64,(.*?)\'\)', css_content)
    for match in matches:
        font_data = base64.b64decode(match)
        with open('extracted_font.woff', 'wb') as f:
            f.write(font_data)
        return 'extracted_font.woff'
    return None

# 使用示例:从CSS内容中提取字体文件
css_content = """
@font-face {
    font-family: 'embedded_font';
    src: url('data:font/woff;base64,...');
}
"""
font_path = extract_base64_font(css_content)
if font_path:
    static_mapping = build_static_mapping(font_path)
    print(static_mapping)

三、破解方法与实战代码示例

(一)字体文件解析与映射

工具:使用Python的fontTools库解析字体文件。

from fontTools.ttLib import TTFont

# 解析字体文件并提取字符映射
font = TTFont("encrypted.woff")
cmap = font.getBestCmap()  # 获取Unicode与字形名称的映射

# 假设数字映射到特定前缀的字形名称
relation_table = {
    'zero': '0',
    'one': '1',
    'two': '2',
    'three': '3',
    'four': '4',
    'five': '5',
    'six': '6',
    'seven': '7',
    'eight': '8',
    'nine': '9'
}

# 构建最终映射表
final_mapping = {}
for unicode_char, glyph_name in cmap.items():
    if glyph_name in relation_table:
        final_mapping[chr(unicode_char)] = relation_table[glyph_name]

print(final_mapping)

(二)动态更新处理

若字体文件频繁变化,需在每次请求时重新下载并解析。

# 动态字体更新处理
import requests
from fontTools.ttLib import TTFont
from io import BytesIO

def get_current_font_mapping(font_url):
    """获取当前字体映射"""
    response = requests.get(font_url)
    font = TTFont(BytesIO(response.content))
    cmap = font.getBestCmap()
    
    final_mapping = {}
    for unicode_char, glyph_name in cmap.items():
        if glyph_name in relation_table:
            final_mapping[chr(unicode_char)] = relation_table[glyph_name]
    
    return final_mapping

# 使用示例:在每次请求前更新映射
font_url = "https://example.com/fonts/dynamic_font.woff"
current_mapping = get_current_font_mapping(font_url)

(三)字形特征匹配

步骤:提取加密字符的坐标点(contour节点中的x、y、on属性),与标准字体库对比相似度。

# 字形特征提取与匹配
from fontTools.pens.recordingPen import RecordingPen

def get_glyph_contours(font, unicode_char):
    """获取字形的轮廓数据"""
    glyph_name = font.getBestCmap().get(ord(unicode_char))
    if not glyph_name:
        return None
    
    pen = RecordingPen()
    font[glyph_name].draw(pen)
    return pen.value

def calculate_similarity(encrypted_contours, standard_contours):
    """计算轮廓相似度(简化版)"""
    if len(encrypted_contours) != len(standard_contours):
        return float('inf')
    
    total_diff = 0
    for enc_op, std_op in zip(encrypted_contours, standard_contours):
        if enc_op[0] != std_op[0]:  # 操作类型不同
            total_diff += 1
        else:
            # 比较坐标点
            enc_args = enc_op[1]
            std_args = std_op[1]
            for enc_coord, std_coord in zip(enc_args, std_args):
                total_diff += abs(enc_coord[0] - std_coord[0]) + abs(enc_coord[1] - std_coord[1])
    
    return total_diff

# 使用示例:匹配加密字符与标准字符
standard_font = TTFont("standard_font.woff")
encrypted_font = TTFont("encrypted_font.woff")

standard_contours = {}
for char in '0123456789':
    standard_contours[char] = get_glyph_contours(standard_font, char)

encrypted_char = '\ue005'
encrypted_contours = get_glyph_contours(encrypted_font, encrypted_char)

matched_char = match_glyph(encrypted_contours, standard_contours)
print(f"匹配结果: {encrypted_char} → {matched_char}")

(四)自动化替换与数据还原

流程:解析网页源码 → 提取加密字符 → 替换为真实值。

# 自动化数据还原
from bs4 import BeautifulSoup

def decrypt_text(encrypted_text, mapping):
    """解密文本"""
    decrypted = []
    for char in encrypted_text:
        decrypted.append(mapping.get(char, char))  # 保留未映射的字符
    return ''.join(decrypted)

# 使用示例:解析网页并还原加密数据
encrypted_html = """
<div class="price"></div>
<div class="address">北京市朝阳区</div>
"""

soup = BeautifulSoup(encrypted_html, 'html.parser')
price_element = soup.find('div', class_='price')
address_element = soup.find('div', class_='address')

# 假设我们已经构建了映射表
current_mapping = {
    '\ue004': '5',
    '\ue005': '3',
    '\ue006': '8'
}

decrypted_price = decrypt_text(price_element.text, current_mapping)
decrypted_address = decrypt_text(address_element.text, current_mapping)

print(f"加密价格: {price_element.text} → 解密后: {decrypted_price}")
print(f"加密地址: {address_element.text} → 解密后: {decrypted_address}")

四、进阶策略与实战优化

(一)多线程实时更新

在面对频繁更新的字体文件时,可以使用多线程技术实时监控和更新映射表。

import threading
from queue import Queue
import time

class FontUpdaterThread(threading.Thread):
    def __init__(self, font_url, update_interval=60):
        threading.Thread.__init__(self)
        self.font_url = font_url
        self.update_interval = update_interval
        self.mapping = {}
        self._stop_event = threading.Event()

    def run(self):
        while not self._stop_event.is_set():
            try:
                self.mapping = get_current_font_mapping(self.font_url)
                print("字体映射已更新")
            except Exception as e:
                print(f"更新字体映射失败: {e}")
            finally:
                time.sleep(self.update_interval)

    def stop(self):
        self._stop_event.set()

# 使用示例:启动字体更新线程
font_updater = FontUpdaterThread("https://example.com/fonts/dynamic_font.woff", update_interval=30)
font_updater.start()

# 在主程序中使用当前映射
current_mapping = font_updater.mapping

# 停止更新线程
font_updater.stop()
font_updater.join()

(二)结合机器学习的字形匹配

对于复杂变形的字形,可以使用机器学习模型进行特征匹配。

import numpy as np
from sklearn.neighbors import NearestNeighbors

# 示例:使用NearestNeighbors进行字形匹配
def vectorize_contours(contours):
    """将轮廓数据转换为特征向量"""
    features = []
    for op in contours:
        op_type = op[0]
        args = op[1]
        features.append(len(args))  # 操作参数数量
        for coord in args:
            features.extend([coord[0], coord[1]])  # 坐标点
    return np.array(features)

# 准备训练数据
X = []
y = []
for char in '0123456789':
    contours = get_glyph_contours(standard_font, char)
    X.append(vectorize_contours(contours))
    y.append(char)

# 训练模型
model = NearestNeighbors(n_neighbors=1)
model.fit(X)

# 使用模型进行匹配
encrypted_char = '\ue005'
encrypted_contours = get_glyph_contours(encrypted_font, encrypted_char)
X_encrypted = [vectorize_contours(encrypted_contours)]

distances, indices = model.kneighbors(X_encrypted)
matched_char = y[indices[0][0]]
print(f"机器学习匹配结果: {encrypted_char} → {matched_char}")

(三)处理字体加密的完整流程

以下是一个处理字体加密的完整示例,模拟从网页请求到数据解密的全过程。

# 完整流程示例:处理字体加密
from selenium import webdriver
from selenium.webdriver.common.by import By
from fontTools.ttLib import TTFont
import requests
from io import BytesIO
import time
from bs4 import BeautifulSoup

def get_font_mapping_from_page(driver):
    """从页面获取字体映射"""
    # 提取字体文件URL
    font_urls = []
    style_elements = driver.find_elements(By.TAG_NAME, 'style')
    for style in style_elements:
        style_text = style.get_attribute('innerText')
        if 'font-face' in style_text and '.woff' in style_text:
            import re
            matches = re.findall(r'src: url\((.*?)\)', style_text)
            for url in matches:
                if url.endswith('.woff'):
                    font_urls.append(url)
    
    # 下载并解析字体文件
    mappings = {}
    for font_url in font_urls:
        try:
            response = requests.get(font_url)
            font = TTFont(BytesIO(response.content))
            cmap = font.getBestCmap()
            
            for unicode_char, glyph_name in cmap.items():
                # 假设数字映射到特定前缀的字形名称
                if glyph_name.startswith('num_'):
                    actual_digit = glyph_name.split('_')[1]
                    mappings[chr(unicode_char)] = actual_digit
        except Exception as e:
            print(f"处理字体文件失败: {e}")
    
    return mappings

def decrypt_page_data(driver, url):
    """解密页面数据"""
    driver.get(url)
    time.sleep(2)  # 等待页面加载
    
    # 获取字体映射
    font_mapping = get_font_mapping_from_page(driver)
    if not font_mapping:
        print("未找到字体映射")
        return
    
    # 提取页面内容
    page_source = driver.page_source
    soup = BeautifulSoup(page_source, 'html.parser')
    
    # 解密价格信息
    price_elements = soup.find_all('div', class_='price')
    for element in price_elements:
        encrypted_text = element.text
        decrypted_text = ''.join([font_mapping.get(c, c) for c in encrypted_text])
        print(f"加密价格: {encrypted_text} → 解密后: {decrypted_text}")
    
    # 更多数据解密逻辑...

# 使用示例:解密58同城房产信息
driver = webdriver.Chrome()
try:
    decrypt_page_data(driver, "https://house.58.com/")
finally:
    driver.quit()

五、总结

加密字体替换是网站常用的反爬虫技术,其核心是通过自定义字体文件改变字符的显示方式。针对不同类型的加密字体,我们可以采用以下策略:

  • 动态字体加密:实时下载并解析字体文件,构建映射表。

  • 静态字体加密:一次性解析字体文件,建立永久映射表。

  • Base64内嵌字体:提取Base64字符串,解码后解析字体文件。

在实际操作中,结合字体文件解析、字形特征匹配和自动化替换技术,可以有效应对加密字体替换带来的挑战。同时,建议使用多线程和机器学习等技术优化处理流程,提高识别准确率和效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值