在前一篇博客文章 《使用 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. CF757 C hash

    一种数字可以变成另一种数,要求每组中变换前后各种数字数量不变,问方案数 对现有每组中的每个数字构造出现在各个组情况的序列,如2 出现在第一组和第二组各一次那么就要加入组别的标号1,2,出现重复次仍要加 ...

  2. 2017 Multi-University Training Contest - Team 1

    1006(6038) 就是对a,b分别求循环节,先统计一下b中所有长度循环节的出现次数,再对a求循环节时只要满足: a的循环节长度 % b的循环节长度=0,那么这个b的循环节就可以计入答案,尼玛只要是 ...

  3. GridControl详解(七)事件

    private void gridView1_RowCellClick(object sender, DevExpress.XtraGrid.Views.Grid.RowCellClickEventA ...

  4. 【CodeForces】671 C. Ultimate Weirdness of an Array

    [题目]C. Ultimate Weirdness of an Array [题意]给定长度为n的正整数序列,定义一个序列的价值为max(gcd(ai,aj)),1<=i<j<=n, ...

  5. 【BZOJ】2134: 单选错位 期望DP

    [题意]有n道题,第i道题有ai个选项.把第i道题的正确答案填到第i+1道题上(n填到1),问期望做对几道题.n<=10^7. [算法]期望DP [题解]正确答案的随机分布不受某道题填到后面是否 ...

  6. Oracle笔记之表空间

    Oracle中有一个表空间的概念,一个数据库可以有好几个表空间,表放在表空间下. 1. 创建表空间 创建表空间使用create tablespace命令: CREATE TABLESPACE foo_ ...

  7. 60、简述 yield和yield from关键字。

    1.可迭代对象与迭代器的区别 可迭代对象:指的是具备可迭代的能力,即enumerable.  在Python中指的是可以通过for-in 语句去逐个访问元素的一些对象,比如元组tuple,列表list ...

  8. c语言学习笔记.内存管理.

    内存: 每个程序的内存是分区的:堆区.栈区.静态区.代码区. 1.代码区:放置所有的可执行代码,包括main函数. 2.静态区:存放所有的全局变量和静态变量. 3.栈区:栈(stack),先进后出.存 ...

  9. 利用Jsoup模拟跳过登录爬虫获取数据

    今天在学习爬虫的时候想着学习一下利用jsoup模拟登录.下面分为有验证码和无验证码的情况进行讨论. ---------------------------无验证码的情况---------------- ...

  10. elk系列2之multiline模块的使用【转】

    preface 上回说道了elk的安装以及kibana的简单搜索语法,还有logstash的input,output的语法,但是我们在使用中发现了一个问题,我们知道,elk是每一行为一个事件,像Jav ...