Linux入门教程:用 Docker Compose 部署 PySpider,采用 Python


PySpider 是一个国人编写的强大的网络爬虫系统并带有强大的 WebUI。采用 Python 语言编写,分布式架构,支持多种数据库后端,强大的 WebUI 支持脚本编辑器,任务监视器,项目管理器以及结果查看器。

上面这段是从「PySpider 中文网」摘录的。总而言之,它就是一个 All-in-one 的爬虫系统,比 Scrapy 强的地方,主要就是上手更容易,打开 web 页面就可以开始写爬虫。但「开包即食」这一点,仅限于 demo 阶段,比如在 http://demo.pyspider.org 试用一下。真正到自己用的时候,还是要考虑一个部署的问题。尤其是要充分利用它的分布式架构的时候,怎么部署更是一个避不过去的问题。

我之前做过一个 side project,功能是从国内各大视频网站抓取动漫新番,如果发现更新,就推送通知到 iOS 客户端,iOS 客户端仅展示订阅的动漫列表,观看还需要跳转到各官方应用去。

做这个的动机是现在各大视频网站的版权竞争,导致要追一季新番,不得不来回切换各个视频网站,我又不想打开一堆通知,让它们给我推垃圾信息。所以就搞一个只推送动漫更新的应用吧。

这个项目爬虫端已经完成,iOS 客户端也提交了审核,然而就是审核没有通过,没过的原因有以下三点:

用了一些版权图片 在 webView 预览动漫的时候,某视频网站乱跳红包 审核人员不看动漫,把鸣人的儿子当成了我山寨的一个动漫形象。

后来发现自己已经有点脱宅了,也就没有再去和审核人员争论了,项目就这么流产,和许多其他 side project 一样。

下面是我在做这个 side project 的时候,部署 PySpider 的方案。

Docker + LeanCloud

现在要部署一个 web 项目,用 Docker 已经是首选了,可以节约不少时间。iOS 后端用现成的 LeanCloud 来做,不用写后端代码,又可以节约不少时间。

要用 LeanCloud,需要引入 LeanCloud 的 SDK,而 binux/pyspider 这个 image 并不包含。所以需要自己 build 一个 image 了。方法就是写一个 Dockerfile,内容如下:

FROM binux/pyspider:latest
MAINTAINER suosuopuo <suosuopuo@gmail.com>

# include the LeanCloud SDK
RUN pip install leancloud-sdk

然后执行 docker build -t my/pyspider – < Dockerfile,如果需要用到 LeanCloud SDK,只要用 my/pyspider 代替 binux/pyspider 就好了。

数据库和消息队列

尽管最终结果放在 LeanCloud 上,但 PySpider 各个组件运行还是需要数据库支持的。这部分主要参考这个 Deployment of demo.pyspider.org。数据库是 postgresql,消息队列用 redis。

数据库和消息队列手动用 docker 启动,不用 docker-compose 管理,这和 Deployment of demo.pyspider.org 也是一致的。

启动就两条命令:

docker run --name redis -d -p 6379:6379 redis

docker run --name postgres -v /data/postgres/:/var/lib/postgresql/data -d -e POSTGRES_PASSWORD="" postgres

数据库和用户需要手动创建:

docker exec -it postgres bash

然后输入:

psql -U postgres

CREATE USER myname  WITH PASSWORD ‘mypassword’;

CREATE DATABASE taskdb WITH OWNER= myname LC_COLLATE='en_US.utf8' LC_CTYPE='en_US.utf8' ENCODING='UTF8' TEMPLATE=template0;

CREATE DATABASE projectdb WITH OWNER= myname LC_COLLATE='en_US.utf8' LC_CTYPE='en_US.utf8' ENCODING='UTF8' TEMPLATE=template0;

CREATE DATABASE resultdb WITH OWNER= myname LC_COLLATE='en_US.utf8' LC_CTYPE='en_US.utf8' ENCODING='UTF8' TEMPLATE=template0;

Docker Compose

除了数据库和消息队列,其他组件都用 docker-compose 来管理了,
docker-compose.yml 的内容主要参考这个 Running pyspider with Docker,和这个 Deployment of demo.pyspider.org。

主要是让各个组件连接外部的数据库和消息队列,限制一下内存占用,另外设置一个 WebUI 的用户名和密码。Result worker 要使用之前 build 的 image,因为它需要用到 LeanCloud。

然后 docker-compose up -d 就可以启动各组件了。

phantomjs:
  image: binux/pyspider:latest
  command: phantomjs
  mem_limit: 256m
  restart: always
result:
  image: my/pyspider
  external_links:
    - postgres
    - redis
  volumes:
    - ./share:/opt/share
  working_dir: /opt/share
  command: '--taskdb "sqlalchemy+postgresql+taskdb://username:password@postgres/taskdb"  --projectdb "sqlalchemy+postgresql+projectdb://username:password@postgres/projectdb" --resultdb "sqlalchemy+postgresql+resultdb://username:password@postgres/resultdb" --message-queue "redis://redis:6379/1" result_worker --result-cls=resultWorker.VResultWorker'
  mem_limit: 128m
  restart: always
processor:
  image: binux/pyspider:latest
  external_links:
    - postgres
    - redis
  command: '--projectdb "sqlalchemy+postgresql+projectdb://username:password@postgres/projectdb" --message-queue "redis://redis:6379/1" processor'
  mem_limit: 128m
  restart: always
fetcher:
  image: binux/pyspider:latest
  external_links:
    - redis
  links:
    - phantomjs
  command : '--message-queue "redis://redis:6379/1" --phantomjs-proxy "phantomjs:80" fetcher --xmlrpc'
  mem_limit: 128m
  restart: always
scheduler:
  image: binux/pyspider:latest
  external_links:
    - postgres
    - redis
  command: '--taskdb "sqlalchemy+postgresql+taskdb://username:password@postgres/taskdb"  --projectdb "sqlalchemy+postgresql+projectdb://username:password@postgres/projectdb" --resultdb "sqlalchemy+postgresql+resultdb://username:password@postgres/resultdb" --message-queue "redis://redis:6379/1" scheduler'
  restart: always
webui:
  image: binux/pyspider:latest
  external_links:
    - postgres
    - redis
  links:
    - scheduler
    - phantomjs
  command: '--taskdb "sqlalchemy+postgresql+taskdb://username:password@postgres/taskdb"  --projectdb "sqlalchemy+postgresql+projectdb://username:password@postgres/projectdb" --resultdb "sqlalchemy+postgresql+resultdb://username:password@postgres/resultdb" --message-queue "redis://redis:6379/1" webui --need-auth --username ui_username --password ui_password'
  ports:
    - "5000:5000"

ResultWorker

关于 result worker,官方文档并没有给出 demo,我这里贴一下我的 result worker。首先初始化 LeanCloud SDK,每次抓到数据的时候,去 LeanCloud 查询是不是已经存在同名的动漫(一个动漫可以有多个别名)。如果不存在,就创建这个别名,如果已经存在,就继续执行其他的逻辑(简洁起见,不全部贴出了)。

resultWorker.py 放在和 docker-compose.yml 相同的位置,在启动 result worker 的时候已经通过选项指定自定义的 VResultWorker

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# vim: set et sw=4 ts=4 sts=4 ff=unix fenc=utf8:
# Author: suosuopuo<suosuopuo@gmail.com>
#
# Created on 2017-06-15 15:37:46

from pyspider.result import ResultWorker
import leancloud
from datetime import datetime

appKey = ''
appID  = ''

class VResultWorker(ResultWorker):
  def __init__(self, resultdb, inqueue):
    self.resultdb = resultdb
    self.inqueue = inqueue
    self._quit = False

    leancloud.init(appID, appKey)
    leancloud.use_region('CN')

  def on_result(self, task, result):
    if result['type'] == 'anime-update':
      self.handle_anime_update(task, result)

  def handle_anime_update(self, task, result):
    title = result['title']

    try:
      alias_query = leancloud.Query('Alias')
      alias_query.include('anime')
      alias_query.equal_to('title', title)

      alias = alias_query.first()
    except leancloud.LeanCloudError as e:
      if e.code == 101:
        Alias = leancloud.Object.extend('Alias')
        Anime = leancloud.Object.extend('Anime')

        alias = Alias()
        alias.set('title', title)

        anime = Anime()
        anime.set('title', title)
        anime.set('area', result['area'])
        anime.set('cover', result['cover'])
        anime.set('weekday', result['weekday'])
        anime.set('ep_num', 12)
        anime.set('is_finished', result['is_finished'])
        anime.set('verified', False)

        anime.save(fetch_when_save = True)

        alias.set('targetAnime', anime)
        alias.save()
      return

...

还有话说

我是很喜欢这样的「全家桶」式的方案的,可以快速地试验一些想法,但是很遗憾 PySpider 的代码已经很久没有更新了。好在目前为止 PySpider 还足够强大。

我没有解释每一条命令、每一个选项,因为这不是 PySpider 的文档,如果需要更详细的解释,还是需要去挖掘官方文档。

写这篇的时间距离写这个 side project 已经隔了几个月。我也懒到没有去再次验证各个配置和命令,所以「仅供参考」。

另外提醒一下,如果有人也打算用 LeanCloud 做后端,需要注意 LeanCloud 是有访问次数限制的。可以根据抓取的页面特点,用 age, itag等功能减少对 LeanCloud 的访问次数。

不过就算没有访问次数限制,对一个远程服务高频访问,也不是好的策略。

相关内容