CSS反爬虫 大众点评

  • Post author:
  • Post category:其他


在爬虫时,我们会经常遇到一些反爬虫的例子,网站上通过反爬虫便使得我们无法获取真实的数据信息,有兴趣的同学可以看下这篇文章

(点我呀)

,其中介绍了多种的反爬虫和对应的策略。

在大多数数据较多的网站中,其经常会使用CSS反爬虫机制来阻止我们对其中信息的访问,因此想要获取我们需要的数据信息,就必须要对这种反爬虫加密进行破解。我们以大众点评中的点评数量为例,来讲解一下其具体的破解方法。

我们首先在浏览器种打开大众点评的一个链接(

http://www.dianping.com/nanjing/ch10/g34014

),并打开控制台定位到评论数处,从控制台中可以看出我们所需要的数据并没有全部显示出来,只是显示部分,其余的则是一串css代码。

在这里插入图片描述

在上图的一串css代码中便藏着我们想要的数据,因此我们有必要对这段css代码进行分析。

在这里插入图片描述

在上图中我们可以看到其中一个class的样式‘.vdk4iq’(见编号1),其中只有一个background,所以我们可以猜测我们需要的信息必然在这两个像素值当中。

当然,在一个网页中加密数据所用的class样式肯定不相同,因此我们需要将所有的class样式的像素值给保存下来。那我们如何去从源码中获取样式中的像素值呢?在网页源码中存在着很多的css后缀的文件地址(如下图),而我们只需要得到有着加密数据css样式的文件地址即可,上图中的编号2即为我们需要的地址,对于该地址我们可以在网页源码中通过正则表达式得到。

图1

我们打开所得到的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加密的破解方法类似)

  1. 获取所要抓取的网页源码
  2. 找到加密数据所使用的css代码地址,获取网页中所有加密数据所使用的css样式的像素值元组的映射
  3. 根据加密数据所使用的css样式名的公共前缀来获取加密数据所使用的svg文件的地址
  4. 通过2中得到的像素值元组的第二个元素来决定选用svg文件中的第几个数字串(根据阈值的范围)
  5. 将2中像素值元组中的第一个元素除以svg文件源码中font-size所对应的值,并向上元整,获取索引号(

    需加1

    )
  6. 根据5中的索引号在4中确定的数字串中确定我们所需要的真实数字
  7. 将获取的数字替换显示

完整源码如下:

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

好好学习,天天向上。



版权声明:本文为qq_42293758原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。