攻破网页字符替换反爬虫机制小记

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议.

摘要:python3 进行网页爬取数据出现乱码,出现奇怪的编码方式,关键数据无法爬取,网站特殊字体的反爬机制的完整攻克过程。

最近在爬 实习僧 网站职位数据的时候遇到关键字体无法解析的问题。一开始还以为是chrome的解码格式不符合,解码错误导致的,但随着深入,发现这是可不是简单的编码问题,是网站精心策划的反爬机制。

第一次遇到这种情况,攻克的过程也挺复杂有趣,中间学到了很多东西,新手一开始遇到还真的会和我一样摸不着头脑,即使google可能也会从编码去查,绕很多弯路也百思不得其解。后来用这种发现这种方式的网站还挺多的,毕竟这种反爬机制是很易用的,想绕过去也是很麻烦的,所以写这篇文章,记录下整个攻克的方法和过程分享出来。

初步摸索

用chrome的开发者工具查看源是这样子的:

关键的数据圈闭都是显示为乱码的方框状的,无法正确显示和爬取,很显然,是遇到了网站的反爬机制。

然后查看未被解析的源代码,看看这个字体是怎么一回事:

实际发现网页中的200-300/天在源码中是&#xf161&#xe973&#xe973-&#xebf3&#xe973&#xe973/天的形式,这个

&#xf161是什么编码啊?如果知道这个编码系,爬下来自己解码应该就可以了。

乍一看,很像NCR编码,但仔细对比后,

很可惜,它不属于任何编码,试着用几种方式去解析之后发现这个编码出了和数字一一对应以外,没有什么特别的规律。

会不会是网页控制的异步加载数据类型呢?

尝试用调试工具的network-XHR也没有发现任何的API的response和解析字符有关。

不过细心的回去看源代码的时候发现:

凡是有乱码被保护的数据css样式都出现了cutom_font(自定义字体)。

= =,自定义字体 写的如此明显,看来找到问题的所在了。。。

锁定问题

既然已经发现问题了,那么看怎么样一步一步解决它把!

首先在开发者工具中找到这个自定义字体,看看到底是怎么回事:

发现,font-family里有一个非主流的myfont,看来问题就是出现在这个myfont上了,

锁定,继续查找,发现这是个在线font字体文件,打开它查看:

这个字体看起来正常无比,和微软雅黑没什么区别,

但问题是这个字体有自己一套特别的编码,网页是通过对应关系将所谓的乱码解析成这个字体里的数字,

我们要弄到这套编码,解码字体的问题就自然迎刃而解了。

解决问题

分析字体

从开发者工具点击这个字体文件,从网络源上下载一份字体文件来做分析,font字体文件需要导入python的fontTools第三方库,

pip3 install fonttools安装库

使用这个库:

1
2
3
4
from fontTools.ttLib import TTFont
font = TTFont(r'C:\Users\dell\Downloads\1') # 打开字体文件
font.saveXML('./1.xml') # 转换成 xml 文件方便分析
# print(help(TTFont)),可以使用help来查询这个库的方法,我就是这么用的。。。

对于这个字体文件的解析,最好先导出XML文件进行观察,因为找到网上的例子发现,虽然很多网站都是用字体文件来实现关键数据的反爬机制,但是每个字体文件的编码设计又是复杂多变的,print(help(TTFont))可以帮助你查看函数方法,把字体里的关键数据解析出来。

通过网页上的xe973&#xe973-&#xea32&#xf368&#xe973/天代表100-150

在XML里根据网页上的代码和uni代码的对应关系:

很容易发现,

'uni30', 'uni31', 'uni32', 'uni33', 'uni34', 'uni35', 'uni36', 'uni37', 'uni38', 'uni39'

分别代表数字0~9

&#xuni编码的对应关系,也就是字体编码的部分,集中在font文件导出的XMLcmap字段里,用font.getBestCmap()可以获得cmap的数据

1
2
3
4
from fontTools.ttLib import TTFont
font = TTFont(r'C:\Users\dell\Downloads\1') # 打开文件
font_map = font.getBestCmap() # 获取 GlyphOrder 字段的值
print(font_map)

打印font_map:

1
2
3
{60418: 'uni79', 59262: 'uni50', 62470: 'uni58', 58119: 'uni6b', 57866: 'uni4F5C', 58892: 'uni62', 58645: 'uni72', 57879: 'uni38', 62084: 'uni7AEF', 63238: 'uni77', 63016: 'uni56DB', 59178: 'uni6d', 59954: 'uni31', 59694: 'uni6f', 60466: 'uni53', 63611: 'uni6708', 63381: 'uni4b', 59194: 'uni78', 63038: 'uni4EBA', 57921: 'uni70', 59970: 'uni48', 58851: 'uni73', 59489: 'uni75', 61769: 'uni43', 58702: 'uni37', 58959: 'uni76', 61008: 'uni68', 59475: 'uni62DB', 57942: 'uni7A0B', 60508: 'uni4e', 61882: 'uni7a', 61793: 'uni32', 62050: 'uni94F6', 61028: 'uni59', 60262: 'uni6e', 62312: 'uni35', 61160: 'uni45', 61554: 'uni54', 59763: 'uni30', 62910: 'uni4f', 59511: 'uni573A', 120: 'x', 58746: 'uni8BBE', 61051: 'uni66', 57725: 'uni8054', 57982: 'uni6a', 60031: 'uni52', 60545: 'uni4E09', 60036: 'uni67', 62601: 'uni39', 60812: 'uni4E2A', 57998: 'uni544A', 58769: 'uni63', 58514: 'uni5E7F', 58133: 'uni42', 57749: 'uni4d', 61079: 'uni46', 59631: 'uni7F51', 58016: 'uni44', 63018: 'uni74', 58020: 'uni4F1A', 63401: 'uni5E02', 59029: 'uni5DE5', 59312: 'uni4EF6', 63410: 'uni5929', 62131: 'uni57', 59060: 'uni69', 63670: 'uni6c', 62135: 'uni55', 59609: 'uni8D22', 63420: 'uni5a', 61118: 'uni61', 60098: 'uni884C', 59844: 'uni51', 62410: 'uni4a', 63436: 'uni5E74', 60622: 'uni64', 58319: 'uni4E92', 60883: 'uni47', 62798: 'uni36', 58325: 'uni4c', 61142: 'uni8F6F', 62116: 'uni4E00', 59354: 'uni524D', 58844: 'uni34', 59617: 'uni8058', 62691: 'uni8BA1', 58598: 'uni5468', 60392: 'uni56', 62953: 'uni653F', 60655: 'uni4E94', 61168: 'uni5E08', 60403: 'uni33', 58750: 'uni4E8C', 60150: 'uni49', 58616: 'uni65', 57849: 'uni751F', 58875: 'uni41', 62717: 'uni71'}
进程已结束,退出代码0

发现十六进制导出后变成十进制的map了。

0xe973变成了5976359763: 'uni30'也就是数字0,

完全对应的~

那么有了这张表之后,

最后就是将网页源代码中的’乱码’,去掉&#x之后,转换成十进制查表得到uni编码,再根据之前得到的

'uni30', 'uni31', 'uni32', 'uni33', 'uni34', 'uni35', 'uni36', 'uni37', 'uni38', 'uni39'

分别代表数字0~9,即可解出正确的数字啦!

例:

网页源码获得&#xe973—去掉前缀–>e973—–转为十进制—->59763—查表–> uni30—-查表—>0

编写代码

就根据如此过程编写示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import requests
import hashlib
import urllib.request
from fontTools.ttLib import TTFont # 导包
import re
import os
# 这个网站的font文件链接超级长,md5转换下缩短链接
def to_md5(onestr):
md5=hashlib.md5(onestr.encode("utf8")).hexdigest()
return md5
# 下载字体文件
def download(url, md5):
new_file = urllib.request.urlopen(url)
with open(md5, 'wb') as f:
f.write(new_file.read())
# 已有font文件后解析font文件函数
# 每个网站的font文件不相同,需要分析文件后改写
def analysis_font(font_url,filename):
font = TTFont(font_url) # 打开文件
font.saveXML('./{}.xml'.format(filename)) # 转换成 xml 文件并保存
font_map = font.getBestCmap() #查看font文件里面的单元,并提取关键值
map = {v:k for k,v in font_map.items()}
# print(font_map)
# print(map)
temp_list=font.getGlyphOrder()[2:12]
# print(temp_list)
numberlist=[]
for temp in temp_list:
numberlist.append(hex(int(map[temp])))
return numberlist
# 转换函数,将字符串中的编码按解析出来的数字一一对应的替换
def translate(onestr,filename):
fontfile = filename
# fontfile = r'C:\Users\dell\Downloads\sss'
numberlist = analysis_font(fontfile, filename)
def _fontmatch(matched):#匹配函数
tempstr = matched.group()
shortstr=tempstr[2:]
newstr = 9999
for i in range(10):
if shortstr == numberlist[i][1:]:
newstr=str(i)
return newstr
replacedStr = re.sub("&#x....", _fontmatch, onestr)#这里用了re.sub加入匹配函数
return replacedStr
# 判断字体是否下载过
def get_font_name(font_url):
font_md5=to_md5(font_url)
file_list = os.listdir()
if font_md5 not in file_list:
print('不在字体库中, 下载:', font_md5)
font_list.append(font_md5)
download(font_url,font_md5)
return font_md5
# 用正则解析网页
def get_data(url):
data = requests.get(url)
pattern = re.compile(r'<div class="job_msg">.+</div>')
result = re.search(pattern, data.text)
job_money = re.findall(r'<span class="job_money cutom_font">(.+?)</span>', result.group())
job_week = re.findall(r'<span class="job_week cutom_font">(.+?)</span>', result.group())
job_time = re.findall(r'<span class="job_time cutom_font">(.+?)</span>', result.group())
title_pattern = re.compile(r'<div class="new_job_name" title="(.+?)">.+</div>')
title = re.findall(title_pattern, data.text)
font_url_pattern = re.compile('@font-face \{font-family:myFont; src: url\("(.+)"\)\}')
font_url=re.findall(font_url_pattern,data.text)
font_md5=get_font_name(font_url[0])
print(title[0])
print(translate(job_money[0],font_md5), translate(job_time[0],font_md5), translate(job_week[0],font_md5))
if __name__ == '__main__':
url = "https://www.shixiseng.com/intern/inn_olhzeo1lexqf"
get_data(url)

运行结果:

后记

其实遇到数字信息被自定义font文字文件反爬拦住的时候,不一定要像我这样去从font文件剖析入手,

也许有小伙伴认为可以通过大量分析网页的数和网页源码中的编码是可以自己找到10个数字与编码的一一对应关系,自己手动建立查询表。

但是对于大部分使用这种反爬机制的网站都加入了多个自定义的font文件,并且采用随机使用的方法,

并且对于更复杂的情况,比如加入26个字母,数字组合等,手动去对比源码数据建表就非常繁琐和没有效率了。

本文提到的方法是具有通用性的,主要就是在分析font文件导出的XML的时候需要费点功夫,因为每个网站使用的font都不太一样,今天这个例子属于比较麻烦的一种,就是用了二次编码,要进行两次查表,有的网站用的font可能一次查表就可以得到。

祝你好运~

参考资料:

坚持原创技术分享,您的支持将鼓励我继续创作!