赞
踩
爬取猫眼的全部信息,这里主要指的是电影列表里的电影信息和演员信息,如下界面。
爬去的时候有两个难点。一:字体加密(如今好像机制有更新来些,用网上的方法不行);二:美团检测。下面将分别讲述我解决的过程。
关于字体加密,网络上介绍的很多,思路也都类似。猫眼每次加载的时候会动态的加载不同的字形编码。解决思路呢,就是先下载好它的一个字体文件(.woff结尾的,方法网上多有介绍,我就不在累述了),然后每次爬取新的界面的时候,将新的字体文件的字形坐标与之前下载好的的对比。网上很多方法还是很久之前的,当时猫眼的字体加密机制还很简陋,只是很单纯的比对坐标是否相等,现在的机制是每次加载的字形都是有细微变化的。网上很多人的方法是说其变化在一个范围内,只要比对他们的差异不超过一个范围就是同一个数字。我也用了这个方法,但发现很多数字都识别错误,比如3,5,9;1,4;0,8;这三组数字很容易混淆。说明这个问题不是那么简单的。我用的方法先是取三个基准字体文件,然后将每个基准字体文件分别与动态加载的进行坐标对比,我也设置了一个差异范围,对符合这个差异的进行计数,最后计数最大的就认证为对应的数字。这样做之后还是不免出现错误,然后我发现每个数的坐标数目有差异,比如3,5,9这个三个数的坐标数量明显有很大不同,由于每个数字自身的坐标数每次加载也是不同的,有细微的差别,所以我再进行一个坐标个数差异判断。还有一点比较重要的,就是这个差异值的选择,网上很多选择的是8、10,可能猫眼加密机制改了的原因,这几个预设差异值,识别率很低。我试的时候 5 的识别率最高,这些你们可以自己实验。之前我们选了三组基准,由于我们进行了前面的识别操作,识别率进行很高了,所以再进行一次三局两胜的操作,再次提高识别率。由于代码比较多,我放一些关键的代码。还有一种方法也许可行,我没试过,可以参考这篇博客参考博文,这篇博客使用knn算法来识别的,识别率应该挺高的。但这个需要多准备些字体文件来提高识别率。
- def replace_font(self, response,res):
-
- #基准,比对三次,两次以上一致即默认正确
- #我是“我要出家当道士”,其他非原创
- base_font = TTFont('./fonts/base.woff')
- base_font.saveXML('./fonts/base_font.xml')
- base_dict = {'uniF870': '6', 'uniEE8C': '3', 'uniECDC': '7', 'uniE6A2': '1', 'uniF734': '5',
- 'uniF040': '9', 'uniEAE5': '0', 'uniF12A': '4', 'uniF2D2': '2', 'uniE543': '8'}
- base_list = base_font.getGlyphOrder()[2:]
-
- base_font2 = TTFont('./fonts/base2.woff')
- base_font2.saveXML('./fonts/base_font2.xml')
- base_dict2 = {'uniF230': '6', 'uniEBA1': '3', 'uniF517': '7', 'uniF1D2': '1', 'uniE550': '5',
- 'uniEBA4': '9', 'uniEB7A': '0', 'uniEC29': '4', 'uniF7E1': '2', 'uniF6B7': '8'}
- base_list2 = base_font2.getGlyphOrder()[2:]
-
- base_font3 = TTFont('./fonts/base3.woff')
- base_font3.saveXML('./fonts/base_font3.xml')
- base_dict3 = {'uniF8D3': '6', 'uniF0C9': '3', 'uniEF09': '7', 'uniE9FD': '1', 'uniE5B7': '5',
- 'uniF4DE': '9', 'uniF4F9': '0', 'uniE156': '4', 'uniE9B5': '2', 'uniEC6D': '8'}
- base_list3 = base_font3.getGlyphOrder()[2:]
-
- #网站动态加载的字体
- #我是“我要出家当道士”,其他非原创
- font_file = re.findall(r'vfile\.meituan\.net\/colorstone\/(\w+\.woff)', response)[0]
- font_url = 'http://vfile.meituan.net/colorstone/' + font_file
- #print(font_url)
- new_file = self.get_html(font_url)
- with open('./fonts/new.woff', 'wb') as f:
- f.write(new_file.content)
- new_font = TTFont('./fonts/new.woff')
- new_font.saveXML('./fonts/new_font.xml')
- new_list = new_font.getGlyphOrder()[2:]
-
-
- coordinate_list1 = []
- for uniname1 in base_list:
- # 获取字体对象的横纵坐标信息
- coordinate = base_font['glyf'][uniname1].coordinates
- coordinate_list1.append(list(coordinate))
-
- coordinate_list2 = []
- for uniname1 in base_list2:
- # 获取字体对象的横纵坐标信息
- coordinate = base_font2['glyf'][uniname1].coordinates
- coordinate_list2.append(list(coordinate))
-
- coordinate_list3 = []
- for uniname1 in base_list3:
- # 获取字体对象的横纵坐标信息
- coordinate = base_font3['glyf'][uniname1].coordinates
- coordinate_list3.append(list(coordinate))
-
- coordinate_list4 = []
- for uniname2 in new_list:
- coordinate = new_font['glyf'][uniname2].coordinates
- coordinate_list4.append(list(coordinate))
-
- index2 = -1
- new_dict = {}
- for name2 in coordinate_list4:#动态
- index2 += 1
-
- result1 = ""
- result2 = ""
- result3 = ""
-
- index1 = -1
- max = -1;
- for name1 in coordinate_list1: #本地
- index1 += 1
- same = self.compare(name1, name2)
- if same > max:
- max = same
- result1 = base_dict[base_list[index1]]
-
- index1 = -1
- max = -1;
- for name1 in coordinate_list2: #本地
- index1 += 1
- same = self.compare(name1, name2)
- if same > max:
- max = same
- result2 = base_dict2[base_list2[index1]]
-
- index1 = -1
- max = -1;
- for name1 in coordinate_list3: #本地
- index1 += 1
- same = self.compare(name1, name2)
- if same > max:
- max = same
- result3 = base_dict3[base_list3[index1]]
-
- if result1 == result2:
- new_dict[new_list[index2]] = result2
- elif result1 == result3:
- new_dict[new_list[index2]] = result3
- elif result2 == result3:
- new_dict[new_list[index2]] = result3
- else:
- new_dict[new_list[index2]] = result1
-
- for i in new_list:
- pattern = i.replace('uni', '&#x').lower() + ';'
- res = res.replace(pattern, new_dict[i])
- return res
-
-
- """
- 输入:某俩个对象字体的坐标列表
- #我是“我要出家当道士”,其他非原创
- 输出相似度
- """
- def compare(self, c1, c2):
- count = 0
- length1 = len(c1)
- length2 = len(c2)
- if abs(length2-length1) > 7:
- return -1
- length = 0
- if length1 > length2:
- length = length2
- else:
- length = length1
- #print(length)
- for i in range(length):
- if (abs(c1[i][0] - c2[i][0]) < 5 and abs(c1[i][1] - c2[i][1]) < 5):
- count += 1
- return count

关于美团防爬,网上也有很多博客,但内容大家应该知道的,你抄我,我抄你的,最后差不多都是一样的。但还是有很多优质的博文值得拜读。我水平也有限,给的参考借鉴也有限,所以我这里只给两种我自己用过的,而且成功爬取我需要的3000部电影和9000位演员数据。我是两种方法交织使用来爬取数据的,我使用了正常的requests来爬取和selenium自动工具(配合mitm——proxy)。速度最快的是requests,但很容易被检测;性能最稳定的是selenium,不易被检测。
requests对于爬取猫眼还是很有用的,只是被美团检测后,需要很长时间的冷却,具体时间未知,我使用request配置已经登录过后的cookie成功爬取了全部的电影详情信息。参考代码如下。其实比较麻烦的就是xpath解析网页源码了。
- class getFilmsData(object):
-
- def __init__(self):
- self.headers = {}
- self.headers['User-Agent'] = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.06'
- self.headers['Cookie'] = '填写你的cookie,不知道的话,留言,我会快速告诉你'
- self.dataManager = mongoManager()
- self.fontDecode = FontDecode()
-
- #根据url,获取数据,
- #limitItem为限制页,即其页item之前的已经获取完成
- #我是“我要出家当道士”,其他非原创
- def getData(self,url,limitItem):
-
- s = requests.session()
- s.headers = self.headers
- s.keep_alive = False
-
- content = s.get(url).text
- #print("URLTEXT is :",response.text)
-
- if "验证中心" in content:
- print("目录界面美团验证")
- return False
- sel = etree.HTML(content)
-
- count = 0
-
- urls = sel.xpath('//div[@class="movie-item"]')
- scores = sel.xpath('//div[@class="channel-detail channel-detail-orange"]')
- for box in urls:
- #抓取列表界面的电影url
- count += 1
- if count < limitItem:
- continue
- print("begin ",count,"th item")
- scoreCheck = scores[count-1].xpath('.//text()')[0]
- #无评分的电影不爬取
- if scoreCheck == "暂无评分":
- break
-
- urlBack = box.xpath('.//a/@href')[0]
- #获取电影详情url
- url = "https://maoyan.com"+urlBack
-
- #获取电影名称、时长、上映日期、票房、评分、演员、海报url
- resp = s.get(url)
- realUrl = resp.url
- res = resp.text
- if "验证中心" in res:
- print("信息界面美团验证")
- return False
- #res2= self.replace_font(res)
- selTmp = etree.HTML(res)
- #电影票房
- #我是“我要出家当道士”,其他非原创
- money = selTmp.xpath('//div[@class="movie-index-content box"]/span[1]/text()')
- unit = selTmp.xpath('//div[@class="movie-index-content box"]/span[2]/text()')
- filmMoney = ""
- if len(money) == 0:
- #无票房的电影不爬取
- continue
- else:
- ascll = str(money[0])
- #print("money ascll is:",ascll)
- utfs = str(ascll.encode('unicode_escape'))[1:].replace("'","").replace("\\\\u",";&#x").split('.')
- unicode = ""
- if len(utfs)>1:
- unicode = utfs[0][1:]+";."+utfs[1][1:]+";"
- else:
- unicode = utfs[0][1:]+";"
- filmMoney = self.fontDecode.replace_font(res,unicode)
- if len(unit) > 0:
- filmMoney += unit[0]
- #电影名称
- filmName = selTmp.xpath('//div[@class="movie-brief-container"]/h1[1]/text()')[0]
- #电影海报
- filmImage = selTmp.xpath('//div[@class="avatar-shadow"]/img[1]/@src')[0]
- #电影时长
- filmTime = selTmp.xpath('//div[@class="movie-brief-container"]/ul[1]/li[2]/text()')[0].replace('\n', '').replace(' ', '')
- #电影上映时间
- filmBegin = selTmp.xpath('//div[@class="movie-brief-container"]/ul[1]/li[3]/text()')[0].replace('\n', '')
- #电影评分
- score = selTmp.xpath('//div[@class="movie-index-content score normal-score"]/span[1]/span[1]/text()')
- #由于票房和评分字体编码加密的缘故,所以需要先编码为unicode,在进行解密
- #我是“我要出家当道士”,其他非原创
- filmScore = ""
- if len(score) == 0:
- filmScore = "评分暂无"
- else:
- ascll = str(score[0])
- #print("score ascll is:",ascll)
- utfs = str(ascll.encode('unicode_escape'))[1:].replace("'","").replace("\\\\u",";&#x").split('.')
- unicode = ""
- if len(utfs)>1:
- unicode = utfs[0][1:]+";."+utfs[1][1:]+";"
- else:
- unicode = utfs[0][1:]+";"
- filmScore = self.fontDecode.replace_font(res,unicode)+"分"
- print(filmMoney,filmScore)
- #获取电影演员表,只获取前10个主要演员
- actorSol = selTmp.xpath('//div[@class="tab-celebrity tab-content"]/div[@class="celebrity-container"]/div[@class="celebrity-group"][2]/ul/li')
- #print(actors)
- actors = []
- actorUrls = []
- num = len(actorSol)
- for i in range(10):
- num -= 1
- if num < 0:
- break
- actorUrl = "https://maoyan.com"+actorSol[i].xpath('.//div[@class="info"]/a/@href')[0]
- actorItem = actorSol[i].xpath('.//div[@class="info"]/a/text()')[0].replace('\n', '').replace(' ', '')
- if len(actorSol[i].xpath('.//div[@class="info"]/span[1]/text()')) > 1:
- actorItem += (" "+actorSol[i].xpath('.//div[@class="info"]/span[1]/text()')[0].replace('\n', '').replace(' ', ''))
- actorUrls.append(actorUrl)
- actors.append(actorItem)
- #获取电影简介
- introductionT = ""
- introductionF = selTmp.xpath('//span[@class = "dra"]/text()')
- if len(introductionF) > 0:
- introductionT = introductionF[0]
- print(count,filmName,filmImage,filmBegin,filmTime,filmScore,filmMoney,actors,introductionT)
- time.sleep(1)
- s.close()

第二种方法就是selenium配合mitmproxy,selenium是一个自动化的工具,与request爬取网页内容不同,selenium可以模仿用户打开浏览器来浏览网页,所见的都可爬取。这个也多用在爬取大量数据避免检测时。但如果单纯的使用selenium,还是很容易被检测出来,这里我理解也不是特别深,只是单纯会用而已。大致就是浏览器设置的几个回复参数,如果使用selenium的话,这几个参数就会被赋值,而正常用户浏览的话,这几个参数是未定义的;还有一些其他的防selenium的,我没继续深入了解。
具体的配置方法可以参考我之前写的博客:mitmproxy配合selenium
我发现,我在爬取的时候,这个方法比第一个稳定,出现美团验证的几率很低,而且出现之后复制网站网址手动到对应的浏览器里打开,就会出现验证,你手动多试几次就过去了,然后就可以继续爬取。当然这个速度肯定没request快。举个例子吧,比如我在爬取https://maoyan.com/films/celebrity/28936的时候出现了美团检测,我用的是谷歌的驱动,那么复制这个网址到谷歌浏览器中,这时候一般会出现美团检测,你手动的验证通过后(不通过就关闭浏览器再来几次),再继续爬取就行了。其实selenium自动化使用浏览器还是被网址认为是人在浏览,只有出现检测的时候,才会被网址识别出是selenium,所以我们只要认为的过检测就可以了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。