hei Siri
为什么不能直接显示影片列表?
我只是想知道周末能去看什么电影而已,还要打开一连串的网页。不,生活不应该是这样。
我做了一个程序员应该做的事情,在树莓派上部署爬虫程序,每周末抓取有哪些新电影上映并推送给我,这样就能假装会去看了。
豆瓣、猫眼数据采集
本文接下来会介绍如何实现一个爬虫程序,定期进行豆瓣上正在热映的影片信息和猫眼影片票房数据采集,并部署在树莓派上运行。
豆瓣采集
打开豆瓣正在上映影片的网页:
正在上映影片列表
点击「显示全部影片」按钮会显示出所有影片,这些剩余的影片是点击按钮后再动态获取的吗?我们进一步查看网页源代码会发现其实在第一次访问URL的时候,所有影片的数据就已经加载出来了,点击按钮只是做了一个显示的动作,这实际上只是一个静态页面。
现在我们来分析这个页面。
正在热映影片列表源代码
豆瓣的网页源代码格式很清晰,我们很容易发现影片被定义在
<div id="nowplaying">标签的列表中,并且列表属性中定义了很多影片元数据,如片名、评分、片长等等。
这些信息还不够让我判断是不是应该去看这部电影,或许再获取电影简介和一些评论可以派上用场。我们也不难发现点击列表中的片名就会打开影片详情页,而详情页的链接相应的定义在<li class="stitle">中。详情页面中有简介、有评论,看来解析这两类页面就已经可以满足我的需求。
写代码。
我们先获取影片列表:
movies = tree.xpath('//div[@id="nowplaying"]//ul[@class="lists"]/li')
然后遍历这个列表获取元数据和详情页URL:
for movie in movies:
name = movie.xpath('./@data-title')[0]
score = movie.xpath('./@data-score')[0]
region = movie.xpath('./@data-region')[0]
director = movie.xpath('./@data-director')[0]
actors = movie.xpath('./@data-actors')[0]
link = movie.xpath('.//li[@class="stitle"]/a/@href')[0]
影片简介以多行文本的形式定义在<span property="v:summary">中,头尾还有若干空格、换行等比较杂乱,我们获取后要再简单做个清洗:
summary = '\n'.join([text.strip() for text in tree.xpath('//span[@property="v:summary"]/text()')])
评论我们只抓取热门评论,三言两语了解观众对影片的评价即可:
hot_comment = [comment.strip() for comment in tree.xpath('//p[@class=""]/text()') if comment.strip()]
页面解析就是这些,非常容易。采集的话使用requests抓取页面,由于我们的访问频率很低,因此不需要什么麻烦的反反爬虫手段,替换UA并控制抓取各影片详情页的间隔就好。
唯一要注意的是,由于一般影片的放映周期是肯定大于一周的,因此我们每次抓取列表页的时候,其中肯定会有之前采集过的影片数据,这就涉及到去重的问题。
这里我们使用布隆过滤器做去重,并使用MongoDB数据库做增量抓取。
布隆过滤器使用pybloom模块,用法和原生的set很像,每次在影片列表页中获取片名和详情页URL时,就在过滤器中判断是否存在,若不存在就添加到过滤器和数据库中,并进行进一步抓取。
使用MongoDB数据库是为了程序中断后持久化保存抓取记录,程序初始化时会把数据库的内容查询出来全部存入布隆过滤器,之后的判重仍然使用过滤器来做。
猫眼采集
猫眼的采集流程和豆瓣类似。不同的是猫眼的反爬虫策略,猫眼采用了自定义字体的方式对数字做了转换,页面上看起来正常的数字,在源码中的定义都是类似这样的Unicode码。应对这种反爬虫措施一般有两种方法:1.对数字截图,进行OCR识别;2.找出数字和Unicode码的映射关系。我会使用第二种方式。
其实找出这种映射比较简单,由于猫眼使用的是自定义字体对数字进行转换,因此我们只要解析字体文件,看看是否可以从中分析出映射关系。
查看猫眼票房的源代码,我们会发现字体文件经过base64编码后定义在了<style id="js-nuwa">标签中,我们用正则将其提取出后经过base64解码写入文件:
>>> font_face = re.match(r'.*base64,(.*)\) format.*', font_face_text, re.S).group(1)
>>> font_data = base64.b64decode(font_face)
>>> with open('font.ttf', 'wb') as fp:
... fp.write(font_data)
安装fontTools模块后,使用ttx命令处理字体文件:ttx font.ttf生成font.ttx文件。ttx文件是一种字体信息的xml表示,打开后我们可以看到:
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name="glyph00000"/>
<GlyphID id="1" name="x"/>
<GlyphID id="2" name="uniE3E8"/>
<GlyphID id="3" name="uniF4C0"/>
<GlyphID id="4" name="uniE8CA"/>
<GlyphID id="5" name="uniEB43"/>
<GlyphID id="6" name="uniE1C2"/>
<GlyphID id="7" name="uniEE94"/>
<GlyphID id="8" name="uniF7C9"/>
<GlyphID id="9" name="uniE570"/>
<GlyphID id="10" name="uniE55D"/>
<GlyphID id="11" name="uniEBA2"/>
</GlyphOrder>
字体的glyph order就是一组数字和Unicode码的逻辑映射,而这是不是我们要找的呢?我们检查一下,随便找一组数的Unicode表示 .,将其对应到上表为6.52,正是网页中看到的数字。看来只要获取这个字体文件的glyph order就可以了,fontTools模块提供了现成的方法:getGlyphOrder()。
数据推送
数据采集完后需要推送出来,使用最简单的发邮件方式:
class MailSender:
def __init__(self):
cfg = ConfigParser()
cfg.read('config.ini')
self._host = cfg.get('mail', 'host')
self._pwd = cfg.get('mail', 'pwd')
self._sender = cfg.get('mail', 'sender')
self._receiver = cfg.get('mail', 'receiver')
def send(self, subject, content):
"""发送邮件。
:param subject: 主题。
:param content: 内容。
"""
msg = MIMEText(content, 'plain', 'utf-8')
msg['From'] = Header(self._sender, 'utf-8')
msg['To'] = Header(self._receiver, 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')
smtp = smtplib.SMTP_SSL(self._host)
smtp.ehlo(self._host)
smtp.login(self._sender, self._pwd)
smtp.sendmail(self._sender, self._receiver, msg.as_string())
smtp.quit()
封装一个MailSender类进行邮件的发送,邮件账户信息通过配置文件config.ini读取。
部署运行
整套程序部署在树莓派上完全没有兼容性问题,唯一要注意的是由于raspbian是32位系统,MongoDB在其上只有2.x版本,如果想使用新的MongoDB可以在树莓派上装Fedora系统解决,但Fedora在树莓派上运行的性能不如raspbian,并且对某些硬件接口支持的也不好。
等等,还需要让程序定时运行,这样才能让我在周末及时获取需要的信息!我们使用apscheduler模块解决这个问题,该模块提供兼容cron表达式的语法,并且只需要通过装饰器来调用。好的,让我们设置每周五早上8点执行爬虫程序:
@sched.scheduled_job('cron', minute='0', hour='8', day_of_week='fri')
def fun():
# 抓取代码
pass
搞定,执行结果:
影片更新邮件
完整代码开源在GitHub: https://github.com/Earrow/movies_nowplaying











网友评论