代码解析
- 获取页面数据
- 使用xpath来获取用户评论信息
- 解密特殊标签
- 获取文件, 提取css类
- 获取文字坐标
- 生成对照表
- 保存对照信息
- 进行文字替换
- 将用户和评论进行对应
- 结语
20-8-28: 竟然更新了6位css类名, 不过稍微修改下正则就可以了
上一篇的链接
https://blog.csdn.net/weixin_43553295/article/details/108242732
获取页面数据
首先写一个简单的爬虫, 来获取页面数据
class DaZhongDianPing:
def __init__(self):
self.s = requests.session()
self.url = "http://www.dianping.com/shop/k9oYRvTyiMk4HEdQ/review_all"
self.headers = {
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
"Cookie": "xxxxxx"
}
self.html = None
def get_html(self):
"""获取数据"""
response = self.s.get(self.url, headers=self.headers)
self.html = response.text
记得添加最关键的user-agent
和Cookie
, 否则你连最基本的网页都获取不到, 这里我就不把自己的cookie
放出来了, 自己登陆后随便打开一条请求后复制即可
使用xpath来获取用户评论信息
def get_data_by_xpath(self):
"""使用xpath获取用户信息"""
html_xpath = etree.HTML(self.html)
# 获取评论以及用户
user_data_list = html_xpath.xpath(
"//li/div[@class='main-review']//a[@class='name']/text()|"
"//li/div[@class='main-review']//span[@class='name']/text()")
user_data_list = [x.strip() for x in user_data_list]
user_comment_list = html_xpath.xpath(
"//ul/li/div[@class='main-review']/div[@class='review-words']|"
"//ul/li/div[@class='main-review']/div[@class='review-words Hide']")
if len(user_data_list) == len(user_comment_list):
print("以使用xpath获取信息...")
else:
print("获取用户名个数为: ", len(user_data_list))
print("获取评论信息个数为: ", len(user_comment_list))
raise SyntaxError("用户评论信息不匹配, 请检查xpath...")
# pprint(name) ",口味"
# 获取用户评论div 转html代码为汉字
comments_html_list = [html.unescape(tostring(x).decode('utf-8')) for x in user_comment_list]
# pprint(comments_html_list)
return comments_html_list, user_data_list
分别在网页中取出用户名和他所发的评论, 因为要直接获取div
里的所有内容, 使用xpath
的text()
就明显不够用了, 直接使用tostring()
方法将xpath
对象转换为字符串格式, 在使用unescape()
将html
代码中的字符转换为汉字, 达到能将div中标签和内容一起取出来的效果
解密特殊标签
获取文件, 提取css类
首先获取css
文件内容, 类的坐标还有.svg
文件都在css
文件中
def get_css_file(self):
"""获取css文件名"""
# 获取字体文件
css_file_name = re.search(r'<link rel="stylesheet" type="text/css" href="(//s3plus.+?\.css)', self.html).group(
1)
self.css_file_name = css_file_name[css_file_name.rfind('/') + 1:]
print("下载文件...", css_file_name)
# 下载css文件
self.css = self.s.get("http:" + css_file_name).text
with open(os.path.join("dz_decode", self.css_file_name), "w") as f:
f.write(self.css)
使用正则取出css
文件链接, 提取出三个svg
文件
根据css文件中的三个含有.svg
文件链接的类, 提取出有用的信息, 黄框为.svg链接, 第一个红框为该字体文件所对应的css
类坐标的前缀, 所有以xe
为前缀的类所对应的字都存放在这个svg
中
另外两个margin
就是之前提到的偏移问题, 在最终坐标加去这个值就好了
def get_csv_file_name(self):
# 获取3个csv文件
self.file_data_list = re.findall(
r'\[class\^="(\w+)"\]\{width: (\d+)px;height: (\d+)px;margin-top: (-?\d+)px;'
r'background-image: url\((.+?\.svg)\);background-repeat: no-repeat;display: '
r'inline-block;vertical-align: middle;(margin-left: (-?\d+)px;)?\}',
self.css)
# pprint(self.file_data_list)
# 获取所有的css类
coordinate_dict = {}
for data in self.file_data_list:
css_tuple = re.findall(r'\.(%s\w+)\{background:(-?\d+\.0)px (-?\d+\.0)px;\}' % data[0], self.css)
css_list = [{"name": x[0], "x": x[1], "y": x[2]} for x in css_tuple]
coordinate_dict[data[0]] = css_list
# pprint(coordinate_dict)
return coordinate_dict
def get_csv_file(self):
# 下载三个文件
font_data_list = []
for x in self.file_data_list:
csv = self.s.get("http:" + x[4]).text
# 保存
print("正在下载文件...", x[4][x[4].rfind("/") + 1:])
with open(os.path.join("dz_decode", x[4][x[4].rfind("/") + 1:]), "w", encoding='utf-8') as f:
f.write(csv)
# 组合字典
font_data_list.append({
"class": x[0],
"width": x[1],
"height": x[2],
"top": x[3],
"name": x[4][x[4].rfind("/") + 1:],
"left": x[6] if x[6] else "0",
})
return font_data_list
使用正则匹配出三个svg
文件的重要信息, 在匹配出所有的css
类和x, y
的值组成字典返回, 将三个文件进行下载保存, 将类的名称, x, y
值提取出来
获取文字坐标
经过多次尝试, svg文件内容分两种境况
格式A:
一种是这种使用<text></text>
标签包裹起来, y
坐标在text
标签中, 使用font-size
来计算x
值
格式B:
另一种是使用textPath
标签包裹, 将y
坐标藏在path
标签内, 通过id进行映射, 使用font-size
来计算x
值
@staticmethod
def get_relationship_by_css(ui_jo_rm, font_data_list):
"""根据css文件来获取文字坐标"""
csv_coordinate_dict = {}
for name_, dict_ in ui_jo_rm.items():
# 找到对应的文件及参数
data = [x for x in font_data_list if x["class"] == name_][0]
# 打开文件
with open(os.path.join("dz_decode", data["name"][data["name"].rfind("/") + 1:]), "r",
encoding="utf-8") as f:
csv_file_list = f.readlines()
# 提取字体大小
font_size = int(re.search(r'font-size:(\d+)px;', csv_file_list[3]).group(1)) # 14px
# 判断文件是什么格式
if "defs" not in csv_file_list[4]:
# 格式A
# 筛选出所需的数据
csv_font_list = [x for x in csv_file_list if "/text" in x]
# pprint(csv_font_list)
# 构筑简短列表 [('41', '太恒烫蓬革益闯禁牲配秃葡席决宣疯雨平榆真浆储纺忠洞挖锤尼爆廊傻悲板造邪圆子喂妹体疆蓝'),
csv_font_list = [re.search(r'y="(\d+)">(\w+)</text>', x).group(1, 2) for x in csv_font_list]
else:
# 格式B
# 先获取所有行的y值
ys = re.findall(r'<path id="(\d+)" d="M0 (\d+) H600"/>', csv_file_list[4])
# 对y进行排序整理
# ys.sort(key=lambda x: x[0])
new_ys = [x[1] for x in ys]
# 获取所有行的字
fonts_line = [x for x in csv_file_list if "/textPath" in x]
fonts_list = [re.search(r'textLength="\d+">(\w+)</textPath>', x).group(1) for x in fonts_line]
# print(new_ys)
if len(new_ys) != len(fonts_list):
raise SyntaxError(f"正则匹配失败, {len(fonts_line)}, {len(fonts_list)}")
# 组合为行列数据
csv_font_list = list(zip(new_ys, fonts_list))
# pprint(csv_font_list)
# pprint(csv_font_list)
# 构筑坐标列表 [{'font': '太', 'x': '', 'y': 27},
csv_coordinate_list = [
{"font": y, "x": i * font_size + abs(int(data['left'])), "y": int(x[0]) + int(data["top"])} for x in
csv_font_list for i, y in enumerate(list(x[1]))]
# pprint(csv_coordinate_list)
# 组合最后的字典
csv_coordinate_dict[name_] = csv_coordinate_list
return csv_coordinate_dict
这里我使用了正则来匹配出需要的信息, 将一列信息和y值匹配出来, 再通过font-size计算x坐标, 减去偏移值后组合成小字典, 将它们放在列表中返回
生成对照表
得到两个关系表后之后的事情就变的很简单了, 只需要判断一个字体数据和css
类中的x, y
值相近, 这个字就是对应着这个类, 在进行替换之后就能得到解密后的数据了
for name_, font_list in ui_jo_rm.items():
# 提取出字体的xy坐标
data = font_coordinate_dict[name_]
# 格式化css坐标字典
for font in font_list:
font['x'] = int(abs(float(font['x'])))
font['y'] = int(abs(float(font['y'])))
for font_info in data:
for font_name in font_list:
# 计算偏差的值是否小于设定的值
if abs(font_name["x"] - font_info["x"]) <= self.deviation_value and abs(
font_name["y"] - font_info["y"]) <= self.deviation_value:
# print(font_name["name"], font_info["font"])
new_font_coordinate_dict[font_name['name']] = font_info['font']
遍历两个列表, 进行计算差值, 如果差值小于或等于设置的最大偏移量就忽略, 反之将其进行保存, 最终返回这个字典
保存对照信息
进行尝试后发现, 反爬信息是一天进行一次更换, 虽然文件的是动态的但是文件名是固定的, 没有必要每次运行程序都进行一次下载和遍历对照表, 会影响再次访问的速度, 只要判断我是否分析过这个css
文件, 如果分析过直接将分析后的对照表返回, 没有在进行对照映射.
if len(new_font_coordinate_dict) != all_num:
print("出现匹配异常数据, 建议加大偏差值...")
print("以匹配列表个数", len(new_font_coordinate_dict))
print("匹配列表总个数", all_num)
else:
print("匹配完成, 无异样数据...")
# 将数据保存到文件中, 以加快下次解析
with open(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdwj"), 'w', encoding="utf-8") as f:
f.write(str(new_font_coordinate_dict))
在匹配无异常时进行数据的储存, 这里是直接将字典进行str
之后, 存放在一个文件中, 文件名为原css文件的名字方便下次寻找, 文件的后缀就随便你了
if read_file:
# 判断这次有没有已经映射的文件
if os.path.exists(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdnmd")):
print("以使用之前的映射关系进行映射...", os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdwj"))
# 打开文件读取
with open(os.path.join(self.maps_path, self.css_file_name[:-4] + ".wdnmd"), 'r', encoding="utf-8") as f:
file_data = f.read()
return eval(file_data)
else:
print("未找到映射文件, 自动进行映射...")
else:
print("刷新映射关系")
判断是否开启读取旧文件, 根据有没有这个文件来决定是否进行字体映射, 直接读取文件后使用eval()
函数将字符串类型的字典重新格式化为字典
进行文字替换
回到开始, 将最开始获得到的整个评论的div
数据进行处理, 提取出文字和类名, 如下图
之后进行对五位编码进行替换, 就能得到包含这篇评论的所有文字, 将他们进行组合就得到了最后完整的评论
def data_decode(self, comments_html_list):
"""解密字体文件"""
dzd = DzDecode(self.s, self.html)
# 获取解密字典
csv_decode_dict = dzd.get_font_map(True) # True 使用映射文件
# print(comments_html_list)
# print(comments_tuple_list)
# 使用正则匹配出所需信息
comments_tuple_list = [re.findall(r'>?>?(\s*?.*?)<\w{7} class="(.{5})"|>?>(.*?)\n', x) for x in
comments_html_list]
# pprint(comments_tuple_list[0])
# 过滤数据 re.sub(r'</svgmtsi', '', x) 
# pprint(comments_tuple_list[1])
comments_tuple_list = eval(re.sub(r"</svgmtsi>|</i>|</div>", '', str(comments_tuple_list)))
# 对编码字符进行替换
comments_tuple_decrypted_list = []
num = 0
for comment in comments_tuple_list:
char = ""
for font in comment:
char += font[0]
if font[1]:
for key, value in csv_decode_dict.items():
if key == font[1]:
char += value
break
char += font[2]
comments_tuple_decrypted_list.append(char)
if num:
print(f"出现异常值, 匹配异常个数为 {num}")
comments_tuple_decrypted_list = [x.strip() for x in comments_tuple_decrypted_list]
return comments_tuple_decrypted_list
将用户和评论进行对应
最后对应用户和他所发的评论, 爬取评论的需求就完成了
结语
这次代码虽然只打印了用户和评论, 但核心问题已经解决了, 已经得到了三个字体的映射字典, 店铺的手机号, 地址, 评论都可以使用这个字典进行对照, 也没什么需求, 就没去细抠这些东西
历时近3天时间终于把代码写完并完善好了, 刚开是也是处处碰壁, 慢慢的才摸到门路, 天知道我第二天试代码时发现反爬是动态的表情, 世上无难事, 只怕有心人, 我相信只要下定决心就没有解决不了的问题, 看着跑起来的代码还是蛮开心的
本文仅供交流学习,严禁用于商业或着任何违法用途
如果这篇文章帮助到了你, 那就点个赞来告诉我吧