撰写 Linux 使用的 Python script

Linux、指令稿与 Python

对 Linux 来说,指令稿 (script) 是至为重要的部分。在主要的 Linux distribution 中间,从系统的启动到运作,都离不开 shell 指令稿撰写。在我的主机上面执行一下:

$ ls /usr/bin/* /bin/* | wc -l
2585
$ file /usr/bin/* /bin/* | grep "shell script" | wc -l
267

看,可以找到 267 个 shell 指令稿程式,超过 /usr/bin 和 /usr 目录下所有 (程式) 档案的十分之一。这还只是 shell 指令稿的部分而已。

在一个像 Linux 这样以档案为操作导向的作业系统上面,script 的活跃是理所当然的事情。绝大部分的系统设定都以字串的形式写在组态档里面,而作业系统的执行期资讯也存在档案系统之中 (/proc);直接处理这些字串就能管理系统,用指令稿语言来进行自动化是非常合适的。

像 Python 这种指令稿语言因为开发快速的关系,能够很快地制作出我们想要的系统管理功能出来。除了开发快速之外,Python 也具有容易维护的特性。相比之下,Perl 程式虽然可以写得更短,但也更不容易看懂;shell 指令稿则不是完整的开发环境。Python 是撰写系统管理指令稿的理想工具。

Python 指令稿的格式

Python 指令稿与其它语言的指令稿的基本格式完全一样,本身都是纯文字档,而在档头要以 #! 指定直译程式的位置:

#!/usr/bin/python
print "Hello, world!"

这是我们上一期写过的 hello.py 程式,不要忘记 chmod a+x hello.py,如此便可以在指令行下执行这个指令稿:

$ ./hello.py
Hello, world

我们习惯上会给 Python 程式取个副档名 .py,但 Linux 的指令稿并不需要缀上副档名;把 hello.py 改成 hello,程式一样会正常执行。.py 副档名对 Python 仍有特别的意义,但只在撰写 Python 模组的时候才有用处。

对于指定 Python 直译器标头,我们一般有两种作法。像以上的 hello.py 这种写为绝对路径的方式其实并非必要,我们可以改用相对路径的方式来指定:

#!/usr/bin/env python

于是会以 /usr/bin/env 程式来叫用 python 直译器,处理 Python 程式档案。这么作的好处是当系统中安装有许多个不同的 Python 直译器时,会采用路径在最前面的那一个。如此一来,若使用者另外安装了一版 Python (例如装在自己的家目录),又把自己的 Python 放到路径设定 (PATH 环境变数) 的最前面,即会采用使用者自己安装的 Python。

每一版 Python 除了有 python 这个执行档之外,还会附有内容完全相同的 pythonX.Y 这个执行档,X.Y 是该版 Python 的 major version 和 minor version。譬如 Python 2.3 就会有 python 和 python2.3 这两个直译器,用起来是完全一样的。如果我们写的指令稿程式必须要使用某一个版本的 Python,可以偷偷在指令稿标头上动手脚来进行限制;以 Python 2.3 为例,就把标头写成:

#!/usr/bin/env python2.3

Python 提供了一套正统的方法来检查所使用 Python 及所有相关环境的资讯。在指令稿标头上动手脚虽然方便,但不是保险的正统作法;只是,若程式本身就没多长 (譬如说二三十行),的确不必浪费时间去写一串检查程式。 当指令稿只使用了主流版号的标准程式库时 (这是一般的状况),通常就不必指定 Python 的版本。

若写成 hello.py 里那种绝对路径的标头,就会限定使用安装在某一个位置的 Python。通常我们都会指定在 /usr/bin/python 或 /usr/bin/pythonX.Y (看要指定哪一版),即系统所安装的 Python;写成这样的话,使用者就不好改用自己安装的版本了。

Python 直译器还会读取另一组格式为 # -*- setting -*- 的标头 (通常接在第一行以后),其中常用的是:

# -*- coding: UTF-8 -*-

用途是指定“指令稿档案内纯文字的字元编码 (为 UTF-8)”。如果你想要写中文注解,这就非常重要;Python 自己有一套字元编码转换的机制,实作在 codecs 模组里面,但直到 Python 2.4 之前,繁体中文常用的 Big5 编码并未进入标准的 codecs 模组。如果指令稿档案使用了 Python 看不懂的字元编码 (就是指华文世界用的 Big5 和 GB),程式虽然仍可执行,但 Python 直译器会送出警告。如果想用中文撰写注解,最好把指令稿档案转为 UTF-8 Unicode,并如上指定编码。

上一期已经提过了,Python 也是以 # 当作单行注解符号的 (和 shell script 一样);所有在这个符号之后的文字都是注解。顺带一提,如果你习惯以 VIM 编辑 Python 指令稿,可以在档尾加上 VIM 的设定字串:

# vim:set nu et ts=4 sw=4 cino=>4:

设定显示行号 (nu)、展开跳格键 (et,对 Python 程式来说,跳格键 Tab 是最要不得的东西),指定跳格字元为 4 (ts=4)、偏移字元宽为 4 (sw=4)、C 式缩排为 >4 (cino⇒4);然后再打开语法标示 (syntax highlighting,这个在 .vimrc 里设定比较合适)。使用这样的编辑环境,对撰写 Python 程式来说会很方便。

Python 直译器会依出现顺序来执行程式码档案里的指令。如果我们想撰写比较具组织性的指令稿,可以把平铺直述的:

print "some operations"

改成这样的程式码结构:

def main():
    print "some operations"
 
if __name__ == '__main__':
    main()

亦即自行制作一个“进入点” main() 函式。当指令稿比较长 (超过一百行以上),以及将来在扩充指令稿的时候,就会比较方便。

总结来说,一个 Python 指令稿的常见格式应为:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
 
def main():
    print "Hello, world"
 
if __name__ == '__main__':
    main()
 
# vim:set nu et ts=4 sw=4 cino=>4:

字串处理

在管理 Linux 系统时,(纯文字) 设定档案以及其中的字串处理是至为核心的部分;让我们来看看 Python 如何进行这些工作。因为我们在上一期已经用 Python 处理过字串和档案了,所以在这里,我们应该对字串处理作深入一点的介绍。

首先我们要知道的是,字串在 Python 里面是一种物件。打开 Python 互动式环境 (到 shell 去执行 python 即可进入),执行以下动作:

>>> print type( "" )
<type 'str'>
>>> if type( "I am a string" ) is str: print True
...
True
>>> if type( "Another string" ) is str(): print True
...

type() 是 Python 的内建函式,用来取得变数的型态。我们可以从这三个指令看出来,字串 ””, “I am a string” 都是 str 类别的物件。查看 Python 的线上文件,会发现有两组关于字串处理的程式库;一组是 string 模组里的函式,另一组则是字串物件专用的方法 (String Methods)。两者虽有一些差别,但功能的重覆性相当高;我们讨论的重点在字串方法。

我们常常会需要分析档案中的字串:把字串拆解开来,依照给定的逻辑来判断字串资料的意义。因此,最常用的字串方法就是我们上一期有用到的 split()。split() 传回的是列表 (list),可以用索引值 (以 0 起始) 来存取列表中的各个项目。再来示范一下:

>>> tokens = "This is a sample string used to demo split()".split()
>>> len(tokens)
9
>>> print tokens
['This', 'is', 'a', 'sample', 'string', 'used', 'to', 'demo', 'split()']
>>> print tokens[0], tokens[2]
This a
>>> print tokens[-1], tokens[-2]
split() demo
>>> print tokens[2:5]
['a', 'sample', 'string']

第一个指令把我们的字串切成了 9 个字串,存在 tokens 这个列表里。len() 是个内建函式,用来量测像列表这种可以存放其它东西的物件的长度 (传回所包含的项目个数)。列表只要是整数就可以了,但最大不能到项目个数;可以给入负值,表示从列表尾端开始计算。索引值 -1 即为列表的最后一个项目。

有办法切开字串进行判断了之后,我们常常还需要把分析结果给输出出来,那么就得接合字串;以字串的格式化操作 (string format operations) 就能完成这件工作。我们可以写出以下的表示式:

>>> "%d %f %s" % (1, 1.2, "string")
'1 1.200000 string'

这就是字串格式化操作。以带有特别转换字元 (conversion character) 的格式化字串,后接 % 运算子,再接一个 tuple 作为参数,就能把 tuple 里的资料填进格式化字串里去。常用的 %d 代表有号整数、%f 代表浮点数、%s 代表字串,完整的转换字元表请参考 Python 的线上文件。

Python 的 tuple 也是一种可以包含其它物件的资料结构,以整数索引存取其中的物件,但其行为与列表不尽相同。在语法上,tuple 用 (1, 2, 3) 来宣告,而列表用 [1, 2, 3] 来宣告。如果 tuple 中只有一个物件,则要写成 (1,),不要忘记右括号前的逗号。在字串格式化操作时,若转换字元只有一个,% 运算元后的 tuple 也可以用单一变数来代替。

字串物件另有一个叫作 join() 的方法可以用来结合字串,用法如下:

>>> "".join([ "a", "b", "c" ])
'abc'
>>> "-".join([ "a", "b", "c" ])
'a-b-c'

在处理字串时,最后要注意的是,Python 的字串不可变。也就是说,想变更字串中的某一个字元,不能直接设:

>>> a = "write"
>>> a[2] = "o"
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: object doesn't support item assignment

那是不合法的。那该怎么办呢?可以这样作:

>>> print a[:2]+"o"+a[3:]
wrote

字串的内容虽然不能变更,但字串本身可以加起来 (串接)。a[:2] 表示取出 a 字串到索引 2 为止的部分;a[3:] 表示取出 a 字串从索引 3 开始到结尾的部分;然后在中间接入 “o”。最后我们还是可以得到 wrote 字串。这种操作索引的技巧,也可以用在一般的列表上。

Python 同样具有常规表示式 (regular expression) 的操作能力,实作在 re 模组里面。用来执行字串取代是非常方便的。

转换字元编码

Python 有一套处理字元编码的 codecs 模组;我们以之即可自由地将字元转换为各种不同的编码,这是我们在处理多国语言资料时常需处理的问题。然而,字串物件本身就提供有 encode() 与 decode() 方法,我们不必汇入 codecs 模组就可以使用这两个方法为我们提供的 codecs 能力。

此处我们得要注意一个事实,那就是 Python 拥有两种字串物件。其一是我们刚刚一直在处理的 str 字串,而另一种呢,就是对多国语言处理非常重要的 unicode 字串。一般我们用引号或双引号表示的都是普通的字串 (str),而用 u”string” 表示的呢,就是 unicode 字串。decode() 能把普通字串解码成 unicode 物件,而 encode() 则能把 unicode 物件编码成各种支援的字元集。

在处理中文编码之前,我们要为 Python 2.3 安装相关的外加套件:cjkcodecs 与 iconvcodecs;前者是中日韩专用的 codecs 物件,而后者允许 Python 直接使用 GNU iconv 工具所提供的编码,作为 codecs 物件。假设我们得把原本是 Big5 的编码重编为 UTF-8,那么可以这样作:

>>> f = open( "file.big5" )
>>> s = f.read()
>>> f.close()
>>> sp = s.decode('Big5').encode('UTF-8')

你可以在电脑上找一个内容是 Big5 编码的档案,把 locale 改成 UTF-8,然后在 Python 互动式环境下执行以上的指令 (该改的地方请改一下)。最后再用 print s, sp 比较一下转换前后的字串。

档案系统与目录

在 Linux 系统中复制、搬移、删除档案与目录也是管理时常见的动作。Python 提供的 os 模组能处理作业系统所支援的大部分档案系统操作,另外还有 shutil 模组,提供更高阶的操作。

档案系统操作

档案系统与档案内容是不一样的议题。我们在进行档案系统操作时,处理的是搬移 (更名)、复制与删除,比较没有机会直接新增档案。这些动作在 os 与 shutil 模组里几乎都有提供;我们应该先汇入这两个模组。

若要复制档案,我们可以这样作:

>>> shutil.copy('data.txt', 'data.new.txt')
删除档案则用 os.unlink():
 
>>> os.unlink('data.new.txt')
搬移 (更名) 有两种方法:
 
>>> os.rename('data.txt', 'data.alter.txt')
>>> shutil.move('data.alter.txt', 'data.txt')

第一种方法,若来源档 (第一个参数) 与目的档不在同一个档案系统内 (分割区),此动作可能会失效 (不同的 Unix 有不同的处理方法)。第二种方法比较高阶,无论来源档与目的档是否在相同的档案系统内,都可以使用。

路径的处理

管理系统的时候多半不会只处理当前目录内的档案,所以常要对路径字串进行处理。os.path 模组提供了处理路径的函式,常用的有:

  1. abspath():接受一个路径字串,传回该路径所代表的绝对路径。
  2. realpath():接受一个路径字串,计算该路径中包含的符号连结 (symbolic link),传回所代表的真正路径。
  3. split(), dirname(), basename():split() 接受一个路径字串,从最后一个路径项目前切开,分成包含该项目的目录与该项目名本身,以 tuple 传回。dirname() 是 split() 传回值的第一个元素;basename() 是第二个元素。
  4. join():接受一个路径列表,把该列表中的每个元素接成一个完整路径字串后传回。
  5. splitext():接受一个路径字串,分开其副档名,将主档名与副档名用一个 tuple 传回。
  6. exists():测试传入的路径字串是否存在,传回布林值。
  7. isfile(), isdir(), islink(), isabs():分别用来测试所传入的路径字串是否为档案、目录、符号连结或绝对路径;传回布林值。

实际要使用的时候,大概会像是这样子:

>>> os.path.split( "a/b/c" )
('a/b', 'c')
>>> os.path.join( "a", "b", "c" )
'a/b/c'
>>> os.path.splitext( "dir/file.ext" )
('dir/file', '.ext')

你可以在你的目录结构里,用真正的路径来试试看!

外部程式呼叫

许多在 shell 指令稿中要靠呼叫外部程式才能完成的作业,都能用 Python 的内建模组来完成,例如上面提到的字串处理、档案处理、目录处理等等。而若遇到 Python 不足的地方,或是有非常特别的操作,当然也可以呼叫外部的程式。

os 模组有一个 system() 函式可以用来呼叫外部程式:

>>> os.system( 'ls' )
weekly20051204.doc
weekly20051211.doc
0
>>>

最后显示出来的 0 不是 ls 程式的输出,而是其传回值。

os.system() 函式能进行最简单的外部程式呼叫,不能对该程式的输出入资料进一步处理;如果我们只想简单执行程式,os.system() 函式将是最佳的选择。

管线

当我们也需要对外部程式的输出入资料进行处理的时候,os.system() 就不够用了。Python 另外有 popen2 模组,可以让我们管理外部程式子行程的输出入管线 (pipe)。在 popen2 模组里有 popen2(), popen3() 和 popen4() 三个工具函式,分别会重导向子行程的标准输出入、标准输出入及错误输出、标准输出合并错误输出及标准输入。

简单用范例来说明最常用的 popen2() (别忘了先 import popen2 喔):

>>> stdout, stdin = popen2.popen2("ls")
>>> ostr = stdout.read()
>>> print ostr
weekly20051204.doc
weekly20051211.doc
 
>>>

popen2.popen2() 会传回连结到 ls 程式输出入的两个档案物件,我们取名为 stdout 与 stdin。呼叫了 popen2.popen2() 之后,外部程式马上就会执行,然后我们就能从 stdout 档案物件里读出该外部程式的标准输出资料了。如此一来,该程式的执行结果就不会直接显示在终端机上,我们可以在 Python 里面先处理过以后,再决定该怎么办。

如果我们想呼叫的程式也会进行错误输出 (stderr),而我们想要处理的话,就改用 popen3() 或 popen4() 函式。popen3() 的错误输出会连接至一个独立的档案物件,而 popen4() 则会把错误输出一起放到标准输出所连结的档案物件里;你可以视需要使用。

在 Python 2.4 里有一个新的 subprocess 模组,可以执行所有的外部程式呼叫功能。所以在 Python 2.4 里不再需要 os 与 popen2 模组里的相关函式了;当然,旧模组不会消失,所以在 Python 2.4 里还是可以用 popen2,我们的旧程式不会出问题。

网际网路通讯

Python 内建的程式库里就具备相当方便的网际网路通讯功能,不必呼叫外部程式。

网际网路通讯是个大范围,其中最常用到的大概数全球资讯网了;我们举 Zope 应用程式伺服器来作例子。Zope 使用 ZODB 物件资料库来储存资料,这个系统会把存取动作纪录下来,当使用者删除其中的资料时,资料不会实际删除,要等到手动压缩 (pack) 资料库的时候,才会真正把资料删除。这个压缩功能的动作选项是放在 web-based 的 ZMI 里面,没有指令行介面;如果我们不想手动连进 ZMI 来执行压缩,就得写一个能进行 HTTP 操作的指令稿。

我们要写的程式应该具有以下的命令列介面:

packzope.py -u<URL of Zope server> -d<day> -U<username> -P<password>

这个 packzope.py 程式要负责用 HTTP 和伺服器沟通,把从命令列取得的使用者名称和密码提供给 Zope 伺服器,并且用 GET 方法把要压缩的天数 (舍弃指定天数前的资料) 告诉 Zope 伺服器。以下是写好的程式:

#!/usr/bin/env python
 
import sys
import urllib
 
class parameters:
  def __init__(self):
    from optparse import OptionParser, OptionGroup
    op = OptionParser(
        usage = "usage: %prog -u URL -d DAYS -U USERNAME -P PASSWORD",
        version = "%prog, " + "%s" % __revision__
        )
    op.add_option("-u", action="store", type="string", \
        dest="url", \
        help="URL of site to open"
        )
    op.add_option("-d", action="store", type="int", \
        dest="days", default=1, \
        help="erase days before"
        )
    op.add_option("-U", action="store", type="string", \
        dest="username", \
        help="username"
        )
    op.add_option("-P", action="store", type="string", \
        dest="password", \
        help="password"
        )
    self.op = op
    (self.options, self.args) = self.op.parse_args()
params = parameters()
 
if not params.options.url or \
   not params.options.username or \
   not params.options.password :
  params.op.print_help()
  sys.exit(1)
 
url = "%s/Control_Panel/Database/manage_pack?days:float=%d" % \
     (params.options.url, params.options.days)
 
class MyOpener(urllib.FancyURLopener):
  def get_user_passwd(self, host, realm, clear_cache = 0):
    return params.options.username, params.options.password
 
def main():
  try:
    f = MyOpener().open(url).read()
    print "Successfully packed ZODB on host %s" % params.options.url
  except:
    print "Cannot open URL %s, aborted" % url
    raise
 
if __name__ == '__main__':
  main()

程式前半段在处理命令行参数 (class parameters),而在 main() 函式里实际进行连线动作。packzope.py 利用 urllib 模组来连结 Zope 伺服器,并利用 subclassing urllib.FancyURLopener 类别来自订使用者名称与密码的输入。压缩完毕之后,程式会输出以下的字样:

Successfully packed ZODB on host http://someplace:port

我们可以把 packzope.py 放到 crontab 里定期执行。这就是一种自动化网路操作。

结语

本文藉由讨论以 Python 进行 Linux 操作自动化的技巧,对 Python 的应用作了进一步的介绍。当然,在进行任何种类的 Python 程式开发时,都可以参考 Python 的线上说明文件。Dive into Python 是一本容易上手的自由 Python 书籍,你也可以在网路上找到中文译本。

文章来源: http://blog.seety.org/everydaywork/2008/4/9/1012/

shells/program/python/linux-python-script.txt · 最后更改: 2010/09/06 02:26 (外部编辑)
[unknown link type]到顶部
GNU Free Documentation License 1.3
京ICP备05034962号 Debian Driven by DokuWiki get firefox browser Recent changes RSS feed Valid XHTML 1.0