0%

揭秘去哪儿网在用的 CSS 偏移反爬虫手段!

内容选自即将出版的《Python3 反爬虫原理与绕过实战》,本次公开书稿范围为第 6 章——文本混淆反爬虫。本篇为第 6 章中的第 2 小节,第 3、4 小节已发,直达链接:

  • 一线大厂在用的反爬虫手段,看我破!
  • 用前考虑清楚,伤敌一千自损八百的字体反爬虫

其余小节将逐步放送

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 所示。 图 6-4 示例 5 页面 在编写 Python 代码之前,我们需要确定目标数据的元素定位。航空公司名称元素定位如图 6-5 所示。 图 6-5 航空公司名称元素定位结果 航空公司名称包裹在没有属性的 span 标签中,但该 span 标签包裹在 class 属性为 air g-tips 的 div 标签中。接下来我们看一下航站名称的元素定位,定位结果如图 6-6 所示。 图 6-6 航站名称元素定位结果 航站名称包裹在没有属性的 h2 标签中,h2 标签包裹在 class 为 sep-lf 的 div 标签中。 我们再看一下票价的元素定位,定位结果如图 6-7 所示。 图 6-7 票价的元素定位结果 页面中显示的票价为 467,但是在网页中却有两组不同的数字,其中一组是[7, 7, 7],而另一组是 [6, 4],这看起来就有点奇怪了。 难道是网页显示有问题? 按照正常排序来说,这架航班的票价应该是 77 764 才对。我们可以查看第二架航班信息的价格,思考是网页显示问题还是做了什么反爬虫措施。第二架航班的票价元素定位结果如图 6-8 所示。 图 6-8 第二架航班的票价元素定位结果 结果与第一架航班的票价显示有同样的问题:网页显示内容和 HTML 代码中的内容不一致。我们分析一下 HTML 代码,看一看是否能找到什么线索。第一架航班票价的 HTML 代码为:

1
2
3
4
5
6
7
8
9
10
11
<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 所示。 图 6-9 数字组合推测 5 个数字的组合结果太多了,我们必须找出其中的规律,这样就能知道网页为什么显示 467 而不是 764 或者 776 。在仔细查看过后,发现每个带有数字的标签都设定了样式。第 1 对 b 标签的样式为:

1
width:48px;left:-48px

第 2 对 b 标签的样式为:

1
width: 16px;left:-32px

第 3 对 b 标签的样式为:

1
width: 16px;left:-48px

i 标签对的样式是相同的,都是:

1
width: 16px;

另外,还注意到最外层的 span 标签对的样式为:

1
width:48px

如果按照 CSS 样式这条线索来分析的话,第 1 对 b 标签中的 3 对 i 标签刚好占满 span 标签对的位置,其位置如图 6-10 所示。 图 6-10 span 标签对和 i 标签对位置图 此时网页中显示的价格应该是 777,但是由于第 2 和第 3 对 b 标签中有值,所以我们还需要计算它们的位置。此时标签位置的变化如图 6-11 所示。 图 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。 图 6-12 覆盖结果 根据结果来看这种算法是合理的,不过我们还需要对其进行验证,现在将第二架航班的 HTML 值 和 CSS 样式按照这个规律进行推算。最后推算得到的结果与页面显示结果相同,说明这个位置偏移的计算方法是正确的,这样我们就可以编写 Python 代码获取网页中的票价信息了。因为 b 标签包裹在 class 属性为 rel 的 em 标签下,所以我们要定位所有的 em 标签。对应的 Python 代码如下:

1
2
3
4
5
6
7
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 代码如下:

1
2
3
4
5
6
7
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 代码如下:

1
2
3
4
5
6
7
8
9
10
11
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 中的操作。

1
2
3
4
5
6
7
8
9
10
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)

最后将数据列表打印出来,得到的输出结果为:

1
2
['4', '6', '7'] 
['8', '7', '0', '5']

令人感到奇怪的是,输出结果中第一组票价数字与页面中显示的相同,但第二组却不同。这是因为第二架航班的票价基准数据有 4 个值。航班票价对应的 HTML 代码如下:

1
2
3
4
5
6
7
8
9
10
11
<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 标签宽度时,我们就对列表进行切片,否则按照原来的替换规则进行。因此,需要对代码做一些调整。调整内容如下:

1
2
3
4
5
6
7
8
9
# 减号代表删除此行代码,加号代表新增代码
+ 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]),在页面中也仅显示前面的两个数字。代码调整完毕后,再次运行代码。运行结果为:

1
2
['4', '6', '7'] 
['8', '7', '0', '5']

第二架航班的票价结果仍然跟页面显示的内容不同,但根据 CSS 宽度规则,我们之前分析的逻辑是正确的。为什么结果还是跟页面显示的不一样呢? 实际上并不是我们的逻辑和代码有错,而是页面显示错误。要注意的是,页面数据显示错误是常发生的事,我们只需要按照正确的逻辑编写代码即可。

6.2.2 去哪儿网反爬虫案例

去哪儿网是中国领先的在线旅游平台,覆盖全球 68 万余条航线,并与国内的旅游景点和航空公司进行了深度的合作。去哪儿网也有用到类似的反爬虫手段,我们一起来了解一下。 打开浏览器并访问 https://dwz.cn/d05zNKyq,页面内容如图 6-13 所示。 图 6-13 去哪儿网航班信息 航班票价对应的 HTML 代码如图 6-14 所示。 图 6-14 去哪儿网航班票价 HTML 代码 去哪儿网航班票价所对应的 HTML 代码结构和 CSS 与我们在示例 5 中见到的类似。我们可以大胆猜测,去哪儿网航班票价的显示规律与示例 5 中所用的方法也是类似的,感兴趣的同学可以按照 6.2.1 节的思路进行票价推算。去哪儿网航班票价中第 1 对 b 标签下的 i 标签数量与 width 是相匹配的,并未出现显示错误的问题。

6.2.3 小结

CSS 样式可以改变页面显示,但这种“改变”仅存在于浏览器(能够解释 CSS 的渲染工具)中,即使爬虫工程师借助渲染工具,也无法获得“见到”的内容。

新书福利

真是翘首以盼!《Python3 反爬虫原理与绕过实战》一书终于要跟大家见面了!为了感谢大家对韦世东和本书的期待与支持,在新书发布时会举办多场送书活动和限时折扣活动。 想要与作者韦世东交流或者参加新书发布活动的朋友可以扫描二维码进群与我互动哦!

转载说明

本篇内容摘自出版图书《Python3 反爬虫原理与绕过实战》,欢迎各位好友与同行转载! 记得带上相关的版权信息哦😊。