在爬虫时,我们会经常遇到一些反爬虫的例子,网站上通过反爬虫便使得我们无法获取真实的数据信息,有兴趣的同学可以看下这篇文章
(点我呀)
,其中介绍了多种的反爬虫和对应的策略。
在大多数数据较多的网站中,其经常会使用CSS反爬虫机制来阻止我们对其中信息的访问,因此想要获取我们需要的数据信息,就必须要对这种反爬虫加密进行破解。我们以大众点评中的点评数量为例,来讲解一下其具体的破解方法。
我们首先在浏览器种打开大众点评的一个链接(
http://www.dianping.com/nanjing/ch10/g34014
),并打开控制台定位到评论数处,从控制台中可以看出我们所需要的数据并没有全部显示出来,只是显示部分,其余的则是一串css代码。
在上图的一串css代码中便藏着我们想要的数据,因此我们有必要对这段css代码进行分析。
在上图中我们可以看到其中一个class的样式‘.vdk4iq’(见编号1),其中只有一个background,所以我们可以猜测我们需要的信息必然在这两个像素值当中。
当然,在一个网页中加密数据所用的class样式肯定不相同,因此我们需要将所有的class样式的像素值给保存下来。那我们如何去从源码中获取样式中的像素值呢?在网页源码中存在着很多的css后缀的文件地址(如下图),而我们只需要得到有着加密数据css样式的文件地址即可,上图中的编号2即为我们需要的地址,对于该地址我们可以在网页源码中通过正则表达式得到。
我们打开所得到的css地址(我们这里为:
http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/7d654bba0e3d1b0a8dc5bbf352344a69.css
)可以看到一串串的css样式,看不懂没关系,只要会把‘background’里的像素值会通过正则表达式提出出来就行了。
回到我们一开始的图上,我们会发现加密的数据所使用的css样式名称有一段前缀是相同的,如下,
同样在上面有很多编号的图中,我们可以发现编号3中也有这个公共前缀‘vdk’(每个店铺的前缀不一定相同),而同样,在编号3下面发现有个编号4,其是一个链接,我们点开看看
上面有很多数字,我们所要的真实数据便包含在其中。首先可以发现3个text标签中的x值都是一样的,显然不需要这个值来获取真实数据。上图中标签中的y值是一个控制阈值,相当于数学上的区间(上图中的三个数字区间为[0,48),[48,98),[98,146))。而那一大串的数字中则藏着我们需要的真实数据。
还记得在一开始的时候所得到的css样式代码中的background的两个像素值,我们在这里将他写成元组的形式(-67,-122)(
其对应的真实数据为3
)。
(敲黑板)重点来了。我们将上面所得到的像素元组取其绝对值(67,122)。我们先将第一个值看作是控制阈值的部分,67的话则是在第二个区间内,因此选用上图中的第二段数字串,那122怎么用呢?我们都知道这个加密的数据是一个数,因此可以通过索引的方式来获取数字串中所对应的数字,那问题又来了,怎么才能获得索引号呢?再上图中,仔细观察的话会发现还有一个以px结尾的数字(之前的background的数字也是px为单位),其是对应的font-size,为12,我们将122除以12,可以得到122/12=10.167,我们分别向上和向下元整,得到索引为11和12(索引号需要加1),则对应的第10个或第11个数字,我们数一下可以知道其对应的数据为1或3,哈哈,巧了,得到了一个真实的数据3,(笑哭,这太巧了,随便找一个这样都能碰上,这太巧了,真实的数据破解过程不是这样的)。其实,事实是这样的,第二个像素值122是控制阈值部分,用来决定选择第几段数据串,第一个像素值67用来决定选用数据串中的第几个数字(
向上元整
)。有兴趣的同学可以多选几组数据来验证一下(我这上面那个太巧了,笑尿)。
现在将大众点评的CSS加密破解过程总结如下(其他网站的CSS加密的破解方法类似)
- 获取所要抓取的网页源码
- 找到加密数据所使用的css代码地址,获取网页中所有加密数据所使用的css样式的像素值元组的映射
- 根据加密数据所使用的css样式名的公共前缀来获取加密数据所使用的svg文件的地址
- 通过2中得到的像素值元组的第二个元素来决定选用svg文件中的第几个数字串(根据阈值的范围)
-
将2中像素值元组中的第一个元素除以svg文件源码中font-size所对应的值,并向上元整,获取索引号(
需加1
) - 根据5中的索引号在4中确定的数字串中确定我们所需要的真实数字
- 将获取的数字替换显示
完整源码如下:
import requests
import re
import functools
from pyquery import PyQuery as pq
from lxml import etree
import math
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.96 Mobile Safari/537.36'
}
def try_except_decorate(f):
"""
报错装饰器函数,用于其他函数的报错装饰器
:param f: 被装饰的函数
:return: 若没有异常的话返回被装饰函数的执行结果
"""
@functools.wraps(f)
def func(*args):
try:
return f(*args)
except Exception as e:
print('==== insert exception -->>> %s'% e)
return func
@try_except_decorate
def download_html(url):
"""
根据所提供的url将网页源码下载下来
:param url: 要下载的链接url
:return: 得到的网页源码
"""
response = requests.get(url, headers = HEADERS)
if response.status_code != 200:
raise Exception('Web page url:%s download failed, please try again'% url)
else:
return response.content.decode('utf-8')
def get_common_prefix(tags_list):
"""
获取列表中所有字符串的最长公共前缀
:param tags_list: 所有加密数据所使用的class对应的值(如'vdkeks')组成的列表
:return: 最长公共前缀
"""
pre = ''
if not tags_list:
return ''
for x, y in zip(min(tags_list), max(tags_list)):
if x == y:
pre += x
else:
return pre
return pre
@try_except_decorate
def get_cssurl_tags_svgurl(content):
"""
抓取加密数据所使用的css链接,分析当前网页中加密数据所使用的class所对应的值的公共前缀,并根据css地址以及公共前缀来获取svg的地址
需要注意的是,不知道大众点评里面有什么机制,我在爬取css链接的时候有时候会失败,需要多尝试几次,也有可能是因为我爬的次数多了
:param content: 所要爬取的数据的url的网页源码
:return:
"""
search_css_url = re.search(r'([^"]+svgtextcss[^"]+)', content, re.M) # 匹配css地址
if search_css_url:
css_url = 'https:' + search_css_url[0]
else:
raise Exception("css_url not found, please check or try again")
doc = pq(content)
tags_list = [tag.attr('class') for tag in doc('div.comment a.review-num b svgmtsi').items()] # 所有加密数字所使用的class标签对应的值
tag_value = {} # 用于存放所有加密数据使用的class标签属性其对应的像素值,例如'{'vdkxo1': [('-43.0', '-122.0')]}'
css_content = download_html(css_url)
for tag in set(tags_list): # 利用集合去重
result = re.findall(r'%s{background:(.*?)px (.*?)px;}'%tag, css_content, re.M)
tag_value[tag] = result
common_prefix = get_common_prefix(set(tags_list)) # 获取所有加密class属性value的公共前缀,通过公共前缀来寻找svg文件的地址
svg_url = re.findall(r'\[class\^="%s"\].*?background\-image: url\((.*?)\);' % common_prefix, css_content)
return (tag_value, 'https:' + svg_url[0])
@try_except_decorate
def get_svg_num_threshold(svg_url):
"""
获取svg文件所对应的数字序列和阈值映射
:param svg_url: svg文件链接
:return: 对应的数字序列和阈值映射
"""
content = download_html(svg_url)
num_str_to_threshold_range = {}
start = 0
for result in re.findall(r'y="(.*?)">(.*?)</text>', content, re.M):
num_str_to_threshold_range[result[1]] = range(start, int(result[0]) + 1) # 加密数据所使用的svg文件的映射
start = int(result[0])
return num_str_to_threshold_range
@try_except_decorate
def get_comment_count(restaurants, tag_value, num_str_to_threshold_range):
"""
用来获取真实的评论数数据信息,由于在数据显示时,有的显示为真实数据,有的是由css加密的,因此我们需要通过xpath对类型进行判断,
判断其是字符串类型,还是元素类型
对上述逐一判断得到的数据,想到的有两种方法进行组合
1.将数据逐一放入列表中,最后再进行转化(使用join方法),再转化为int类型
2.使用数学公式进行计算,x = x * 10 + y, 其中y为解密后的数字,x为当前的累计值
在这里我们使用的是第二种方法
:param restaturants: 所要抓取网页中的所有商铺
:param tag_value: 所要抓取网页中加密数字所使用的所有css样式映射
:param num_str_to_threshold_range: 获取的svg文件中的数字串及阈值映射
:yield: 店铺名以评论数
"""
for restaurant in restaurants:
restaurant_name = restaurant.xpath('.//div[@class="tit"]/a')[0].attrib["title"] # 店名
comment_num = 0 # 评论初始值
comment_datas = restaurant.xpath('.//div[@class="comment"]')
for comment_data in comment_datas:
comment_data_nodes = comment_data.xpath('a[@class="review-num"]/b/node()')
for node in comment_data_nodes:
if isinstance(node, etree._Element): # 判断是否是元素类型
node_class = node.get("class")
offset, position = tag_value[node_class][0] # offset为真实数据的偏移值,position为真实数据所用的数据源的位置
index, position = abs(float(offset)), abs(float(position))
for key, value in num_str_to_threshold_range.items(): # 根据阈值寻找所使用的数字串
if position in value:
comment_num = comment_num * 10 + int(key[int(math.ceil(index/12)) - 1]) # 得到解密后的数字进行计算
else:
comment_num = comment_num * 10 + int(node) # 字符串直接计算
yield {'restaurant_name': restaurant_name, 'comment_num': comment_num}
def main(url):
"""
爬虫入口,用来调用其他函数,获取网页源码,css代码名映射,svg文件地址链接以及获取解密后的数据信息
:param url: 所要爬取的链接
:return None
"""
content = download_html(url)
tag_value, svg_url = get_cssurl_tags_svgurl(content)
num_str_to_threshold_range = get_svg_num_threshold(svg_url)
html = etree.HTML(content, etree.HTMLParser())
restaurants = html.xpath('//div[@id="shop-all-list"]/ul/li')
for result in get_comment_count(restaurants, tag_value, num_str_to_threshold_range):
print(result)
if __name__ == '__main__':
url = 'http://www.dianping.com/nanjing/ch10/g34014'
main(url)
部分结果显示为:
在上面代码中我把异常处理都放在装饰器当中了,为避免写太多的代码(就是懒)。
具体的一些过程都写在注释里了。
参考地址:
https://cuiqingcai.com/6341.html
好好学习,天天向上。