字体反爬之实习僧

    xiaoxiao2022-06-26  354

    字体反爬是爬虫不可避免的一道关卡,因为这是成本比较低,而且效果还不错的一种方式。

    今天我们先看看实习僧的字体爬虫怎么破解。首先我们先随便搜索一个职业,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爬取实习僧所有数据

    原文博客:https://blog.csdn.net/Qwertyuiop2016/ 现在这个是小号,给爬虫用的

    欢迎关注我的微信公众号,分享我的学习经验和一些觉得不错的资源。如果有问题的话,也可以在微信公众号联系我。


    最新回复(0)