字体反爬是爬虫不可避免的一道关卡,因为这是成本比较低,而且效果还不错的一种方式。
今天我们先看看实习僧的字体爬虫怎么破解。首先我们先随便搜索一个职业,https://www.shixiseng.com/interns?k=数据库&p=1。F12查看源码发现,职业的某些汉字字母和所有数字都是框框,这基本可以确定使用了自定义字体。
这里可以看到li标签有一个font属性,点击一下这个标签,右边就会出现详细的css属性。我们只看.font,发现有font-family: myFont这个信息。我们先找到这个字体文件,这个可以去原网页或者加载的js里搜索,可以找到这段代码是包含在原网页的。而字体文件是以base64字符串的格式传输的,先用python自带的base64库解码一下字符串,然后另存为ttf文件。
import base64
s = '加密字符串'
with open('a.ttf', 'wb') as f:
f.write(base64.b64decode(s))
然后用FontCreato这个工具打开字体文件:
这就是被重新编码的文字,他们的编码和utf-8是不一样的,所以浏览器会显示为框框。但浏览器在文字渲染(浏览器通过绘制相应像素点来达到显示整个汉字)的时候被显示成想要的汉字,因为它们的渲染代码在字体文件中被更改。
到了这里,我想很多人肯定是去百度找别人怎么解决这种问题的。当然,我也是,我看了很多关于字体反爬的文章,基本上是使用fonttools来寻找渲染字体代码是否相同,只要渲染代码相同,则判定为同一个字。为了方便人理解,我们先使用fonttools库将ttf文件转化为xml文件。
from fontTools.ttLib import TTFont
font = TTFont('a.ttf')
font.saveXML('a.xml')
使用文本编辑器打开xml文件,这里我使用的是editplus。
初看文件是看不懂文件表达的含义的,这里我粗略的说明我看懂的部分,也是爬虫要用到的那部分,至于文件的格式就不深究了。首先第一张图有id和name,每个ID会对应一个name,这两个值暂时是没有用的。我们再看第二张图,包含code和name,code代表这个字的编码的16进制,将0x改成\u就是网页源码中的框框的字符了,name会在第三张图中用到,第三种图表示将name所表示的code渲染成某种形状(即相应文字),只要对照每一个文字的这一段代码就可以判断是不是同一文字了,既然要对照,首先我们手里肯定要有一份已经知道渲染是什么文字的字体了。所以我们必须手工解码一份字体。
到这里后面我就不多说了,因为如果使用这种方法,那么爬虫的效率就有点低了,这样解码一套字体会耗费一定时间,虽然网站一般是每天或者每几天更新一次字体文件的,但工作量也不小。这样大量的工作也会长时间占用电脑大量的CPU。而且字体渲染不同其实浏览器也有可能显示为同一汉字,稍微改变一下字体形状就行(参考一下不同字体为什么不一样,你也能看成同一个字)。这样爬虫就不是要判断相等,而是判断一个范围,效率就更低了。于是我就秉承着程序员的核心思想继续思考:不会偷懒的程序员不是好程序员
看着别人的博客,发现别人用fontcreator打开的字体文件和我打开的文件不仅字是一样的,而且数量和顺序都是一样的(博客时间是2018年了),也就是说实习僧至少有一年没有更新网页代码了。虽然这个字体文件变化很频繁,但有没有可能所有的字体文件都有一个特定的顺序排序这些文字。这个猜想是很合理的,每个程序员都会有一个归一化的思想,比如代码结构,代码排版等。
我们看第一张图的ID就知道,文字顺序应该是ID来决定的,而右边的name只是文字的一个别名,这个别名不确定会不会变化,我们就当他会变化吧,那么我只需要获取ID和name对应的字典,还有code和name对应的字典,组合成ID和code对应的字典,再将fontcreator里面显示出来的文字按顺序放在一个列表,再使用ID作为索引取出列表对应的值,不就做成了一个code和文字的密码表了。操作一番后发现,猜想完全是正确的,当然我只是实验了一次,后面还需要 靠时间来验证。希望不要被打脸。
还有一种思路:只获取当然页面每个职业的具体url,然后访问子页面,子页面是只对十个数字重新编码的,那么我们只要手工获取这十个数字的编码表给爬虫就行。这是一种思路,但一般情况下不可取,因为它给爬虫增加了相当多的工作量,有多少数据就需要多访问多少个网页,这并不程序员。如果只是少量数据,可以这么操作,但如果需要大量数据的时候就显得很不合理了。
代码如下:
# -*- coding: utf-8 -*-
import base64
import re
import pyquery
import requests
from fontTools.ttLib import TTFont
def get():
url = 'https://www.shixiseng.com/interns?k=Python&p=31'
headers = {'Host': 'www.shixiseng.com',
'Referer': 'https://www.shixiseng.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
return resp.text
class UniDecrypt(object):
def __init__(self, ciphertext):
self.ciphertext = ciphertext
self.decrypt()
self.analysis()
def __call__(self, word):
return word.translate(self.tab)
def decrypt(self):
s = base64.b64decode(self.ciphertext)
with open('temp.ttf', 'wb') as f:
f.write(s)
font = TTFont('temp.ttf')
font.saveXML('temp.xml')
def analysis(self):
words = ' 0123456789一师X会四计财场DHLPT聘招工d周l端p年hx设程二五天tCG前KO网SWcgkosw广市月个BF告NRVZ作bfjnrvz三互生人政AJEI件M行QUYaeim软qu银y联'
with open('temp.xml') as f:
xml = f.read()
temp1 = re.findall(r'<GlyphID id="(\d+)" name="(.*?)"/>',xml)
temp2 = list(set(re.findall(r'<map code="(.*?)" name="(.*?)"/>',xml)))
d2 = {x[1]:x[0] for x in temp2}
#print(d2)
wordtab = {chr(int(d2[x[1]], 16)):words[int(x[0])] for x in temp1 if not (x[0] == '0' or x[0] == '1')}
self.tab = str.maketrans(wordtab)
if __name__ == '__main__':
# with open('a.html') as f:
# html = f.read()
html = get()
ciphertext = re.search(r'base64,(.*?)"', html).group(1)
uni = UniDecrypt(ciphertext)
doc = pyquery.PyQuery(html)
position_list = doc('.position-list .position-item.clearfix.font').items()
for position in position_list:
job_name = position('.position-name').text()
url = position('.position-name').attr('href')
salary = position('.position-salary').text()
place = position('.info2.clearfix span:first-of-type').text()
work_day = position('.info2.clearfix span:nth-child(2)').text()
least_month = position('.info2.clearfix span:last-of-type').text()
company = position('.company-name').text()
category = position('.company-more-info.clearfix span:first-of-type').text()
scale = position('.company-more-info.clearfix span:last-of-type').text()
d = {'job_name':uni(job_name),'url':uni(url),'salary':uni(salary),
'place':uni(place),'work_day':uni(work_day),'least_month':uni(least_month),
'company':uni(company),'category':uni(category),'scale':uni(scale)}
print(d)
这样拿到密码表所花费的时间是非常短的(一两秒就行),基本拿到一次就可以在整个爬虫周期使用,如果哪天失效,只需要在获取一次就行。就算他每个网页都返回一个不同的字体文件,我们所花费的时间也不会太多,效率会远远高于对比字体。当然,这只是针对个例,而开始介绍的方法是比较通用的,另外,如果连fonttools都解决不了了,就只能使用OCR识别了。OCR的效率很低很低,不到万不得已,不要使用。
既然拿到了密码表,那么该如何快速替换爬虫中的字符呢?而且拿到的也只是0x一样的字符串,怎么变成\u一样的字符编码呢(replace(‘0x’, ‘\u’)或者replace(‘0x’, ‘\u’)就不用想了)。首先回答第一个问题,0x的字符变成\u的字符编码只需要使用int将0x字符串变成十六进制的数字,然后使用chr(数字)变成\u形式的字符编码了。替换文本中的一些字符,写100个replace当然可行,但是不是有点太不程序员了。其实python提供了内置的方法,请百度str.maketrans和str.translate。
下一篇博客:scrapy爬取实习僧所有数据