文章目录
- 字体反爬简介
- 发送请求,获取网页源码
- 提取字体信息,并将字体文件下载到本地
- 建立基准字典
- 引例
- 提取需要字体反爬处理的信息
- 提取不需要字体反爬的信息
- 整理提取到的所有信息,并存入excel
字体反爬简介
什么是字体反爬?
就是我们在网页上看到的内容和我们直接解析出来的内容不一样,以大众点评网站为例:
我们在网页上看到的是这样:
这些数字是正常显示的,但是我们点击F12,查看HTML时,却是这样的:
用requests发送请求,获取到网页信息,发现是这样的:
也就是说,这个网页中,有些数字和字是经过了加密处理,像原来那样直接解析的话,得到的是每个字的‘密码’,而不是我们想要的汉字或者数字。
字体反爬大致可以分为两种,一种是不同字体文件中,每个字的Unicode编码不一样,但字形完全一样(即每个字的contour的坐标完全一样),比如大众点评;一种是Unicode编码不一样,并且在不同字体文件中,字形也不完全一样,只是非常相似,比如美团。
对于第一种,我们的处理方式如下:
设置基准字典,以contour为key,以该contour描绘出来的字作为value;当遇到需要解析的字时,我们通过这个字的‘密码’先找到这个字的Unicode,然后用Unicode来找到这个字的contour,最后用这个contour去基准字典中找到对应的汉字或数字。
对于第二种,我们的处理方式如下:
设置基准字典,以contour为key,以该contour描绘出来的字作为value;当遇到需要解析的字时,我们通过这个字的Unicode来找到这个字的contour;然后对比基准字典中的所有contour,看哪个与待解析字的contour最接近(比如可以使用K近邻的方法),然后把最接近的那个contour的value赋给待解析的字。
本文主要介绍第一种。
import requests
import re
from lxml import etree
import fontTools
from fontTools.ttLib import TTFont
import hashlib
import pandas as pd
发送请求,获取网页源码
url ='http://www.dianping.com/dayi/ch10'
headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Mobile Safari/537.36'}
html = requests.get(url,headers=headers)
提取字体信息,并将字体文件下载到本地
一个网页中,可能使用了多种字体文件来对页面中的某些文字进行加密,这些字体文件信息保存在一个css文件中,因此我们首先要拿到该css文件的链接,然后在该链接的网页中查找字体信息
#拿到含有字体的css url
css_url = re.findall('href="(.*s3plus.*)"',html.text)#拿到字体的url
woff_html = requests.get('http:'+css_url[0],headers=headers)
woff_url = ['http:'+url for url in re.findall(",url\(\"(.*?)\"\);}",woff_html.text)]#拿到字体的名字
woff_name = re.findall('font-family: "PingFangSC-Regular-(.*?)";',woff_html.text)#将字体名字和url一一对应起来
woff_name_url = {}
for i in range(len(woff_name)):if woff_name[i] != 'reviewTag': #这样处理的原因是本例中用不到reviewTag这个字体文件,并且它是重复的,所以去掉它。woff_name_url[woff_name[i]] = woff_url[i]
woff_name_url
{'shopNum': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/3afae22b.woff','tagName': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/276defdb.woff','address': 'http://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/33f1a1f2.woff'}
可以看到,一共有3个字体文件,名字分别是shopNum、tagName、address,目前我们都拿到了它们对应的链接。
#下载字体文件到本地
for key in woff_name_url:name = keyurl = woff_name_url[key]response = requests.get(url,headers=headers)with open('%s.woff'%name,'wb') as f:f.write(response.content)f.close()fonts = {}
for key in woff_name_url:file_name = '{}.woff'.format(key)fonts[key] = TTFont(file_name)
fonts
{'shopNum': <fontTools.ttLib.ttFont.TTFont at 0x2609b2bdf48>,'tagName': <fontTools.ttLib.ttFont.TTFont at 0x2609b2bd4c8>,'address': <fontTools.ttLib.ttFont.TTFont at 0x2609b2c1bc8>}
我们使用在线的字体解析工具看看这些字体文件:
可以看到,每个字都有对应的Unicode编码(红色圈起来的那个)
建立基准字典
# basefont_char是基准字典中所含有的全部字符,是有一定顺序的(按照font.getGlyphOrder()的顺序排的)
# 一共有601个字符,不知道是哪位大神按顺序整理的,十分感谢!
# 这个顺序和上图的顺序是一样的
basefont_char = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '店', '中', '美', '家', '馆', '小', '车', '大', '市', '公', '酒','行', '国', '品', '发', '电', '金', '心', '业', '商', '司', '超', '生', '装', '园', '场', '食', '有', '新', '限', '天', '面','工', '服', '海', '华', '水', '房', '饰', '城', '乐', '汽', '香', '部', '利', '子', '老', '艺', '花', '专', '东', '肉', '菜','学', '福', '饭', '人', '百', '餐', '茶', '务', '通', '味', '所', '山', '区', '门', '药', '银', '农', '龙', '停', '尚', '安','广', '鑫', '一', '容', '动', '南', '具', '源', '兴', '鲜', '记', '时', '机', '烤', '文', '康', '信', '果', '阳', '理', '锅','宝', '达', '地', '儿', '衣', '特', '产', '西', '批', '坊', '州', '牛', '佳', '化', '五', '米', '修', '爱', '北', '养', '卖','建', '材', '三', '会', '鸡', '室', '红', '站', '德', '王', '光', '名', '丽', '油', '院', '堂', '烧', '江', '社', '合', '星','货', '型', '村', '自', '科', '快', '便', '日', '民', '营', '和', '活', '童', '明', '器', '烟', '育', '宾', '精', '屋', '经','居', '庄', '石', '顺', '林', '尔', '县', '手', '厅', '销', '用', '好', '客', '火', '雅', '盛', '体', '旅', '之', '鞋', '辣','作', '粉', '包', '楼', '校', '鱼', '平', '彩', '上', '吧', '保', '永', '万', '物', '教', '吃', '设', '医', '正', '造', '丰','健', '点', '汤', '网', '庆', '技', '斯', '洗', '料', '配', '汇', '木', '缘', '加', '麻', '联', '卫', '川', '泰', '色', '世','方', '寓', '风', '幼', '羊', '烫', '来', '高', '厂', '兰', '阿', '贝', '皮', '全', '女', '拉', '成', '云', '维', '贸', '道','术', '运', '都', '口', '博', '河', '瑞', '宏', '京', '际', '路', '祥', '青', '镇', '厨', '培', '力', '惠', '连', '马', '鸿','钢', '训', '影', '甲', '助', '窗', '布', '富', '牌', '头', '四', '多', '妆', '吉', '苑', '沙', '恒', '隆', '春', '干', '饼','氏', '里', '二', '管', '诚', '制', '售', '嘉', '长', '轩', '杂', '副', '清', '计', '黄', '讯', '太', '鸭', '号', '街', '交','与', '叉', '附', '近', '层', '旁', '对', '巷', '栋', '环', '省', '桥', '湖', '段', '乡', '厦', '府', '铺', '内', '侧', '元','购', '前', '幢', '滨', '处', '向', '座', '下', '県', '凤', '港', '开', '关', '景', '泉', '塘', '放', '昌', '线', '湾', '政','步', '宁', '解', '白', '田', '町', '溪', '十', '八', '古', '双', '胜', '本', '单', '同', '九', '迎', '第', '台', '玉', '锦','底', '后', '七', '斜', '期', '武', '岭', '松', '角', '纪', '朝', '峰', '六', '振', '珠', '局', '岗', '洲', '横', '边', '济','井', '办', '汉', '代', '临', '弄', '团', '外', '塔', '杨', '铁', '浦', '字', '年', '岛', '陵', '原', '梅', '进', '荣', '友','虹', '央', '桂', '沿', '事', '津', '凯', '莲', '丁', '秀', '柳', '集', '紫', '旗', '张', '谷', '的', '是', '不', '了', '很','还', '个', '也', '这', '我', '就', '在', '以', '可', '到', '错', '没', '去', '过', '感', '次', '要', '比', '觉', '看', '得','说', '常', '真', '们', '但', '最', '喜', '哈', '么', '别', '位', '能', '较', '境', '非', '为', '欢', '然', '他', '挺', '着','价', '那', '意', '种', '想', '出', '员', '两', '推', '做', '排', '实', '分', '间', '甜', '度', '起', '满', '给', '热', '完','格', '荐', '喝', '等', '其', '再', '几', '只', '现', '朋', '候', '样', '直', '而', '买', '于', '般', '豆', '量', '选', '奶','打', '每', '评', '少', '算', '又', '因', '情', '找', '些', '份', '置', '适', '什', '蛋', '师', '气', '你', '姐', '棒', '试','总', '定', '啊', '足', '级', '整', '带', '虾', '如', '态', '且', '尝', '主', '话', '强', '当', '更', '板', '知', '己', '无','酸', '让', '入', '啦', '式', '笑', '赞', '片', '酱', '差', '像', '提', '队', '走', '嫩', '才', '刚', '午', '接', '重', '串','回', '晚', '微', '周', '值', '费', '性', '桌', '拍', '跟', '块', '调', '糕']
#假设我们用address作为基准字典
address_font = TTFont('address.woff')
unicodes = address_font.getGlyphOrder()[2:]
#上面说的basefont_char的顺序就是这个getGlyphOrder的顺序
#这里[2:]是因为前两个字符是没有用的,从上图也可以看出来
base_dict = {}
for i in range(len(unicodes)):contour = hashlib.md5(bytes(str(fonts['address']['glyf'][unicodes[i]].coordinates),encoding='utf-8')).hexdigest()base_dict[contour] = basefont_char[i]
引例
首先以店铺地址信息的提取来解释一下大概的过程:
addrs = re.findall('<span class="addr">(.*?)</span>',html.text)
#addrs得到的是15个店的地址print(addrs[0]) #这个是第一个店的信息
#<svgmtsi class="address"></svgmtsi><svgmtsi class="address"></svgmtsi><svgmtsi class="address"></svgmtsi><svgmtsi class="address"></svgmtsi>60<svgmtsi class="address"></svgmtsi>
addr = addrs[0].replace('<svgmtsi class="address">','').replace('</svgmtsi>','')
print(addr) #可以看到,地址信息中每一个汉字的‘密码’都是以&#x开头,#然后是4个字符,然后以;分割 #并且经过多次观察我们还可以发现,每个字的‘密码’中间的那4个字符,刚好就是该字对应的Unicode编码的后4位。#因此,按照此规律我们就可以进行‘解密’了。
# 60#按照上述发现的规律进行解密
str_need_replace = re.findall('&#x(.*?);',addr)
print(str_need_replace) #['eb29', 'e6b0', 'e568', 'e013', 'f31d']
for i in str_need_replace:unicode = 'uni'+i #拿到这个字对应的Unicode编码contour_code=hashlib.md5(bytes(str(address_font['glyf'][unicode].coordinates),encoding='utf-8')).hexdigest()#这里是将contour进行一下md5处理,有如下原因:#1.一般一个字的字形的坐标都很长(即有很多个点组成),不进行md5处理的话,就会是很长一串#2.不同字的字形的坐标一般长短不一样,不进行md5处理的话,放在一起不好看#address_font['glyf'][unicode].coordinates 就是拿到这个字的字形的坐标old_str = '&#x'+i+';'new_str = base_dict[contour_code]addr=addr.replace(old_str,new_str)
print(addr) #长春西路60号
提取需要字体反爬处理的信息
# 定义一个提取信息的函数
def mes_extract(re_str,woff_name):if woff_name in fonts.keys():font = fonts[woff_name]final_mes = []replace_str = '<svgmtsi class="{}">'.format(woff_name)mes_for15 = re.findall(re_str,html.text,flags=re.S)if len(mes_for15)==15:if re_str.count('(.*?)')==1:for i in range(15):mes = mes_for15[i].replace(replace_str,'').replace('</svgmtsi>','')str_need_replace = re.findall('&#x(.*?);',mes)for j in str_need_replace:unicode = 'uni'+jcontour_code=hashlib.md5(bytes(str(font['glyf'][unicode].coordinates),encoding='utf-8')).hexdigest()old_str = '&#x'+j+';'new_str = base_dict[contour_code]mes=mes.replace(old_str,new_str)final_mes.append(mes)if re_str.count('(.*?)')==2:for i in range(15):mes = mes_for15[i][1].replace(replace_str,'').replace('</svgmtsi>','')str_need_replace = re.findall('&#x(.*?);',mes)for j in str_need_replace:unicode = 'uni'+jcontour_code=hashlib.md5(bytes(str(font['glyf'][unicode].coordinates),encoding='utf-8')).hexdigest()old_str = '&#x'+j+';'new_str = base_dict[contour_code]mes=mes.replace(old_str,new_str)final_mes.append(mes)else:print('字体有误')return final_mes
region_re = 'click-name="shop_tag_region_click"(.*?)><span class="tag">(.*?)</span>'
addr_re = '<span class="addr">(.*?)</span>'
dish_type_re = 'click-name="shop_tag_cate_click"(.*?)><span class="tag">(.*?)</span>'
taste_score_re = '口味<b>(.*?)</b>'
surroundings_score_re = '环境<b>(.*?)</b>'
service_score_re = '服务<b>(.*?)</b>'
review_num_re = 'class="review-num"(.*?)<b>(.*?)</b>'
regions = mes_extract(region_re,'tagName')
addrs = mes_extract(addr_re,'address')
dish_types = mes_extract(dish_type_re,'tagName')
taste_scores = mes_extract(taste_score_re,'shopNum')
surrounding_scores = mes_extract(surroundings_score_re,'shopNum')
service_scores = mes_extract(service_score_re,'shopNum')
review_nums = mes_extract(review_num_re,'shopNum')
# 人均消费价格的提取
# 人均消费价格由于有缺失值,所以需要单独处理
avgprices_nodes = re.findall('shop_avgprice_click(.*?)<b>(.*?)</b>',html.text,flags=re.S)avg_prices = []
for i in range(len(avgprices_nodes)):if '¥' in avgprices_nodes[i][1]:avg_price = avgprices_nodes[i][1].replace('<svgmtsi class="shopNum">','').replace('</svgmtsi>','')str_need_replace = re.findall('&#x(.*?);',avg_price)for j in str_need_replace:unicode = 'uni'+jcontour_code=hashlib.md5(bytes(str(fonts['shopNum']['glyf'][unicode].coordinates),encoding='utf-8')).hexdigest()old_str = '&#x'+j+';'new_str = base_dict[contour_code]avg_price=avg_price.replace(old_str,new_str)avg_prices.append(int(avg_price.strip('¥')))else: avg_prices.append('-')
提取不需要字体反爬的信息
html_xpath = etree.HTML(html.text)# 店铺名称的提取:
shop_names = []
shop_names_nodes = html_xpath.xpath('//div [@class="tit"]/a/h4')
for i in range(15):shop_names.append(shop_names_nodes[i].text)# 店铺详细页面链接的提取
shop_urls = []
shop_urls_nodes = html_xpath.xpath('//div [@class="tit"]/a')
for i in range(15):shop_urls.append(shop_urls_nodes[i].attrib['href'])# 店铺总体评分的提取
shop_scores = []
shop_scores_nodes = html_xpath.xpath('//div [@class="nebula_star"]/div[2]')
for i in range(15):shop_scores.append(shop_scores_nodes[i].text)# 店铺推荐菜的提取
recommend_dishs = []
shop_nodes = html_xpath.xpath('//div[@class="recommend"]')
for i in range(15):dish_in_one_shop = []dish_nodes = shop_nodes[i].xpath('.//a')for node in dish_nodes:dish_in_one_shop.append(node.text)recommend_dishs.append(dish_in_one_shop)
整理提取到的所有信息,并存入excel
data = pd.DataFrame([shop_names,dish_types,shop_urls,regions,addrs,shop_scores,taste_scores,surrounding_scores,service_scores,review_nums,avg_prices,recommend_dishs]).Tdata.columns= ['shop_name','dish_type','shop_url','region','addr','overall','taste','surroundings','service','review_num','avgprice','recommend_dish']data.to_excel('./dazong.xls')
最终的结果: