本文记录58同城二手车页面字体反爬的完整破解过程,包含底层逻辑解析、项目关键步骤、可直接运行的完整代码及最终输出结果,适配新手学习和实战复用,所有代码可直接复制运行。

一、字体反爬底层逻辑

字体反爬是爬虫中常见的反爬手段,核心原理是:网站将页面中需要隐藏的关键数据(如本文中的二手车价格),用自定义字体文件(.ttf格式)进行加密,页面渲染时通过字体映射显示正常数据,但爬虫直接抓取时,获取到的是加密后的Unicode字符(而非真实数字),从而达到反爬目的。

58同城字体反爬的具体逻辑的:

  1. 网站在页面源码中,通过Base64编码嵌入自定义.ttf字体文件(每次请求可能动态生成,避免固定映射破解);

  2. 页面中的价格数据,用该字体文件中的特殊Unicode字符表示(如对应数字0);

  3. 浏览器加载页面时,会解析Base64编码的字体文件,通过字体映射将Unicode字符渲染为正常数字;

  4. 爬虫直接抓取页面源码,只能获取到加密的Unicode字符,无法直接得到真实价格,需破解字体映射关系。

本次破解采用「实时Base64提取+实时字体生成+ddddocr实时识别」方案,不依赖固定字体映射字典,适配网站字体动态变化的场景,通用性更强。

二、项目关键步骤(全程实战可复现)

本次破解核心是“绕过固定映射,实时破解字体”,共分为5个关键步骤,每一步都对应代码中的核心逻辑,新手可按步骤逐步理解:

步骤1:请求目标页面,获取页面源码

携带浏览器请求头(Headers)和Cookie,模拟正常浏览器请求58同城二手车页面,获取包含加密字体Base64编码的页面源码。

关键注意:请求头需包含User-Agent、Referer,Cookie需保持有效(可从浏览器中复制),否则会被网站识别为爬虫,导致无法获取正常源码。

步骤2:从页面源码中提取字体Base64编码

通过正则表达式,从页面源码中精准提取嵌入的字体Base64编码(仅提取纯Base64字符,过滤多余中文、空格等干扰内容),避免解码失败。

核心正则:匹配Base64编码的特征(仅包含A-Z、a-z、0-9、+、/、=),确保提取到的编码可正常解码为.ttf文件。

步骤3:将Base64编码解码,保存为.ttf字体文件

将提取到的Base64编码进行解码,生成自定义.ttf字体文件,保存到系统临时目录(避免权限不足问题,无需手动创建路径)。

关键处理:解码前先清洗Base64字符串中的空白字符(换行、空格),防止解码时出现UnicodeEncodeError报错。

步骤4:生成字体映射关系(ddddocr实时识别)

解析.ttf字体文件,获取其中的Unicode字符与字形的对应关系,通过PIL生成每个Unicode字符的居中清晰图片,再用ddddocr识别图片中的数字,生成“Unicode字符→真实数字”的实时映射字典。

核心优化(解决识别乱码问题):

  • 图片采用RGB格式(白底黑字),尺寸120×120,字体放大至80号,确保清晰;

  • 文字严格居中(通过textbbox计算文字尺寸,自动计算居中坐标),避免ddddocr识别偏差;

  • 过滤非数字识别结果,仅保留单个数字的识别结果,杜绝“元、万”等乱识别情况。

步骤5:解密价格,输出最终结果

用步骤4生成的字体映射字典,将页面中抓取到的加密价格(Unicode字符)解密为真实数字,保留价格中的小数点、“万”等符号,最终输出二手车名称和对应真实价格。

三、完整可运行代码(复制即用)

代码已修复所有报错(正则匹配、Base64解码、权限、图片识别、小数点报错),无需修改任何配置,复制后安装依赖即可运行。

1. 安装依赖(CMD/终端运行)

pip install requests lxml fonttools pillow ddddocr

2. 完整代码

import base64
import re
import requests
from lxml import etree
from fontTools.ttLib import TTFont
from PIL import Image, ImageDraw, ImageFont
import ddddocr
import os
import io

# 初始化ddddocr(核心识别工具)
ocr = ddddocr.DdddOcr()

# ===================== 配置信息(无需修改,直接使用) =====================
cookies = {
    'f': 'n',
    'commontopbar_new_city_info': '414%7C%E9%95%BF%E6%B2%99%7Ccs',
    'userid360_xml': '419C0DF0CC09883983903B04ED8FD531',
    'time_create': '1776918678169',
    'id58': 'CkwAkmnCEzwM56s4CahkAg==',
    'city': 'cs',
    '58home': 'cs',
    'commontopbar_ipcity': 'haikou%7C%E6%B5%B7%E5%8F%A3%7C0',
    '58tj_uuid': '24cab507-fed9-446c-9f96-b73013b860e8',
    'new_uv': '1',
    'utm_source': '',
    'spm': '',
    'init_refer': '',
    'als': '0',
    'fzq_h': '6f947197f1fbed10d6fad84152303238_1774326678228_eddb97ff788a425888fea9fb603ae302_47901684142303259776624704217772197497',
    'sessionid': '12b250b8-3d50-4644-8211-2a4d1fa54066',
    'fzq_js_usdt_infolist_car': '48e920fbda35dd609d30dc67281bee6b_1774326677408_9',
    'new_session': '0',
    '58ua': '58pc',
    'wmda_uuid': '42e2caa55814d28c27f89228b9734244',
    'wmda_new_uuid': '1',
    'wmda_report_times': '1',
    'wmda_session_id_1732038237441': '1774326677797-919f27f1-d029-4c6b-8d7c-afb31ce43ab6',
    'wmda_visited_projects': '%3B1732038237441',
    'xxzlclientid': '7af6e643-a099-4dde-a0ff-1774326678772',
}

headers = {
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'accept-language': 'zh-CN,zh;q=0.9',
    'cache-control': 'max-age=0',
    'referer': 'https://cs.58.com/',
    'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
}

params = {
    'PGTID': '0d100000-0019-e1c5-7133-0546f9746d09',
    'ClickID': '6',
}

# 安全字体路径(系统临时目录,无权限问题)
FONT_PATH = os.path.join(os.getenv("TEMP"), "font58.ttf")

# ===================== 1. 精准提取纯Base64编码 =====================
def get_font_base64(html):
    # 仅匹配纯Base64字符,避免混入中文/乱码
    pattern = re.compile(r'base64,([A-Za-z0-9+/=]+?)\'', re.S)
    return pattern.findall(html)[0]

# ===================== 2. Base64解码,保存.ttf字体文件 =====================
def save_font(b64_str):
    # 清洗空白字符,确保解码成功
    b64_str = re.sub(r'\s+', '', b64_str)
    font_data = base64.b64decode(b64_str)
    with open(FONT_PATH, 'wb') as f:
        f.write(font_data)

# ===================== 3. 生成字体映射(文字居中+ddddocr实时识别) =====================
def build_font_map():
    font = TTFont(FONT_PATH)
    cmap = font['cmap'].getBestCmap()
    font_map = {}

    for code, _ in cmap.items():
        char = chr(code)
        # 生成120x120清晰图片(白底黑字)
        img_size = 120
        img = Image.new("RGB", (img_size, img_size), "white")
        draw = ImageDraw.Draw(img)
        fnt = ImageFont.truetype(FONT_PATH, 80)

        # 文字严格居中(核心优化,解决识别偏差)
        text_bbox = draw.textbbox((0, 0), char, font=fnt)
        text_w = text_bbox[2] - text_bbox[0]
        text_h = text_bbox[3] - text_bbox[1]
        x = (img_size - text_w) // 2
        y = (img_size - text_h) // 2 - 8  # 垂直微调,确保完全居中

        draw.text((x, y), char, font=fnt, fill="black")

        # 生成PNG二进制流,供ddddocr识别
        buf = io.BytesIO()
        img.save(buf, format="PNG")
        res = ocr.classification(buf.getvalue())

        # 过滤非数字结果,仅保留单个数字
        if res and res.isdigit() and len(res) == 1:
            font_map[char] = res

    return font_map

# ===================== 4. 解密价格,保留符号 =====================
def decode_price(encrypted_str, font_map):
    result = []
    for c in encrypted_str:
        # 加密字符用映射替换,小数点、万等符号直接保留
        result.append(font_map.get(c, c))
    return ''.join(result)

# ===================== 主程序(执行入口) =====================
if __name__ == '__main__':
    # 1. 请求目标页面
    response = requests.get('https://cs.58.com/ershouche/', params=params, cookies=cookies, headers=headers)
    
    # 2. 提取Base64 → 保存.ttf字体
    b64_data = get_font_base64(response.text)
    save_font(b64_data)

    # 3. 构建实时字体映射
    font_map = build_font_map()

    # 4. 解析页面,提取并解密价格
    html = etree.HTML(response.text)
    li_list = html.xpath('//li[@class="info"]')

    # 5. 输出最终结果
    print("58同城二手车字体反爬破解结果:")
    print("="*100)
    for li in li_list:
        name = li.xpath('.//span[@class="info_link"]/text()')[0].strip()
        encrypted_price = li.xpath('.//b[@class="info_price fontSecret"]/text()')[0]
        real_price = decode_price(encrypted_price, font_map)
        print(f'名称:{name} | 真实价:{real_price}万')

四、代码运行结果

运行代码后,控制台将输出清晰的二手车名称和对应真实价格,无乱码、无报错,示例结果如下(与页面真实价格完全一致):

欢迎使用ddddocr,本项目专注带动行业内卷,个人博客:wenanzhe.com
训练数据支持来源于:http://146.56.204.113:19199/preview
爬虫框架feapder可快速一键接入,快速开启爬虫之旅:https://github.com/Boris-code/feapder
谷歌reCaptcha验证码 / hCaptcha验证码 / funCaptcha验证码商业级识别接口:https://yescaptcha.com/i/NSwk7i
58同城二手车字体反爬破解结果:
====================================================================================================
名称:五菱汽车 五菱宏光V 2022款 1.5L劲享版液压助力LAR | 真实价:3.08万
名称:金杯 新海狮X30L 2016款 1.3L商务版DLCG12 | 真实价:1.18万
名称:吉利汽车 星瑞 2021款 2.0TD 豪华型 | 真实价:4.08万
名称:荣威 350 2014款 1.5L 自动迅悦版 | 真实价:1.38万
名称:依维柯 欧胜 2021款 3.0T 自动运瑞短轴高顶手动门 | 真实价:23.8万
名称:保时捷 Panamera新能源 2021款 Panamera 4 EHybrid Sport Turismo 2.9T | 真实价:72.68万
名称:金杯 新海狮X30L 2019款 1.5L财富版国VI SWC15M | 真实价:1.68万
名称:北汽幻速 S3 2014款 1.5L 豪华型 国IV | 真实价:1.18万
名称:江汽集团 瑞风S3 2016款 1.5L CVT豪华智能尊享版 | 真实价:1.68万
名称:奔驰 GLC 2022款 改款二 GLC 260 L 4MATIC 动感型 | 真实价:22.98万
名称:奔驰 E级 2021款 E 300 L 运动尊贵型 | 真实价:29.38万
名称:海马 福美来F7 2017款 1.5T 7座自动标准型 | 真实价:1.68万
名称:哈弗 H1 2016款 红标 1.5L AMT舒适型 | 真实价:1.68万
名称:奔驰 GLA 2020款 GLA 200 | 真实价:12.88万
名称:丰田 赛那SIENNA 2021款 2.5L混动 铂金版 | 真实价:25.58万
名称:雪佛兰 科鲁兹 2013款 1.6L SL MT | 真实价:0.68万
名称:广汽传祺 影豹 2021款 270T DCT J16版 | 真实价:4.68万
名称:猎豹汽车 猎豹CS10 2015款 2.0T 手动卓越型 | 真实价:1.38万
名称:五菱汽车 五菱宏光 2014款 1.5L S舒适型 | 真实价:0.95万
名称:长城 M4 2014款 1.5L 手动舒适型 | 真实价:1.18万

五、常见问题排查

若运行代码出现报错,可参考以下排查方向(覆盖本次实战中所有遇到的问题):

  1. ModuleNotFoundError:未安装依赖,执行步骤3.1的依赖安装命令即可;

  2. IndexError(正则匹配失败):页面源码中Base64编码格式变化,可调整正则表达式中的匹配规则;

  3. ValueError(Base64解码失败):提取的Base64包含非ASCII字符,检查get_font_base64函数中的正则是否正确;

  4. PermissionError(权限不足):字体保存路径无写入权限,代码已默认使用系统临时目录,无需修改;

  5. ImageProcessError(图片识别失败):图片格式/尺寸错误,确保代码中图片为RGB格式、120×120尺寸,文字居中;

  6. KeyError(小数点报错):解密时未保留非加密符号,检查decode_price函数是否保留了.、万等符号。

六、总结

本次58同城字体反爬破解,核心是“放弃固定映射,采用实时识别”,避免了网站字体动态变化导致的破解失效问题。关键在于:精准提取Base64编码、生成清晰居中的字体图片、用ddddocr精准识别数字,全程无硬编码,可直接复用至其他采用类似字体反爬的网站。

新手可重点理解“字体映射生成”和“图片识别优化”两个环节,掌握后可轻松应对大部分字体反爬场景。

更多推荐