Python Linux 系统管理与自动化运维-读书笔记

  |   0 评论   |   0 浏览

第2章 Python生态工具

2.1 python内置小工具

2.1.1 快速启动一个下载服务器

  • python2
python -m SimpleHTTPServer
  • python3
python -m http.server

2.1.2 字符串转为json

利用json.tool模块,能够格式化json字符串,如:

echo '{"job": "dev", "name": "pcm", "sex": "male"}' |python -m json.tool

2.1.3 检查第三方库是否正确安装

确认第三方库是否正常安装,不可能每次都进入到python命令行中使用import语句导入观察结果来判断,可以使用下面的方式进行判断:

python -c "import paramiko"

2.2 pip的高级用法

2.2.2 pip常用命令

pip的子命令

子命令解析说明
install安装软件包
download下载安装包
uninstall卸载安装包
freeze按照requirements格式输出安装包,而已到其他服务器上执行pip instal -r requirements.txt直接安装软件
list列出当前系统中的安装包
show查看安装包的信息,包括版本、依赖、许可证等
check检查安装包的依赖是否完整
search查找安装包
wheel打包软件到whell格式
hash计算安装包的hash值
completion生成命令补全配置
help帮助信息

2.2.3 加速pip安装的技巧

  1. 使用豆瓣或者阿里云的源加速软件安装

可以直接指定镜像地址

pip install -i https://pypi.douban.com/simple/ flask

也可以把镜像源写入配置文件中,在linux系统中创建 ~/.pip/pip.conf文件,内容如下(清华源):

[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
  1. 将软件下载到本地部署

下载到本地

pip install --download=`pwd` -r requirements.txt

本地安装

pip install --no-index -f file://`pwd` -r requirements.txt

2.3 Python编辑器

Linux下推荐使用vim编辑器,但是需要做比较多的修改才能方便地用来编写python程序。windows下推荐使用PyCharm编辑器,有语法高亮等高级功能。

2.4 Python编程辅助工具

2.4.1 Python交互式编程

最简单的python的交互式编程的方法就是使用标准的python shell,在命令行中直接输入python即可。
虽然很方便,但是标准的python shell没有语法高亮,不支持tab自动补全,也没有自动缩进的功能。
所以我们更加推荐使用IPython来进行交互式的编程学习,IPython是增强型的Python shell,除了解决上面说的问题外,还可以方便地进行交互式编程和数据分析。

2.4.2 使用IPython交互式编程

Linux下可以使用yum或者apt命令直接安装

yum install ipython

具体使用方法略过,忘记的可以上网查找教程。

2.4.3 jupyter的使用

jupyter就是以前的IPython Notebook,是一种新兴的交互式数据分析与记录工具,它可以通过浏览器访问本地或者远程的IPython进程,并利用浏览器的图形界面,增强IPython的可视化输出。
安装方法如下:

pip install jupyter

启动方法如下:

jupyter notebook --no-browser --ip=0.0.0.0

具体使用方法略过,可以上网找对应的教程。

2.5 Python调试器

在编写较大的程序的时候,调试器的使用是必不可少的。这里我们介绍两种调试器,分别是Python标准库自带的pdb和开源的ipdb。

2.5.1 标准库pdb

pdb的部分调试命令

有两种方法启动Python调试器,一种是在命令行参数中指定使用,如下所示:

python -m pdb test_db.py

另外一种实在python代码中调试pdb模块的set_strace方法设置一个断点。当程序运行至断点时,将会暂停执行并打开调试器。

#!/usr/bin/python

from __future__ import print_function
import pdb

def sum_nums(n):
    s=0
    for i in range(n):
        pdb.set_trace()
        s += i
        print(s)

if __name__ == '__main__':
    sum_nums(5)

这两种方法没什么本质的区别,通常是较小的程序我们可以直接通过命令行去启动调试,这样从程序的第一行开始进行调试;如果程序较大,我们会在需要调试的地方设置 set_trace 方法设置断点,这样只有在执行到设置断点的地方才开始启动调试器。

2.52 开源的ipdb

ipdb之于pdb,就相当于IPython之于python。虽然实现同样的功能,但是在易用性方面做了很多改进。

由于ipdb是一个第三方库,我们需要先安装才能使用。

pip install ipdb

前面的例子用ipdb进行改造的话,就会变成这样:

#!/usr/bin/python

from __future__ import print_function
import pdb

def sum_nums(n):
    s=0
    for i in range(n):
        ipdb.set_trace()
        s += i
        print(s)

if __name__ == '__main__':
    sum_nums(5)

除了使用pdb和ipdb之外,我们还可以使用PyCharm的图形化界面进行调试。

2.6 Python代码规范检查

2.6.1 PEP8 编码规范介绍

PEP8是事实上的代码风格标准,由于内容比较多就举个简单的例子进行介绍。
在Python中,包的导入在使用import时应该一次只导入一个模块,不同的模块应该独立一行,例如

#正确的示例
import os
import sys

#反面的示例
import os, sys

并且在导入模块的顺序也是有要求的,先是导入标准模块,其次导入第三方模块,最后导入当前应用程序,各个分组之间以空行分割。

import time
import json

import yaml
import psutil

from mango_agent.cation.common import kill_mongod,start_mogo_node

2.6.2 使用pycodestyle检查代码规范

遵循相同标准的代码风格非常重要,为了帮助大家形成统一的代码风格,Python官方提供了同名的命令行工具,该工具能够检查Python代码是否违反PEP8规范,并对违反的位置做出相应的提示.

pip install pep8

为了避免检查工具和代码风格名称一样带来的困惑,这个工具已经重命名为pycodestyle,安装方法同上。

pip install pycodestyle

使用方法

pycodestyle --first test.py

2.6.3 使用autopep8将代码格式化

autopep8是一个开源的命令行工具,它能够将Python代码自动格式化为PEP8风格。autopep8使用pycodestyle工具来决定代码中哪部分需要被格式化。安装和使用非常简单。

#安装
pip install autopep8

#使用--in-place直接修改文件,否则不会修改文件,会把结果输出到命令行
autopep8 --in-place test.py

2.7 Python工作环境管理

因为Python2和Python3之间存在着较大的差异,然后在很多场景下我们都需要同时用到Python2和Python3.所以,对Python的版本进行管理就很有必要了。
在这一节里,我们将会介绍两个工具,即pyenv和virtualenv。前者用于管理不同的Python版本,后者用下管理不同的工作环境。有了这两个工具,Python相关版本问题将不再是问题。

2.7.1 使用pyenv管理不同的Python版本

pyenv是一个python版本管理工具,它能够进行全局的Python版本切换,也可以为单个项目提供对应的Python版本。使用pyenv后,可以在服务器上安装多个不同的Python版本,也可以安装不同的Python实现。不同的Python版本之间的切换也非常简单。

  1. pyenv的安装
  2. pyenv的使用
    我们通过pyenv的instal命令,可以查看pyenv当前支持哪些Python版本,如下:
pyenv install --list

使用pyenv安装不同版本的Python版本:

pyenv install -v 3.6.0
pyenv install -v 2.7.13

查看当前系统中包含的Python版本:

pyenv versions

通过pyenv选择不用版本的Python版本,如下

pyenv global 3.6.0

切换Python版本以后,与版本相关的依赖也会一起切换。因此,我们不用担心不同的版本在系统是否会相互干扰。

如果要删除某个Python版本,使用uninstall命令即可。

pyenv uninstall 2.7.10

2.7.2 使用virtualenv管理不同的项目

virtualenv本身是一个独立的项目,用以隔离不同项目的工作环境。例如,用户pcm希望在项目A中使用flask0.8这个版本,与此同时,又想在项目B中使用Flask0.9这个版本。如果我们全局安装flask,必然无法满足用户的需求。这个使用我们就可以使用virtualenv。
读者需要注意pyenv和virtualenv的区别。pyenv用以管理不同的Python版本,例如,你的系统工作时使用Python2,学习的时候使用Python3.virtualenv用以隔离项目的工作环境,如同上面的举例。我们只要组合pyenv和virtualenv这两个工具,就能够构造Python和第三方库的任意版本组合,拥有很好的灵活性,也避免了项目之间的相互干扰。
virtualenv本身是一个独立的工具,用户 可以不使用pyenv而单独使用virtualenv。但是,如果你使用了pyenv,就需要安装pyenv-virtualenv插件,而不是通过virtualenv软件使用virtualenv的功能。

  1. pyenv-virtualenv的安装
    略过
  2. pyenv-virtualenv的使用
#创建项目A的工作环境
pyenv virtualenv 2.7.13 first_project

#创建项目B的工作环境
pyenv virtualenv 2.7.13 second_project

#激活项目A的工作环境
pyenv activete first_project
#关闭项目A的工作环境
pyenv deactivate

如果要删除虚拟环境,则使用

pyevn virtualenv-delete first_project

第3章 打造命令行工具

3.1 与命令行相关的Python语言特性

3.1.1 使用sys.argv获取命令行参数

编写Linux下的命令行工具,很多时候都需要解析命令行的参数。如果参数很简单,则可以不使用解析参数的库,直接访问命令行参数。在Python中,sys库下有一个名为argv的列表,该列表保存了所有的命令行参数。argv列表中第一个元素是命令行晨星的名称,其余的命令行参数以字符串的形式保存在该列表中。
例如,现在有一个名为test_argv.py的Python文件,该文件仅仅是导入sys库,然后使用print函数打印argv列表中的内容。test_argv.py的文件内容如下:

from __future__ import print_function
import sys

print(sys.argv)

下面是一个或者命令行参数,判断文件是否存在,如果存在判断其是否可读的示例:

#!/usr/bin/python


from __future__ import print_function
import sys
import os

def main():
    sys.argv.append("")
    filename = sys.argv[1]
    if not os.path.isfile(filename):
        raise SystemExit(filename + ' does not exists')
    elif not os.access(filename,os.R_OK):
        raise SystemExit(filename + ' is not accessible')
    else:
        print(filename, ' is accessible')

if __name__ == '__main__':
    main()

3.1.2 使用sys.stdin 和fileinput读取标准输入

众所周知,Shell能够通过管道把多个命令组合在一起来实现一个复杂的功能。因此,我们也希望在Python语言中使用管道来结合Python语言和Shell脚本的优势。
在Python标准库的sys库中,有三个文件描述符,分别是stdin、stdout和stderr,我们不需要调用open函数打开这几个文件就可以直接使用。例如,我们有一个名为read_stdin.py的文件,该文件仅仅是从标准输入中读取内容,然后打印到命令行终端。文件内容如下:

from __future__ import print_function
import sys

for line in sys.stdin:
    print(line, end="")

接下来,我们就可以想shell脚本一样,通过标准输入给该程序输入内容,如下所示:

cat /etc/passwd |python read_stdin.py

sys.stdin是一个普通的文件对象,该对象还有一些方法,通过这些方法我们可以进一步去处理标准输入中的内容。如通过readlines函数将标准输入的内容读取到一个例表中。

如果我们可以使用fileinput来进行多文件的处理。fileinput是Python语言的一个标准库,它提供了比sys,stdin更加通用的功能。使用fileinput,可以依次读取命令行参数中给出的多个文件。也就是说,fileinput会遍历sys.argv[1:]列表,并按行一次读取列表中的文件。如果列表为空,则fileinput默认读取标准输入中的内容。示例同上:

from __future__ import print_function
import fileinput

for line in fileinput.input():
    print(line, end="")

fileinput读取内容比sys.stdin更加灵活,看示例

cat /etc/passwd |python read_from_fileinput.py
python read_from_fileinput.py < /etc/passwd
python read_from_fileinput.py  /etc/passwd /etc/hosts

因为fileinput可以读取多个文件的内容,所以,fileinput提供了一些方法让我们知道当前读取的内容属于哪个文件。fileinput中常用的方法有:

  • filename: 当前正在读取的文件名
  • fileno: 文件的描述符
  • filelineno: 正在读取的行是当前文件的第几行
  • isfirstline: 正在读取的行是否为当前文件的第一行
  • isstdin fileinput: 正在读取文件还是直接从标准输入读取内容。

这些方法的使用也非常简单,看示例:

from __future__ import print_function
import fileinput

for line in fileinput.input():
    meta = [fileinput.filename(),fileinput.fileno(),fileinput.filelineno(),
            fileinput.isfirstline(),fileinput.isstdin()]
    print(*meta,end="")
    print(line,end="")

3.1.3 使用SystemExit异常打印错误信息

前面介绍了标准输入,如果我们要输出标准输出和错误输出的,可以使用sys.stdout.write方法和sys.stderr.write方法。如果我们的程序执行失败了,通常情况下我们会需要在标准版错误中输出错误信息,然后以非零的返回码退出程序。示例如下:

import sys

sys.stderr.write('error message')
sys.exit(1)

3.1.4 使用getpass库读取密码

getpass是一个非常简单的Python标准库,主要包含getuser函数和getpass函数。前者用来从环境变量中获取用户名,后者用来等待用户输入密码。getpass函数和input函数的区别在于,它不会将我们输入的密码显示在命令行中,从而避免我们输入的密码被他们看到。如下所示:

from __future__ import print_function
import getpass

user=getpass.getuser()
passwd=getpass.getpass('your password: ')
print(user,passwd)

3.2 使用ConfigParse解析配置文件

使用配置文件配置参数是很常见的需求,因此,各个语言都提供了相应的模块来解析配置文件。在Python语言中,标准库的ConfigParser模块用以解析配置文件。ConfigParser模块中包含了一个ConfigParser类,一个ConfigParser对象可以同时解析多个配置文件,一般情况下,我们只会使用ConfigParser解析一个配置文件。ConfigParser类提供了很多方法,我们可以使用这些方法解析、读取和修改配置文件。
要解析一个配置文件,首先要创建一个ConfigParser对象。创建ConfigParser时有多个参数,其中,比较重要的是allow_no_value。allow_no_value默认取值为False,表示在配置文件中不允许没有值的情况。但是有一些特殊环境,会有这种情况,如mysql的配置文件。

有了ConfigParser对象以后,可以使用read方法从配置文件中读取配置内容,也可以使用readfp方法从一个已经打开的文件中读取配置内容。

from __future__ import print_function
import ConfigParser

cf=ConfigParser.ConfigParser(allow_no_value=True)
cf.read('my.cnf')

ConfigParser中有很多方法,其中与读取配置文件,判断配置相关的方法有:

  • sections: 返回一个包含所有章节的列表
  • has_section: 判断章节是否存在
  • items:以元祖的形式返回所有选项的列表
  • options: 返回一个包含章节下所有选项的列表;
  • has_option: 判断某个选项是否存在
  • get、getboolean、getini、getfload: 判断选项的值

以上面打开的配置文件为例

In [5]: cf.sections()
Out[5]: ['client', 'mysql', 'mysqld', 'mysqldump']

In [6]: cf.has_section('client')
Out[6]: True

In [7]: cf.options('client')
Out[7]: ['port', 'socket']

In [8]: cf.has_option('client','port')
Out[8]: True

In [9]: cf.get('client','port')
Out[9]: '3306'

In [10]: cf.getint('client','port')
Out[10]: 3306

ConfigParser提供了很多方法便于我们修改配置文件。如下:

  • remove_section: 删除一个章节
  • add_section: 添加一个章节
  • remove_option: 删除一个选项
  • set: 添加一个选项
  • write: 将ConfigParser对象中的数据保存到文件中。

示例略过。

3.3 使用argparse解析命令行参数

对于命令行工具来说,命令行参数比陪你文件的使用更加广泛。在Python中,agrparse是标准库中用来解析命令行参数的模块,用来替代已经过时的optparse模块。argparse能够根据程序中的定义从sys.argv中解析出这些参数,并自动生成帮助和使用信息。

3.3.1 ArgumentParse解析器

使用argparse解析命令行参数时,首先需要创建一个解析器,创建方式如下所示:

import argparse
parser=argparse.ArgumentParser()

ArgumentParse类的初始化函数有多个参数,其中比较常用的是description。description是程序的描述信息,即帮助信息前的文字。参数内容先略过,下面是一个例子。

from __future__ import print_function
import argparse

def _argparse():
    parser = argparse.ArgumentParser(description="This is description")
    parser.add_argument('--host', action='store',
            dest='server',default="localhost", help='connect to host')
    parser.add_argument('-t', action='store_true',
            default=False, dest='boolean_switch', help='Set a switch to true')
    return parser.parse_args()

def main():
    parser = _argparse()
    print(parser)
    print('host =', parser.server)
    print('boolean_switch=', parser.boolean_switch)

if __name__ == '__main__':
    main()

由于我们为所有的选项都提供了默认值,因此,即使不传递任何参数也不会出错,如下:

[pangcm@blog_vm py_script]$ python test_argparse.py 
Namespace(boolean_switch=False, server='localhost')
host = localhost
boolean_switch= False
[pangcm@blog_vm py_script]$ python test_argparse.py  --host=127.0.0.1 -t
Namespace(boolean_switch=True, server='127.0.0.1')
host = 127.0.0.1
boolean_switch= True

使用argparse进行参数解析还有一个好处就是,它会自动生成帮助信息,如下:

[pangcm@blog_vm py_script]$ python test_argparse.py  --help
usage: test_argparse.py [-h] [--host SERVER] [-t]

This is description

optional arguments:
  -h, --help     show this help message and exit
  --host SERVER  connect to host
  -t             Set a switch to true

3.3.2 模仿MySQL客户端的命令行参数

from __future__ import print_function
import argparse

def _argparse():
    parser = argparse.ArgumentParser(description='A Python-MySQL client')
    parser.add_argument('--host', action='store', dest='host',
            required=True, help='connect to host')
    parser.add_argument('-u', '--user', action='store', dest='user',
            required=True, help='user for login')
    parser.add_argument('-p', '--password', action='store',
            dest='password',required=True, help='password to use when connecting to server')
    parser.add_argument('-P', '--port', action='store', dest='port',
            default=3306, type=int, help='port number to use for connection or 3306 for default')
    parser.add_argument('-v', '--version', action='version', version='%(prog)s 0.1')
    return parser.parse_args()

def main():
    parser = _argparse()
    conn_args = dict(host=parser.host, user=parser.user,
            password=parser.password, port=parser.port)
    print(conn_args)

if __name__ == '__main__':
    main()

3.4 使用loggin记录日志

对比自己写print函数打印程序的中间结果,使用标准库的日志模块有很多好处,包括:

  • 所有日志具有统一的格式,便于后续处理
  • 丰富的日志格式,只需要通过配置文件就可以修改日志的格式,不需要修改代码
  • 根据重要性对日志进行分类,可以只显示重要的日志
  • 自动管理日志文件,如按天切换一个新的文件,只保留一个月的日志文件等。

3.4.1 日志的作用

重要到不用说吧,比如诊断日志,排查问题;审计日志,为商业行为分析日志,如pv。

3.4.2 Python的logging模块

在最简单的使用中,我们直接导入logging模块,然后调用它的debug、info、warn、error、critical等函数记录日志。默认情况下,logging模块将日志打印到屏幕终端,日志级别为WARNING,也就是说,只有日志级别比WARNING高的日志才会被显示,如下所示:

#!/usr/bin/python

import logging

logging.debug('debug message')
logging.info('info message')
logging.warn('warn message')
logging.error('error message')
logging.critical('critical message')

日志的级别是一个逻辑上的概念,用来区分日志的重要程度。将日志分为不同的级别后,一方面可以在大多数时间只保存级别比较高的日志来提供性能;另一方面也便于日志的分析。
在Python的logging模块中,分为5分级别,其含义为:

日志级别权重含义
CRITICAL50严重错误,表明程序已经不能继续运行了
ERROR40发生了严重错误,必须马上处理
WARNING30应用程序可以容忍这些信息,软件可以正常运行,不过他们应该被检查及修复,否则将在不久的将来发生问题
INFO20证明事情按预期工作,突出强调应用程序的运行过程
DEBUG10详细信息,只有开发人员调试程序时才需要关注的事情

3.4.3 配置日志格式

在使用logging记录日志之前,我们可以进行一些简单的配置,如下:

#!/usr/bin/python

import logging

logging.basicConfig(filename='app.log',level=logging.INFO)

logging.debug('debug message')
logging.info('info message')
logging.warn('warn message')
logging.error('error message')
logging.critical('critical message')

执行上面的程序,会在当前目录下产生一个app.log文件。该文件存在INFO及INFO以上级别的日志。
尅看到,我们可以通过basicConfig方法对日志进行简单的配置,我们也可以进行更加复杂的日志配置。在这之前,需要先了解logging模块中的几个概念,即Logger、Handler及Formatter。

  • Logger: 日志记录器,是应用程序中能直接使用的接口
  • Handler: 日志处理器,用以表名将日志保存到什么地方以及保存多久
  • Formatter: 格式化,用以配置日志输出的格式。

对于比较简单的脚本,可以直接使用basicConfig在代码中配置日志。对于比较复杂的项目,可以将日志的配置保存到一个配置文件中,然后再代码中使用fileConfig函数读取配置文件。

下面是一个Python源码中配置日志的例子。

#!/usr/bin/python

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s : %(levelname)s : %(message)s',
    filename='app.log')

logging.debug('debug message')
...
logging.critical('critical message')

对于复杂的项目,一般将日志配置保存在配置文件中,如下:

[loggers]
keys = root

[handlers]
keys = logfile

[formatters]
keys = generic

[logger_root]
handlers = logfile

[handler_logfile]
class = handlers.TimedRotatingFileHandler
args = ('app.log', 'midnight', 1, 10)
level = DEBUG
formatter = generic

[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s] %(message)s

在这个日志配置文件中,我们首先在[loggers]中声明一个名为root的logger,在[handlers]中声明一个名为logfile的handler,并在[formatters]中声明一个名为generic的formatter。然后,我们在[logger_root]中定义root这个logger所使用的handler,在[handler_logfile]中定义了handler输出日志的方式、日志文件的切换时间等。最后,在[formatter_generic]中定义了日志的格式,包括日志产生的时间、日志的级别、产生日志的文件名和行号等信息。

有了配置文件以后,在Python代码中使用logging.config模块的fileConfig函数加载日志配置,如下所示:

import logging
import logging.config

logging.config.fileConfig('logging.cnf')

logging.debug('debug message')
...
logging.critical('critical message')

3.5 与命令行相关的开源项目

3.5.1 使用click解析命令行参数

Click是Flask的作者开发的一个第三方模块,用于快速创建命令行。它的作用与Python标准库的argprese相同,但是,使用更加简单。Click相对于标准库的argparse,就好比requests相对于标准库的urllib.
click是一个第三方库,首先要先安装才能使用。

pip install click

Click对argparse的主要改进在易用性,使用Click分为两个步骤:

  1. 使用@click.command()装饰一个函数,使之成为命令行接口;
  2. 使用@click.option()等装饰函数,为其添加命令行选项等。

示例如下:

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

在上面的例子中,函数hello接受两个参数,分别是count和name,它们的取值从命令行中获取。在这段程序中,我们使用了click模块中的command、option和echo,他们的作用如下:

  • command:使函数hello成为命令行接口
  • option: 增加命令行选项
  • echo: 输出结果,使用echo进行输出是为了获得更好的兼容性,因为Python2和Python3的print选项不是同一个东西来的。

运行上面的程序,可以通过命令行指定count和name的取值。由于我们在option函数中使用了prompt选项,因此,当我们没有直接指定name这个参数的时候,Click会提示我们在交互模式下输入,如下所示:

python hello.py --count=3

至于Click如何实现这些功能的,略过。

3.5.2 使用 prompt_toolkit 打造交互式命令行工具

如果你要打造一个用户体验良好的交互式命令行程序,那么可以了解prompt_toolkit的特性。使用prompt_toolkit能够支持语法高亮、支持代码补全可以使用Vi风格的快捷键等特性。我这里略过,有需要的百度去了解。

第4章 文本处理

文本是软件工程师日常工作中处理最多的数据类型,几乎无时无刻不在与文本打交道。Linux下有很多的文本处理工具,但是有很多的高级特性都依赖于正则表达式,学习曲线较为陡峭。
Python语言内嵌的字符类型包含大量的文本处理函数,Python的标准库对文本处理提供了很好的支持。所以,我们也能使用Python来处理文本的需求。

4.1 字符串常量

4.1.1 定义字符串

在Python中,所有的字符都是字符串。因为Python不区分字符和字符串,所以Python可以使用单引号或者双引号来定义字符串。如: name = "super man"

在遇到特殊字符的时候,我们需要转义字符""对字符进行转移。场景的转移字符有

如我们在处理windows的路径的时候就需要转义字符的帮忙了

#这个会报错的
path= "c:\next"

#这样才行
path="c:\\next"

除了使用转义字符以外,还可以使用原始字符串(raw string)。原始自负床的使用非常简单,就是在字符串定义前面加上一个"r",如: r"Hello world"。原始字符串会抑制所有的转移,打印字符中的所有反斜杠。所以上面的例子也可以这样写:

path=r"c:\\next"

在Python中,如果你定义的字符串较长,可以使用三引号来定义字符串。使用三引号定义的字符串一般称为多行字符串。多行字符串不受代码缩进规则的限制,因为它本身就不是代码,而是字符串,此外也不会被转义,
Python字符串还有一个容易被忽略的小特性,就是两个字符串会自动主从一个新的字符串。如下:

#s的结果是 helloworld
s="hello" "world"

4.1.2 字符串是不可变的有序集合

Python的字符串有两个特点: 1是不可变的,2是有序的集合。

由于Python字符串是不可变的,所以不能直接对字符串进行修改。但是可以通过字符运算、切片操作、格式化表达式和字符串方法调用等方式创建新的字符串。然后把结果赋值给最初的变量名,达到修改字符串的目的。此外,正式因为不可变的特性。所以对字符串进行操作的时候都会产生一个新的字符串,新的字符串会占用一块独立的内存。因此,操作字符串时需要避免产生太多的中间结果。下面的反面例子:

fruits = ['apple1','apple2','apple3','apple4']
statement = fruits[0]
for item in fruits[1:]:
    statement = statement + ', ' + item

print(statement)

在这个例子中,产生了大量的中间结果。产生了临时字符串,它们已产生就被销毁了,白白浪费了程序的运行时间。示例的正确做法是使用join方法,如下:

', '.join(fruits)

Python字符串的第二个特点就是通过下标和切片进行访问。在Python语言中,元组、列表和字符串都是元素的有序集合,都可以使用下标和切片进行访问。

4.1.3 字符串函数

Python提供与字符串处理相关的方法可以分为两大类。一类是可以用于多种类型的通用操作,以内置函数或表达式的方式提供。如len(s)、'x' in s等。另一类是只作用于字符串的特定类型操作,以方法调用的形式提供,如str.split()和str.upper()等。

  1. 通用操作
    如 len('pcm')、 'c' in 'pcm'
  2. 与大小写相关的方法
  • upper: 转为大写
  • lower: 转为小写
  • isupper: 判断是否为大写
  • islower: 判断是否为小写
  • swapcase: 大写转小写,小写转大写
  • capitalize: 将首字母转换为大写
  • istitle: 判断字符串是不是标题
  1. 判断类方法
    除了前面介绍的几个is开头的方法,还有下面的这些也是很常见的:
  • isalpha: 是否只包含字母
  • isalnum: 是否只包含字符串字母和数字
  • isspace: 是否只包含空格、制表符、换行符
  • isdecimal: 是否自包含数字字符
  1. 字符串方法startswith和endswith
    这两个判断类方法用来判断字符串的前缀或后缀。下面示例一个常见的场景:统计出MongoDB日志文件占用磁盘的大小。
import os

mongod_logs = [item for item in os.listdir('/var/mongo/log') if item.startswith('mongod.log')]
sum_size=sum(os.path.getsize(os.path.join('/var/mongo/log'),item)) for item in mongod_logs)
  1. 查找类函数
  • find:查找子串在字符串中的位置,如果查找失败,返回-1
  • index: 与find函数类似,如果查找失败,抛出ValueError异常
  • rfind: 与find函数类似,区别在于rindex是从后向前查找
  • rindex: 与index函数类似,区别在于rindex是从后向前查找

这几个函数的用法和作用都差不多,这里介绍find的用法

s = 'wo you yi tou xiao mao lv.'
s.find('yi')
  1. 字符串操作方法
    字符串的join函数用来连接字符串列表,组成一个新的、更大的字符串。join是一个字符串处理函数,需要先有字符串,再调用这个函数。因此,如果仅仅需要将几个字符串连接起来,并且不需要插入任何额外的字符,则可以使用空字符串调用join方法,如下所示:
#结果为abc
"".join(['a','b','c'])

#结果为a,b,c
",".join(['a','b','c'])

join函数比前面介绍的更加通用,join函数只有一个参数,并且是iterable而不是列表。也就是说,join接受任何可迭代的对象。因此,如果我们需要将文件中的内容拼接起来组成一个更大的字符串,我们自需要将文件对象传递给join函数即可,因为文件对象本身就是一个可迭代的对象。

with open('/etc/passwd') as f:
  print("###".join(f))

join函数最容易被滥用的地方是打印字符串列表时,print函数本身可以通过sep参数指定分隔符:

print('root','/root','/bin/bash',seq=':')

如果使用join函数先组成字符串然后再打印,就很容易出错,并且性能也变差。如join的列表中存在数字,join函数不会自动将数字转换为字符串,然后会抛出异常信息,这样就不能做打印处理了。

接下来看一下与join函数起反作用的split函数。join函数用以将字符串列表(更准确地说,是可迭代对象)拼接成更大的字符串,而split函数正好相反,它用以将一个字符串拆分成一个字符串列表。

split默认使用空白字符作为分隔符,当然也可以指定分隔符。如下:

'super:man'.split(':')

strip、rstrip和lstrip这几个函数用来多字符串进行剪裁,最常用的场景就是去除两端的空白字符。当然也可以传递参数,去除特定的字符,如下:

"##Hello,world####.strip('#')"

replace函数非常简单,顾名思义就是将字符串中的子串替换成一个新的子串。

4.1.4 案例:使用Python分析Apache的访问日志

下面的代码可以统计网站访问的PV和UV。

#!/usr/bin/python
from __future__ import print_function

ips = []
with open('access.log') as f:
    for line in f:
        ips.append(line.split()[0])

print("PV is {0}".format(len(ips)))
print("UV is {0}".format(len(set(ips))))

下面我们接触collections.Counter保存资源的热度,Count是dict的子类,使用方式与字典类似。对于普通的计数功能,Count比字典更加好用。如下所示:

#变量c的结果是 Count({'a':2,'b':2,'c':1})
c=Counter('abcba')

Counter作为一个计数器,还提供了一个名为most_common的函数,用来显示Counter中取值最大的几个元素。下面的代码使用Counter统计网站中最热门的十项资源:

#!/usr/bin/python
#-*- coding: UTF-8 -*-
from __future__ import print_function
from collections import Counter

c = Counter()
with open('access.log') as f:
    for line in f:
        c[line.split()[6]] += 1

print("Popular resources : {0}".format(c.most_common(10)))

4.1.5 字符串格式化

在Python中,存在两种格式化字符串的方法,即%表达式和format函数。%表达式从Python诞生之日就开始存在了,是基于C语言的printf模型,目前还广泛使用。format函数是Python2.6和Python3.0新增加的技术,是Python独有的方法,并且和字符串格式表达式的功能有不少的重叠。虽然%表达式目前广泛使用,但是,format函数才是字符串格式化的未来,%表达式在Python未来的版本可能会弃用。

最简单的format函数使用应该是通过参数的位置访问参数。如下所示,通过{}来表示一个占位符,Python会自动将format函数的参数依次传递给{}占位符。

#需要按顺序来填写参数
"{} is better than {}.".format('A','B')
#通过下标来访问
"{0} is better than {1}.".format('A','B')

4.2 正则表达式

正则表达式是个处理文本的强大工具,在文本处理程序中广泛使用,如Offic world、vim等。正则表达式在Linux命令行中使用更加广泛,大部分文本处理工具都支持正则表达式,如grep、awk、sed等。正则表达式,最大的问题就是学习的难度较大,很容易学完就忘了。

4.2.1 正则表达式语法

正则表达式由普通文本和具有特殊意义的符号组成,工程师根据具体的需要,使用它们构造出合适的正则表达式来匹配文本。例如:

  1. 要匹配给定文本中的所有单词
#"?"用于匹配单词前后可能出现的空格,[a-zA-Z]+代表一个或多个英文字母
?[a-zA-Z]+
  1. 要匹配一个IP地址,可以使用下面的正则表达式:
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}

下表给出正则表达式的基本组成部分

4.2.2 利用re库处理正则表达式

正则表达式虽然本身比较负暂,但是,在Python中使用正则表达式却出奇地简单。在Python中,标准库re模块用来处理正则表达式,它能够顺利处理Unicode和普通字符串。这个模块包含了与正则表达式相关的函数、标志和一个异常。

下面的示例使用re模块下的findall函数来匹配符合并输出符合模式的子串

import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re.findall('python [0-9]\.[0-9]\.[0-9]',data)

如果希望re模块在模式匹配的时候忽略字符的大小写,可以通过传递标志的形式告诉re模块忽略大小写这个需求,如:

re.findall('python [0-9]\.[0-9]\.[0-9]',data,flags=re.IGNORECASE)

除了直接使用re模块中的函数之外,还有另外一种使用正则表达式的方法,那就是创建一个特定模式编译的正则表达式对象,然后使用这个对象中方法。
什么事编译的正则表达式呢?它是一个简单的对象,通过传递模式给re.compile函数创建。编译与非编译方式使用正则表达式的区别,除了使用方法之外,主要是性能方面的差异。编译的性能明显好于非编译的。

import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re_obj = re.compile('python [0-9]\.[0-9]\.[0-9]')
re_obj.findall(data)

4.2.3 常用的re方法

  1. 匹配类的函数

re模块中最简单的辨识findall函数,该函数在字符串中查找模式匹配,将所有的匹配字符串以列表的形式返回。如果文本中没有任何字符串匹配模式,则返回一个空的列表。如果有一个子串匹配模式,则返回一个元素的列表。所以,不管怎么匹配,都不会出错。这对工程师编写程序来说,减少异常情况的处理,代码逻辑更加整洁。

match函数类似于字符串中startswith函数,只是match更强大。match函数用以匹配字符串的开始部分,如果匹配成功,返回一个SRE_Match类型的对象,如果失败,则返回一个None。例如,我们判断data字符串是否以"What"和"Not What"开头:

re.match('What',data)
re.match('Not What',data)

下面示例匹配一个字符串是否以数字开头:

re.match('\d+',"123 is one hundred and twenty-three")

re模块中的search函数模式匹配成功时,也会返回一个SRE_Match对象。其与match函数的用法几乎一样,区别在于前者在字符串的任意位置进行匹配,后缀仅对字符串开始部分进行匹配。要注意的是search仅仅查找第一次匹配,如果要返回多个匹配的结果,最简单的做法是使用findall函数。

  1. 修改类函数
    re模块的sub函数类似于字符串的replace函数,只是sub函数支持使用正则表达式,所以更加强大。例如,下面的正则表达式模式,可以同时匹配"2.7.13"和"3.6.0",并将他们都替换为"x.x.x"。
import re
data = "waht is the difference between python 2.7.13 and Python 3.6.0 ?"
re.sub('[0-9]+\.[0-9]+\.[0-9]+','x.x.x',data)

re模块的split函数与Python字符串的split函数功能一样,都是将一个字符串拆分为子串的列表,只是re模块的能够使用正则表达式。例如,对于下面这一段包含了冒号,逗号,单引号和若干空格的文本,我们希望拆分出每一个单次。

text = "MySQL slave binlog position: master host '10.1.1.1',filename 'mysql-bin.000002',position '524994060'"
re.split(r"[':,\s]+",text.strip("'"))
  1. 大小写不敏感
    在re模块中,要忽略大小写的差异,就如同前面的示例那样,使用flags=re.IGNORECASE。
  2. 非贪婪模式
    在正则表达式的字符串匹配中,有贪婪匹配和非贪婪匹配的区别。贪婪匹配总是匹配到最长的那个字符串,非贪婪模式正好相反。例如:我们要匹配以"Beautiful"开头并且以点好结尾的字符串,默认情况下正则表达式使用贪婪匹配,如果要使用非贪婪匹配,只需要在匹配字符串时加上一个"?"。如下所示:
text = "Beautiful is better than ugly.Explicit is better than implicit."
re.findall('Beautiful.*\.',text)
re.findall('Beautiful.*?\.',text)

4.3 字符集编码

在Python编程中,如果不使用Unicode,处理中文时将会遇到一些令人困惑的地方。需要注意的是,Python2.7默认使用的不是Unicode,Python3默认使用的是Unicode。

In [18]: name='超人'
In [19]: print(name)
超人
In [20]: print(name[0:1])
#输出没有结果
In [21]: name=u'超人'
In [22]: print(name)
超人
In [23]: print(name[0:1])
超

上面的示例可以看到使用中文的话,使用分片的时候得不到我们想要的结果。解决的方法也很简单,只要在前面加个"u"来定义Unicode字符串就行了。

4.3.4 Python2和Python3中的Unicode

前面说到Python2中如果要使用Unicode编码,则必须在字符串前面显式地加上一个"u"前缀。其实,Python2也可以默认使用Unicode的字符串的,只需要执行下面的导入即可:

from __future__ import unicode_literals

Python的字符串具有encode方法和decode方法。我们可以使用这两个方法对字符串进行编码或者解码,下面是一个在Python2下运行的例子:

name='超人'
new_name=name.decode('utf8')
#能正常输出"超"
print(new_name[:1])

#使用encode进行编码
new_name.encode('utf-8')
new_name.encode('utf-16')

我们既然已经知道使用encode对Unicode进行编码,使用decode对字符进行解码,那么,如果我们要存储中文的话,可以这样操作:

name=u'超人'
#使用encode进行编码
with open('/tmp/data.txt', 'w') as f:
    f.write(name.encode('utf-8'))

#使用decode解码
with open('/tm/data.txt', 'r') as f:
    data = f.read()

data.decode('utf-8')

如果需要写入的字符串比较多,而每次都需要进行编码,程序将会变得非常抵消。在Python2中可以使用codecs模块,在Python3中内置的open函数就已经支持指定编码格式。指定编码格式以后,当我们写入时会自动将Unicode转换为特定的编码,读取文件时,自动以特定的UTF进行编码。

在python2中,使用codecs模块进行编码和解码

import codecs

name=u'超人'
with open('/tmp/data.txt', 'w',encoding='utf-8') as f:
    f.write(name)

with open('/tm/data.txt', 'r',encoding='utf-8') as f:
    data = f.read()

在Python3中,内置的open函数可以指定字符集编码:

name='超人'
with open('/tmp/data.txt', 'w',encoding='utf-8') as f:
    f.write(name)
...

4.4 Jinja2模版

4.4.1 模版介绍

模版在Python的web开发中广泛使用,它能够有效地将业务逻辑和页面逻辑分离,使得工程师编写出可读性更好、更加容易维护的代码。

试想一下,要为一个大型的表格构建HTML代码,表格中的数据由数据库中读取的数据以及必要的HTML字符串连接在一起。这个时候,最简单也就是最直接的方式就是Python代码中使用字符串拼接的方式生成HTML代码。如果真的这么做了,对工程师来说将是个噩梦,而且代码无法维护。

此时,可以使用模块将业务逻辑与页面逻辑分隔开来。模块包含的是一个响应文本的文件,其中包含用占位变量表示的动态部分,其具体值只在请求的上下文中才能知道。使用真实的值替代变量,再返回最终得到的响应字符串,这一过程成为渲染。

web开发是最需要使用模版的地方,但是,并不是唯一可以使用模版的地方。模版使用范围比大多数工程师接触的都要广泛,因为模版使用所有基于文本的格式,如HTML,XML,CSV等。使用模版能够编写出可读性更好、更容易理解和维护的代码,并且使用范围非常广泛,因此怎么使用模版主要取决于工程师的想象力和创造力。比如,我们在使用Ansible就会用到。作为工程师,我们也可以使用Jinja2管理工作中的配置文件。一旦学会使用模版管理配置文件,就可以拜托无数繁琐的文本替换工作。

Python的标准库自带了一个简单的模版,下面的代码便是一个模版使用的例子。模版包含的是一个响应信息,其中包含用占位变量表示的动态部分,动态部分的取值取决于具体的应用,并且只有在请求的上下文才能知道。渲染就是使用真实的值替换变量,再返回最终得到的响应字符串。

from string import Template
s=Template('$who is a $role')
s.substitute(who='bob',role='teacher')
s.substitute(who='lily',role='student')

Python自带的模版功能非常有限,例如无法在模版中使用控制语句和表达式,不支持继承和重用等操作。这对于web开发来说是远远不够,因此,出现了第三方的模版系统。目前市面上有非常多的模版系统,其中最知名的是Jinja2和Mako。

4.42 Jinja2语法入门

Jinja2模版引擎之所以使用广泛,是因为它具有以下优点:

  • 相对于Template,Jinja2更加灵活,它提供了控制结构、表达式和继承等,工程师可以在模版中做更多的事情。
  • 相对于Mako,Jinja2提供了仅有的控制结构,不允许在模版中编写太多的业务逻辑,避免了工程师的乱用行文。
  • 相对于Django模版,Jinja2的性能更好
  • Jinja2模版的可读性很好。

Jinja2是Flask的一个依赖,如果已经安装了Flask,Jinja2也会随之安装。当然,也可以单独安装Jinja2.

pip install jinja2

接下来将详细介绍Jinja2的语法。

  1. 语法块
    Jinja2可以应用于任何基于文本的格式,如HTML、XML等。Jinja2使用大括号的方式表示Jinja2的语法。在Jinja2中,存在3中语法:
  • 控制结构 {%%}
  • 变量取值 {{}}
  • 注释{##}

下面是使用Jinja控制结构的和注释的一个例子:

{# note: disabled template because we no longer use this
    {% for user in users %}
        ...
    {% endfor %}
#}

可以看到,for循环的使用和Python比较类似,但是,没有了复合语句末尾的冒号。此外需要使用endfor作为结束标志。Jinja2中的if语句也一样,没有复合语句末尾的冒号,需要使用endif作为结束标志。

  1. 变量
    Jinja2模版中使用的{{ }}语法表示一个变量,他是一种特殊的占位符,缟素模块引擎这个位置的值在渲染模版时获取。Jinja2识别所有的Python数据类型,甚至是一些复杂的类型,如列表、字典和对象等。如下:
A value from a dictionary: {{ mydict['key'] }}
A value from a list: {{ mydict[3] }}
A value from a list,with a variable index: {{ mydict[myintvar] }}
A value from a object's method: {{ myobj.somemethod() }}
  1. Jinja2中的过滤器
    变量可以通过“过滤器”进行修改,过滤器可以理解为Jinja2里面的内置函数和字符串处理函数。例如,存在一个名为lower的过滤器,它的作用与字符串对象的lower方法一模一样。下面列表是常见的Jinja2过滤器。

在Jinja2中,变量可以通过"过滤器"修改,过滤器与变量用管道(|)分割。多个过滤器可以链式调用,前一个过滤器的输出作为后一个过滤器的输入,如下所示:

{{ "Helle World" | replace("Hello","Goodbye") }}
{{ "Helle World" | replace("Hello","Goodbye") | upper}}
{{ 42.55 | round }}
{{ 42.55 | round |int }}
  1. Jinja2的控制结构
    Jinja2中的if语句类似于Python中的if语句,但是,需要使用endif语句作为条件判断的结束。我们可以使用if语句判断一个变量是否定义是否为空,是否为真值。与Python中的if语句一样,也可以使用elif和else构建多个分支,如下所示:
{% if kenny.sick %}
    kenny is sick.
{% elif kenny.dead %}
    You killed Kenny!
{% else %}
    Kenny looks okay
{% endif %}
  1. Jinja2的for循环
    Jinja2中的for语句可以用于迭代Python的数据类型,包括列表、元组和字典。在Jinji中不存在while循环,这样符合了Jinja2的提供仅有的控制结构的设计原则。
    在Jinja2中迭代列表:
<h1>Members</h1>
<u1>
  {% for user in users %}
    <li>{{ user.username }}</li>
  {% endfor %}
</u1>

在Jinja2中迭代字典:

<d1>
  {% for key,value in d.iteritems() %}
    <dt>{{ key }</dt>}
    <dd>{{ value }}</dd>
  {% endfor %}
</d1>

除了基本的for循环使用以外,Jinja2还提供了一些特殊的变量,我们不用定义就可以直接使用这些变量。

有关宏和继承函数的内容,请看书籍。貌似一般的维护工作中也用不到的了。

  1. Jinja2的其他运算
    Jinja2可以定义变量,对变量进行操作,Jinja2提供了算数操作、比较操作和逻辑操作。使用Jinja2模板时,应该尽可能在Python代码中进行逻辑处理,在Jinja2中仅处理显示问题。因此,一般很少用到Jinja2的变量和变量的运算操作。常用的运算操作:
  • 算数操作 + - * / // % * **
  • 比较操作 == != > >= < <=
  • 逻辑运算 not and or

第5章 Linux系统管理

5.1 文件读写

文件可以从多个维度进行管理,例如,重命名文件、获取文件属性、判断文件是否存在、备份文件、读写文件等。

5.1.1 Python内置的open函数

在Python中,要对一个文件进行操作,只需要使用内置的open函数打开文件即可。open函数接受文件名和打开模式作为参数,返回一个文件对象。工程师通过文件对象操作文件,完成以后,调用文件对象的close方法关闭文件,如下:

f=open('data.txt')
f.close()

5.1.2 避免文件句柄泄露

在计算机程序中,每打开一个文件就要占用一个文件句柄,而一个进程拥有的文件句柄数事有限。此外,文件句柄也会消耗资源。所以,我们在打开文件的时候,记得要关闭文件,释放资源。在Python中,可以使用finally语句来保证,无论在什么情况下文件都会被关闭。如下:

try:
    f=open('data.txt')
    ...
finally:
    f.close()

Python中还有更加简洁优美的写法,就是使用上下文文管理器,使用上下文管理器的代码如下:

with open('data.txt') as f:
    print(f.read())

5.1.3 常见的文件操作函数

Python的文件对象有多种类型的函数,如刷新缓存的flush函数。获取文件位置的tell函数,改变文件读写偏移量的seek函数。但是,工作中使用最多的还是与读写相关的函数。
Python提供了三个读相关得到函数,分别是read、readline和readlines,他们的作用如下:

  • read:读取文件中的所有内容
  • readline: 一次读取一行
  • readlines: 将文件内容存到一个列表中,列表中的每一行对应于文件中的一行。

可以看到read和readlines函数都是一次性把文件的所有内容都读出,如果是大文件,有OOM的风险。

Python提供了两个写函数,分别是write和writelines,它们的区别如下:

  • write: 写字符串到文件中,并返回写入的字符数
  • writelines: 写一个字符串列表到文件中。

在Python中,除了使用文件对象的write和writelines函数向文件写入数据以外,也可以使用print函数将输出结果输出到文件中。print函数比write和writelines函数更加灵活,如下:

from __future__ import print_function
with open('/tmp/data.txt', 'w') as f:
    print(1,2,'hello world',sep=",", file=f)

5.1.4 Python的文件是一个可迭代对象

如果我们要以行为单位依次处理文件中的每一行,应该怎么读取文件呢?如果使用readline或者readlines函数,但是每读一行就调用一次readline函数这种方式效率很低,并且会使得我们的代码非常混乱;如果使用readlines,加载大文件又是一个问题。
在Python中,还有更好的方式来依次处理文件的内容,即使用for循环遍历文件。为什么在Python中能使用for来循环遍历文件呢?因为Python的for循环比大家看到的还要通用,它不但可以遍历如字符串、列表、元组这个可迭代的序列,我们还可以使用迭代器协议来遍历可迭代对象。而Python的文件对象实现了迭代器协议,因此我们可以在for循环中遍历文件内容。如下:

with open('data.txt') as f:
    for line in f:
        print(line.upper)

5.2 文件与文件路径管理

介绍os模块,os模块对操作系统的API做了封装,并且使用统一的API访问不同的操作系统的相同功能。

5.2.1 使用os.path进行路径和文件管理

在os模块中最常见的就是getcwd和listdir函数了,前者用来获取当前目录,后者用来列出目录下的所有文件和文件夹。如下:

import os
os.getcw
os.listdir('.')
  1. 拆分路径
    os.path模块用来对文件和路径进行管理,显然,它会包含很多拆分路径的函数。相关的函数有:
  • split: 返回一个二元组,包含文件的路径与文件名
  • dirname: 返回文件的路径
  • basename: 返回文件的文件名
  • splitext: 返回一个除去文件拓展名的部分和拓展名的二元组
path='/home/pangcm/test.txt'
os.path.split(path)
os.path.basename(path)
os.path.dirname(path)
os.path.splitext(path)
  1. 构建路径
    os.path能拆分路径,自然也能构建路径。最常用的函数是expanduser、abspath和join函数。它们的作用:
  • expanduser: 展开用户的HOME目录,如~、~username
  • abspath: 得到文件或路径的绝对路径
  • join: 根据不同的操作系统,使用不同的路径分隔符拼接路径
import  os
os.getcwd()
os.path.expanduser('~')
os.path.expanduser('~pangcm')
os.path.expanduser('~pangcm/test')
os.path.abspath('.')
os.path.abspath('./test')
os.path.join('~','t','a.py')

前面介绍os.path模块构建路径时,介绍了abspath来返回绝对路径。相应地,os.path还存在一个函数用判断一个路径是否为绝对路径。

os.path.isabs('.')

在Python代码中,可以使用 '__file__' 这个特殊的变量表示当前代码所在的源文件。在编写代码时,有时需要导入当前源文件父目录下的软件包。因此,需要到这里的路径函数获取源文件的父目录。如下:

import os
os.path.abspath(__file__)
  1. 获取文件属性
    下面几个函数用来获取文件的属性:
  • getatime: 获取文件的访问时间
  • getmtime: 获取文件的修改时间
  • getctime: 获取文件的创建时间
  • getsize: 获取文件的大小
  1. 判断文件类型
    下面截个行数用来判断文件路径是否存在以及路径所指文件的类型。
  • exists: 参数path所指向的路径是否存在
  • isfile: 参数path所指向的路径存在,并且是一个文件
  • isdir: 参数path所指向的路径存在,并且是一个文件夹
  • islink: 参数path所指向的路径存在,并且是一个链接
  • ismount: 参数path所指向的路径存在,并且是一个挂载点
    下面有几个应用示例:
#获取当前用户home目录下所有的文件列表
[item for item in os.listdir(os.path.expanduser('~')) if os.path.isfile(item)]
#获取当前用户home目录下所有的目录列表
[item for item in os.listdir(os.path.expanduser('~')) if os.path.isdir(item)]
#获取当前用户home目录下所有的目录名到绝对路径之间的字典
{item: os.path.realpath(item) for item in os.listdir(os.path.expanduser('~')) if os.path.isdir(item)}

5.2.2 使用os模块管理文件和目录

前面已经介绍了getcwd函数,该函数用来获取当前目录,与之相关的chdir函数,该函数用来修改当前目录。

import os
os.getcwd()
os.chdir(os.path.expanduser('~pangcm'))

os模块还包含了文件和目录的操作函数,包括创建目录、删除目录、删除文件、重命名文件。

  • unlink/remove: 删除path路径所指向的文件
  • rmdir:删除path路径所指向的文件夹,该文件夹必须为空,否则报错
  • rename: 重命名文件或文件夹
  • mkdir: 创建一个文件夹

os模块也包含了修改文件权限、判断文件权限的函数,即chmod和access。chmod用来修改文件的权限,access用来判断文件是否具有相应的权限。在Linux中,权限分为读、写和执行。因此,os模块也提供了三个常量来表示读、写、可执行权限,即R_OK、W_OK和X_OK。
下面实例演示chmod和access的用法:

#!/usr/bin/python

from __future__ import print_function
import os
import sys

def main():
    sys.argv.append("")
    filename = sys.argv[1]
    if not os.path.isfile(filename):
        raise SystemExit(filename + ' does not exits')
    elif not os.access(filename, os.R_OK):
        os.chmod(filename,0777)
    else:
        with open(filename) as f:
            print(f.read)
if __name__ == '__main__':
    main()

5.3 查找文件

5.3.1 使用fnmatch找到特定的文件

大部分情况下,使用字符串匹配查找特定的文件就能满足需求,如果需要更加灵活的字符串匹配,可以使用标准库的fnmatch。这个库专门用来进行文件名匹配,支持使用通配符进行字符串匹配。支持的字符串列表如下:

fnmatch这个库比较简单,只有4个函数,分别是:

  • fnmatch: 判断文件名是否符合特定的模式
  • fnmatchcase: 判断文件名是否符合特定的模式,不区分大小写
  • filter: 返回输入列表中,符合特定模式的文件名列表
  • translate: 将通配符模式转换成正则表达式

使用示例:

import fnmatch
[ name for name in os.listdir('.') if fnmatch.fnmatch(name, '*.gz')]
[ name for name in os.listdir('.') if fnmatch.fnmatch(name, '[a-c]*')]
[ name for name in os.listdir('.') if fnmatch.fnmatch(name, '[a-c]*.gz')]

fnmatchcase函数与fnmatch函数几乎一样,只是在匹配文件名时会忽略文件名中字母的大小写。filter函数与fnmatch函数比较类似,区别在于fnmatch每次对一个文件名进行匹配判断,filter函数每次都一组文件名进行匹配判断。filter函数接受文件名列表为第一个参数,文件名模式为第二个参数,然后以列表的形式返回输出例表中符合模式的文件名。

5.3.2 使用glob找到特定的文件

在前面我们都是通过os.listdir获取文件列表,然后再去匹配。在Python中还有更加简单的方式,那就是使用标准库的glob库。glob的作用相当于os.listdir加上fnmatch。如下:

import glob
glob.glob('*.txt')
glob.glob('[a-c]?.txt')

5.3.3 使用os.walk遍历目录树

前面的示例都是查找当前目录下的内容,很多时候我们还需要查找其子目录里面匹配的内容。这时候就需要使用os模块中的walk函数了。对于每一个目录,walk返回一个三元组(dirpath,dirnames,filenames)。其中dirpath保存的是当前目录,dirnames是当前目录的子目录列表,filenames是当前目录下的文件列表。下面演示其用法

import os
import fnmatch

images = ['*.jpg','*.jpeg','*.png','*.tif']
matches = []

for root,dirnames,filenames in os.walk(os.path.expanduser('~pangcm')):
    for extensions in images:
        for filename in fnmatch.filter(filenames,extensions):
            matches.append(os.path.join(root,filename))
        print(matches)

如果要忽略某个子目录,只需要修改三元组中的dirnames即可。

if 'exclude_dir' in dirnames:
    dirnames.remove('exclude_dir')

5.4 高级文件处理接口shutil

如果读者对比一下os模块的函数和shutil模块中的函数,会发现它们有一些重叠。例如os.rename和shutil.move都可以用来重命名一个文件。那么,为什么会存在两个模块提供相同功能的情况呢?这就涉及标准库模块定位的问题,os模块是对操作系统的接口进行封装,主要作用是跨平台。shutil模块包含复制、移动、重命名和删除文件及目录的函数,主要作用是管理文件和目录。因此,他们并不冲突,并且是互补的关系。对于常见的文件操作,shutil更易于使用。我们应该尽可能使用shutil里面的函数,在shutil里面没有相应的功能情况下再使用os模块下的函数。

5.4.1 复制文件和文件夹

shutil里面的复制类函数很多,用得最多的是copy和copytree。前者是复制一个文件,后者用来复制整个目录。

import shutil
shutil.copy('a.py','b.py')
shutil.copytree('dir1','dir2')

5.4.2 文件和文件夹的移动与改名

shutil模块中的move函数的作用几乎等同于Linux下的mv命令。可以用来移动或者重命名文件

shutil.move('b.py','a.py')
shutil.move('a.py','dir1')

5.4.3 删除目录

shutil.rmtree函数能够删除非空的目录,比起os.rmdir实用多了。

shutil.rmtree('dir1')

5.5 文件内容管理

5.5.1 目录和文件比较

filecmp模块包含了比较目录和文件的操作。其中filecmp模块最简单的函数是cmp函数,该函数用来比较两个文件是否相同,如果文件相同就返回True,否则返回False。

import filecmp

filecmp.cmp('a.txt','b.txt')

filecmp目录下还有一个名为cmpfiles的函数,该函数用来同时比较两个不同目录下的多个文件,并且返回一个三元组。分别包含相同的文件、不同的文件和无法比较的文件。

filecmp.cmpfiles('dir1','dir2',['a.txt', 'b.txt', 'c.txt', 'a_copy.txt'])

filecmp也可以用来对比两个目录,但是需要传入两个目录的所有文件,这个比较麻烦。比较目录的话,可以使用dircmp函数。调用dircmp函数以后会返回一个dircmp类的对象,该对象保存了诸多属性,工程师可以通过读取这些属性的方式获取目录之间的差异。要注意的是,dircmp不会去比较子目录下的文件的差异。

5.5.2 MD5校验和比较

在Python中计算文件的MD5校验码可以使用标准库中的hashlib模块

import hashlib

d = hashlib.md5()
with open('/etc/passwd') as f:
    for line in f:
        d.update(line)

d.hexdigest()

5.6 使用Python管理压缩包

5.6.1 使用tarfile库读取与创建tar包

在Linux中,tar命令可以创建一个tar包,而不进行压缩,这是为了方便传输。在Python中,tarfile标准库提供了相同的功能,我们可以使用它来创建一个压缩或者不压缩的tar包。

  1. 读取tar包
    tarfile中有不少函数,其中最常用的有:
  • getname: 获取tar包的文件列表
  • extract: 提取单个文件
  • extractall: 提取所有文件

示例:

import tarfile
with tarfile.open('tarfile_add.tar') as t:
    for member_info in t.getmembers():
        print(member_info.name)
  1. 创建tar包
    创建tar包的方式和写一个文件比较类似,如下:
import tarfile
with tarfile.open('tarfile_add.tar',mode=w) as out:
    out.add('Readme.txt')

5.6.2 使用tarfile库读取与创建压缩包

下面演示读取和创建经过压缩过的tar包

#读取
with tarfile.open('tarfile_add.tar',mode='r:gz') as t:
#创建
with tarfile.open('tarfile_add.tar',mode='w:bz2') as out:

5.6.4 使用zipfile库创建和读取zip压缩包

在windows下zip格式的压缩方式更为常见,下面介绍zipfile库来读取和创建zip压缩包

  1. 读取zip文件
    zipfile的常用方法有:
  • namelist: 返回zip文件中包含的所有文件和文件夹的字符串列表
  • extract: 从zip文件中提取单个文件
  • extractall: 从zip文件中提取所有文件

使用示例:

import zipfile
example_zip = zipfile.ZipFile('example.zip')
example_zip.namelist()
  1. 创建zip文件
import zipfile
new_zip = zipfile.ZipFile('new.zip','w')
new_zip.write('spam.txt')
new_zip.close()
  1. 使用Python的命令行工具创建zip格式的压缩包
    在Linux中,我们使用zip命令来创建zip压缩包,使用unzip命令来解压。但是,Linux默认是没有安装这两个软件的。如果不想安装这两个软件,由于实现这功能可以使用Python中的zipfile模块,其提供了命令行接口。zipfile模块的命令行接口包含以下几个选项:
  • -l: 显示zip格式压缩包中的文件列表
  • -c: 创建zip格式的压缩包
  • -e: 提取zip格式压缩包
  • -t: 验证文件是一个有效的zip格式的压缩包

下面是示例:

pythom -m zipfile -c monty.zip  spam.txt eggs.txt
pythom -m zipfile -e monty.zip target-dir/
pythom -m zipfile -l monty.zip

5.6.6 使用shutil创建和读取压缩包

shutil模块是高层次的文件接口,除了包含文件和目录的操作函数以外,还包含了压缩包的创建和压缩的函数。并且使用起来更加简单,shutil支持的格式可以通过get_archive_formats函数获取。

  1. shutil创建压缩包
    使用shutil模块创建压缩包,只需要调用shutil模块下的make_archive函数即可。make_archive函数有多个参数,其中只有base_name与format这两个参数是必填的。参数base_name说是创建压缩包的名称,参数format用来指定压缩的格式。下面是一个示例:
import shutil

shutil.make_archive('backup','gztar')

需要注意的是base_name是压缩包的名称,但是不包含文件拓展名,拓展明会更加压缩的格式自动添加上去的。

  1. 在Python3中使用shutil读取压缩包
    在Python2中,shutil模块包含了创建压缩包的函数,并没有解压的函数。在Python3中,shutil模块包含了解压函数,名为unpack_archive函数。示例:
import shutil

shutil.unpack_archive('backup.tar.gz')

5.7 Python中执行外部命令

5.7.1 subprocess模块简介

subprocess模块最早在Python2.4版本中引入的,正如它名字所反应的,这个模块用于创建和管理子进程。它提供了高层次的接口,用来替换os.system(),os.spawn*()等模块和函数。subprocess其实非常简单,它提供了一个名为Popen的类来启动和设置子进程的参数。由于这个类比较复杂,subprocess还提供了若干便利函数。这些便利函数都是对Popen这个类的封装,以便于工程师能够快速启动一个子进程并获取他们的输出结果。

5.7.2 subprocess模块的便利函数

在subprocess模块中启动子进程,最简单的方式就是使用这一节介绍的便利函数。当这些便利函数不能满足需求时,再去使用底层的Popen类。便利函数包括:call、check_call与check_ouput.

  1. call
    call 函数的定义如下
subprocess.call(args,*,stdin=None,stdout=None,stderr=None,shell=False)

call函数运行由args参数指定的命名直到命令结束。call函数的返回值是命令的退出状态码,工程师可以退出状态码判断命令是否执行成功。如下:

import subprocess
subprocess.call(['ls','-l'])
subprocess.call('exit 1',shell=True)

call函数执行的外部命令以一个字符串列表的形式进行传递,如果设置了shell为True,则可以使用一个字符串命令,而不是一个字符串列表来运行子进程。如果设置了shell为True,Python将先运行一个shell,再用这个shell来解析整个字符串。

  1. check_call
    与call函数类似,不同是是如果命令执行失败,check_call不是返回非0,而是抛出subprocess.CalledProcessError异常。
In [7]: subprocess.check_call('exit 1',shell=True)
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
<ipython-input-7-0e1de6e31a15> in <module>()
----> 1 subprocess.check_call('exit 1',shell=True)

/usr/lib64/python2.7/subprocess.pyc in check_call(*popenargs, **kwargs)
    540         if cmd is None:
    541             cmd = popenargs[0]
--> 542         raise CalledProcessError(retcode, cmd)
    543     return 0
    544 

CalledProcessError: Command 'exit 1' returned non-zero exit status 1
  1. check_output
    上面两个函数的输出结果都是再命令行终端,check_out的输出结果不会输出到命令行终端,而是可以进一步处理。如下
output=subprocess.check_output(['df','-h'])
 print(output)

check_output函数通过方式之来返回命令的执行结果,但是却没办法和call函数一样返回退出状态码来表示异常。因此,check_output函数通过抛出一个subprocess.CalledProcessError异常来表示命令执行出错。此外,check_output命令捕获命令的标准输出,要捕获错误输出,需要将错误输出重定向到标准输出,如下:

output=subprocess.check_output(['cmd','arg1','arg2'],stderr=subprocess.STDOUT)

5.7.3 subprocess模块的Popen类

当便利函数不能满足需求的时候,需要使用Popen类,它能处理更多的情况。Popen的基本使用方式与上一节中介绍的便利函数类似。在Linux系统中,到shell设置为True时,shell默认使用/bin/sh。args时需要执行的命令,可以时一个命令字符串,也可以时一个字符串列表。

Popen对象创建后,子进程便会运行。Popen类提供了若干方法来控制子进程的运行,包括:

  • wait: 等待子进程结束
  • poll: 检查子进程状态
  • kill: 给子进程发送SIGKILL信号终止子进程
  • send_signal: 给子进程发送信息
  • terminate: 给子进程发送SIGTERM信号终止子进程
  • communicate: 与子进程交互

其中,使用communicate函数可以与子进程进行交互,包括输入数据,获取子命令的标准输出和错误输出。下面的函数对Popen执行shell命令进行封装,封装以后,只需要将要执行的命令传递给函数即可。当命令执行成功时,将返回命令的退出状态码和标准输出,当命令执行失败时,将返回退出状态码和错误输出。

def excute_cmd(cmd):
    p = subprocess.Popen(cmd,shell=True,stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    stdout,stderr = p.communicate()
    if p.returncode != 0:
        return p.returncode,stderr
    return p.returncode, stdout

第6章 使用Python监控Linux系统

6.1 Python编写的监控工具

这小节介绍两个Python语言编写的监控工具,分别是dstat和glances。

6.1.1 多功能系统资源统计工具 dstat

  • 安装
yum install -y dstat
  • 使用
    略过

6.1.2 交互式监控工具glances

glances是一款使用Python语言开发、基于psutil的跨平台的系统监控工具。在所有的Linux命令行工具中,它与top命令最为相似,都是命令行交互式监控工具。但是,glances实现了比top命令更加齐全的监控,提供了更加丰富的功能。

  • 安装
yum install -y glances
  • 使用
    略过

6.2 使用Python打造自己的监控工具

第10章 深入浅出Ansible

10.2 Ansible使用入门

10.2.4 Ansible的ad-hoc模式

在Ansible中,通过-m参数指定模块名称,通过 -a 参数指定模块的参数。如:

ansible test -m shell -a "hostname"

大部分情况下,Ansible的模块包含多个参数,参数使用 "key=value" 的形式表示,各个参数之间使用空格分割。如下所示:

#复制文件
ansible test -m copy -a "src=/tmp/data.txt dest=/tmp/data.txt"

#在远程服务器中安装软件
ansible test -m yum -a "name=httpd state=present" -become

10.2.5 使用playbook控制服务器

ad-hoc命令能够执行一些简单的参数,在生产环境中使用ansible更多是通过ansible-playbook来完成任务。我们会把要执行的内容写在一个YAML配置文件中。如下:

---

- hosts: test
  become: yes
  become_method: sudo
  tasks:
  - name: copy file
    copy: src=/tmp/data.txt dest=/tmp/data.txt

然后通过ansible-playbook命令来执行上面的yml文件,如:

ansible-playbook test_playbook.yml

10.3 Inventory 管理

10.3.1 hosts文件位置

从上面两节的示例中可以看出,ansible在执行操作的时候需要指定目标主机。在默认的情况下,ansible读取 /etc/ansible/hosts 文件中的服务器配置,获取需要操作的服务器列表。此外,我们还可以在其他位置定义hosts文件。

在ansible中,有三种方式指定hosts文件,按照优先级从低到高,分别是:

  • 默认位置 /etc/ansible/hosts
  • 通过ansible.cfg 文件中的inventory选项
  • 通过命令行参数的 -i 指定host文件

10.3.2 灵活定义hosts文件内容

下面给个例子,演示hosts文件灵活的定义方式。首先,hosts文件是ini格式的文件.

#直接添加主机IP
192.168.1.1

#定义web组
[webserver]
192.168.1.1
192.168.1.2

#定义db组
[dbserver]
192.168.2.1
192.168.2.2

#包含web和db两个组的common组
[common:children]
webserver
dbserver

通过上面定义的hosts信息,我们可以通过ansible命令对目标主机进行操作,如:

ansible webserver -m ping

我们除了使用指定hosts中定义的名称或者IP外,我们还可以使用 all 或者 * 来表示hosts文件中定义的所有主机。此外,我们可以通过 --list-hosts参数来查看匹配的主机列表,如:

ansible dbserver --list-hosts

10.3.3 灵活匹配hosts文件内容

除了上述两种方式匹配hosts文件的内容之外,还有下列以下方式:

规则含义
192.168.1.1 或者web.example.com匹配目标IP地址或者服务器名,如果有多个IP或者服务器,使用 ":" 分隔
webserver匹配目标组,如果有多个组,使用 ":" 分割
all 或者 *匹配所有服务器
webserver:!dbserver匹配在webserver中,不在dbserver组中的服务器
webserver:&dbserver匹配在webserver中,并且也在dbserver组中的服务器
.example.com 或者 192.168.使用通配符进行匹配
webserver[0],webserver[1:],webserver[-1]使用索引或者切片的方式匹配组中的服务器
~(webdb).*example.com

10.3.4 动态Inventory获取

除了使用hosts文件来管理主机信息,我们还可以通过调用云计算服务的API,编写自定义脚本的方式获取服务器列表。再或者,很多公司使用CMDB系统来管理服务器信息。那么,工程师可以通过读取CMDB数据库中的记录得到服务器列表。

脚本部分暂时略过。

10.3.5 Inventory行为参数

我们使用ansible来连接目标服务器时,默认使用root用户,22端口来进行SSH连接。需要自定义用户以及端口,可以使用 ansible_user 和 ansible_port 这两个参数指定。这种参数在Ansible中称为行为参数。下面列出一些重要的行为参数。

名称默认值描述
ansible_host主机的名称SSH目的主机名或IP
ansible_user当前用户SSH连接的用户名
ansible_port22SSH连接的端口号
ansible_ssh_private_key_filenoneSSH连接使用的私钥
ansible_connectionsmartAnsible使用的连接模式,取值为smart、ssh或者paramiko
ansible_becomenone类似于Linux下的sudo
ansible_become_usernone切换到哪个用户执行命令
ansible_shell_typesh执行命令所使用的shell
ansible_python_interpreter/usr/bin/python使用哪一个Python解析器
ansible_*_interpreternone指定其他语言的解析器

上面的这些行为参数,可以通过Ansible的配置文件(ansible.cnf)更改默认值。

10.3.6 定义服务器变量

hosts文件除了能定义主机信息以及行为参数外,还能定义普通的变量,下面是个示例:

#为单个主机定义变量
127.0.0.1 mysql_port=3306

#为主机组的各个主机分别定义变量
[test]
192.168.1.1 mysql_port=3306
192.168.1.2 mysql_port=3307

#定义主机组的变量
[test:vars]
mysql_user=root

要检查变量的值,我们可以通过echo的方式来显示,如:

ansible test -m shell -a 'echo {{ mysql_port }}'

当主机信息越来越多或者变量信息越来越多的时候,hosts文件会变得非常臃肿。ANsible提供了更好的方式来管理主机和主机组的变量信息。其定义的方法为,将组的变量存放在一个名为 group_vars目录下,目录下的文件名和组的名称相同,文件的拓展名可以为.yml或者.yaml或者不需要拓展名。服务器的变量存放在一个名为hosts_vars的目录下。

Ansible将依次在Playbook所在的目录、host文件所在的目录和 /etc/ansible 目录下寻找group_vars目录和host_vars目录。如在 /etc/ansible 目录下,存在一个test.yml的文件,里面的内容为:

mysql_user: root

这里需要注意的是,在hosts文件里面变量的定义使用的是 "key=value"的形式。但是在独立的变量文件中,我们使用的是 "key: value" 的格式来定义。原因是hosts文件是一个ini格式的文件,而保存变量的文件是一个YAML格式的文件。

10.4 YAML语法

YAML的语法规则如下:

  • YAML文件的第一行为 "---",表示这是一个YAML文件;
  • YAML中的字段大小写敏感;
  • YAML与Python一样,使用缩进表示层级关系;
  • YAML的缩进不允许使用Tab键,只允许使用空格,且空格的数目不重要,只要相同层级的元素左侧对齐即可;
  • "#"表示注释,从这个字符一直到行尾都会被解析器忽略

YAML支持三种格式的数据,分别是:

  • 对象: 键值对的集合,又称为映射,类似于Python中的字典;
  • 数组: 一组按次序排列的值,又称为序列(sequence),类似于Python中的列表
  • 纯量(scalars): 单个的、不可分的值,如字符串、布尔值与数字。

下面有一些实例

---

#一个美味的水果列表
- Apple
- Orange
- Mango

#一位职工的记录
name: pcm
job: ops
skill: python

#表示布尔值的方式
beta_env: yes
beta_env: True
beta_env: TRUE

#列表和对象的嵌套
name: pcm
job: ops
skill: python
foods:
  - Apple
  - Orange
  - Mango

我们可以使用PyYAML模块来解析YAML文件为Python的内部对象,使用方法略过。

在YAML中定义字符串时,甚至不需要使用单引号或双引号,直接将字符串写在文件中即可。如果字符串中包含特殊字符,则需要使用双引号包括起来。

10.5 Ansible模块

10.5.1 Ansible的工作原理

Ansible对远程服务器的从左实际上是通过模块来完成的,其工作原理如下:

  1. 将模块拷贝到远程服务器;
  2. 执行模块定义的操作,完成对服务器的修改
  3. 在远程服务器中删除模块

10.5.2 模块列表与帮助信息

ansible模块众多,要获取模块列表以及查看模块的帮助信息,可以使用下面的命令

#列出模块列表
ansible-doc -l

#或者file模块的帮助信息
ansible-doc file

10.5.3 常用的Ansible模块

  1. ping
    最简单的模块,测试远程服务器的连通性
  2. 远程命令模块
    command、raw、script和shell模块都能在远程服务器上执行Linux命令,其中commad是默认的模块。他们的区别是:command模块不能使用管道,raw可以使用管道,shell模块除了也能使用管道外还能执行shell脚本文件,script可以执行shell脚本,但是不用把事先把脚本复制远程服务器上。
  3. file
    file模块用于对文件(包括文件夹、链接)的操作。
  4. copy
    copy模块用于复制文件到远程服务器,如果src是一个目录,那么dest也需要是个目录.
  5. user/group
    user模块请求的是useradd,userdel,usermod三个指令,group模块请求的是groupadd、groupdel、groupmod三个指令。
  6. apt
    apt模块用来在ubuntu系统中安装软件、删除软件
  7. get_url
    用来下载文件到本地,类似类似于Linux中的curl命令。
  8. unarchive
    unarchive模块用于解压文件,作用类似于Linux下的tar命令。默认情况下,unarchive的作用是将控制节点的压缩包拷贝到远程服务器,然后进行解压。
  9. git
    git模块用于在远程服务器上执行git相关的操作。需要注意的是,该模块依赖于git软件,因此要先在远程服务器上安装好git。
  10. stat
    stat模块用于或者远程服务器上的文件信息,类似于Linux的stat命令。
  11. cron
    cron是管理Linux下计划任务的模块。
  12. service
    service模块类似于Linux下的service命令,用来启动、停止、重启服务。
  13. sysctl
    该模块的作用于Linux下的sysctl命令类似,用于控制Linux的内核参数。
  14. mount
    在远程服务器上挂载磁盘,当进行挂盘操作时,如果挂载点指定的路径不存在,将创建该路径。
  15. synchronize
    synchronize模块是对rsync命令的封装,以便对常见的rsync任务进行处理。

10.5.4 模块的返回值

返回值的名称返回值的含义
changed几乎所有的ansible模块都会返回该变量,表示模块是否对远程主机执行了修改操作
failed如果模块未能执行完成,将返回failed为true
msg模块执行失败的原因,常见的错误如ssh连接失败,没有权限等
rc与命令行工具相关的模块会返回rc,表示执行的Linux命令的返回码
stdout与rc类似,返回的是标准输出的结果
stderr与rc类似,返回的是错误输出的结果
backup_file所有存在backup选项的模块,用来返回备份文件的路径
results应用在playbook中存在循环的情况,返回多个结果

10.6 Playbook

10.6.1 Playbook的定义

在ansible中,一个play必须包含以下两项:

  • hosts: 需要对哪些远程的服务器执行操作
  • tasks: 需要在这些服务器上执行的任务列表

在前面介绍YAML语法的时候我们说过,YAML的字符串不需要使用单引号或者双引号,直接编写即可。因此,在安装Apache的playbook中这样的写法 'name=apache2 update_cache=yes state=present' 是一个完整的字符串,而不是一个字典。
在字符串较长的时候,我们可以使用 ">" 符号进行折叠换行。在参数较多的时候,可以增强可读性,如下:

- name: install apache
  apt: >
    name= apache2
    update_cache=yes
    state=present

在Ansible中,除了使用这种方式外,也可以使用缩进子块的形式:

- name: install apache
  apt: 
    name= apache2
    update_cache=yes
    state=present

虽然从字面上看,这两种指定参数的方式相差不大。但是,从YAML语法的角度来看,这是两种完全不同的方法。前者是一个较长的字符串,后者是一个字典。

在上面的示例中,我们有定义name,实际上name只充当了注释的作用。让我们在执行playbook的时候,知道执行到哪一步了。并且也增加了可维护性。

当任务比较多的时候,我们可以把一个大的任务拆分为几个小任务,把大的playbook拆分为若干个小的playbook。然后再编写一个playbook来调用这些小的playbook。如下:

---
- include: db.yml
- include: web.yml

10.6.2 使用ansib-playbook执行Playbook

在编写好playbook之后,我们可以使用ansible-playbook命令来执行,如:

ansible-playbook test.yml

ansible-playbook有若干的参数可以留意下,这些命令在ansible命令下也会有的:

  • -T timeout: 建立SSH连接的超时时间
  • --key-file --private-key: 建立ssh连接的私钥文件
  • -i: 指定inventory文件,,默认是/etc/host/hosts
  • -f --forks: 并发执行的数量,默认是5
  • --list-hosts: 匹配到服务器的列表

下面这些参数数ansible-play特有的:

  • --list-tasks: 列出任务列表
  • --step: 每执行 一个任务后,等待用户确认
  • --syntax-check: 检查playbook的语法
  • -C --check: 检查当前的playbook是否会修改远程服务器,相当于预测Playbook的执行结果。

10.6.3 Playbook的详细语法

  1. 权限
    在Ansible中,默认使用当前用户去连接远程主机。我们也可以修改ansible.cfg文件中修改配置连接远程服务器的默认用户。如果需要指定特定的用户来执行操作,可以执行play的用户:
---
- hosts: webserver
  remote_user: root

甚至我们可以具体到某个一个task

---
- hosts: webserver
  remote_user: root
  tasks:
    - name: test connection
      ping:
      remote_user: pangcm

很多时候,我们需要的不是以某个特定的用户去连接远程服务器,而是需要更高级别的权限时,需要管理员身份去执行。在Ansible中,可以用become和become_method选项实现:

---
- hosts: webserver
  remote_user: pangcm
  #可以在hosts的位置使用become
  become: yes
  tasks:
  - name: test task
    service: name=nginx state=started
    #也可以在某个任务里面使用
    become: yes
    become_method: sudo
  1. 通知
    我们知道,在Ansible中,模块是幂等的。例如,我多次创建一个用户,只有第一次会返回changed,后面会直接返回成功。现在我们有个需求,在修改nginx的配置文件后,我们需要重启nginx的服务。这时候我们需要判断配置文件是否修改了,在ansible中我们通过notify与handler机制来实现这里的功能。如下所示:
---
- hosts: webserver
  tasks:
  - name: write nginx config file
    template: src=nginx.conf.j2 dest=/etc/nginx.conf
    notify:
    - restart nginx

  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

需要注意的事,handler只会在所有的task执行完成后执行。并且一个handler只会执行一次,及时被触发多次。例如我们因为修改nginx配置文件多次触发了重启nginx 的操作,但是只有在任务的最后我们才会去重启nginx。

  1. 变量
    变量可以说是ansible中的重头戏,前面我们介绍inventory的时候介绍过如何定义变量。在Ansible中,还有其他的几种定义变量的方式。最简单的,就是直接定义在playbook中。如下:
---
- hosts: dbserver
  vars:
    mysql_port: 80

在playbook定义的变量,也可以在模版文件中使用(其他地方定义的变量也一样可以的)。如

[mysqld]
user=mysql
port={{ mysql_port }}

当变量较多的时候,我们可以把变量定义在一个文件中,然后使用vars_files选项引用。变量文件为一个yml文件,格式为 "key:value" 的形式。

---
- hosts: all
  vars_files:
    - /vars/external_vars.yml

在shell脚本中,我们可以通过获取上一条命令的返回码判断命令是否执行成功。在Ansible中,我们也可以获取任务的执行结果,将任务的执行结果保存在一个变量中,并且在后面引用这个变量。这里我们使用register选项,获取到的变量也叫做注册变量。如下所示:

- hosts: webserver
  tasks:
    - shell: /usr/bin/foo
      register: foo_result
      ignore+errors: True

    - shell: /usr/bin/bar
      when: foo_result.rc == 5

这里有两个新的知识点,一个是ignore_errors和when。前者表示忽略当前task中的错误,后者是一个条件语句,只有条件为真的时候才去执行task。

  1. Facts
    Facts变量是Ansible执行远程部署之前从远程服务器中获取的系统信息,包括服务器的IP、名称、操作系统、分区信息、硬件信息等等。Facts变量可以配合Playbook实现更加个性化的功能需求。我们可以通过setup模块来查看Facts变量的列表。

那么怎么使用Facts变量呢,答案是直接使用。如果需要使用Facts变量中子项,需要使用嵌套结构,有两种方式,如:

ansible_eth0["ipv4"]["address"]
ansible_eth0.ipv4.address

收集Facts信息,会降低Ansible的部署效率。想要竟然收集facts变量信息,可以把 gather_facts 设置为no。如下:

---
- hosts: webserver
  gather_facts: no
  tasks:
  1. 循环
    可以使用列表 with_items来实现循环
---
- name: install mysql package
  yum: name={{ item }} state=installed
  with_items:
    - mysql-server
    - MySQL-python
    - libselinux-python
  1. 条件
    在Ansible中使用when选项来执行条件语句,类似于编程语言中的if。when能支持多个条件语句,也可以使用and或者or来进行定义,如下:
---
tasks:
  - name: "shut down CentOS 6 system"
    command: /sbin/shutdown -t now
    when:
      - ansible_distribution == 'CentOS'
      - ansible_distribution_major_version == '6'

  - name: "shut down CentOS 6 and CentOS7"
    command: /sbin/shutdown -t now
    when: (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6' ) or 
          (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '7' )

此外,when选项还可以使用Jinja2的过滤器,例如:

tasks:
  - command: /bin/false
    register: result
    ignore_errors: True

  - command: /bin/something
    when: result|failed

  - command: /bin/something_else
    when: result|succeeded

在when选项可以读取变量的取值,例如:

---
vars:
  epic: true
tasks:
  - command: echo "This certainly is epic"
    when: epic

when选项还可以和循环一起使用,以实现过滤功能:

tasks:
  - command: echo {{ item }}
    with_items: [ 1,2,3,4,5 ]
    when: item > 2
  1. 任务执行策略
    在Ansible中,Playbook的执行是以task为单位进行的。Ansible默认使用5个进程对远程服务器执行任务。在默认情况的任务执行策略中,Ansible首先执行task1,并且等到所有服务器执行完之后再开始执行task2,以此类推。我们可以通过free选项来允许执行较快的服务器提前完成Play的部署,不用等待其他远程服务器一起执行task。如下:
- hosts: all
  strategy: free
  tasks:
  ...

10.6.6 Playbook中的高级语法

  1. 线性更新服务器
    Ansible默认使用5个并发来对远程服务器进行操作,我们知道如果在执行更新操作的时候,这样或许不是一个好的选择。一个更好的选择是一台一台服务器地去更新,这样能降低对线上服务的影响。为了实现线性更新,可以使用serial选项。该选项可以取值为一个数字,表示一次更新多少台服务器;也可以取值为一个百分比,表示一次更新多少比例的服务器,还可以去职位一个数字或百分比的列表,实现渐进式更新,例如:
#一台一台地更新
- name: test paly
  hosts: webserver
  serial: 1

#按百分比更新
- name: test paly
  hosts: webserver
  serial: 30%

#渐进式更新
- name: test paly
  hosts: webserver
  serial: 
    - 1
    - 5
    - 10
  1. 使用delegate_to实现任务委派功能
    在大部分场景下我们在hosts选项上定义好一组服务器去执行操作,但是有些场景下我们希望对某台服务器进行特殊处理。这时候,需要对Ansible的任务委派功能做了解了。使用delegate_to选项的方式如下:
- name: take out of load balancer pool
  command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
  delegate_to: 127.0.0.1
  1. 使用local_action 在控制服务器执行操作
    如果我们需要在本地执行操作,除了使用delegate_to功能委派本机执行外,还可以使用local_action选项来实现。如下:
- name: take out of load balancer pool
  local_action: command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
  1. 使用run_once保证任务仅执行一次
    有这样的一个需求:有多台应用服务器运行在负载均衡器之后,现在需要进行数据迁移,应该如何实现?这个任务的特殊之处在于,只需要在一台应用服务器上执行这个迁移操作,在任意一台都可以,并且只要执行一次。
- commnad: /opt/application/migrate_db.py
  run_once: true

默认情况下,Ansible会选择第一台服务器执行run_once的操作。这里我们可以配合delegate_to的选项来指定某个服务器执行。

  1. 高级循环结构
    前面我们介绍了with_items的循环结构,除此之外,Ansible还有下面的循环选项:
  • with_lines with_fileglob with_first_found with_dict with_flattend with_indexd_items
  • with_nested with_random_choice with_sequence with_together with_subelements with_file

下面介绍常用的几种循环:

  • with_items 是最简单的也是最常用的循环方式。在with_items中,每一项都可以是一个字典。使用时通过item.key的方式引用字典即可,如下:
- name: add serveral users
  user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
  with_items:
    - { name: 'testuser1', groups:'wheel'}
    - { name: 'testuser2', groups:'root'}

如何循环的元素是一个嵌套字典,需要使用with_dict选项遍历元素。如下

---
users:
  alice:
    name: Alice Appleworth
    telephone: 123-456-789
  bob:
    name: Bob bananarama
    telephone: 123-456-789
  tasks:
    - name: Print phone records
      debug:
        msg: "User {{ item.key }} is {{ item.value.name }} {{ item.value.telephone }}"
      with_dict: "{{ users }}"

如果循环的每一项不是字典,而是一个列表。那么我们可以使用with_nested选项进行遍历,然后通过下标的方式访问列表中的元素。如下所示:

- name: give users access to multiple databases
  mysql_user:
    name: "{{ item[0] }}"
    priv: "{{ item[1] }}.*:ALL"
    append_privs: yes
    password: "foo"
  with_nested:
    - ['alice', 'bob']
    - ['clientdb','employeedb','providerdb']
#变量组合的结果是: alice:clientdb alice:employeedb alice:providerdb;bob:clientdb ....

我们也可以使用with_sequence选项产生数字列表,其作用类似于python中range函数。在使用with_sequence时,我们可以指定起点、终点和步长。如下所示:

- user:
    name: "{{ item }}"
    state: present
    groups: "evens"
  with_sequence: start=0 end=32 format=testuser%02x

Asnbile中支持with_random_choice选项。改选项的含义是随机选择某一项。使用方法如下:

---
- debug:
    msg: "{{ item }}"
  with_random_choice:
    - "do thing 1"
    - "do thing 2"
    - "do thing 3"
  1. 使用标签灵活控制Play的执行
    在任何比较复杂的时候,我们可以用过tags选项给某个子任务打上标签。在后面执行的时候,可以选定执行某个tags的任务,或者跳过某个子任务。如下:
- name: install package
  yum: name={{ item }} state=installed
  with_items:
    - httpd
    - memcached
  tags: packages1

- name: install package
  yum: name={{ item }} state=installed
  with_items:
    - httpd
    - memcached
  tags: packages2

- name: install package
  yum: name={{ item }} state=installed
  with_items:
    - httpd
    - memcached
  tags: packages3

然后我们在使用ansible-playbook命令的时候,可以使用 --skip-tags和--tags选项来跳过或者执行某个任务。如下:

ansible-playbook example.yml --tags "package1,package2"
ansible-playbook example.yml --skip-tags "package1"
  1. 使用changed_when控制对changed字段的定义

当我们使用shell模块来执行任务的时候,只要shell模块执行了,ansible都会返回changed字段返回给我们。这时候,我们应该手动去修正changed字段的返回值。如我们执行一个自定义的程序billybasss,只有当退出码是2的时候,才会对远程服务器产生修改。因此,可以这样修改:

tasks:
  - shell: /usr/bin/billybass --mode='take me to the river'
    register: bass_result
    changed_when: "bass_result.rc !=2"
  1. 使用failed_when 控制对failed字段的定义

与changed类似,我们也可以通过自定义的方式判断命令是否执行成功。如果使用Ansible执行shell命令,默认情况下通过命令 的返回码是否为0判断命令是否执行成功。对于一些特殊命令,无法通过返回码知道命令执行成功,那么,我们也可以failed_when选项来自定义命令执行失败的标准。如下:

- name: this command prints Failed when it fails
  command: /usr/bin/example-command -x -y -z
  register:: command_result
  failed_when: "'Failed' in command_result.stderr"

10.7 role的定义与使用

10.7.1 role的概念

role是一种规范的文件组织方式,role的文件组织方式如下,一般一个role会包含以下内容:

  • default/main.yml: 可以被覆盖的默认变量
  • files: 目录,保存需要上传到远程服务器的文件
  • handlers/main.yml: 与Playbook中handlers选项类似,包含所有的handler
  • meta/main.yaml: role的依赖信息
  • README.md: role的说明文件
  • tasks/main.yml: 与Playbook中的tasks选项类似,包含了任务列表
  • templates: 目录,保存了Jinja2模版文件
  • vars/main.yml: 不应该被覆盖的变量,与Playbook中的vars或者vars_file类似,包含所有定义的变量。

这里要注意的是,引用template目录中的模版与files目录中的文件不需要写路径,默认是相对引用。

10.7.2 使用ansible-galaxy 命令管理role

#初始化一个roles的目录结构
ansible-galaxy init /etc/ansible/roles/webserver
#安装别人写好的roles
ansible-galaxy install -p /etc/ansible/roles bennojoy.mysql
#列出已经安装的roles
ansible-galaxy list
#查看安装的roles信息:
ansible-galaxy info benoojoy.mysql
#卸载roles
ansible-galaxy remove benoojoy.mysql

Ansible Galaxy 是Ansible提供一个在线的Playbook分析平台,地址是 https://galaxy.ansible.com/

10.7.3 如何使用role

虽然我们已经有了role,比如咱们编写了一个部署mysql的role。但是怎么使用role呢?但是是编写一个调用role的playbook,如下:

---
- hosts: dbserver
  become: yes
  become_method: sudo
  roles:
    - role: mysql

然后我们使用ansible-playbook命令执行playbook,即可完成mysql的部署:

ansible-playbook -i host deploy_mysql.yml

10.8 Ansible的配置文件

前面我们不断有提到ansible.cfg文件,下面我们具体来介绍一波。

10.8.1 配置文件的查找路径

Ansible可以有多个配置文件,查找的路径按优先级从高到低排序为:

  1. ANSIBLE_CONFIG 环境变量指定的配置文件
  2. 当前目录下的ansible.cfg文件
  3. 当前用户home目录下的.ansible.cfg文件
  4. Ansible默认的/etc/ansible/ansible.cfg文件

10.8.2 Ansible中常用的配置

  1. 默认配置
  • inventory: 指定Inventory文件的路径
  • remote_user: SSH连接使用的用户名
  • remote_port: SSH连接时使用的端口号
  • private_key_file: SSH连接使用的私钥文件
  • roles_path: 查找role的路径,可以指定多个路径,多个路径之间用冒号分割
  • log_path: Ansible的日志文件路径
  • host_key_checking: 类似于ssh命令中的StrictHostChecking选项,当该选项配置为False时,不检查远程服务器是否存在于know_hosts文件中
  • forks: 并行进程的数量
  • gathering: 控制收集Facts变量的策略
  1. SSH连接配置
  • ssh_args: 可以通过这个参数控制Ansible的ssh连接
  • pipelining: 多个task之间共享SSH连接,开启pipelining能有效提升Ansible的执行速度。
  • control_path: 保存 ControlPath socket的路径
  1. 权限提升配置
  • become: 是否进行权限提升
  • become_method: 权限提升的方式,默认是sudo
  • become_user: 提升为哪个用户的权限,默认是root
  • become_ask_pass: 默认为False,表示权限提升时不需要密码

10.9 Ansible的最佳实践

略过


标题:Python Linux 系统管理与自动化运维-读书笔记
作者:pangchaoming
地址:http://pangcm.club/articles/2021/10/11/1633936515405.html