Python爬虫:Scrapy框架采集猫眼TOP100电影数据(js逆向+字体反爬)

概述:本文采用scrapy爬虫框架对猫眼电影中的TOP100电影数据进行采集,过程采用了JavaScript逆向和字体反爬技术,其中字体反爬用到图像识别技术,可关注本人公众号【Python是我哥】下载源码,也可加入Q群(756788325)讨论学习。

万事开头难,但爬虫开头不难,先创建项目和爬虫。

scrapy startproject maoyan
cd maoyan
scrapy genspider top100 https://www.maoyan.com/

在频繁地点击几次链接之后,大家可以发现,网页会重定向到一个滑块缺口验证页面。

1.PNG

这其实是猫眼电影设计的IP限制反爬,不能让你过度频繁请求。用随机IP和随机UA应该可以绕过,不过我这里只是在setting.py中设置一下请求并发和下载延时,这样可以通过降低请求的频繁度来避开验证页面。毕竟咱们只是学习,而且100条数据也不多,没必要硬刚。

CONCURRENT_REQUESTS = 1
DOWNLOAD_DELAY = 4

先观察TOP100电影列表页,它共有10页,每页10部电影,每页的URL不同点在最后的offset参数。offset参数应该代表的是当页第一条数据的排名偏移数,第一页offset=0,第二页offset=10,……第十页offset=90。

2.PNG

因此我们只需要利用循环语法构造出URL,然后分别请求即可。由于整篇文章涉及的代码比较多,也不可能全部代码都贴出来,可以关注公众号【Python是我哥】获取源码,然后对照的这篇文章看会更好。

def start_requests(self):
    for offset in range(10):
        url = 'https://www.maoyan.com/board/4?offset={}'.format(offset*10)
        yield scrapy.Request(url=url, callback=self.parse)

请求回来的列表页响应中,可以很容易找到每部电影详情页的URL。

3.PNG

使用CSS选择器解析出所有详情页的URL

urls = response.css('p.name a::attr(href)').getall()

不过该URL是相对路径(如:/films/344881),可以补全成绝对路径(如:https://www.maoyan.com/films/344881)。分析请求回来的详情页响应,发现没有我们想要的数据。像电影名称、播放时长、上映地点这些数据应该是通过Ajax异步加载的,果断在网络资源中全局搜索查找。

4.PNG

发现真正把数据请求回来的URL后面带有很多参数,甚至还有加密的(如:https://www.maoyan.com/ajax/films/344881?timeStamp=1672649608583&index=1&signKey=b6eca8c0b54f72c8d9c840007188e446&channelId=40011&sVersion=1&webdriver=false

5.PNG

有加密,咋办?别怂,干就是了,果断js逆向分析解密。先XHR打个断点,通过调用堆栈跟踪一下,发现所有请求就是在这里发起的,参数在这里已经准备好了。

channelId: 40011
index: 7
sVersion: 1
signKey: "66b11f57783161333ec6905b48677c35"
timeStamp: 1672650667533
webdriver: false
6.PNG

那就逆着js程序的逻辑,不断地打断点逆向跟踪分析,这里就不一一展开了,看得懂的不需要展开,看不懂的展开了也没有用。经过分析得到channelId固定为40011,sVersion固定为1,webdriver固定为false,index为10以内的随机整数,timeStamp为当前的时间戳,signKey是通过加密算法算出来的。

7.PNG
8.PNG
9.PNG

既然分析出来了,那剩下的就是修改js代码,让其按照原来的加密算法跑出参数来。js代码写好后,通过python中的execjs库执行js代码,取得返回的参数,然后就可以拼接出正确的URL。

with open('maoyan.js', 'r', encoding='utf-8') as f:
    js_code = f.read()
maoyan_js = execjs.compile(js_code)
params = self.maoyan_js.call('getParams')
ajax_url = 'https://www.maoyan.com/ajax' + url
ajax_url = ajax_url + '?timeStamp=' + str(params['timeStamp'])
ajax_url = ajax_url + '&index=' + str(params['index'])
ajax_url = ajax_url + '&signKey=' + str(params['signKey'])
ajax_url = ajax_url + '&channelId=' + str(params['channelId'])
ajax_url = ajax_url + '&sVersion=' + str(params['sVersion'])
ajax_url = ajax_url + '&webdriver=' + str(params['webdriver'])
yield scrapy.Request(url=ajax_url, callback=self.detail_parse)

其中的JavaScript代码的篇幅有点长,这里就不贴出来了,全部源代码都可以关注本人公众号【Python是我哥】获取。

到这里你以为这就完了?那你就想多了,顶多是完成了一半。你会发现请求回来详情页的响应中,有电影名称,播放时长,剧情简介等数据,但是猫眼口碑,评价人数和累计票房是一堆以&#x开头的转义序列。那咋办呢?还是一个字,干。

10.PNG

这应该是猫眼电影设计的字体反爬,果断在响应的源码中搜索下看有没有woff或者ttf文件之类的,果不其然。

11.PNG

根据链接下载字体文件并使用FontCreator打开,发现woff文件中字体编号的后四位与源码中转义序列的后四位是对应的。那么我只需要创建字体编号与数字对应关系的字典,问题就迎刃而解了。之后在网页源码中拿到转义序列,就可以根据转义序列转化成字体编号,再根据字体编号查找字典得到对应数字。

12.PNG

字体文件中也就10个数字,手动创建一个字典还不简单吗?那你又想多了。在刷新网页之后,你会发现转义序列是变化的,字体文件也是变化的。不同的字体文件中,相同数字的编号不一样,数字的排列顺序不一样,连数字的字形都不一样(应该是做了随机的细微调整,使用TTFont将字体文件保存为xml文件可以看到)。也就是说你根据固定一个字体文件手动创建的字典,在网页刷新后就用不了了。下图是同一页面刷新前后两次的对比,都是9和2,但源码中的转义序列和字体文件中的编号都发生了变化。

14.PNG

那怎么样才能让程序自动根据当前的字体文件创建字典呢?这就需要用到PIL和pytesseract了,前者可以把字体文件中的数字按编号的顺序写在一张图片上,后者可以根据图片识别出数字,那么就可以自动创建出编号和数字对应关系的字典了。

font_path = 'font/' + item['files'][0]['path']
font = TTFont(font_path)
code_list = font.getGlyphOrder()[2:]
for i in range(len(code_list)):
    code_list[i] = code_list[i].replace("uni", "\\u").lower()
text_content = "".join(code_list)
text_content = text_content.encode('utf-8').decode('unicode_escape')
text_font = ImageFont.truetype(font_path, 100)
image = Image.new("RGB", (600, 100), (255, 255, 255))
image_draw = ImageDraw.Draw(image)
image_draw.text((0, 0), text_content, font=text_font, fill="#000000")
result = pytesseract.image_to_string(image)
code_map = dict(zip(code_list, list(result)))

有了这个字典,那在请求回来详情页的响应中取到转义序列之后,就可以转换成编号,然后再查找字典取得对应的数字。

def process_num(self, num, code_map):
    if num:
        all_num = num.encode('unicode_escape').decode('utf-8')
        digits = re.findall(r'\\u[0-9a-z]{4}', all_num)
        for digit in digits:
            if digit == '\\u4e07':
                d = '万'
            else:
                d = code_map[digit]
            all_num = all_num.replace(digit, d)
        return all_num
    else:
        return ''

能看到这里的人应该算是志同道合的人了,有什么问题也可以加Q群:756788325,大家一起讨论学习爬虫技术。最后展示一下采集到的数据。

15.PNG

免责声明:
本公众号所有源码均为个人学习所编写,仅可用于计算机技术学习及研究等合法行为,禁止利用本公众号的源码从事任何违反本国(地区)法律法规的业务,如有发现存在违法违规行为我会举报到网监部门。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容