汽车之家口碑字体反爬


写在前面

汽车之家的反爬可以说是教科书级别,其反爬虫策略经过多次迭代,已经从最初的静态字体反爬变成了动态字体+js混淆,动态字体的破解与猫眼大同小异,加密的字多一些而已。js混淆比较麻烦,通过requests库请求网页无法得到字体对应的编码,看到的是一堆js函数,看着头都大了,不知道汽车之家是否对每一个加密的字都写了函数,无法洞悉其中的规则,那只能选择selenium获取网页了,效率比较低,但是很实用(小声bb:时间主要花在计算新旧字体的欧式距离上了,因为有90个字,不改进算法要执行差不多8000次)

动态字体映射

参考以前的文章


破解猫眼字体反爬


这里写一下如何建立基础字体映射,首先获取一套字体,用fontcreater打开:



如上图所示,“小”对应“EC41”,依次类推,需要将它们保存为字典形式,这么多字,怎么办呢,一个一个敲吧。


写成下面的格式就行了,前面加uni是为了处理xml时稍微方便一点,如果这里不加那后面就要多一步操作。ps:直接复制是没用的,除非你的字体跟我的一样,很不巧,本站不支持附件的上传,如果你觉得实在麻烦可以给我发邮件,我把字体发给你。

relation_table = {
    'uniEC41': '小', 'uniED81': '响', 'uniEDD3': '排', 'uniED20': '冷',
    'uniED72': '动', 'uniECBE': '公', 'uniEDFF': '五', 'uniEC5D': '更',
    'uniED9D': '是', 'uniECEA': '有', 'uniED3C': '性', 'uniEC88': '过',
    'uniECDA': '无', 'uniEC27': '外', 'uniED67': '量', 'uniEDB9': '耗',
    'uniED06': '不', 'uniEC52': '少', 'uniECA4': '矮', 'uniEDE5': '八',
    'uniEC43': '真', 'uniED83': '中', 'uniECD0': '近', 'uniED22': '坏',
    'uniEC6E': '内', 'uniEDAF': '比', 'uniEE01': '档', 'uniED4D': '皮',
    'uniED9F': '好', 'uniECEC': '一', 'uniEC38': '加', 'uniEC8A': '电',
    'uniEDCB': '远', 'uniED17': '只', 'uniED69': '地', 'uniECB6': '多',
    'uniED07': '实', 'uniEC54': '油', 'uniED95': '左', 'uniEDE6': '坐',
    'uniED33': '很', 'uniEC80': '灯', 'uniECD1': '得', 'uniEC1E': '了',
    'uniEC70': '盘', 'uniEDB0': '机', 'uniECFD': '二', 'uniED4F': '开',
    'uniEC9B': '十', 'uniEDDC': '硬', 'uniEC3A': '手', 'uniED7A': '低',
    'uniEDCC': '呢', 'uniED19': '雨', 'uniEC65': '右', 'uniECB7': '软',
    'uniEDF8': '保', 'uniED44': '着', 'uniED96': '控', 'uniECE3': '空',
    'uniED35': '高', 'uniEC81': '里', 'uniEDC2': '大', 'uniEC20': '味',
    'uniED60': '养', 'uniECAD': '九', 'uniECFF': '副', 'uniEC4B': '级',
    'uniEC9D': '短', 'uniEDDE': '来', 'uniED2A': '长', 'uniED7C': '六',
    'uniECC9': '的', 'uniEE09': '上', 'uniEC67': '启', 'uniEDA8': '自',
    'uniEDF9': '泥', 'uniED46': '下', 'uniEC93': '门', 'uniECE4': '路',
    'uniEC31': '四', 'uniEDC3': '问', 'uniED10': '三', 'uniED62': '七',
    'uniECAE': '当', 'uniEDEF': '孩', 'uniEC4D': '身', 'uniED8D': '音',
    'uniED2C': '光', 'uniEC78': '和'
    }

部分代码

用到的库

#python处理字体文件的库
from fontTools.ttLib import TTFont
#将上面的基础字体映射保存为font_config.py
import font_config
#正则表达式
import re
#用requests下载新字体文件
import requests
#解析网页
from bs4 import BeautifulSoup as bs
#计算欧式距离
import numpy as np
#时间、json字典(这里用于重新对字符串进行Unicode编码)
import time,json
#selenium获取网页
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

全局变量

#获取基础字体映射
relation_table = font_config.relation_table
#请求头
headers = {
            'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
        }
#自定义selenium属性
chrome_options = Options()
"""
设置代理,这里有一点小问题,如果本机通过代理上网
不为selenium设置代理的话就不能隐藏selenium界面,不然会出现connection错误
一般来说,此问题会出现在公司内网通过固定代理访问外网,而内网本身无法访问外网
"""
chrome_options.add_argument("--proxy-server=http://ip:port")
#selenium无界面
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')

获取网页信息

def get_html():
    #目标口碑的url
    url = 'https://k.autohome.com.cn/detail/view_01e9wkyqv26cr3ge1k6grg0000.html#pvareaid=2112108'
    #初始化chromedriver
    browser = webdriver.Chrome(executable_path = 'chromedriver.exe',options = chrome_options)
    #访问目标地址
    browser.get(url)
    #暂停2秒,不然可能抓不到信息
    time.sleep(2)
    #获取网页对象
    page = browser.page_source
    #使用beautifulsoup解析网页
    html = bs(page,'lxml')
    #获取口碑信息的div模块
    info = html.find('div',class_ = 'mouth-item koubei-final').find('div',class_ = 'text-con')
    #正则匹配该口碑对应字体文件下载地址
    pattern_ttf = re.compile('k2.autoimg.cn.*?ttf')
    #正则匹配口碑内容
    pattern_text = re.compile('<!--@athm_BASE64@-->(.*?)<!--@athm_js@-->')
    #拼接字体url
    font_url = 'https://' + re.findall(pattern_ttf,str(html))[0]
    #获取口碑内容,一般来说有四段
    text = re.findall(pattern_text,str(info))
    #下载新字体,保存为"new_font.ttf"
    res = requests.get(font_url,headers = headers)
    open('new_font.ttf','wb').write(res.content)
    #以第一段口碑内容为例,再次调用beautifulsoup获取所有的spans标签
    spans = bs(text[0],'lxml').find_all('span')
    #处理Unicode字符(将其转换为字符串,即'\u'->'\\u')
    #注意:这里连同未加密汉字一起处理了,后面需要调用json模块重新编码
    new_text = text[0].encode('unicode-escape').decode('utf-8')
    #处理所有的span标签,将整个span模块替换为span.text的,需要进行一系列的编码解码
    for span in spans:
        new_text = new_text.replace(str(span).encode('unicode-escape').decode('utf-8'),span.text.encode('unicode-escape').decode('utf-8').replace('\\u','&#x'))
    #退出chromedriver
    browser.quit()
    #返回清洗后的字符串
    return new_text

解密字体并对字符串重新编码

def run():
    #调用上述函数获取口碑内容
    text = get_html()
    #处理基础字体
    base_font = TTFont('base_font.ttf')
    base_font.saveXML('base_font.xml')
    #处理新字体
    new_font = TTFont('new_font.ttf')
    new_font.saveXML('new_font.xml')
    #从xml中获取字体信息
    ttglyph_base = Read_Font('base_font.xml')
    ttglyph_new = Read_Font('new_font.xml')
    #建立新旧字体文件之间的映射,Uni->Uni
    basetonew = BaseToNew(ttglyph_base, ttglyph_new)
    #建立新的字体映射,Uni->汉字
    newtotext = NewToText(basetonew)
    #遍历字典中每一个键
    for key in newtotext.keys():
        #替换口碑中的内容
        text = text.replace(key,newtotext[key])
    #使用json模块对未加密的内容重新编码
    text = json.loads(f'"{text}"')
    #返回口碑明文内容
    return text

其余代码

这部分的注释可以参考猫眼字体反爬那篇文章,重新写注释太麻烦。

def compare_axis(axis1,axis2):
    if len(axis1) < len(axis2):
        axis1.extend([0,0] for _ in range(len(axis2) - len(axis1)))
    elif len(axis2) < len(axis1):
        axis2.extend([0,0] for _ in range(len(axis1) - len(axis2)))
    axis1 = np.array(axis1)
    axis2 = np.array(axis2)
    return np.sqrt(np.sum(np.square(axis1 - axis2)))

def Read_Font(file):
    f = open(file,'r',encoding = 'utf-8')
    html_base = bs(f.read(),'lxml')
    ttglyph = html_base.find('glyf').find_all('ttglyph')
    f.close()
    return ttglyph

def Get_Axis(string):
    axis = []
    pattern = re.compile('<pt\son="[0,1]?"\s(.*?)>')
    axis_list = re.findall(pattern,str(string))
    for item in axis_list:
        zb = item.split(' ')
        x = int(zb[0].split('=')[1].replace('"',''))
        y = int(zb[1].split('=')[1].replace('"',''))
        axis.append([x,y])
    return axis

def BaseToNew(ttglyph_base,ttglyph_new):
    basetonew = []
    for base in ttglyph_base[1:]:
        value = []
        axis_base = Get_Axis(base)

        for index,new in enumerate(ttglyph_new[1:]):
            axis_new = Get_Axis(new)
            value.append(compare_axis(axis_base,axis_new))

        for i in range(len(value)):
            if value[i] == min(value):
                new_code = ttglyph_new[i + 1].get('name')
                base_code = base.get('name')
                basetonew.append({"basecode":base_code,"newcode":new_code})
                break
    return basetonew

def NewToText(basetonew):
    newtonum = {}
    for key in relation_table.keys():
        value = relation_table[key]
        for i in range(len(basetonew)):
            if key == basetonew[i]['basecode']:
                newtonum[basetonew[i]['newcode'].lower().replace('uni','&#x')] = value
                break
    return newtonum

写在后面

如果哪天我把汽车之家的js函数给弄懂了...那么我会不会秃呢..