前言
最近一直在为找工作烦恼,刚好遇到一家公司要求我先做几道反爬虫的题,看了之后觉得自己还挺菜的,不过也过了几关,刚好遇到一个之前没遇到过的反爬虫手段 — 字体反爬
正文 一、站点分析 题目要求:这里有一个网站,分了1000页,求所有数字的和。注意,是人看到的数字,不是网页源码中的数字哦~
就这,从图里能看出数字 的字体有些不同,看看源码是什么样的
可以看到,源码里的内容和网页上显示的内容根本不一样,当然,题目也说了;那么这是怎么回事呢,切换到 Network 栏,刷新网页看看请求
可以看到,这里有两个字体请求,选择后可以预览字体
很明显,数字有点问题,被改过了,上面那一个请求的字体文件是正常的字体(下图),可以拿来做比较,以便于我们分析
一般来说字体文件的数字就是这样的顺序 1 2 3 4 5 6 7 8 9 0 ,以这个为模板,被修改后的字体 中的数字 2 处与 正常字体 中 9 的位置。回到网页源码和内容,网页上显示 274 ,实际源码中是 920 (下图),用上面的字体做替换我们会发现,2 在被 修改过的字体 中的位置是 8 ,而 8 在 正常字体 中就是 8,由此可得结论:我们只要把这 修改过的字体 搞到手,然后把网页上显示的内容逐个拆分为单个数字,然后从字体中匹配出正常字体就行了,不过,根据题目,我们需要反着来做,也就是从源码入手,获取到内容后拆分为单个字体,接着从字体中获取网页上显示的内容。
我自己写的时候都觉得头晕,直接写代码,这样能更好的表达我要说什么,不过,这里要说一点,据我分析,这个网页有1000页,每一页的字体都是不同的,就需要每获取一个网页就得重新获取被修改的字体。我这里用的是 scrapy 框架。
二、代码阶段 首先新建一个scrapy 项目
1 2 3 4 5 6 7 8 ➜ ~ scrapy startproject glidedsky New Scrapy project 'glidedsky', using template directory '/usr/local/lib/python3.7/site-packages/scrapy/templates/project', created in: /Users/zhonglizhen/glidedsky You can start your first spider with: cd glidedsky scrapy genspider example example.com ➜ ~
接着创建一个Spider
1 2 3 4 ➜ ~ cd glidedsky ➜ ~ glidedsky scrapy genspider glidedsky glidesky.com Cannot create a spider with the same name as your project ➜ ~ glidedsky
scrapy 怎么用我就不说了,直接看代码 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 import scrapyimport requestsimport refrom glidedsky.items import GlidedskyItemfrom glidedsky.spiders.config import *class GlidedskySpider (scrapy.Spider ): name = 'glidedsky' start_urls = ['http://glidedsky.com/level/web/crawler-font-puzzle-1' ] def __int__ (self ): self.headers = { 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' , } def request (self, url, callback ): request = scrapy.Request(url=url, callback=callback) request.cookies['XSRF-TOKEN' ] = XSRF_TOKEN request.cookies['glidedsky_session' ] = glidedsky_session request.headers['User-Agent' ] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' return request def start_requests (self ): for i, url in enumerate (self.start_urls): yield self.request(url, self.parse_item) def parse_item (self, response ): """ 解析numbers :param response: :return: """ body = response.css('html' ).get() self.save_font(body) col_md_nums = response.css('.col-md-1::text' ).extract() items = GlidedskyItem() for col_md_num in col_md_nums: items['numbers' ] = col_md_num.replace('\n' , '' ).replace(' ' , '' ) yield items next = response.xpath('//li/a[@rel="next"]' ) if len (next ) > 0 : next_page = next [0 ].attrib['href' ] url = response.urljoin(next_page) yield self.request(url=url, callback=self.parse_item) def save_font (self, body ): """ 保存字体到本地 :param response: 网页源代码 :return: """ pattern = r'src:.url\("(.*?)"\).format\("woff"\)' woff_font_url = re.findall(pattern, body, re.S) print (woff_font_url) resp = requests.get(woff_font_url[0 ], headers={'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36' }) with open (WOFF_FONT_FILENAME, 'wb' ) as f: f.write(resp.content)
在解析字体之前先分析一下字体文件的内容,因为这里面有坑(起码我这个站点是这样),下载好字体后,用python的 fontTools 库把 woff格式 转成 xml文件 ,然后打开;或者用 font-creator 直接打开,但是这个工具只有windows上有,所以这里就用第一种方法。
1、先把 woff格式 转成 xml格式 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsfrom fontTools.ttLib import TTFonturl = "https://guyujiezi.com/fonts/LQ1K9/1A7s3D.woff" filename = url.split('/' )[-1 ] resp = requests.get(url) with open (filename, 'wb' ) as f: f.write(resp.content) font = TTFont(filename) font.saveXML(filename.replace(filename.split('.' )[-1 ], 'xml' ))
2、用文本编辑器打开
只需要看 GlyphOrder 项就行了,其实直接看 GlyphOrder 一个屁都看不出来,完全和之前做的分析不一样,不过仔细观察后发现这里面也被人做了手脚,1703589624 这跟电话号码一样的就是上面看到的 修改后的字体 预览到的,可能这样还是看不出什么;其中 id 属性的值为 修改后的字体 中的数字,name 属性为 正常字体 ,但是根本不对,之前算过,网页中的 274 ,正常内容是 920 ,而下面,2 明显对应着 one ,其实我在这里被坑了,如果把 2+1=3 ,3 不就是对应着 nine 了吗,然后发现后面 74 也是对应着 20 ,有 12 项 GlyphID 的目的就是坑我们的(我猜的),不过这确实挺坑的。分析过后可以开始写代码了
3、代码如下,这是 pipelines.py 文件
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 from scrapy.exceptions import DropItemfrom fontTools.ttLib import TTFontfrom glidedsky.spiders.config import *class GlidedskyPipeline (object ): result = 0 def process_item (self, item, spider ): if item['numbers' ]: numbers = item['numbers' ] font = TTFont(WOFF_FONT_FILENAME) true_number = "" for num in range (len (numbers)): fn = NUMBER_TEMP[numbers[num]] glyph_id = int (font.getGlyphID(fn)) - 1 true_number += str (glyph_id) self.result += int (true_number) print ("@@@@@ 计算结果: %d" % self.result) else : return DropItem('Missing Number.' )
config.py
1 2 3 4 5 DATA_PATH = '/Volumes/HDD500G/Documents/Python/Scrapy/glidedsky/glidedsky/data' WOFF_FONT_FILENAME = DATA_PATH + '/woff-font.woff' XSRF_TOKEN = '' glidedsky_session = '' NUMBER_TEMP = {'1' : 'one' , '2' : 'two' , '3' : 'three' , '4' : 'four' , '5' : 'five' , '6' : 'six' , '7' : 'seven' , '8' : 'eight' , '9' : 'nine' , '0' : 'zero' }
items.py
1 2 3 4 5 6 7 8 9 10 11 12 13 import scrapyclass GlidedskyItem (scrapy.Item ): numbers = scrapy.Field()
settings.py ,设置我就不全部贴了,只贴需要改的部分
1 2 3 4 ITEM_PIPELINES = { 'glidedsky.pipelines.GlidedskyPipeline' : 300 , }
接着直接运行即可
1 2 ➜ cd /你项目存储地址/glidedsky/ ➜ scrapy startpoject glidedsky
输出结果就不展示了,贼鸡儿多
结论 这种反爬虫手段是我第一次遇到,以前遇到的也就验证码和ip限制,不过也算是涨了知识,最后结果是我解决了