LOADING STUFF...

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

Python2年前 (2022)发布 safedragon
75 0

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

这个章节会收集一些被市场公认好用的模块,对于编码代码很有帮助,同样还是需要时间慢
慢沉淀。

7.1 远程登陆服务器的最佳利器

在使用 Python 写一些脚本的时候,在某些情况下,我们需要频繁登陆远程服务去执行一次命令,并返回一些结果。
在 shell 环境中,我们是这样子做的。

$ sshpass -p ${passwd} ssh -p ${port} -l ${user} -o StrictHostKeyChecking=no xx.xx.xx.xx "ls -
l"

然后你会发现,你的输出有很多你并不需要,但是又不去不掉的一些信息(也许有方法,请留言交流),类似这样

host: xx.xx.xx.xx, port: xx
Warning: Permanently added '[xx.xx.xx.xx]:xx' (RSA) to the list of known hosts.
Login failure: [Errno 1] This server is not registered to rmp platform, please confirm whether
cdn server.
total 4
-rw-r--r-- 1 root root 239 Mar 30 2018 admin-openrc

对于直接使用 shell 命令,来执行命令的,可以直接使用管道,或者将标准输出重定向到文件的方法取得执行命令返回的结果

1. 使用 subprocess

若是使用 Python 来做这件事,通常我们会第一时间,想到使用 os.popen,os.system,commands,subprocess 等一些命令执行库来间接获取 。
但是据我所知,这些库获取的 output 不仅只有标准输出,还包含标准错误(也就是上面那些多余的信息)
所以每次都要对 output 进行的数据清洗,然后整理格式化,才能得到我们想要的数据。
用 subprocess 举个例子,就像这样子

import subprocess
ssh_cmd = "sshpass -p ${passwd} ssh -p 22 -l root -o StrictHostKeyChecking=no xx.xx.xx.xx 'ls
-l'"
status, output = subprocess.getstatusoutput(ssh_cmd)
# 数据清理,格式化的就不展示了
<code...>

通过以上的文字 + 代码的展示 ,可以感觉到 ssh 登陆的几大痛点

痛点一:需要额外安装 sshpass(如果不免密的话)
痛点二:干扰信息太多,数据清理、格式化相当麻烦
痛点三:代码实现不够优雅(有点土),可读性太差
痛点四:ssh 连接不能复用,一次连接仅能执行一次
痛点五:代码无法全平台,仅能在 Linux 和 OSX 上使用

为了解决这几个问题,我搜索了全网关于 Python ssh 的文章,没有看到有完整介绍这方面的技巧的。

为此,我就翻阅了一个很火的 Github 项目: awesome-python-cn
(https://github.com/BingmingWong/awesome-python-cn)。

期望在这里,找到有一些关于 远程连接 的一些好用的库。
还真的被我找到了两个
sh.ssh
Paramiko

2. 使用 sh.ssh

首先来介绍第一个, sh.ssh
sh 是一个可以让你通过函数的调用来完成 Linxu/OSX 系统命令的一个库,非常好用,关于它有机会也写篇介绍。

$ python3 -m pip install sh

今天只介绍它其中的一个函数: ssh
通常两台机器互访,为了方便,可设置免密登陆,这样就不需要输入密码。
这段代码可以实现免密登陆,并执行我们的命令 ls -l

from sh import ssh
output=ssh("root@xx.xx.xx.xx", "-p 22", "ls -l")
print(output)

但有可能 ,我们并不想设置互信免密,为了使这段代码更通用,我假定我们没有设置免密,只能使用密码进行登陆。
问题就来了,要输入密码,必须得使用交互式的方法来输入呀,在 Python 中要如何实现呢?

原来 ssh 方法接收一个 _out 参数,这个参数可以为一个字符串,表示文件路径,也可以是一个文件对象(或者类文件对象),还可以是一个回调函数,意思是当有标准输出时,就会调用将输出内容传给这个函数。
这就好办了呀。
我只要识别到有 password: 字样,就往标准输入写入我的密码就好了呀。
完整代码如下:

import sys
from sh import ssh
aggregated = ""
def ssh_interact(char, stdin):
 global aggregated
 sys.stdout.write(char.encode())
 sys.stdout.flush()
 aggregated += char
 if aggregated.endswith("password: "):
 stdin.put("you_password\n")
output=ssh("root@xx.xx.xx.xx", "-p 22", "ls -l",_tty_in=True, _out_bufsize=0,
_out=ssh_interact)
print(output)

这是官方文档(http:/amoffat.github.io/sh/tutorials/interacting_with_processes.html?
highlight=ssh)给的一些信息,写的一个demo。
尝试运行后,发现程序会一直在运行中,永远不会返回,不会退出,回调函数也永远不会进入。

通过调试查看源代码,仍然查不到问题所在,于是去 Github 上搜了下,原来在 2017 年就已经存在这个问题了,到现在 2020 年了还没有修复,看来使用 sh.ssh 的人并不多,于是我又“追问”了下,期望能得到回复。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

以上这个问题,只有在需要输入密码才会出现,如果设置了机器互信是没有问题的。
为了感受 sh.ssh 的使用效果,我设置了机器互信免密,然后使用如下这段代码。

from sh import ssh
my_server=ssh.bake("root@xx.xx.xx.xx", "-p 22")
# 相当于执行登陆一次执行一次命令,执行完就退出登陆
print(my_server.ls())
# 可在 sleep 期间,手动登陆服务器,使用 top ,查看当前有多少终端在连接
time.sleep(5)
# 再次执行这条命令时,登陆终端数将 +1,执行完后,又将 -1
print(my_server.ifconfig())

惊奇地发现使用 bake 这种方式, my_server.ls()my_server.ifconfig() 这种看似是通过同一个ssh连接,执行两次命令,可实际上,你可以在远程机器上,执行 top 命令看到已连接的终端的变化,会先 +1-1 ,说明两次命令的执行是通过两次连接实现的。
如此看来,使用 sh.ssh 可以解决痛点一(如果上述问题能得到解决)、痛点二、痛点三。
但是它仍然无法复用 ssh 连接,还是不太方便,不是我理想中的最佳方案。
最重要的一点是, sh 这个模块,仅支持 Linxu/OSX ,在 Windows 你得使用它的兄弟库 -pbs ,然后我又去 pypi 看了一眼 pbs,已经 “年久失修”,没人维护了。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

至此,我离 “卒”,就差最后一根稻草了。

3. 使用 paramiko

带着最后一丝希望,我尝试使用了 paramiko 这个库,终于在 paramiko 这里,找回了本应属于 Python 的那种优雅。
你可以通过如下命令去安装它

$ python3 -m pip install paramiko

然后接下来,就介绍几种常用的 ssh 登陆的方法
方法1:基于用户名和密码的 sshclient 方式登录
然后你可以参考如下这段代码,在 Linux/OSX 系统下进行远程连接

import paramiko
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 建立连接
ssh.connect("xx.xx.xx.xx", username="root", port=22, password="you_password")
# 使用这个连接执行命令
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("ls -l")
# 获取输出
print(ssh_stdout.read())
# 关闭连接
ssh.close()

方法2:基于用户名和密码的 transport 方式登录

方法1 是传统的连接服务器、执行命令、关闭的一个操作,多个操作需要连接多次,无法复用连接[痛点四]。
有时候需要登录上服务器执行多个操作,比如执行命令、上传/下载文件,方法1 则无法实现,那就可以使用 transport 的方法。

import paramiko
# 建立连接
trans = paramiko.Transport(("xx.xx.xx.xx", 22))
trans.connect(username="root", password="you_passwd")
# 将sshclient的对象的transport指定为以上的trans
ssh = paramiko.SSHClient()
ssh._transport = trans
# 剩下的就和上面一样了
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("ls -l")
print(ssh_stdout.read())
# 关闭连接
trans.close()

方法3:基于公钥密钥的 SSHClient 方式登录

import paramiko
# 指定本地的RSA私钥文件
# 如果建立密钥对时设置的有密码,password为设定的密码,如无不用指定password参数
pkey = paramiko.RSAKey.from_private_key_file('/home/you_username/.ssh/id_rsa',
password='12345')
# 建立连接
ssh = paramiko.SSHClient()
ssh.connect(hostname='xx.xx.xx.xx',
 port=22,
 username='you_username',
 pkey=pkey)
# 执行命令
stdin, stdout, stderr = ssh.exec_command('ls -l')
# 结果放到stdout中,如果有错误将放到stderr中
print(stdout.read())
# 关闭连接
ssh.close()

方法4:基于密钥的 Transport 方式登录

import paramiko
# 指定本地的RSA私钥文件
# 如果建立密钥对时设置的有密码,password为设定的密码,如无不用指定password参数
pkey = paramiko.RSAKey.from_private_key_file('/home/you_username/.ssh/id_rsa',
password='12345')
# 建立连接
trans = paramiko.Transport(('xx.xx.xx.xx', 22))
trans.connect(username='you_username', pkey=pkey)
# 将sshclient的对象的transport指定为以上的trans
ssh = paramiko.SSHClient()
ssh._transport = trans
# 执行命令,和传统方法一样
stdin, stdout, stderr = ssh.exec_command('df -hl')
print(stdout.read().decode())
# 关闭连接
trans.close()

以上四种方法,可以帮助你实现远程登陆服务器执行命令,如果需要复用连接:一次连接执行多次命令,可以使用 方法二方法四
用完后,记得关闭连接。
实现 sftp 文件传输
同时,paramiko 做为 ssh 的完美解决方案,它非常专业,利用它还可以实现 sftp 文件传输。

import paramiko
# 实例化一个trans对象# 实例化一个transport对象
trans = paramiko.Transport(('xx.xx.xx.xx', 22))
# 建立连接
trans.connect(username='you_username', password='you_passwd')
# 实例化一个 sftp对象,指定连接的通道
sftp = paramiko.SFTPClient.from_transport(trans)
# 发送文件
sftp.put(localpath='/tmp/11.txt', remotepath='/tmp/22.txt')
# 下载文件
sftp.get(remotepath='/tmp/22.txt', localpath='/tmp/33.txt')
trans.close()

到这里,Paramiko 已经完胜了,但是仍然有一个痛点我们没有提及,就是多平台,说的就是Windows,这里就有一件好事,一件坏事了,。
好事就是:paramiko 支持 windows
坏事就是:你需要做很多复杂的准备,你可 google 解决,但是我建议你直接放弃,坑太深了。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

注意事项
使用 paramiko 的时候,有一点需要注意一下,这个也是我自己 “踩坑” 后才发现的,其实我觉得这个设计挺好的,如果你不需要等待它返回数据,可以直接实现异步效果,只不过对于不知道这个设计的人,确实是个容易掉坑的点
就是在执行 ssh.exec_command(cmd) 时,这个命令并不是同步阻塞的。
比如下面这段代码,执行时,你会发现 脚本立马就结束退出了,并不会等待 5 s 后,再 执行ssh.close()

import paramiko
trans = paramiko.Transport(("172.20.42.1", 57891))
trans.connect(username="root", password="youpassword")
ssh = paramiko.SSHClient()
ssh._transport = trans
stdin, stdout, stderr = ssh.exec_command("sleep 5;echo ok")
ssh.close()

但是如果改成这样,加上一行 stdout.read(), paramiko 就知道,你需要这个执行的结果,就会在 read() 进行阻塞。

import paramiko
trans = paramiko.Transport(("172.20.42.1", 57891))
trans.connect(username="root", password="youpassword")
ssh = paramiko.SSHClient()
ssh._transport = trans
stdin, stdout, stderr = ssh.exec_command("sleep 5;echo ok")
# 加上一行 read()
print(stdout.read())
ssh.close()

4. 写在最后
经过了一番对比,和一些实例的展示,可以看出 Paramiko 是一个专业、让人省心的 ssh 利器,个人认为 Paramiko 模块是运维人员必学模块之一,如果你恰好需要在 Python 代码中实现 ssh 到远程服务器去获取一些信息,那么我把 Paramiko 推荐给你。

7.2 代码 BUG 变得酷炫的利器

当我们写的一个脚本或程序发生各种不可预知的异常时,如果我们没有进行捕获处理的时候,通常都会致使程序崩溃退出,并且会在终端打印出一堆 密密麻麻 的 traceback 堆栈信息来告诉我们,是哪个地方出了问题。
就像这样子,天呐,密集恐惧症要犯了都

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

上面这段 traceback

  • 只有黑白两个颜色,无法像代码高亮那样,对肉眼实现太不友好了
  • 无法直接显示报错的代码,排查问题慢人一步,效率太低

那有没有一种办法,可以解决这些问题呢?
当然有了,在 Python 中,没有什么问题是一个库解决不了的,如果有,那就等你去开发这个库。
今天要介绍的这个库呢,叫做 pretty-errors ,从名字上就可以知道它的用途,是用来美化错误信息的。
通过这条命令你可以安装它

$ python3 -m pip install pretty-errors

1. 环境要求
由于使用了 pretty-errors 后,你的 traceback 信息输出,会有代码高亮那样的效果,因此当
你在使用测试使用 pretty-error 时,请确保你使用的终端可以输出带有颜色的字体。

在 windows 上你可以使用 Powershell,cmder 等
在 Mac 上你可以使用自带的终端,或者安装一个更好用的 iTerm2

2. 效果对比

随便写一个没有使用 pretty-errors ,并且报错了的程序,是这样子的。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

而使用了 pretty_errors 后,报错信息被美化成这样了。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

是不是感觉清楚了不少,那种密密麻麻带来的焦虑感是不是都消失了呢?
当然这段代码少,你可能还没感受到,那就来看下 该项目在 Github上的一张效果对比图吧

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

3. 配置全局可用
可以看到使用了 pretty_errors 后,无非就是把过滤掉了一些干扰我们视线的无用信息,然后把有用的关键信息给我们高亮显示。
既然既然这样,那 pretty_errors 应该也能支持我们如何自定义我们选用什么样的颜色,怎么排版吧?
答案是显而易见的。
pretty_errors 和其他库不太一样,在一定程度上(如果你使用全局配置的话),它并不是开箱即用的,你在使用它之前可能需要做一下配置。
使用这一条命令,会让你进行配置,可以让你在该环境中运行其他脚本时的 traceback 输出都自动美化。

$ python3 -m pretty_errors
Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

配置完成后,你再运行任何脚本,traceback 都会自动美化了。
不仅是在我的 iTerm 终端下

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

在 PyCharm 中也会

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

唯一的缺点就是,原先在 PyCharm 中的 traceback 可以直接点击 文件路径 直接跳转到对应错
误文件代码行,而你如果是在 VSCode 可以使用 下面自定义配置的方案解决这个问题(下面会讲到,参数是: display_link )。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

因此,有些情况下,你并不想设置 pretty_errors 全局可用。
那怎么取消之前的配置呢?
只需要再次输出 python -m pretty_errors ,输出入 C 即可清除。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

4. 单文件中使用
取消全局可用后,你可以根据自己需要,在你需要使用 pretty-errors 的脚本文件中导入 pretty_errors ,即可使用

import pretty_errors

就像这样

import pretty_errors
def foo():
 1/0
if __name__ == "__main__":
 foo()

值得一提的是,使用这种方式,若是你的脚本中,出现语法错误,则输出的异常信息还是按照之前的方式展示,并不会被美化。

因此,为了让美化更彻底,官方推荐你使用 python -m pretty_errors

5. 自定义设置

上面的例子里,我们使用的都是 pretty_errors 的默认美化格式,展示的信息并没有那么全。
比如

  • 它并没有展示报错文件的绝对路径,这将使我们很难定位到是哪个文件里的代码出现错误。
  • 如果能把具体报错的代码,给我们展示在终端屏幕上,就不需要我们再到源码文件中排查原因了。

如果使用了 pretty_errors 导致异常信息有丢失,那还不如不使用 pretty_errors 呢。
不过,可以告诉你的是, pretty_errors 并没有你想象的那么简单。
它足够开放,支持自定义配置,可以由你选择你需要展示哪些信息,怎么展示?
这里举一个例子

import pretty_errors
# 【重点】进行配置
pretty_errors.configure(
 separator_character = '*',
 filename_display = pretty_errors.FILENAME_EXTENDED,
 line_number_first = True,
 display_link = True,
 lines_before = 5,
 lines_after = 2,
 line_color = pretty_errors.RED + '> ' + pretty_errors.default_config.line_color,
 code_color = ' ' + pretty_errors.default_config.line_color,
)
# 原来的代码
def foo():
 1/0
if __name__ == "__main__":
 foo()

在你像上面这样使用 pretty_errrs.configure 进行配置时,抛出的的异常信息就变成这样了。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

当然了, pretty_errors.configure() 还可以接收很多的参数,你可以根据你自己的需要进行配置。

5.1 设置颜色

  • header_color :设置标题行的颜色。
  • timestamp_color :设置时间戳颜色
  • default_color :设置默认的颜色
  • filename_color :设置文件名颜色
  • line_number_color :设置行号颜色。
  • function_color :设置函数颜色。
  • link_color :设置链接的颜色。

在设置颜色的时候, pretty_errors 提供了一些常用的 颜色常量供你直接调取。

  • BLACK :黑色
  • GREY :灰色
  • RED :红色
  • GREEN :绿色
  • YELLOW :黄色
  • BLUE :蓝色
  • MAGENTA :品红色
  • CYAN :蓝绿色
  • WHITE :白色

而每一种颜色,都相应的匹配的 BRIGHT_ 变体 和 _BACKGROUND 变体,
其中, _BACKGROUND 用于设置背景色,举个例子如下。

Python黑魔法手册 2.0 文档第七章:神奇魔法模块【1-2】

5.2 设置显示内容

  • line_number_first 启用后,将首先显示行号,而不是文件名。
  • lines_before : 显示发生异常处的前几行代码
  • lines_after : 显示发生异常处的后几行代码
  • display_link :启用后,将在错误位置下方写入链接,VScode将允许您单击该链接。
  • separator_character :用于创建标题行的字符。默认情况下使用连字符。如果设置为 或者 None ,标题将被禁用。
  • display_timestamp :启用时,时间戳将写入回溯头中。
  • display_locals 启用后,将显示在顶部堆栈框架代码中的局部变量及其值。
  • display_trace_locals 启用后,其他堆栈框架代码中出现的局部变量将与它们的值一起显示。

5.3 设置怎么显示

  • line_length :设置每行的长度,默认为0,表示每行的输出将与控制台尺寸相匹配,如果你设置的长度将好与控制台宽度匹配,则可能需要禁用 full_line_newline ,以防止出现明显的双换行符。
  • full_line_newline :当输出的字符满行时,是否要插入换行符。
  • timestamp_function 调用该函数以生成时间戳。默认值为 time.perf_counter
  • top_first 启用后,堆栈跟踪将反转,首先显示堆栈顶部。
  • display_arrow 启用后,将针对语法错误显示一个箭头,指向有问题的令牌。
  • truncate_code 启用后,每行代码将被截断以适合行长。
  • stack_depth 要显示的堆栈跟踪的最大条目数。什么时候 0 将显示整个堆栈,这是默认值。
  • exception_above 启用后,异常将显示在堆栈跟踪上方。
  • exception_below : 启用后,异常显示在堆栈跟踪下方。
  • reset_stdout 启用后,重置转义序列将写入stdout和stderr;如果您的控制台留下错误的颜色,请启用此选项。
  • filename_display设置文件名的展示方式,有三个选项: pretty_errors.FILENAME_COMPACTpretty_errors.FILENAME_EXTENDED ,或者 pretty_errors.FILENAME_FULL

以上,就是我对 pretty_errors 的使用体验,总的来说,这个库功能非常强大,使用效果也特别酷炫,它就跟 PEP8 规范一样,没有它是可以,但是有了它会更好一样。对于某些想自定义错误输出场景的人, pretty_errors 会是一个不错的解决方案,明哥把它推荐给你。

7.3 少有人知的 Python “重试机制”

为了避免由于一些网络或等其他不可控因素,而引起的功能性问题。比如在发送请求时,会因为网络不稳定,往往会有请求超时的问题。
这种情况下,我们通常会在代码中加入重试的代码。重试的代码本身不难实现,但如何写得优雅、易用,是我们要考虑的问题。

这里要给大家介绍的是一个第三方库 – Tenacity ,它实现了几乎我们可以使用到的所有重试场景,比如:

1. 在什么情况下才进行重试?
2. 重试几次呢?
3. 重试多久后结束?
4. 每次重试的间隔多长呢?
5. 重试失败后的回调?
在使用它之前 ,先要安装它

$ pip install tenacity

最基本的重试
无条件重试,重试之间无间隔

from tenacity import retry
@retry
def test_retry():
 print("等待重试,重试无间隔执行...")
 raise Exception
test_retry()

无条件重试,但是在重试之前要等待 2 秒

from tenacity import retry, wait_fixed
@retry(wait=wait_fixed(2))
def test_retry():
 print("等待重试...")
 raise Exception
test_retry()

设置停止基本条件
只重试7 次

from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(7))
def test_retry():
 print("等待重试...")
 raise Exception
test_retry()

重试 10 秒后不再重试

from tenacity import retry, stop_after_delay
@retry(stop=stop_after_delay(10))
def test_retry():
 print("等待重试...")
 raise Exception
test_retry()

或者上面两个条件满足一个就结束重试

from tenacity import retry, stop_after_delay, stop_after_attempt
@retry(stop=(stop_after_delay(10) | stop_after_attempt(7)))
def test_retry():
 print("等待重试...")
 raise Exception
test_retry()

设置何时进行重试
在出现特定错误/异常(比如请求超时)的情况下,再进行重试

from requests import exceptions
from tenacity import retry, retry_if_exception_type
@retry(retry=retry_if_exception_type(exceptions.Timeout))
def test_retry():
 print("等待重试...")
 raise exceptions.Timeout
test_retry()

在满足自定义条件时,再进行重试。
如下示例,当 test_retry 函数返回值为 False 时,再进行重试

from tenacity import retry, stop_after_attempt, retry_if_result
def is_false(value):
 return value is False
@retry(stop=stop_after_attempt(3),
 retry=retry_if_result(is_false))
def test_retry():
 return False
test_retry()

重试后错误重新抛出
当出现异常后,tenacity 会进行重试,若重试后还是失败,默认情况下,往上抛出的异常会变成 RetryError,而不是最根本的原因。
因此可以加一个参数( reraise=True ),使得当重试失败后,往外抛出的异常还是原来的那个。

from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(7), reraise=True)
def test_retry():
 print("等待重试...")
 raise Exception
test_retry()

设置回调函数
当最后一次重试失败后,可以执行一个回调函数

from tenacity import *
def return_last_value(retry_state):
 print("执行回调函数")
 return retry_state.outcome.result() # 表示返回原函数的返回值
def is_false(value):
 return value is False
@retry(stop=stop_after_attempt(3),
 retry_error_callback=return_last_value,
 retry=retry_if_result(is_false))
def test_retry():
 print("等待重试中...")
 return False
print(test_retry())

输出如下

等待重试中...
等待重试中...
等待重试中...
执行回调函数
False

 

© 版权声明

相关文章

暂无评论

暂无评论...