写在前面
汽车之家的反爬可以说是教科书级别,其反爬虫策略经过多次迭代,已经从最初的静态字体反爬变成了动态字体+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函数给弄懂了...那么我会不会秃呢..