一个在IT行业摸爬打滚的程序猿

0%

CSS字体反爬实战,10分钟就能学会

前言
本次来解锁新姿势——CSS字体反爬。
1
2
3
4
在解决这个字体反爬的路上,当我以为解决这个反爬手段的时候,
最后验证总的答案的时候,被打脸了!!!
又被默默设埋伏了,踩了一个坑,巨大的,为何悲伤辣么大 <(-︿-)>
不将html源码页面下载下来还真发现不了在哪写错了,不多说,赶紧来看一下呗~~
0x01、分析目标网站
  1. 还是同样的手段,打开F12进行选中数字,查看它的标签内容是什么
    在这里插入图片描述
    很明显,看了三个标签,只有第一个是对上的,其他两个是对不上的,难道是所有页面都是第一个对上,后面数字都打乱?并不是的,经过多次请求发现,每次都是随机打乱的,打乱的看起来好像没有规则。

  2. 那我也随便找找,看看有什么突破口不,首先去找id属性去全局搜索,看一下有什么相关内容
    在这里插入图片描述
    出现关联的数据如下:
    在这里插入图片描述
    在这里插入图片描述

  3. 出现的这两个东西,突然不知道是啥,那我就去谷歌一下呗,发现这两个是css知识里面的东西,又涨知识了

Tips:这些属性在文末有提到,可以翻到后面一起对照看

  1. 竟然没有什么突破口,那我继续搜索多几个看看
    在这里插入图片描述
    在这里插入图片描述
  • [x] 看,这个content:”202”不就是我们想要的吗,太爽了,终于找到一点点突破口了,谷歌百度一下这个:before你会发现它是CSS里面的属性
  1. 再继续找找其他的数字看看,比如看到这个数字128:
    在这里插入图片描述

  2. 搜索这个属性看看,又发现了两个属性:position、left。position顾名思义就是位置的属性,用relative表示,翻译过来就是相对位置的意思
    在这里插入图片描述
    跟前端展示的综合起来,大概的意思就是:相对的位置,然后left表示偏移的方向及偏移多少,然后最终才是我们肉眼在前端看到的正确位置的数字,那开始造起来呗,just do IT

0x02、核心代码
(1)、下面代码就是核心判断字体是否是出现偏移或者是before属性的类型
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
import re


def parse_offset(div_list, response_str):
"""提取那种是偏移量的情况"""
if len(div_list) == 4:
real_div = div_list[1:]
num = ["0", "0", "0"]
else:
real_div = div_list
if len(real_div) == 3:
num = ["0", "0", "0"] # 用来存储正确位置的数值,最后将它们用字符串连接起来,再转化为int
elif len(real_div) == 2:
num = ["0", "0"]
elif len(real_div) == 1:
num = ["0"]
print("---特殊情况长度为:{},直接就是原来标签内容real_div:{}".format(len(div_list), real_div))
else:
print("----严重致命错误,real_div长度匹配不上,请检查.len(real_div):{}".format(len(real_div)))
num = []
pattern_left = r"(?<=.%s { left:)(.*?)(?=em)" # 提取位移值

for index, div in enumerate(real_div):
class_name = div.xpath("./@class").extract_first("")
content = div.xpath("./text()").extract_first("") # 提取数字
is_left = re.search(pattern_left%class_name, response_str, re.S) # 偏移情况

if not is_left: # 顺序完全一致的情况下,可以添加
num[index] = content
continue
else: # 数值出现异位情况
left_num = int(is_left.group()) if is_left else None
if not left_num:
print("在匹配到位置的re对象不为空的情况下,为啥还是取不到位移值?请检查是否出现错误.class_name:{} num:{} re对象left_num:{}".format(class_name, num,left_num))
raise ValueError("获取偏移量的时候,出现异常,理论上这里是不会触发的,因为前面已经判断是已经有值了,所有这里是不会出现这种情况的")
num[index + left_num] = content
result = "".join(num)
print("当前标签匹配最终结果如下 num:{} real_div:{} div_list:{}".format(num, real_div, div_list))
return int(result)


def parse_numbers(div_html, response_str):
"""
:param div_html: 传进来的xpath对象,div[@class='col-md-1']
:param response_str: html源码,也就是response.text
:return:int,返回当前div标签的真正数值
"""
div_list = div_html.xpath("./div") # 获取div[@class='col-md-1']下的所有div标签

if len(div_list) < 3: # todo: 长度小于3的(1和2),若为2,那么提取第二个标签的值
div = div_list[-1] # 取最后一个. 长度为1和2都是适用的
class_name = div.xpath("./@class").extract_first("")
pattern = "(?<=.%s:before { content:\")(.*?)(?=\")" % class_name # .sklgp0fhDg:before { content:"116" } 这种情况就是
num = re.search(pattern, response_str, re.S)
num = num.group() if num else None
if not num: # 当取不到值的时候,这里默认此时两位数的情况跟三位数的情况是一直的,就是出现位移的情况
num = parse_offset(div_list=div_list, response_str=response_str)
return int(num)
elif len(div_list) == 3 or len(div_list) == 4: # todo: 长度为3的,这里是直接通过计算便宜量,复原数字;长度为4的就是直接就是有个div不展示
num = parse_offset(div_list=div_list, response_str=response_str)
return num
else:
print("长度找不到匹配项.当前长度为:{} div_list:{}".format(len(div_list), div_list))
raise ValueError("长度找不到匹配项.当前长度为:{} div_list:{}".format(len(div_list), div_list))

到此,我==以为==我是用正确的姿势==解决==这种问题了,可惜==并不是!!并不是!!!==
这中间我通过一边下载html源码一边慢慢核对前端展示的数值跟代码返回的是否一样的时候,当我发现有些两位数的数值出现匹配错误的情况,匹配出来是3位数,这到底为什么呢

(2)、下面将继续来看一下到底是为什么这样?

将这种特殊两位数的,匹配出三位数的情况,进行分析,搜索id属性
在这里插入图片描述

  1. 这次又发现了一个属性,==opacity属性==,这个翻译过来是“不透明度”的意思,点击我进行了解

查看介绍,大概明白了,html源码可以通过设置元素的透明度,来达到前端肉眼是否看到的效果,如果设置为0则表示完全透明,为1则表示完全不透明

  1. 下面我通过修改html源码的opacity属性进行剖析,发现设置opacity属性为0和1是得到不同的结果
    在这里插入图片描述
    在这里插入图片描述
    那为什么是重叠的呢?其实刚刚搜索出来的结果还有一个属性,叫==margin-right==,这个属性在CSS里面用来设置边距的,那么我们将原来的属性注释掉或者修改为0看看得到什么结果:
    在这里插入图片描述
    (3)、小结
    • [x] 来小结一下前面几个属性
      如果不是很明白那些属性,那么去谷歌或者百度一下,看看这些都是代表什么含义
属性 含义
width:2em 是字体宽度大小。 它是描述相对于应用在当前元素的字体尺寸,所以它也是相对长度单位。一般浏览器字体大小默认为16px,则2em == 32px;详解
float:left 把图像向左浮动,详解
:before { content “202” } :before 选择器向选定的元素前插入内容,使用content 属性来指定要插入的内容,详解
left:-2em 把当前元素向右移-2em单位,即等于向左移2em单位,详解
opacity 透明度属性,取值范围为0-1,。为0表示完全透明,为1表示完全不透明,详解
margin 外边距,详解

总结一下这种CSS反爬手段解决顺序:

  1. 首先判断元素是否透明:当我们遇到存在opacity属性为0的就是可以忽略它,不是0的就继续判断后面的
  2. 判断其大致规律性,如下(根据==不同网站==来设置,本文教程的大致规律性不一定符合所有CSS反爬手段的网站,需要==适当调整一下==):
     div标签长度为3的可能:乱序的,或者顺序的,或者第一个标签是透明的,真正展示的应该是两位数字的;
     div标签长度为4的可能:有一个标签是不展示的,剩下三个标签是乱序的或者是顺序的(这个使用属于长度为3的情况去判断)
     div标签长度为2的可能:目前发现的,只有包含:before的div标签是可用的,另外一个标签的内容是可以忽略的
    

由于本文教程是,最后才发现opacity属性的,所以我代码里面并不是第一步判断opacity属性,但是我在总结的时候,个人推荐,先判断opacity属性,如果为透明的话,都可以直接跳过那个元素了

(4)、最终代码
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time : 2019/8/20 13:34
# @Author : qizai
# @File : crawl_decode_css.py
# @Software: PyCharm
"""
# .jOjM12plqO {position: relative} # 存在这种情况就是表示,是相对的位置,如果没有这个属性则表示不用调整位置
# .jOjM12plqO {float: left} # 或者说,存在这种情况也可以是不用发生位移的,就是原位
# .jOjM12plqO {left: 1em} # 存在这种情况就是,需要发生位移的,也可以使用这个来判断是否需要位移
# .jOjM12plqO {width: 1em} # 这个是没有参考价值的
# .huD10lVv { opacity:0 } # 这个神坑的属性,是叫透明度,取值范围为0-1,为0表示完全透明,为1表示完全不透明,可为小数
"""

import re
import requests
from scrapy import Selector


def parse_offset(div_list, response_str):
"""这里是单独的提取出来的,专门是提取那种是偏移量的情况"""
if len(div_list) == 4:
real_div = div_list[1:]
num = ["0", "0", "0"]
else:
real_div = div_list
if len(real_div) == 3:
num = ["0", "0", "0"] # 用来存储正确位置的数值,最后将它们用字符串连接起来,再转化为int
elif len(real_div) == 2:
num = ["0", "0"]
elif len(real_div) == 1:
num = ["0"]
else:
print("----严重致命错误,real_div长度匹配不上,请检查.len(real_div):{}".format(len(real_div)))
num = []
pattern_opacity = r".%s { opacity:0 }" # 出现opacity:0的表示完全透明,这时候不用理会这个字
pattern_left = r"(?<=.%s { left:)(.*?)(?=em)" # 提取位移值

for index, div in enumerate(real_div):
# print("当前标签的num:{} index:{} div:{} real_div:{} div_list:{}".format(num, index, div, real_div, div_list))
class_name = div.xpath("./@class").extract_first("")
content = div.xpath("./text()").extract_first("") # 提取数字
is_opacity = re.search(pattern_opacity % class_name, response_str, re.S) # 是否为透明状态
is_left = re.search(pattern_left%class_name, response_str, re.S) # 偏移情况

# 后来新增透明度判断
if is_opacity:
print("匹配到为完全透明的情况,请检查是否真的是完全透明,当前的class_name:{} num:{} num[{}]='-'".format(class_name, num, index))
num[index] = "-"
continue

if not is_left: # 顺序完全一致的情况下,可以添加
print("匹配不到偏移量,请检查是否真的是出现不偏移的情况,当前的class_name:{} num:{} num[{}]={}".format(class_name, num, index, content))
num[index] = content
continue
else: # 数值出现异位情况
left_num = int(is_left.group()) if is_left else None
if not left_num:
print("在匹配到位置的re对象不为空的情况下,为啥还是取不到位移值?请检查是否出现错误.class_name:{} num:{} re对象left_num:{}".format(class_name, num,left_num))
raise ValueError("获取偏移量的时候,出现异常,理论上这里是不会触发的,因为前面已经判断是已经有值了,所有这里是不会出现这种情况的")
num[index + left_num] = content
last_num = [i for i in num if i!="-"]
result = "".join(last_num)
print("当前标签匹配最终结果如下 num:{} real_div:{} div_list:{}".format(num, real_div, div_list))
return int(result)


def parse_numbers(div_html, response_str):
"""
:param div_html: 传进来的xpath对象,div[@class='col-md-1']
:param response_str: html源码,也就是response.text
:return:int,返回当前div标签的真正数值
"""
div_list = div_html.xpath("./div") # 获取div[@class='col-md-1']下的所有div标签

if len(div_list) < 3: # todo: 长度小于3的(1和2),若为2,那么提取第二个标签的值
div = div_list[-1] # 取最后一个. 长度为1和2都是适用的
class_name = div.xpath("./@class").extract_first("")
pattern = "(?<=.%s:before { content:\")(.*?)(?=\")" % class_name # .sklgp0fhDg:before { content:"116" } 这种情况就是
num = re.search(pattern, response_str, re.S)
num = num.group() if num else None
if not num: # 当取不到值的时候,这里默认此时两位数的情况跟三位数的情况是一直的,就是出现位移的情况
num = parse_offset(div_list=div_list, response_str=response_str)
print("[flag=2] num:{} 当前div长度小于3,但是处理的却是按照3/4的处理,返回的结果情况前面".format(num))
return int(num)
elif len(div_list) == 3 or len(div_list) == 4: # todo: 长度为3的,这里是直接通过计算便宜量,复原数字;长度为4的就是直接就是有个div不展示
num = parse_offset(div_list=div_list, response_str=response_str)
print("len(div_list) == 3 or len(div_list) == 4的返回值num:{}".format(num))
return num
else:
print("长度找不到匹配项.当前长度为:{} div_list:{}".format(len(div_list), div_list))
raise ValueError("长度找不到匹配项.当前长度为:{} div_list:{}".format(len(div_list), div_list))


def get_html():
url = "xxxx"
resp = requests.get(url)
selec = Selector(resp)
div_html = selec.xpath('//div[@class="col-md-1"]')
for one in div_html:
real_num = parse_numbers(div_html=one, response_str=resp.text)
print("解析之后真正的num为:{}".format(real_num))


if __name__ == '__main__':
get_html()
至此本文教程写完了,希望能够帮助到各位在爬虫路上的小伙伴们,觉得不错点个赞呗
感谢认真读完这篇教程的您

先别走呗,这里有可能有你需要的干货文章:

woff字体反爬实战,10分钟就能学会
爬虫:js逆向目前遇到的知识点集合