一只Python小爬虫的Linux定时任务之旅

目录

  1. 起因
  2. systemd守护进程
  3. 单元(Units)
    3.1. 单元加载路径(Unit load path)
    3.2. 通用节(section)
    3.2.1. [Unit]
    3.2.2. [Install]
  4. Service配置选项
  5. Timer配置选项
  6. systemctl命令
    6.1. 与查看Unit状态相关
    6.2. 与操作Unit相关
    6.3. 与守护进程相关
  7. systemd如何启动服务
  8. References
  9. Materials

1. 起因

由于众所周知的原因,谷歌搜索引擎正常情况下无法使用,某度又实在令人气愤,因此,从16年开始,我就一直使用微软的必应(Bing)搜索引擎。必应有个特色就是它的主界面是自带背景图的(如图1),而且每天一换。
Fig 1 Bing bg image

个人感觉每天选取的图片都还挺不错的,清晰度也很高,因此萌生了一个将它背景图片下载下来的想法。整好周末闲来无事,说干就干。

这其实就是一个再简单不过的爬虫,不需要任何复杂的欺骗手段,唯一需要做的就是查看浏览器是怎么将背景图下载下来的,然后模仿一下浏览器就行。由于一开始使用谷歌浏览器的开发者功能无法直接定位背景图元素位置,还走了点小弯路:查看源码的时候发现有一大串base64编码的字符,我以为这就是背景图的编码,用正则表达式提取后解码发现就只是一些简单的图标而已。

下载背景图片的核心代码非常简单(GitHub Base:https://github.com/zmychou/bing-bg-downloader
),也就二十来行,也不需要除标准库外的任何三方库,主要问题是如何运行。因为背景图片每天更新,因此我们的小爬虫只需要每天爬取一次。有三种方案:

  1. 每天手动执行一次。主要问题是麻烦,每天还要定个闹钟;
  2. 程序启动后,爬取一次后休眠,定时醒来检查是否已经是第二天。缺点是当机器重启后需要重新手动启动爬虫,并且,一直占用着系统的资源,虽然是个小爬虫,但是这种占着茅坑不拉shi的做法并不可取,除非走投无路;
  3. 使用系统的定时任务功能。缺点是需要适配不同的系统,但是自己日常只用乌班图,简单点只关注Linux下的定时任务系统即可。

敲定了,就是Linux下的定时任务来实现我们每天爬取一张背景图的需求。

2. systemd守护进程

Linux系统中有一个叫做systemd的守护进程(Linux中一般以d结尾的程序都是守护进程),他是系统和服务的管理者(也可以说是守护者),它可以说是Linux系统的中流砥柱,很多系统功能的实现都是以它为基础。例如主机名、日期、本地化等的配置文件的管理;已登陆用户、容器、虚拟机、简单网络处理的守护进程、域名解析、网络时间同步和日志转发等功能的维护,等等。

而定时任务是systemd提供的功能之一。想要在Linux下实现定时任务,需要三个伙计的帮忙:

  1. systemctl:他是查看systemd状态和控制systemd行为的主要命令;
  2. .service单元(Unit):告诉systemd做什么;
  3. .timer单元:告诉systemd什么时候做。

其中.service.timer称为单元(Unit),其他类型的还有.devicesocket.mount等,都是通过一个INI格式的配置文件来实现的,这写配置文件分别以后缀.service.timer等结尾,都称为单元文件(Unit files),并且这些单元文件有固定的目录来存放,用于实现不同的服务。

因此,开启一个定时服务的操作可以分为以下四步:

  1. 编写.service单元文件;
  2. 编写.timer单元文件;
  3. 将前两步编写好的单元文件放到指定目录;
  4. 使用systemctl来使单元文件生效。

在我们的爬虫例子中,我们为定时任务编写的.service.timer单元文件如下所示:

# downloader.service

[Unit] 
Description= Bing background image downloader 

[Service]
Type= simple
WorkingDirectory=/home/zhou/workspace/bing-bg-downloader
ExecStart=/usr/bin/python3 /home/zhou/workspace/bing-bg-downloader/downloader.py
# downloader.timer

[Unit] 
Description= Bing background image downloader

[Timer] 
Unit= downloader.service 
OnCalendar=*-*-* 15:00:07
Persistent=true

[Install] 
WantedBy=basic.target

然后,我们将它们放置在$HOME/.config/systemd/user/目录下:

mkdir -p ~/.config/systemd/user
cp downloader.service ~/.config/systemd/user/downloader.service
cp downloader.timer ~/.config/systemd/user/downloader.timer

最后我们通过systemctl命令来激活我们的定时任务:

systemctl --user daemon-reload 
systemctl --user enable downloader.service
systemctl --user enable downloader.timer
systemctl --user start downloader.timer
systemctl --user list-timers --all

至此,我们为我们的小爬虫设置的定时器就算完成了,我们的小爬虫就会在每天的下午3点启动,然后对背景图片进行爬取,爬取完成后就退出了,等待第二天定时器在次将它启动。

但是显然,这个例子只对我们的小爬虫好使。我们只知其然,却不知其所以然。例如,我们如果想让这只小爬虫每个星期三爬取一次,我们要如何改呢?为了能够随心所欲的定制我们的定时任务,我们有必要来了解单元文件里都可以有哪些内容,每一条内容代表什么意思。

3. 单元(Units)

3.1. 单元加载路径(Unit load path)

首先,特定服务的单元文件,也就是配置文件,是要被放置在一组特定目录下,这些特定的目录称为单元加载路径(Unit load path)。
加载路径分两大类:

  1. 系统模式:当使用systemctl --system的时候的搜索路径,系统模式的搜索路径如表1所示;
  2. 用户模式:当使用systemctl --user的时候的搜索路径,用户模式的搜索路径如表2所示。在我们的例子中,我们就是将我们对额单元文件放到了用户模式的$HOME/.config/systemd/user加载路径下。

值得注意的是,每一类搜索路径在表格中的先后顺序是有意义的,从上到下优先级依次降低。也就是说,如果排在前面的路径中与排在后面的路径中有同名文件,systemd会选择排在前面的文件夹中的文件,忽略后面优先级比较低的目录中的同名文件。除此以外,还可以通过指定$SYSTEMD_UNIT_PATH这个环境变量来指定一个最高优先级的搜索目录。

Tab 1 Load path when running in system mode (--system)
Tab 2 Load path when running in user mode (--user)

3.2. 通用节(section)

如前所述,定时任务等服务是通过一个称为单元文件的配置文件来实现的,单元文件的格式采用类似.ini文件格式。单元文件由节(section)分成不同的部分,内个节可以包含很多选线(Option),选项由键值对组成。形式如下:

# Some comments here
[Section1]
key1=value1 
key2=value2

[section2]
key3=value3
key4=value4
# More sections below.

其中,有两个节是通用的,就是不管什么类型的单元文件都可以有的节,他们就是[Unit][Install]。我们首先来看看这两个节都可以包含什么选项以及他们的含义。

3.2.1. [Unit]

[Unit]节中描述的是本单元文件的通用信息。可以有的选项(Option)如下,为了方便描述,这里约定键值对的表现形式为keyname= <key value>

  1. Description= <A human readable name for the unit>:主要用于描述本单元文件是什么,一般只是用于给此单元文件命名而不是具体描述这个单元文件都做了什么,因为systemd 会使用它作为一些log的宾语,例如Starting xxxxStarted xxxxx,因此它的值只要能描述这个文件是什么就行。例如:
[Unit]
Description= Bing backround image downloader
  1. Documentation= <A space-separated list of URIs referencing documentation for this unit or its configuration>:由于选项Description=推荐的用法是指描述本单元文件的名称,因此对于这个单元文件的详细描述你可以放在这个选项中,它的值是URI地址,可以使用的URI类型为"http://", "https://", "file:", "info:", "man:"这五种,多个URI中使用空格区分;例如:
[Unit]
Documentation= http://cn.bing.com https://github.com/zmychou/bing-bg-downloader
  1. Wants= <Configures requirement dependencies on other units>:列出对其他单元文件的依赖,如果有多个依赖可以使用多个Wants=选项或者在一个Wants=现象中使用空格分隔多个依赖。Wants=只列出了依赖,而不指定依赖执行的先后。并且如果某个依赖执行失败不会影响本单元执行;
    Before=, After= <Configures requirement dependencies on other units>:与Wants=类似,列出本单元间的依赖,这些依赖执行的先后是有要求的,其顺序是After==>本单元=>Before=
    Requires= <Configures requirement dependencies on other units>:与Wants=类似,与Wants=不同的是,如果某个依赖无法正常启动,则本单元也不会启动;

  2. Conflicts= <A space-separated list of unit names>:描述一种互斥关系,如果启动本单元,就会终止列出的其他单元,反过来也成立;

  3. OnFailure= <A space-separated list of units>:描述当本单元进入failed状态后,那些单元会被启动;

  4. FailureAction=, SuccessAction= <Action>:Action可以是none, reboot, reboot-force, reboot-immediate, poweroff, poweroff-force, poweroff-immediate, exit, exit-force的任何一种,当使用--user模式的时候,只能是none, exit, exit-force中的一种;

3.2.2. [Install]

节[Install]包含了本单元的一些安装信息。当使用systemctl enablesystemctl disable命令的时候就会使用到这部分信息。

  1. Alias= <A space-separated list of additional names>:给出本单元的别名,别名也和真名类似,需要以类型的后缀结尾;

  2. WantedBy=, RequiredBy= <A space-separated list of unit names>:当列出的单元被启动的时候,本单元也会被启动,使用这个选项的就类似于再别的选项中使用了之前介绍过的Wants=选项;

以上介绍的这连个节是单元文件通用的,也是可选的。接下来我们就来看看.service.timer单元文件中各自必须有的节。

4. Service配置选项

.service单元文件必须有一个[Service]节,用于描述服务和该服务监管的进程的信息。[Service]节可以包含以下选项。

  1. Type= <type>:type可以是simple, exec, forking, oneshot, dbus, notify, idle 中的一种。用于配置此服务的进程启动类型。

  2. ExecStart= <CMD>:CMD是表示此服务启动的时候需要执行的命令,例如/bin/echo hello。如果有多个命令,每个命令使用分号隔开。当Type= oneshot的时候,可以指定0到多个命令,否则只能且必须指定一条命令。

  3. ExecStartPre=, ExecStartPost= <CMD>:指定在ExecStart=之前、之后所执行的命令;

  4. ExecCondition= <CMD>:CMD会在ExecStartPre的命令之前执行,命令执行的返回值如果是0或者是SuccessExitStatus指定的值则会继续执行接下来的命令,如果返回值为1~254(含)则会跳过接下来的命令但是本段元不会被标记为failed,如果返回值是255则跳过接下来所有命令并标记此单元为failed

  5. ExecStop= <CMD>:用于停止由ExecStart启动的进程,且是在ExecStart执行成功的前提下才会执行;

  6. Restart= <option>:option可以是no, on-success, on-failure, on-abnormal, on-watchdog, on-abort, or always中的一种,分别表示在什么情况下该服务被杀死了会重启,例外是如果该服务是被systemctl杀死的则不会在重启;

  7. SuccessExitStatus=<A list of number>:用于指定返回值是什么表示成功;

  8. WorkingDirectory=, RootDirectory=, RootImage= : 用于配置命令执行的时候的环境,例如如果不指定工作目录,则默认工作目录为用户主目录;

  9. Environment=:用于配置环境,就类似于再终端中使用export命令;;例如:

Environment="VAR1=word1 word2" VAR2=word3 "VAR3=$word 5 6"
  1. EnvironmentFile=:也是设置环境变量,只不过环境变量是通过文件来指定的;

  2. StandardInput=:指定命令的输入流,可选值为null, tty, tty-force, tty-fail, data, file:path, socket or fd:name

  3. StandardOutput=, StandardError=:指定命令执行的输出流、错误流的信息,可选的值为inherit, null, tty, journal, kmsg, journal+console, kmsg+console, file:path, append:path, socket or fd:name

5. Timer配置选项

.timer单元文件必须有一个[Timer]节,用于描述定时器的相关信息。[Timer]可以包含以下选项:

  1. Unit= <unit>:用于指定在指定时间需要激活的服务;

  2. OnActiveSec=, OnBootSec=, OnStartupSec=, OnUnitActiveSec=, OnUnitInactiveSec=:用于描述在什么时间激活Unit=中指定的服务。其值为带单位的时间,单位可以是µs, us, ms, s, m, h, d, w, M, y等,也可以是完整的单词,例如下面的例子中的都是表示系统启动后一个小时会触发:

OnBootSec= 1h
OnBootSec= 1hour
  1. OnCalendar=:用于设定触发的具体的时间。它的一般格式为DayOfWeek year-month-day hour:minute:second TimeZone,有些部分如果不需要是可以省略的,通配符*表示匹配任何时间,..表示一个区间。一些可能的表示方法如图2和图3所示(左边为表示方法,右边为解析后的实际内容):
    Fig 2 Time  example 1

Fig 3 Time  example 2

  1. OnClockChange=, OnTimezoneChange=<boolean>:用一个布尔值表示当对应事件发生后是否触发;

  2. WakeSystem=<boolean>:用一个布尔值表示当时间节点到达是否唤醒系统或者使系统休眠;

  3. RemainAfterElapse=<boolean>:用一个布尔值表示此定时器是否可以重复触发,如果设置成false则此定时器只会触发一次,默认是重复触发。

6. systemctl命令

systemctl是一个复杂的命令,这里只介绍与本文介绍内容相关的命令。可以用systemctl --systemsystemctl --user表示不同模式,如果不指定--system, --user中的任何一个,则默认为--system

systemctl [OPTIONS...] COMMAND [NAME...]

其中OPTIONS可以是--system, --user, --state=STATE, --type=TYPE, --property=NAME, --job-mode=MODE等,其中STATE和TYPE可以使用--state=help--type=help等列出其可能得取值。

COMMAND可以是list-units, list-timers, start, stop, enable, disable等。

6.1. 与查看Unit状态相关

查看Unit可以用以下命令:

systemctl  [--user|--sytem] list-units [--type=[service|timer|...]] [--state=[failed|active|running|help|...]]

例如:

# 列出正在运行的 Unit
systemctl  --user list-units
systemctl list-units

# 列出所有Unit,包括没有找到配置文件的或者启动失败的
systemctl  --user list-units --all
systemctl list-units --all

# 列出所有没有运行的 Unit
systemctl  --user list-units --all --state=inactive
systemctl list-units --all --state=inactive

# 列出所有加载失败的 Unit
systemctl  --user list-units --failed
systemctl list-units --failed

# 列出所有正在运行的、类型为 service 的 Unit
systemctl  --user list-units --type=service
systemctl list-units --type=service

6.2. 与操作Unit相关

  1. systemctl [–user] start :启动某个单元文件;
  2. systemctl [–user] stop :停止某个单元文件;
  3. systemctl [–user] enable :通过单元文件的[Install]节创建符号链接;
  4. systemctl [–user] disable :删除3创建符号链接;

6.3. 与守护进程相关

systemctl [–user] daemon-reload

7. systemd如何启动服务

systemd通过socket和DBus来启动服务。

8. References

[1] https://www.freedesktop.org/software/systemd/man/systemd.unit.html
[2] https://www.freedesktop.org/software/systemd/man/systemd.service.html#
[3] https://www.freedesktop.org/software/systemd/man/systemd.timer.html#
[4] https://www.freedesktop.org/software/systemd/man/systemd.exec.html#

9. Materials

[1] https://github.com/zmychou/bing-bg-downloader

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章