将 Scrapy 项目打包成 Docker 镜像

在本章前三节的内容中,我们了解了 Scrapy 项目的一种部署方式——Scrapyd,这是 Scrapy 官方提供的一种用于部署和管理 Scrapy 项目的解决方案,再配合 Gerapy,我们可以更加方便地管理基于 Scrapyd 部署的 Scrapy 项目。

当然,上述方案并不是唯一的。随着容器化技术的发展,Docker + Kubernetes 的解决方案变得越来越流行,Kubernetes 毫无疑问已经成了最主流的容器化编排工具,而且使用也越来越广泛。那么,我们能否把 Scrapy 打包成 Docker 容器,并迁移到 Kubernetes 进行管理和维护呢?当然是可以的。

接下来,我们就来了解下 Scrapy 项目的另外一种部署方式——基于 Docker + Kubernetes 的部署和维护方案,具体的内容包括:

  • 如何把 Scrapy 项目打包成一个 Docker 镜像;

  • 如何利用 Docker Compose 来方便地维护和打包镜像;

  • 如何使用 Kubernetes 来部署 Scrapy 项目的 Docker 镜像;

  • 如何监控 Scrapy 项目的爬取状态。

接下来,我们先来了解如何把一个 Scrapy 项目制作成一个 Docker 镜像。

准备工作

在本节中,我们要把前文的 Scrapy 项目打包成一个 Docker 镜像。

首先,本节基于前文 Scrapy-Redis 的分布式爬虫进行改写,代码见 https://github.com/Python3WebSpider/ScrapyCompositeDemo/tree/scrapy-redis ,可以直接克隆代码,注意切换到 scrapy-redis 分支,命令如下:

git clone -b scrapy-redis https://github.com/Python3WebSpider/ScrapyCompositeDemo.git

运行上述命令之后,我们得到的就是 ScrapyCompositeDemo 项目的 scrapy-redis 分支的代码。

另外,我们还需要确保已经安装好 Docker 并能正常使用 docker 命令,具体的安装方式可以参考 https://setup.scrape.center/docker

另外,由于本项目需要用到代理池和账号池,所以还需要确保二者可以正常运行,具体的内容可以参考 16.3 节。

创建 Dockerfile

首先,在项目的根目录下新建一个 requirements.txt 文件,将整个项目依赖的 Python 环境包都列出来,如下所示:

scrapy
aiohttp
scrapy-redis
environs

如果库需要特定的版本,我们还可以指定版本号,如下所示:

scrapy>=2.0.0
pymongo>=3.7.3

在项目根目录下新建一个 Dockerfile 文件,文件不加任何后缀名,将其内容改为:

FROM python:3.7
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["scrapy", "crawl", "book"]

第一行的 FROM 代表使用的 Docker 基础镜像,这里我们直接使用 python:3.7 的镜像,在此基础上运行 Scrapy 项目。

第二行的 WORKDIR 是运行路径,这里我们将其设置为 /app,这样在 Docker 中,最终程序所在的路径就是 /app

第三行的 COPY 是将本地的 requirements.txt 复制到 Docker 的工作路径下,即复制到 /app 下。

第四行的 RUN 指定了一个 pip 的命令,用来读取上一步复制到 Docker 工作路径下的 requirements.txt 文件,并安装该文件里面列出的所有依赖库。

第五行的 COPY 是将当前文件夹下所有的文件全部复制到 Docker 的 /app 路径下。这时候大家可能有点疑惑,为什么第三行不直接复制而需要再复制一次呢?这是因为这样可以单独将较为耗费构建时间的安装依赖步骤独立为 Docker 镜像单独的层级。这样的话,只要 requirements.txt 不变,以后再次构建 Docker 镜像的时候,就会直接利用已经构建的层级,不再耗费构建时间。所以在适当的情况下,我们可以试着将一些较为耗时的初始化操作单独放到相对靠前的层级来。

第六行的 CMD 是容器启动命令。在容器运行时,此命令会被执行。我们这里直接用 scrapy crawl book 来启动爬虫。

如果你对 Dockerfile 的编写还不够熟悉,可以额外学习一下 Docker 相关的基础知识和 Dockerfile 的编写教程。

修改代码

由于我们对接的是 Docker,所以需要修改几处代码,比如代理池、账号池的 API 地址以及 Redis 的连接地址,之前是直接写死在代码里面的,现在我们构建了 Docker 镜像,那这些定义改成环境变量的形式。

首先在 middleware.py 文件中,accountpool\_url 和 proxypool\_url 变量的定义需要修改如下:

import os
accountpool_url = os.getenv('ACCOUNTPOOL_URL')
proxypool_url = os.getenv('PROXYPOOL_URL')

这里将固定的 URL 改写成通过 getenv 方法获取的环境变量,这时候需要另外导入 os 这个库。对应的两个环境变量分别为 ACCOUNTPOOL_URLPROXYPOOL_URL

另外,在 settings.py 中,REDIS_URL 的定义也需要修改为通过环境变量获取的方式,具体如下:

REDIS_URL = os.getenv('REDIS_URL')

修改完毕之后,我们就可以构建镜像了。

构建镜像

接下来,我们便可以构建镜像了,相关命令如下:

docker build -t scrapycompositedemo .

注意该命令最后有一个点 .,代表当前运行目录。

输出结果类似如下:

Sending build context to Docker daemon  257.5kB
Step 1/6 : FROM python:3.7
 ---> 22eb61a2cb94
Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 5a965b3af33a
Step 3/6 : COPY requirements.txt .
 ---> 8d949288babe
Step 4/6 : RUN pip install -r requirements.txt
 ---> Running in d4bbd8b879cc
Collecting scrapy
  Downloading Scrapy-2.4.1-py2.py3-none-any.whl (239 kB)
Collecting aiohttp
  Downloading aiohttp-3.7.3-cp37-cp37m-manylinux2014_x86_64.whl (1.3 MB)
...
Successfully installed Automat-20.2.0 PyDispatcher-2.0.5 PyHamcrest-2.0.2 Twisted-20.3.0 aiohttp-3.7.3 async-timeout-3.0.1 attrs-20.3.0 cffi-1.14.4 chardet-3.0.4 constantly-15.1.0 cryptography-3.3.1 cssselect-1.1.0 hyperlink-21.0.0 idna-3.1 itemadapter-0.2.0 itemloaders-1.0.4 jmespath-0.10.0 lxml-4.6.2 multidict-5.1.0 parsel-1.6.0 protego-0.1.6 pyOpenSSL-20.0.1 pyasn1-0.4.8 pyasn1-modules-0.2.8 pycparser-2.20 queuelib-1.5.0 redis-3.5.3 scrapy-2.4.1 scrapy-redis-0.6.8 service-identity-18.1.0 six-1.15.0 typing-extensions-3.7.4.3 w3lib-1.22.0 yarl-1.6.3 zope.interface-5.2.0
Removing intermediate container d4bbd8b879cc
Step 5/6 : COPY . .
 ---> 1d693eedb484
Step 6/6 : CMD ["scrapy", "crawl", "book"]
 ---> Running in 19d954c9137b
Removing intermediate container 19d954c9137b
 ---> a46de4b66276
Successfully built a46de4b66276
Successfully tagged scrapycompositedemo:latest

这就能证明镜像构建成功了,这时执行如下命令,可以查看构建的镜像:

docker images

返回结果中有一行就是:

scrapycompositedemo latest a46de4b66276 2 minutes ago 968MB

这就是我们新构建的镜像。

运行

运行时,我们需要先指定环境变量。可以新建一个 .env 文件,其中内容如下:

ACCOUNTPOOL_URL=http://host.docker.internal:6777/antispider?random
PROXYPOOL_URL=http://host.docker.internal:5555/random
REDIS_URL=redis://host.docker.internal:6379

这里定义了三个环境变量,分别是账号池、代理池、Redis 数据库的连接地址。其中每个变量的 host 地址都是 host.docker.internal,这代表 Docker 所在宿主机的 IP 地址,通过 host.docker.internal,在 Docker 内部便可以访问宿主机的相关资源。

同时,这里需要确保账号池运行在本机的 6777 端口,代理池运行在 5555 端口,Redis 数据库运行在 6379 端口。

我们可以先在本地测试运行,此时可以执行如下命令:

docker run --env-file .env scrapycompositedemo

这样我们就成功运行了刚才构建的 Docker 镜像,运行结果类似如下:

2021-02-04 18:30:49 [scrapy.utils.log] INFO: Scrapy 2.4.1 started (bot: scrapycompositedemo)
2021-02-04 18:30:49 [scrapy.utils.log] INFO: Versions: lxml 4.6.2.0, libxml2 2.9.10, cssselect 1.1.0, parsel 1.6.0, w3lib 1.22.0, Twisted 20.3.0, Python 3.7.9 (default, Jan 12 2021, 17:26:22) - [GCC 8.3.0], pyOpenSSL 20.0.1 (OpenSSL 1.1.1i 8 Dec 2020), cryptography 3.3.1, Platform linux-4.19.76-linuxkit-x86_64-with-debian-10.7
2021-02-04 18:30:49 [scrapy.utils.log] INFO: Using reactor: twisted.internet.asyncreactor.AsyncioSelectorReactor
2021-02-04 18:30:49 [scrapy.utils.log] INFO: Using asyncio event loop: asyncio.unix_events.UnixSelectorEventLoop
2021-02-04 18:30:49 [scrapy.crawler] INFO: Overridden settings:
...
2021-02-04 18:30:49 [scrapy.middleware] INFO: Enabled spider middlewares: ['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware', 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware', 'scrapy.spidermiddlewares.referer.RefererMiddleware', 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware', 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2021-02-04 18:30:49 [scrapy.middleware] INFO: Enabled item pipelines: []
2021-02-04 18:30:49 [scrapy.core.engine] INFO: Spider opened
2021-02-04 18:30:49 [book] DEBUG: Resuming crawl (40 requests scheduled)
2021-02-04 18:30:49 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2021-02-04 18:30:49 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2021-02-04 18:30:49 [middlewares.authorization] DEBUG: set authorization jwt eyJ0eXAiOiJKV1QiLCJhbgcCI6IkJIUzI1NiJ9.eyJic2VyVXItXCI6IOZwYWlkXCm5hbWUiOiJIUzIcbzUjIwZIIWXiiokjYyNTAxOTI1LCJhbfPcBII6IIISMjYwAkFOfjoiXCNjEyNDUANzlL0. IIXmQyF3mAS28P3qGX7XIG7sROHSsk_4RZHz8VsbOK
2021-02-04 18:30:49 [middlewares.proxy] DEBUG: set proxy 183.88.169.162:8080
...
2021-02-04 18:30:51 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://antispider7.scrape.center/api/book/2237621/> (referer: https://antispider7.scrape.center/api/book/?limit=18&offset=72)

可以看到,在 Docker 中可以正常从代理池和账号池获取随机代理和随机 token,同时也能正常爬取到结果,这说明代理池、账号池、Redis 数据库均能正常连接。

推送至 Docker Hub

构建完成之后,我们可以将镜像推送到 Docker 镜像托管平台,如 Docker Hub 或者私有的 Docker Registry 等,这样我们就可以从远程服务器下拉镜像并运行了。

以 Docker Hub 为例,如果项目包含一些私有的连接信息(如数据库),我们最好将 Repository 设为私有或者直接放到私有的 Docker Registry。

首先在 [https://hub.docker.com](https://hub.docker.com) 上注册一个账号,然后新建一个 Repository,名为 scrapycompositedemo。比如,我的用户名为 germey,新建的 Repository 名为 scrapycompositedemo,那么此 Repository 的地址就可以用 germey/scrapycompositedemo 来表示,这里需要修改为你的用户名。

另外,我们还需要使用如下命令来登录 Docker Hub:

docker login

输入 Docker Hub 的用户名和密码之后,就可以完成登录了。接下来,我们就可以往 Docker Hub 推送自己构建的 Docker 镜像了。

为新建的镜像打一个标签,命令如下所示:

docker tag scrapycompositedemo:latest germey/scrapycompositedemo:latest

推送镜像到 Docker Hub 即可,命令如下所示:

docker push germey/scrapycompositedemo

此时 Docker Hub 便会出现新推送的 Docker 镜像了,如图 17-11 所示。

图 17-11 在 Docker Hub 上出现新推送的 Docker 镜像

这个镜像可以供后面使用,如果我们想在其他主机上运行这个镜像,在主机上装好 Docker,运行代理池、账号池、Redis 数据库后,按照同样的方式新建 .env 文件,可以直接执行如下命令:

docker run --env-file .env germey/scrapycompositedemo

这样就会自动下载刚才我们所推送的镜像,然后读取环境变量并运行了,运行效果和刚才一模一样。

总结

我们讲解了将 Scrapy 项目制作成 Docker 镜像并推送到 Docker Hub 的过程,接下来我们会介绍 Docker Compose 和 Kubernetes 的用法,来尝试部署和运行 Scrapy 项目。