写在前面
本项目很久之前就想尝试,觉得难,所以一直在拖,这次终于有时间搞了一下。猫眼的反爬虫策略还是比较强大的,会判断用户行为,如果在一段时间内频繁访问会使ip被暂时屏蔽,这个问题可以使用IP代理来解决。此外还使用了字体反爬来保护票房、票价等重要信息,而且这个字体反爬在不断更新,最初的时候使用单一的字体文件,后来升级到每次刷新网页都会更新字体文件,而且字体文件中的坐标并不一致,这让爬取变的更加困难,但是相同数字之间总有相似的地方,通过聚类思想我们仍然可以找到映射关系,这种方法不保证不会出错,因为真实的映射关系只有猫眼的程序猿那里有,我们只是使用算法进行推断,如果出现错误,可以尝试使用更精确的算法。本文使用的是欧式距离,相对简单一些。主要分为以下几个部分:
1 下载一份基础字体文件(在电影页面中审查元素,找到network-font然后刷新)
2 使用fontcreator工具(百度即可)确定基础字体文件中字形与文本的映射关系
3 计算新字体文件与基础字体文件中坐标点的欧式距离(统计学中的聚类思想)
4 根据最小的欧式距离建立新字体文件中的字形与文本的映射关系
用到的包
#fonttools解析字体文件并生成xml
from fontTools.ttLib import TTFont
#使用随机的代理IP
import random
#re正则表达式,用于匹配woff文件下载地址及字体坐标等
import re
#用于获取网页信息
import requests
#用于解析网页,在本文中部分内容无法解析
from bs4 import BeautifulSoup as bs
#用于计算欧式距离
import numpy as np
建立基础映射
本部分内容暂请参考CSDN,如有时间我会整理一下。
全局变量
#伪装浏览器请求头
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
}
#设置IP代理
proxies=[{'http':'114.103.168.183:3000'},{'http':'175.43.156.42:9999'},
{'http':'218.64.148.47:9000'},{'http':'221.224.136.211:35101'},
{'http:':'175.42.129.65:9999'},{'http':'117.64.225.154:1133'}]
#我所使用的基础映射
relation_table = {'uniEF67':'6','uniF0D5':'3','uniED8E':'9',
'uniEBBA':'4','uniF3D8':'1','uniE6A9':'8',
'uniE01D':'0','uniECE9':'7','uniE106':'2',
'uniF8D2':'5'}
请求网页
def Get_Html(url):
#请求网页,并使用随机的代理IP
req = requests.get(url,headers = headers,proxies = random.choice(proxies))
#将编码设置为utf-8,不然网页中的中文都会变成乱码
req.encoding = 'utf-8'
#有时候requests会请求到验证中心,所以储存一下真实访问的url
real_url = req.url
#网页文本
html = req.text
#爬取到一次网页后可以暂存为文本文件,防止在debug过程中访问时出现验证中心
"""
f = open('movie.txt','w',encoding = 'utf-8')
f.write(html)
f.close()
"""
#访问到正常网页时才能获取到电影标题,所以在这里捕捉一下异常
try:
#从网页中获取电影标题,使用BeautifulSoup进行解析
title = bs(html,'lxml').find('h1',class_ = "name").text
#利用正则表达式匹配到口碑、评分人数、票房对应的加密信息
pattern_span = re.compile('>(&#.*?\..*;)')
ppl = re.findall(pattern_span, html)
#返回四个主要信息
return title,html,ppl,real_url
except:
#如果出现验证中心,就只返回验证中心地址
return 0,0,0,real_url
获取新字体
def Get_Woff(html):
#使用正则表达式匹配到woff文件的下载地址
pattern_woff = re.compile("url\(\'(.*.woff)\'\)\s")
#构造完整地址
woff_url = 'http:' + re.findall(pattern_woff,html)[0]
#将字体文件保存到本地
new_woff = requests.get(url = woff_url,headers = headers)
with open('new_woff.woff','wb') as f:
f.write(new_woff.content)
f.close()
计算欧式距离
详请参阅欧式距离
#向此函数传递两个列表,然后调用numpy计算欧式距离
def compare_axis(axis1,axis2):
#如果向量维度不一致,则对较短的那个补充0元素
if len(axis1) < len(axis2):
axis1.extend([0,0] for _ in range(len(axis2) - len(axis1)))
elif len(axis2) < len(axis1):
axis2.extend([0,0] for _ in range(len(axis1) - len(axis2)))
#将列表转换为数组
axis1 = np.array(axis1)
axis2 = np.array(axis2)
#返回欧式距离
return np.sqrt(np.sum(np.square(axis1 - axis2)))
从XML读取字体坐标
#file为传递的xml文件名
def Read_Font(file):
#以读方式打开
f_base = open(file,'r')
#使用BeautifulSoup进行解析
html_base = bs(f_base.read(),'lxml')
#找到所有包含字体坐标的片段
ttglyph = html_base.find('glyf').find_all('ttglyph')
f_base.close()
#返回片段列表
return ttglyph
#清洗字符串取得坐标值列表
def Get_Axis(string):
axis = []
#正则匹配字符串中所有坐标,此时得到的元素类似"x="1" y="2""
pattern = re.compile('<pt\son="[0,1]?"\s(.*?)>')
#re只能从字符串中匹配,使用str进行转换
axis_list = re.findall(pattern,str(string))
#遍历匹配到的元素列表
for item in axis_list:
#将元素根据空格进行分割,得到[x="1",y="2"]
zb = item.split(' ')
#再根据“=”进行分割,替换掉双引号,并转换为整数
x = int(zb[0].split('=')[1].replace('"',''))
y = int(zb[1].split('=')[1].replace('"',''))
#保存到坐标列表
axis.append([x,y])
#将清洗后的坐标返回
return axis
Unicode映射
#建立基础字体文件中的uni与新字体文件中的uni之间的映射
def BaseToNew(ttglyph_base,ttglyph_new):
basetonew = []
#因为有一个无用的字符,所以从索引1开始遍历
for base in ttglyph_base[1:]:
#找到一个字体坐标与新字体的最小欧式距离后要将value重置
value = []
#还有另外一个无用的字符x,写一个if进行判断
if base.get('name') != 'x':
#调用前述方法获取坐标列表
axis_base = Get_Axis(base)
#枚举新字体文件中的字体坐标字符串,也可以不使用枚举,本来是想记录最小欧式距离索引的
for index,new in enumerate(ttglyph_new[1:]):
#同样的,忽略x
if new.get('name') != 'x':
#调用前述方法获取坐标列表
axis_new = Get_Axis(new)
#计算两个axis之间的欧式距离,并保存到value列表
value.append(compare_axis(axis_base,axis_new))
#新字体文件每被遍历一遍,都要计算最小的欧式距离,并记录新旧对应的Unicode编码
for i in range(len(value)):
#判断是否是最小的欧式距离
if value[i] == min(value):
#如果是,则从字符串中获取新字形的Unicode
#因为忽略了开头的无用信息,所以这里需要加1
new_code = ttglyph_new[i + 1].get('name')
#获取旧字形对应的Unicode
base_code = base.get('name')
#建立一个映射
basetonew.append({"basecode":base_code,"newcode":new_code})
#如果已经找到最小的欧式距离,则无需比较后续数据
break
#返回建立完成的Unicode映射
return basetonew
UNI-NUM映射
#根据基础映射和Unicode映射建立新的UNI-NUM映射
def NewToNum(basetonew):
newtonum = {}
#遍历基础映射中所有的键
for key in relation_table.keys():
#获取值,即基础映射中对应的文本
value = relation_table[key]
#遍历Unicode映射
for i in range(len(basetonew)):
#如果键与Unicode映射中的基础UNI相对应,那么就建立一个新的字形映射
if key == basetonew[i]['basecode']:
#在这里将新的UNI进行转换,改成全小写,并将"uni"替换为"&#x"
#在末尾添加";"号,此时得到的Unicode编码与网页中显示的一致
newtonum[basetonew[i]['newcode'].lower().replace('uni','&#x') + ';'] = value
#返回新的UNI-NUM映射
return newtonum
#根据新的映射获取结果,ppl为之前正则匹配到的加密信息
def Result(newtonum,ppl):
result = []
#遍历ppl列表
for item in ppl:
#遍历新映射中的键
for key in newtonum.keys():
#如果在item中能找到键,就将键替换为对应的值
if key in item:
item = item.replace(key,newtonum[key])
#向result列表中添加替换完成的item,此时的item已经解密完毕
result.append(item)
#返回result
return result
基础函数
def Get_Basic(ppl):
#使用fonttools包读取字体文件
base_font = TTFont('base_font.woff')
base_font.saveXML('base_font.xml')
new_font = TTFont('new_woff.woff')
new_font.saveXML('new_woff.xml')
#从xml文件中读取字体坐标信息
ttglyph_base = Read_Font('base_font.xml')
ttglyph_new = Read_Font('new_woff.xml')
#获取新旧映射关系
basetonew = BaseToNew(ttglyph_base, ttglyph_new)
#获取新的UNI-NUM映射
newtonum = NewToNum(basetonew)
#解析加密信息
result = Result(newtonum,ppl)
return result
其余代码
#这是一个测试用函数,前提是你已经将网页保存为文本,使你debug的时候不会遇到验证中心
def test():
f = open('movie.txt','r',encoding = 'utf-8')
req = f.read()
pattern_span = re.compile('>(&#.*?\..*;)')
ppl = re.findall(pattern_span, req)
f.close()
title = bs(req,'lxml').find('h1',class_ = "name").text
result = Get_Basic(ppl)
print(title)
print('猫眼口碑',ppl[0])
print('{}万人评分'.format(ppl[1]))
print('累计票房{}亿'.format(ppl[2]))
print('猫眼口碑',result[0])
print('{}万人评分'.format(result[1]))
print('累计票房{}亿'.format(result[2]))
#主函数
def main():
#你要爬取的目标url,批量爬取时写成列表并在后面加一个循环
url = 'https://maoyan.com/films/42964'
#获取电影标题,网页信息,加密数据,访问的真实url
title,html,ppl,real_url = Get_Html(url)
#捕捉可能出现的验证中心异常
try:
#从网页中的woff_url下载新的字体文件
Get_Woff(html)
#获取解密信息
result = Get_Basic(ppl)
#输出猫眼程序猿辛苦维护的数据。。。
print(title)
print('猫眼口碑',result[0])
print('{}万人评分'.format(result[1]))
print('累计票房{}亿'.format(result[2]))
except:
"""
如果出现了验证中心,那么就去浏览器访问一下输出的real_url
拖动滑动条完成验证,然后重新执行程序即可
"""
print(real_url)
#程序入口
if __name__ == '__main__':
#test()
main()
写在后面
整整一天,从早8点到晚22点破解了这个反爬,期间参阅了很多资料,最终发现使用聚类算法才能解析猫眼现有反爬策略。 之前学习的时候积累了一些语法的使用,不然今天肯定搞不完这个东西,或者说就算搞完了,也只是复制别人的代码,而不能完全读懂。 学习,重在积累。