![4d29a03d8ba4446cc7785d4fc9d740c0.png](https://i-blog.csdnimg.cn/blog_migrate/bc0aa39c3cb323a19dea954525b49ca7.jpeg)
CSS 偏移反爬虫
CSS 偏移反爬虫指的是利用 CSS 样式将乱序的文字排版为人类正常阅读顺序的行为。这个概念不是很好理解,我们可以通过对比两段文字来加深对这个概念的理解。
- HTML 文本中的文字:我的学号是 1308205,我在北京大学读书。
- 浏览器显示的文字:我的学号是 1380205,我在北京大学读书。
爬虫提取到的学号是 1308205,但用户在浏览器中看到的却是 1380205。如果不细心观察,爬虫工程师很容易被爬取结果糊弄。这种混淆方法和图片伪装一样,是不会影响用户阅读的。让人好奇的是,浏览器如何将 HTML 文本中的数字按照开发者的意愿排序或放置呢?这种放置规则是如何运作的呢?我们可以通过一个具体的例子来了解 CSS 偏移反爬虫的应用和绕过方法。
6.2.1 CSS 偏移反爬虫绕过实战
示例 5:CSS 偏移反爬虫示例。
网址:http://www.porters.vip/confusion/flight.html。
任务:爬取航班查询和机票销售网站页面中的航站名称、所属航空公司和票价,页面内容如图 6-4 所示。
![4e5289228d3b306e4558715a480bc7aa.png](https://i-blog.csdnimg.cn/blog_migrate/d7a23f00faef5be20f08307ecae27c74.jpeg)
图 6-4 示例 5 页面
在编写 Python 代码之前,我们需要确定目标数据的元素定位。航空公司名称元素定位如图 6-5 所示。
![4a1bd54addde5988e84669c49b88c645.png](https://i-blog.csdnimg.cn/blog_migrate/138faf88b624551b71e05548e1080599.jpeg)
图 6-5 航空公司名称元素定位结果
航空公司名称包裹在没有属性的 span 标签中,但该 span 标签包裹在 class 属性为 air g-tips 的 div 标签中。接下来我们看一下航站名称的元素定位,定位结果如图 6-6 所示。
![ef138037d9858c447e27ca07cc7a94a6.png](https://i-blog.csdnimg.cn/blog_migrate/dfbafd25ec1456673d5160cb9b33564f.jpeg)
图 6-6 航站名称元素定位结果
航站名称包裹在没有属性的 h2 标签中,h2 标签包裹在 class 为 sep-lf 的 div 标签中。
我们再看一下票价的元素定位,定位结果如图 6-7 所示。
![92892896a1f9f29809bf004008117a70.png](https://i-blog.csdnimg.cn/blog_migrate/4f5c6a6e9f3ca0523eab3b6792eba64e.jpeg)
图 6-7 票价的元素定位结果
页面中显示的票价为 467,但是在网页中却有两组不同的数字,其中一组是[7, 7, 7],而另一组是 [6, 4],这看起来就有点奇怪了。
难道是网页显示有问题?
按照正常排序来说,这架航班的票价应该是 77 764 才对。我们可以查看第二架航班信息的价格,思考是网页显示问题还是做了什么反爬虫措施。第二架航班的票价元素定位结果如图 6-8 所示。
![238ccb413ece0c23673a6880b9e8b468.png](https://i-blog.csdnimg.cn/blog_migrate/3c3e8d44ba0181ace0bc3231eb5b583e.jpeg)
图 6-8 第二架航班的票价元素定位结果
结果与第一架航班的票价显示有同样的问题:网页显示内容和 HTML 代码中的内容不一致。我们分析一下 HTML 代码,看一看是否能找到什么线索。第一架航班票价的 HTML 代码为:
<span class="prc_wp" style="width:48px">
<em class="rel">
<b style="width:48px;left:-48px">
<i style="width: 16px;">7</i>
<i style="width: 16px;">7</i>
<i style="width: 16px;">7</i>
</b>
<b style="width: 16px;left:-32px">6</b>
<b style="width: 16px;left:-48px">4</b>
</em>
</span>
代码中有 3 对 b 标签,第 1 对 b 标签中包含 3 对 i 标签,i 标签中的数字都是 7,也就是说第 1 对 b 标签的显示结果应该是 777。而第 2 对 b 标签中的数字是 6,第 3 对 b 标签中的数字是 4。
这些数字与页面所显示票价 467 的关系是什么呢?
这一步找到的标签和数字有可能是数据源,但是数字的组合有很多种可能,如图 6-9 所示。
![daf0b604fff7b1fffe902cfbfb904b86.png](https://i-blog.csdnimg.cn/blog_migrate/7ae4c98e0276ad531dc118640e3d8160.png)
图 6-9 数字组合推测
5 个数字的组合结果太多了,我们必须找出其中的规律,这样就能知道网页为什么显示 467 而不是 764 或者 776 。在仔细查看过后,发现每个带有数字的标签都设定了样式。第 1 对 b 标签的样式为:
width:48px;left:-48px
第 2 对 b 标签的样式为:
width: 16px;left:-32px
第 3 对 b 标签的样式为:
width: 16px;left:-48px
i 标签对的样式是相同的,都是:
width: 16px;
另外,还注意到最外层的 span 标签对的样式为:
width:48px
如果按照 CSS 样式这条线索来分析的话,第 1 对 b 标签中的 3 对 i 标签刚好占满 span 标签对的位置,其位置如图 6-10 所示。
![2c7f69836879b24b97ad7f57b67b7657.png](https://i-blog.csdnimg.cn/blog_migrate/9ab91ab2d05f8a8d799714a368597978.png)
图 6-10 span 标签对和 i 标签对位置图
此时网页中显示的价格应该是 777,但是由于第 2 和第 3 对 b 标签中有值,所以我们还需要计算它们的位置。此时标签位置的变化如图 6-11 所示。
![5a56f4aad45b9054c737ecb568154089.png](https://i-blog.csdnimg.cn/blog_migrate/7414263277eb31c4c8c3931968f43b67.jpeg)
图 6-11 标签位置变化
右侧是标签位置变化后的结果,由于第 2 对 b 标签的位置样式是 left:-32px,所以第 2 对 b 标签中的值 6 就会覆盖原来第 1 对 b 标签中的中的第 2 个数字 7,此时页面应该显示的数字是 767。
按此规律推算,第 3 对 b 标签的位置样式是 left:-48px,这个标签的值会覆盖第 1 对 b 标签中的第 1 个数字 7,覆盖结果如图 6-12 所示,最后显示的票价是 467。
![ea4d7c9de9887ce153d3e1624127c1de.png](https://i-blog.csdnimg.cn/blog_migrate/af4f02e72b83b622a458a0a5fb68152d.png)
图 6-12 覆盖结果
根据结果来看这种算法是合理的,不过我们还需要对其进行验证,现在将第二架航班的 HTML 值 和 CSS 样式按照这个规律进行推算。最后推算得到的结果与页面显示结果相同,说明这个位置偏移的计算方法是正确的,这样我们就可以编写 Python 代码获取网页中的票价信息了。因为 b 标签包裹在 class 属性为 rel 的 em 标签下,所以我们要定位所有的 em 标签。对应的 Python 代码如下:
import requests
import re
from parsel import Selector
url = 'http://www.porters.vip/confusion/flight.html'
resp = requests.get(url)
sel = Selector(resp.text)
em = sel.css('em.rel').extract()
接着定位所有的 b 标签。由于 b 标签中还有 i 标签,而且 i 标签的值是基准数据,所以可以直接提取。对应的 Python 代码如下:
for element in em:
element = Selector(element)
# 定位所有的<b>标签
element_b = element.css('b').extract()
b1 = Selector(element_b.pop(0))
# 获取第 1 对<b>标签中的值(列表)
base_price = b1.css('i::text').extract()
接下来要提取其他 b 标签的偏移量和数字。对应的 Python 代码如下:
alternate_price = [] for eb in element_b:
eb = Selector(eb)
# 提取<b>标签的 style 属性值
style = eb.css('b::attr("style")').get()
# 获得具体的位置
position = ''.join(re.findall('left:(.*)px', style))
# 获得该标签下的数字
value = eb.css('b::text').get()
# 将<b>标签的位置信息和数字以字典的格式添加到替补票价列表中
alternate_price.append({'position': position, 'value': value})
然后根据偏移量决定基准数据列表的覆盖元素,实际上是完成图 6-11 中的操作。
for al in alternate_price:
position = int(al.get('position'))
value = al.get('value')
# 判断位置的数值是否正整数
plus = True if position >= 0 else False
# 计算下标,以 16px 为基准
index = int(position / 16)
# 替换第一对<b>标签值列表中的元素,也就是完成值覆盖操作
base_price[index] = value
print(base_price)
最后将数据列表打印出来,得到的输出结果为:
['4', '6', '7']
['8', '7', '0', '5']
令人感到奇怪的是,输出结果中第一组票价数字与页面中显示的相同,但第二组却不同。这是因为第二架航班的票价基准数据有 4 个值。航班票价对应的 HTML 代码如下:
<em class="rel">
<b style="width:64px;left:-64px">
<i style="width: 16px;">8</i>
<i style="width: 16px;">3</i>
<i style="width: 16px;">9</i>
<i style="width: 16px;">5</i>
</b>
<b style="width: 16px;left:-32px">0</b>
<b style="width: 16px;left:-48px">7</b>
<b style="width: 16px;left:-16px">5</b>
</em>
覆盖操作是根据由偏移量计算得出的下标进行的,实际上就是列表元素的替换。当基准数据列表的元素数量超过包裹着 i 标签的 b 标签宽度时,我们就对列表进行切片,否则按照原来的替换规则进行。因此,需要对代码做一些调整。调整内容如下:
# 减号代表删除此行代码,加号代表新增代码
+ import re
- base_price = b1.css('i::text').extract()
+ b1_style = b1.css('b::attr("style")').get()
# 获得具体的位置
+ b1_width = ''.join(re.findall('width:(.*)px;', b1_style))
+ number = int(int(b1_width) / 16)
# 获取第 1 对 <b> 标签中的值(列表)
+ base_price = b1.css('i::text').extract()[:number]
如果列表中元素的数量超过标签宽度,那么后面的元素是不会显示的。比如 width:32px,每个标签占位宽度 16 px,那么即使 b 标签下有 5 个 i 标签(base_price=[1, 2 ,3 ,4 , 5]),在页面中也仅显示前面的两个数字。代码调整完毕后,再次运行代码。运行结果为:
['4', '6', '7']
['8', '7', '0', '5']
第二架航班的票价结果仍然跟页面显示的内容不同,但根据 CSS 宽度规则,我们之前分析的逻辑是正确的。为什么结果还是跟页面显示的不一样呢?
实际上并不是我们的逻辑和代码有错,而是页面显示错误。要注意的是,页面数据显示错误是常发生的事,我们只需要按照正确的逻辑编写代码即可。
6.2.2 去哪儿网反爬虫案例
去哪儿网是中国领先的在线旅游平台,覆盖全球 68 万余条航线,并与国内的旅游景点和航空公司进行了深度的合作。去哪儿网也有用到类似的反爬虫手段,我们一起来了解一下。
打开浏览器并访问 https://dwz.cn/d05zNKyq,页面内容如图 6-13 所示。
![3198b425ddd3f71cba9372319935bbd2.png](https://i-blog.csdnimg.cn/blog_migrate/31a1b590ec2fc7eed2ebeaea801f4192.jpeg)
图 6-13 去哪儿网航班信息
航班票价对应的 HTML 代码如图 6-14 所示。
![f198423693f1c524ec5e28c57cd5bbf5.png](https://i-blog.csdnimg.cn/blog_migrate/db79926b01721539b11a9913b140b6b6.jpeg)
图 6-14 去哪儿网航班票价 HTML 代码
去哪儿网航班票价所对应的 HTML 代码结构和 CSS 与我们在示例 5 中见到的类似。我们可以大胆猜测,去哪儿网航班票价的显示规律与示例 5 中所用的方法也是类似的,感兴趣的同学可以按照 6.2.1 节的思路进行票价推算。去哪儿网航班票价中第 1 对 b 标签下的 i 标签数量与 width 是相匹配的,并未出现显示错误的问题。
6.2.3 小结
CSS 样式可以改变页面显示,但这种“改变”仅存在于浏览器(能够解释 CSS 的渲染工具)中,即使爬虫工程师借助渲染工具,也无法获得“见到”的内容。