前言
本次来解锁新姿势——CSS字体反爬。
在解决这个字体反爬的路上,当我以为解决这个反爬手段的时候,
最后验证总的答案的时候,被打脸了!!!
又被默默设埋伏了,踩了一个坑,巨大的,为何悲伤辣么大 <(-︿-)>
不将html源码页面下载下来还真发现不了在哪写错了,不多说,赶紧来看一下呗~~
0x01、分析目标网站
-
还是同样的手段,打开F12进行选中数字,查看它的标签内容是什么
很明显,看了三个标签,只有第一个是对上的,其他两个是对不上的,难道是所有页面都是第一个对上,后面数字都打乱?并不是的,经过多次请求发现,每次都是随机打乱的,打乱的看起来好像没有规则。 -
那我也随便找找,看看有什么突破口不,首先去找id属性去全局搜索,看一下有什么相关内容
出现关联的数据如下:
-
出现的这两个东西,突然不知道是啥,那我就去谷歌一下呗,发现这两个是css知识里面的东西,又涨知识了
Tips:这些属性在文末有提到,可以翻到后面一起对照看
- 竟然没有什么突破口,那我继续搜索多几个看看
- 看,这个content:"202"不就是我们想要的吗,太爽了,终于找到一点点突破口了,谷歌百度一下这个:before你会发现它是CSS里面的属性
-
再继续找找其他的数字看看,比如看到这个数字128:
-
搜索这个属性看看,又发现了两个属性:position、left。position顾名思义就是位置的属性,用relative表示,翻译过来就是相对位置的意思
跟前端展示的综合起来,大概的意思就是:相对的位置,然后left表示偏移的方向及偏移多少,然后最终才是我们肉眼在前端看到的正确位置的数字,那开始造起来呗,just do IT!
0x02、核心代码
(1)、下面代码就是核心判断字体是否是出现偏移或者是before属性的类型
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属性
- 这次又发现了一个属性,opacity属性,这个翻译过来是“不透明度”的意思,点击我进行了解
查看介绍,大概明白了,html源码可以通过设置元素的透明度,来达到前端肉眼是否看到的效果,如果设置为0则表示完全透明,为1则表示完全不透明
- 下面我通过修改html源码的opacity属性进行剖析,发现设置opacity属性为0和1是得到不同的结果
那为什么是重叠的呢?其实刚刚搜索出来的结果还有一个属性,叫margin-right,这个属性在CSS里面用来设置边距的,那么我们将原来的属性注释掉或者修改为0看看得到什么结果:
(3)、小结
- 来小结一下前面几个属性
如果不是很明白那些属性,那么去谷歌或者百度一下,看看这些都是代表什么含义
属性 | 含义 |
---|---|
width:2em | 是字体宽度大小。 它是描述相对于应用在当前元素的字体尺寸,所以它也是相对长度单位。一般浏览器字体大小默认为16px,则2em == 32px;详解 |
float:left | 把图像向左浮动,详解 |
:before { content “202” } | :before 选择器向选定的元素前插入内容,使用content 属性来指定要插入的内容,详解 |
left:-2em | 把当前元素向右移-2em单位,即等于向左移2em单位,详解 |
opacity | 透明度属性,取值范围为0-1,。为0表示完全透明,为1表示完全不透明,详解 |
margin | 外边距,详解 |
总结一下这种CSS反爬手段解决顺序:
- 首先判断元素是否透明:当我们遇到存在opacity属性为0的就是可以忽略它,不是0的就继续判断后面的
- 判断其大致规律性,如下(根据不同网站来设置,本文教程的大致规律性不一定符合所有CSS反爬手段的网站,需要适当调整一下):
div标签长度为3的可能:乱序的,或者顺序的,或者第一个标签是透明的,真正展示的应该是两位数字的;
div标签长度为4的可能:有一个标签是不展示的,剩下三个标签是乱序的或者是顺序的(这个使用属于长度为3的情况去判断)
div标签长度为2的可能:目前发现的,只有包含:before的div标签是可用的,另外一个标签的内容是可以忽略的
由于本文教程是,最后才发现opacity属性的,所以我代码里面并不是第一步判断opacity属性,但是我在总结的时候,个人推荐,先判断opacity属性,如果为透明的话,都可以直接跳过那个元素了
(4)、最终代码
#!/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逆向目前遇到的知识点集合;
爬虫js解密分析:某某猫小说;
爬虫js解密分析:某某云文学;
个人总结-js逆向解析思路;
打赏喝咖啡
你的支持就是我的动力,感谢感谢