在前一篇博客文章 《使用 Python 编写脚本并发布》 中,我介绍了如何使用 Python 进行脚本编程,说实话这是我在尝试 Python 进行网站和网络编程之后首次使用 Python 进行脚本编程,前面也说过之前虽然使用 Bash 构建过一些脚本,但是由于我对 Bash 不熟练,对它的使用都仅限于最基础的命令行操作,仅仅是比 alias 别名操作稍微简单一点。上次介绍的脚本是如何添加命令行参数以及将现有的操作流程用一个脚本简单化,这一次介绍的脚本是一个非常实用而且经过优化的文件变动事件监视脚本。


P1 Python 脚本:文件变动检测

在廖雪峰 Python 教程实战部分的 Day 13 - 提升开发效率 中,他给我们介绍了一种用于提升开发效率的方法:

  1. 首先执行我们需要的命令
  2. 监听当前目录,并判断变动文件的后缀名,若后缀名为 .py,则触发回调函数
  3. 回调函数触发后,自动重新启动命令

流程很清楚,实现起来也很简答,廖雪峰利用 Python 的 subprocess 和第三方库 watchdog 分别实现了重启命令和监听当前目录的文件变动情况。大概 70 余行代码就能完成这样一个简单且实用的脚本。

在我编程的过程中,经常需要用到这样一个监控文件变动并自动重新执行预设命令的操作,比如我在编写 SSPYMGR 这个网站程序时经常要用到文件改动后自动重启服务器的操作,或者我经常需要在改动某些文件后自动上传到虚拟机上。当我有这些需求时,我之前的做法就是将上面廖雪峰介绍的脚本复制到我要监视的文件夹中,然后直接修改脚本里面的命令参数,这样做很直接,但是很繁琐。

我要做的是:将上面的简单脚本进行优化,使得可以通过命令行参数对脚本的行为进行设置。主要的优化目标有:

  1. 可以预设命令,并且该命令可以带参数
  2. 可以设置监听的目录,并且设置是否递归监听子目录
  3. 可以设置监听的文件后缀名,并设置可以排除在监听范围内的文件名
  4. 增加保存参数功能,并且能够读取保存的配置文件

P2 优化脚本

为了实现上面这些目标,就像我们在上一篇博客那样,用 argparse 库来对复杂的命令行参数进行解析,这一次我们换一种代码的组织方式,将命令行参数的解析和配置文件封装到类中,然后通过实例化类对象解析参数,然后将配置写入到字典中,程序执行流程以指定的配置文件为主:

若指定了要读取的配置文件,则将配置文件中的内容作为配置,忽略掉其他选项。指定配置文件主要可以简化命令行的参数输入过程。若没有指定读取的配置文件,则以命令行中其他的选项为配置。

monitor.py 这个脚本中我将配置和命令行参数读取封装到类 Configuration 中:

class Configuration(object):
_DEFAULT_LOC = _CONFIG_DIR / "monitor_default.json" def __init__(self):
self.config = {}
self._addArgs() def readConfig(self, file: Path):
pass def _addArgs(self):
pass def parseArgs(self):
pass

监听目录

接下来就要用到第三方库 watchdog 来监听指定的目录及指定事件触发时的操作了。事件处理器要用到 watchdog.events.FileSystemEventHandler,我们用继承的方式处理事件:

from watchdog.events import FileSystemEventHandler

class MyFileSystemEventHander(FileSystemEventHandler):

    def __init__(self, fn, config: Configuration):
super(MyFileSystemEventHander, self).__init__()
self.restart = fn
self.config = config
self.last = time.time()

构造事件处理器时需要传入回调函数和配置对象,接下来定义事件处理函数,这里会监听目标文件夹中所有的文件事件 on_any_event,但是该事件会在保存文件时触发两次,因此需要对它做一个防抖处理,防抖处理就是判断两次事件触发的时间间隔是否超过预设值,若两次事件时间间隔过短,则忽略第二次事件。

以下时事件处理的代码:

class MyFileSystemEventHander(FileSystemEventHandler):
def on_any_event(self, event):
# for debounce
cur = time.time()
if cur - self.last < 0.25:
return
self.last = cur
ext_able = False
src = Path(event.src_path)
if src.name not in self.config["exclude"]:
for ext in self.config["mon_ext"]:
if src.suffix == ext:
ext_able = True
break if ext_able:
logger.info('File changed: {}'.format(src))
self.restart()

上面的防抖处理时以第一次事件为准,忽略掉之后一段时间内的其它事件,这样做更方便。

还有另一种复杂但更合理的处理方式,即事件触发时不立即调用处理函数,延迟一段时间,在该段时间内若有其他事件发生,则以新事件为准,重新计算延迟时间,超过时间后再执行事件处理的代码。

第二种处理方式更合理。打个比方,我在很短的时间内先后保存了两个不同的文件 A 和 B,用第一种方式,程序重启后只会重新加载 A 文件而 B 文件的改动很可能被忽略掉了;而用第二种方式 A 文件改动后程序并不会立即重新加载,而 B 文件的改动会被监听到,最终就是在延迟一段时间后程序会重新加载 A 和 B 这两个文件。

自动重启程序

自动重启程序时依靠 subprocess.Popen 对象实现的,启动的时候实例化一个 Popen 对象,停止程序时调用它的 kill() 方法;重启就是先 kill 再重新实例化。这个过程用 NewProcess 类进行封装:

import sys
from watchdog.observers import Observer
import subprocess class NewProcess(object): def __init__(self, config: dict):
self.process = None
self.config = config
self.command = self.config["cmd_args"][:]
self.command[0:0] = self.config["cmd"]
self.args = ' '.join(self.command)

然后还需要用到 watchdog.observers.Observer,用来监听目录,并且通知处理器进行处理:

class NewProcess(object):
def start_watch(self):
observer = Observer()
observer.schedule(MyFileSystemEventHander(self._restart, self.config),
path=self.config["mon_dir"],
recursive=self.config["recursive"]
)
observer.start()
logger.info('Watching directory: {}'.format(self.config["mon_dir"]))
self._start()
try:
while True:
time.sleep(0.5)
except KeyboardInterrupt:
observer.stop()
observer.join()

脚本就完成了。可以在命令行中尝试一下,输入 bf_monitor -c echo -a test 可以看到类似的输出:

它还有些缺陷,不能在 -a 后面添加的参数里带有 - 前缀:bf_monitor -c echo -a -test 是不允许的:

为了解决这个问题,只有在 -c 后面将这些命令用引号包裹起来,bf_monitor -c "python -V" :

关于 watchdog 的详细使用或者 API,请参阅其 官方文档.


P3 python 国际化 i18n

到目前为止,我已经用 Python 做了两个脚本:bf_gitrepo 和 bf_monitor,并且我给他们都加上了命令行帮助信息,但是它们的帮助信息都是英文,我们要把这些信息翻译成中文。翻译工作主要依靠 Python 的 gettext 模块和第三方的 pybabel 模块。

事实上,国际化只要尝试一遍流程之后就很简单了,我第一次使用 pybabel 时,大部分时间都是在提取可翻译文本上,之后做 monitor.py 脚本的翻译时就轻车熟路,完成的很快,只在翻译上花了点时间。

brifuture-facilities 中,我将 gettext 模块简单的封装了一下,程序会在脚本的同级目录下寻找 locale 文件夹中的 .mo 文件,然后替换脚本中的文本:

LANGUAGE_DIR = (Path(__file__).parent / "locale").resolve()
import gettext
def initGetText(domain="myfacilities") -> gettext.gettext:
gettext.bindtextdomain(domain, LANGUAGE_DIR)
gettext.textdomain(domain)
gettext.find(domain, "locale", languages=["zh_CN", "en_US"])
return gettext.gettext

一般会将 gettext.gettext 以其他的名称导入到 Python 程序中,如 from gettext import gettext as _,由于之前我习惯用 Qt 翻译方法 tr,所以我将 gettext.gettext 用别名 tr 代替。在程序中要替换文本的位置用 tr 方法包裹起来:

parser.add_argument("-d", "--directory", help=tr("The directory to monitor, . by default."))

然后我们需要配置 babel,要读取的只有 python 文件(如果你要读取其他文件,可以看看 [文档](http://babel.pocoo.org/en/latest/):

# file: babel.cfg
# Extraction from Python source files [python: **.py]
keywrods = tr

文本查找

接下来使用 pybabel 程序进行文本查找,我们只用查找 monitor.py 文件:

# pybabel extract -F ./babel.cfg -o ./bffacilities/locale/{}.pot -k tr ./bffacilities/{}.py

pybabel extract -F ./babel.cfg -o ./bffacilities/locale/monitor.pot -k tr ./bffacilities/monitor.py

尽管前面的配置文件中指定了关键字为 tr,但我在使用中发现调用 extract 子命令时最好还是加上选项 -k tr,保证能够提取出文本。

查看 bffacilities/locale 目录下,应该有 monitor.pot 文件,里面有很多的 msgid、msgstr。这个文件就保存了所有要翻译的文本。当程序中的文本更新后,重新调用上面的命令再次提取文本即可。

文本翻译

然后我们要对提取出来的文本进行翻译,如果是初次翻译要使用 init 子命令,但若是更新翻译就不是用 init 子命令而是用 update 子命令了:

# pybabel init -i ./bffacilities/locale/{}.pot -d ./bffacilities/locale/ -l zh_CN -D {}

pybabel init -i ./bffacilities/locale/monitor.pot -d ./bffacilities/locale/ -l zh_CN -D monitor

# pybabel update -i ./bffacilities/locale/{}.pot -d ./bffacilities/locale/ -l zh_CN -D {}
pybabel update -i ./bffacilities/locale/monitor.pot -d ./bffacilities/locale/ -l zh_CN -D monitor

之后我们就可以开始翻译了,在 ./bffacilities/locale/zh_CN/LC_MESSAGES/ 目录下找到 monitor.po 文件,用编辑器打开,或者用 Poedit 打开,在 msgid 对应的 msgstr 下面填入文本即可。注意有些语句的 msgid 可能会跨多行,不用管它直接翻译就行。

翻译完成后对其进行打包:

# pybabel compile -d ./locale/ -D {}
pybabel compile -d ./bffacilities/locale/ -D monitor

查看翻译效果

制作完翻译文件后,来看看脚本帮助信息是不是输出中文了,先检查一下 locale 的输出:

查看 bf_monitor 的帮助信息:

修改 locale,LANG=en_US.UTF-8 && LANGUAGE=en_US,locale 输出变为:

再看看 bf_monitor 的帮助信息:

修改 setup.py

最后我们要将写好的程序打包,为了防止在打包过程中丢失翻译文件,我们要将 setup 参数中的 zip_safe 改为 false: zip_safe=False,

然后在 setup.py 的同级目录下添加 MANIFEST.in 文件,内容如下:

recursive-include bffacilities/locale *
global-exclude *.pyc

最后上传到 pypi 上面即可通过 pip 下载安装。


P4 小结

之前使用 Nodejs 时,我用 Node 编写过一个文件变动检测的脚本,但是现在我找不到之前的那篇博客了,文件变动检测的 Python 脚本和 Node.JS 脚本原理都是一样的,都是通过监听文件事件,然后执行回调函数。

另外通过这次的翻译过程我掌握了如何国际化 Python 程序,之前我做 Qt 程序时对 Qt 的翻译流程比较清楚,转用 Python 程序后发现其实国际化的流程都很类似,在文件中查找调用翻译函数,提取之后用软件或编辑器进行翻译。最后转换成程序可以直接读取的格式(可能这样做能够提高程序的效率吧)。


这段程序的代码可以在 github 上找到,你也可以看到整个项目的源代码。如果你觉得这篇文章对你有所帮助或者你认为这篇文章还不错,就给我点个赞吧,感谢你的支持。

参考

python的国际化gettext模块

Flask-Babel 简介

The Invent with Python Blog

http://babel.pocoo.org/en/latest/messages.html

Python 脚本编程及国际化的更多相关文章

  1. Linux 利器- Python 脚本编程入门(一)

    导读 众所周知,系统管理员需要精通一门脚本语言,而且招聘机构列出的职位需求上也会这么写.大多数人会认为 Bash (或者其他的 shell 语言)用起来很方便,但一些强大的语言(比如 Python)会 ...

  2. Shell脚本编程30分钟入门

    Shell脚本编程30分钟入门 转载地址: Shell脚本编程30分钟入门 什么是Shell脚本 示例 看个例子吧: #!/bin/sh cd ~ mkdir shell_tut cd shell_t ...

  3. Linux shell脚本编程(一)

    Linux shell脚本编程: 守护进程,服务进程:启动?开机时自动启动: 交互式进程:shell应用程序 广义:GUI,CLI GUI: CLI: 词法分析:命令,选项,参数 内建命令: 外部命令 ...

  4. Python黑客编程2 入门demo--zip暴力破解

    Python黑客编程2 入门demo--zip暴力破解 上一篇文章,我们在Kali Linux中搭建了基本的Python开发环境,本篇文章为了拉近Python和大家的距离,我们写一个暴力破解zip包密 ...

  5. python web编程-CGI帮助web服务器处理客户端编程

    这几篇博客均来自python核心编程 如果你有任何疑问,欢迎联系我或者仔细查看这本书的地20章 另外推荐下这本书,希望对学习python的同学有所帮助 概念预热 eb客户端通过url请求web服务器里 ...

  6. 9 本免费的 Python 语言编程书籍(转载)

    9 本免费的 Python 语言编程书籍 原文地址:http://linuxtoy.org/archives/9-free-python-books.html 2010-03-03 Toy Poste ...

  7. Python核心编程--学习笔记--3--Python基础

    本章介绍基本的Python语法.编程风格:并简要介绍标识符.变量和关键字,以及变量占用内存的分配和回收:最后给出一个较大的Python样例程序来体验这些特性. 1 语句和语法 1.1 注释 可以在一行 ...

  8. 【转】关于Python脚本开头两行的:#!/usr/bin/python和# -*- coding: utf-8 -*-的作用 – 指定文件编码类型

    原文网址:http://www.crifan.com/python_head_meaning_for_usr_bin_python_coding_utf-8/ #!/usr/bin/python 是用 ...

  9. 用 Python 脚本实现对 Linux 服务器的监控

    目前 Linux 下有一些使用 Python 语言编写的 Linux 系统监控工具 比如 inotify-sync(文件系统安全监控软件).glances(资源监控工具)在实际工作中,Linux 系统 ...

随机推荐

  1. Codeforces 450B div.2 Jzzhu and Sequences 矩阵快速幂or规律

    Jzzhu has invented a kind of sequences, they meet the following property: You are given x and y, ple ...

  2. 安装HDP时的报错信息

    1,安装ambari时报错:Bootstrap process timed out. It will be destroyed. 报错原因:/etc/sudoers文件中未设置免密权限 解决办法:ha ...

  3. 【CodeForces】671 D. Roads in Yusland

    [题目]D. Roads in Yusland [题意]给定n个点的树,m条从下往上的链,每条链代价ci,求最少代价使得链覆盖所有边.n,m<=3*10^5,ci<=10^9,time=4 ...

  4. VC连接access

    (1)首先拷贝 c:\program files\common files\system\ado\ 目录中的 msado15.dll 文件到项目中. (2)在VC中加入DLL,具体方法如下: (3)创 ...

  5. python 第二章 对象与类型

    可变对象和不可变对象 1,可变对象,list(列表),dict(字典),集合(set),字节数组. 2,不可变对象,数值类型,字符串,字节串,元组(具体形式 ()). 注意条件:可变和不可变指的是该对 ...

  6. 蓝色简单的cms文档管理系统模板——后台

    链接:http://pan.baidu.com/s/1qYMwHis 密码:xyiw

  7. nc-使用方法

    nc-远程克隆硬盘 A 接收端:  nc -lp 333 | dd of=/dev/sda          #用nc开启333监听端口  将收到的数据 写入到sda的硬盘上(前提是挂一块硬盘) B ...

  8. MongoDB之主从复制和副本集(四)

    简单主从复制 采用一主一从或一主多从的布署模式,可以将读写分离开来,提高数据库的可用性,不过mongodb的主从模式并不能在主节点崩溃后,从节点替换主节点的工作,一般可以在开发阶段使用. 实现步骤 设 ...

  9. kernel编译速度提高

    1. 使用tmpfs来代替部分IO读写 2. ccache,可以将ccache的缓存文件设置在tmpfs上,但是这样的话,每次开机后,ccache的缓存文件会丢失 3.distcc,多机器编译 4.将 ...

  10. Nim 游戏、SG 函数、游戏的和

    Nim游戏 Nim游戏定义 Nim游戏是组合游戏(Combinatorial Games)的一种,准确来说,属于“Impartial Combinatorial Games”(以下简称ICG).满足以 ...