爬虫反爬系列之破解雪碧图反爬
一、雪碧图
1.前言
我们都知道,HTTP 协议即超文本传输协议,是 Web 应用的基础,HTTP 协议又是基于 TCP 协议的,而 TCP 连接的建立是需要时间和资源的。当网页加载时,会需要下载图片资源,如果有非常多的小图片,就需要建立很多 TCP 连接。
但勤劳勇敢的前端工作者们,想到把所有小图片放到一张图片里面去,这样就可以通过一次 TCP 连接,下载所有的小图片,再通过前端的奇技淫巧,来展示正确的图片。这种由很多小图片组成的图片,被称为雪碧图,雪碧图在节约 TCP 连接的同时,也为爬取带来了难度。
2.定义
CSS 雪碧图即 CSS Sprite,也被称为 CSS 精灵,是一种 CSS 图像合成技术,该技术是将小图标和背景图像合成到一张图片中,然后利用 CSS 的背景定位来显示需要显示的图像部分。
CSS 雪碧的基本原理是把你的网站上用到的一些图片整合到一张单独的图片中,然后使用 CSS 中的 background 和 background-position 属性进行渲染,但当图片数量更多更复杂时,定位就需要更加准确,可能就会用到更多的数值来到达更准的定位。
3.雪碧图的优缺点
通过前面的描述,可以很清楚地知道雪碧图有如下优点:
- 减少加载网页图片时对服务器的请求次数,降低服务器压力,同时提高了页面的加载速度,节约服务器的流量;
- 减少图片加载所需要的时间,提高页面的加载速度。
除了上述优点,雪碧图还有一些无法避免的缺点:
- 雪碧图的最大问题是内存使用,因为雪碧图中会有大量的留白空间;
- 影响浏览器的缩放功能,如果使用雪碧图的页面缩放了,就需要做一些额外的工作来纠正图片边缘;
- 拼图维护比较麻烦,无论是拼图合成,还是修改图片,都会很麻烦不便于操作;
- 使 CSS 的编写变得困难,尤其是当图片数量较多时,会大大增加 CSS 的代码量和复杂度。
二、破解实例
1.站点分析
该站点的链接为:http://www.glidedsky.com/level/web/crawler-sprite-image-1。
打开网站,打开开发者工具,选择查看网页上的数字,发现这些数字其实都是 div,通过 CSS 来显示图片:
再查看该元素的 CSS 样式,发现除了宽高之外还有 background-position-x 属性,该属性就是用来控制显示的数字。除此之外,我们可以看到 sprite 类里面定义了一个背景图,打开链接后发现这个背景图如下:
每次加载网页时,都会下载类似上图的包含0-9十个数字的背景图,再通过 CSS 样式中的 background-position-x 属性来显示所需要的数字。
2.破解思路
要应对这种使用雪碧图来实现反爬的措施,有如下思路:
1)获取所有 background-position-x 并求对应的数字
因为每个数字对应的 background-position-x 的数值是一样的,所以思路一是获取所有 background-position-x 的数值,再求集合,并根据数值从小到大排列,每个数值就对应一个数字。但这种思路是有问题的,例如上面的截图中并没有数字0,也就没有数字0对应的位置,这就会导致我们获取到的数据是不完整的,也就无法正确表示了。
2)下载图片,根据 background-position-x 的值进行划分
首先是将图片下载下来,并计算出图片的大小,然后根据从 CSS 样式中获取到的 background-position-x 的数值进行划分,就能得到每个数字对应的位置区间。但这种思路也无法解决数字不全所带来的问题,尤其是数字是扭曲的,大小也不一样。
3)下载图片,估算每个数字所占的宽度
将图片下载下来,得到图片的宽度,因为每个数字的宽度其实是差不多的,所以我们可以简单地将图片的宽度除以10来估算每个数字的宽度,再用 background-position-x 的值和这个宽度进行整数除法,就能得到对应的数字了。使用这种方法,即使数字是不全的,也能够计算出来。
三、破解步骤
1.下载图片
前面已经说过在 sprite 类中指明了背景图片,截图如下:
在上面的 url() 中,data 表示取得数据的协定名称,image/png 是数据类型名称,base64 是数据的编码方法,逗号后面就是这个 image/png 文件 base64 编码后的数据。使用这种方式就把图像文件的内容直接写在了 HTML 文件中,这样做的好处是,节省了一个 HTTP 请求。
要将这个图片下载下来,首先要做的就是得到这个使用 base64 编码后的数据,可以使用正则表达式进行匹配,然后进行解码,再将图片下载到本地,打开并得到该图片的宽度。下载 base64 编码图片的代码如下:
1 def save_img(img_data): 2 """ 3 save image in local directory 4 :param img_data: image base64 data 5 :return: width of image 6 """ 7 img = base64.urlsafe_b64decode(img_data) 8 filename = "{}.{}".format(uuid.uuid4(), "png") 9 filepath = os.path.join("./Images", filename) 10 with open(filepath, "wb") as f: 11 f.write(img) 12 image = Image.open(filepath) 13 return image.width
2.获取位置-数字字典
我们可以知道 background-position-x 都定义在 CSS 代码中了,要获取所有 background-position-x 的数值,可以使用正则表达式 re 模块中的 findall() 方法进行匹配,使用方法如下:
re.findall(r"background-position-x:-?(\d+)?px", html)
前面已经得到图片宽度了,除以10的结果就可以当做每个数字所占的宽度,再用 background-position-x 的数值和这个宽度进行整数除法,得到的结果就是这个 CSS 所对应的数字。为了方便后面将数字进行组合,还要转换成 str 形式。具体代码如下:
1 def parse(num_list: list, gap: int): 2 """ 3 translate position to digit 4 :param num_list: number list 5 :param gap: average gap between numbers 6 :return: 7 """ 8 return {str(num): str(int(num // gap)) for num in num_list}
3.获取数字并求和
在示例的网址中有十二个三位数,也就有三十六个数字,我们要做的就是获取每个数字的 CSS 类名,再根据这个类名得到 background-position-x 的数值,再根据前面得到的字典就能得到每个数字,将三个数字组成一个三位数,最后使用 sum() 方法进行求和。
下面就是获取每个数字并进行求和的代码,其中 pos_dict 就是位置和数字对应的字典:
1 def get_digits(html, pos_dict): 2 """ 3 get digit according to the class and sum up the numbers 4 :param html: html 5 :param pos_dict: position to digit 6 :return: 7 """ 8 et = etree.HTML(html) 9 pos_classes = et.xpath('//*[@id="app"]/main/div[1]/div/div/div/div/div/@class') 10 digits, d = [], "" 11 for pos in pos_classes: 12 if len(d) == 3: 13 digits.append(d) 14 d = "" 15 pos_x = re.findall(pos.split(" ")[0] + r" { background-position-x:-?(\d+?)px }", html) 16 d = d + pos_dict[pos_x[0]] 17 digits.append(d) 18 result = sum([int(i) for i in digits]) 19 print("The result is : {}".format(result))
完整代码已上传到 GitHub!