美文网首页
定时抓取豆瓣、猫眼影片

定时抓取豆瓣、猫眼影片

作者: Earrow | 来源:发表于2017-11-14 14:59 被阅读0次
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数据库是为了程序中断后持久化保存抓取记录,程序初始化时会把数据库的内容查询出来全部存入布隆过滤器,之后的判重仍然使用过滤器来做。

猫眼采集

猫眼的采集流程和豆瓣类似。不同的是猫眼的反爬虫策略,猫眼采用了自定义字体的方式对数字做了转换,页面上看起来正常的数字,在源码中的定义都是类似&#xeb43;这样的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表示 &#xf7c9;.&#xee94;&#xe8ca;,将其对应到上表为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

相关文章

网友评论

      本文标题:定时抓取豆瓣、猫眼影片

      本文链接:https://www.haomeiwen.com/subject/vvffvxtx.html