Docker 搭建Selenium执行环境
Keep Team Lv4

  如果能用Docker搭建出一套Selenium执行环境,并部署在云端。当我们把脚本上传至云后,可以自动执行脚本(按需执行),而脚本执行结束后又能自动退出(不占用资源)。让我们小小的云能最大化利用起来,想想就很激动呢。

需求分析

  首先当然是需要一个具有完整Selenium执行环境的Docker,另外需要一个具有浏览器Driver的Docker环境。为什么不合在一个Docker里面呢?一方面希望Docker尽可能功能独立(低耦合),另一方面如果我们自己做Driver的话挺麻烦的(有现成的),所以还是分为两个Docker吧🤔。

流程图

  差不多如上图所示一个docker为另一个docker提供服务,完成一次网页访问。接下来我们看看这两个docker如何搭建吧。

Docker 搭建

  前文提到Driver有现成的docker,那么就先从这里入手吧🤣。

Driver Docker

  在Selenium官方Github上提供了一系列已封装好的Driver Docker,并至今依然在持续更新。相关使用方法可以查看其说明,这里就不啰嗦了。简单来讲使用方法如下:

  • Firefox

    $ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-firefox:4.0.0-beta-4-prerelease-20210517

  • Chrome

    $ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:4.0.0-beta-4-prerelease-20210517

  • Edge

    $ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-edge:4.0.0-beta-4-prerelease-20210517

  对外暴露4444端口提供Driver服务。所以我们只需要pull对应的docker镜像就好。

Selenium Docker

  现在来看看Selenium环境又如何搭建。其实就是选择一个合适的基础镜像(精简),在其基础上安装好必须的软件即可。通过在DockerHub上选择合适的官方镜像Python),然后安装常用爬虫库。Dockerfile如下:

FROM python:3.9.5
WORKDIR /home/code
RUN pip config set global.index-url https://mirrors.
aliyun.com/pypi/simple/ \
    && pip install selenium requests lxml beautifuls
up4 pymysql

  至此我们就完成了所需Docker的搭建。再来看看如何让他们配合执行。

执行

  需要用到我们的老朋友docker-compose😀。将脚本映射进我们的Selenium容器,并联通Driver容器。来看看docker-compose.yml怎么写的呢。

version: "3"
services:
  spider:
    image: selenium-py
    volumes:
      - /home/core/data/python:/home/code/
    command: python your_script.py
    depends_on:
      - chrome
  chrome:
    image: selenium/standalone-chrome
    container_name: chrome
    volumes:
      - /dev/shm:/dev/shm

  是不是超级简单?docker运行时就会去执行python your_script.py,执行完之后就会退出。看上去好完美!而且没有对外暴露任何端口,好安全!来看看这个your_script.py怎么写呢。

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities
mport DesiredCapabilities
driver = webdriver.Remote(
    command_executor="http://chrome:4444/wd/hub",
    desired_capabilities=DesiredCapabilities.CHROME
)
driver.get("https://www.ray0728.cn")
print(driver.title)
driver.close()

  http://chrome:4444/wd/hub中的chrome是Driver Docker的容器名,在docker-compose.yml中有特别的指定。而执行结果可以通过docker-compose logs -f spider进行查看。

问题

  1. 连接Driver失败

  Chrome Docker将Chrome运行起来是需要时间的,注意并不是容器运行起来就行了,毕竟Docker只是容器(可以理解为轻量级的虚拟机),服务是容器中的应用程序,就像我们不会认为系统开机,就是后台服务开始运行了。因此我们最好等到Driver开始提供服务后再执行脚本,那么怎么做呢?答案很简单,通过不停的去探测Driver docker对外提供服务的端口是否可用,来判断服务是否被拉起。这里我们用NetCat工具。可是NC工具不一定是每个Linux发行版本,所以加在Dockerfile里,重新构建一个带有nc的Selenium Docker。

FROM python:3.9.5
WORKDIR /home/code
ADD sources.list /etc/apt/
RUN apt update \
    && apt install -y netcat-openbsd \
    && pip config set global.index-url https://mirrors.
liyun.com/pypi/simple/ \
    && pip install selenium requests lxml beautifuls
up4 pymysql

  选取的Python镜像是以Debian为基础,sources.list是提前准备好的国内apt源。

  在调用your_script.py之前先用nc判断Driver是否准备好,所以重新调整下docker-compose.yml。

version: "3"
services:
  spider:
    image: selenium-py
    volumes:
      - /home/core/data/python:/home/code/
    command: run.sh
    depends_on:
      - chrome
  chrome:
    image: selenium/standalone-chrome
    container_name: chrome
    volumes:
      - /dev/shm:/dev/shm

  与之前的区别就在于用run.sh代替了直接调用script。这样就可以将nc调用放在run.sh当中了。

#!/bin/bash
while ! `nc -z chrome 4444`; do sleep 3; done
python your_script.py
  1. Driver Docker不退出

  Selenium Docker执行完脚本后就会自动退出(无其他执行任务)。但是Driver Docker本身就是提供持续服务,因此并不会因为Selenium Docker退出而退出。怎么才能让Driver Docker知道Selenium退出了呢?方法有两种。

  • 参考通过nc判断服务端口是否下线来推断Docker是否停止

  但是Driver Docker并不是我们自己构建的,而且暂时也不打算以其为基础做二次封装(懒)。所以我们无法加入nc判断逻辑,该方案先搁置吧。

  • 通过sock通知

  这里就需要知道一个关于Docker Sock的背景知识了。简单来说通过Unix Domain Socket可以让Docker相互之间通信,而通信需符合API规范。我们需要的STOP规范如下:

stop api

  按照API所描述的方法,将停止命令加在run.sh最后。

#!/bin/bash
while ! `nc -z chrome 4444`; do sleep 3; done
python test.py
curl -s --unix-socket /var/run/docker.sock -X POST h
tp://localhost/containers/chrome/stop

  并将主机/var/run/docker.sock映射给Selenium Docker。在docker-compose.yml中加入

docker-compose.yml
version: "3"
services:
  spider:
    image: selenium-py
    volumes:
      - /home/core/data/python:/home/code/
      - /var/run/docker.sock:/var/run/docker.sock
    command: /home/code/run.sh
    depends_on:
      - chrome
  chrome:
    image: selenium/standalone-chrome
    container_name: chrome
    volumes:
      - /dev/shm:/dev/shm

  至此Selenium Docker运行完成后就会通知Driver Docker退出。完全满足本文开头所描述的需求。😎

引申

  本文最后通过sock向Docker Deamon发送命令,从而实现所需要的功能。但是有两点需要特别注意。

  • 并不能按照API文档中的参数直接向Docker Deamon发送命令,而是需要将部分特殊字符(如 [{:)转换为html字符,然后再发送给sock。

  • 不要将docker.sock暴露给外部,因为有安全隐患,毕竟Docker Deamon只要收到符合API规范的字符串就会触发对应的操作(包括停止、删除容器等)。