0
点赞
收藏
分享

微信扫一扫

【py】编写完美Python脚本技巧

快乐小码农 2021-09-23 阅读 31

四个技巧

  • 命令行参数解析中设置默认值
  • 异常处理
  • 帮助/说明文档
  • 进度条控件

从一个例程开始

例程
在密码学领域,有一个最古老、最经典的编解码算法——凯撒密码,我们编写该算法的程序,并以此为例介绍以上4个技巧。

我们的加密函数如下:

def encrypt(plaintext, key):
"""
:param plaintext: 源文本
:param key: 偏移量
"""

cyphertext = ''
for character in plaintext:
if character.isalpha():
number = ord(character)
number += key
if character.isupper():
if number > ord('Z'):
number -= 26
elif number < ord('A'):
number += 26
elif character.islower():
if number > ord('z'):
number -= 26
elif number < ord('a'):
number += 26
character = chr(number)
cyphertext += character
return cyphertext

为了对源文本加密,我们需要通过在调用以上加密函数时,需要输入参数,参数来自于命令行。因此,我们需要编写一些其他的代码来获取命令行中的参数输入,然后完成对加密函数encrypt()的调用。

sys.argv

Python中提供了sys.argv处理命令行参数。当我们在命令行运行Python脚本时,sys.argv是一个list,包含我们在命令行中输入的所有参数和参数值。比如,我们输入:python caesar_script.py --key 23 --decrypt my secret message,则打印sys.argv可得:

>>> print(sys.argv)
['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']

现在,我们编写一些代码,用来解析sys.argv。

import sys
from caesar_encryption import encrypt

def caesar():
key = 1
is_error = False

for index, arg in enumerate(sys.argv):
if arg in ['--key', '-k'] and len(sys.argv) > index + 1
key = int(sys.argv[index + 1])
del sys.argv[index]
break

for index, arg in enumerate(sys.argv):
if arg in ['--encrypt', '-e']:
del sys.argv[index]
break
if arg in ['--decrypt', '-d']:
key = -key
del sys.argv[index]
break

if len(sys.argv) == 1:
is_error = True
else:
for arg in sys.argv:
if arg.startswith('-'):
is_error = True

if is_error:
print(f'Usage: python {sys.argv[0]} [ --key <key> ] [ --encrypt|decrypt ] <text>')
else:
print(encrypt(' '.join(sys.argv[1:]), key))

if __name__ == '__main__':
caesar()

上面的代码有点长,因为我们在解析sys.argv的同时,还考虑到了上文提到的4个技巧中的3点:

  1. 在解析命令行参数时,别忘了设置默认值(key)。
  2. 异常处理(如果有参数没有输入,或者有无效参数时,要进行报错或相应处理)
  3. 要有相应的帮助文档(如果报错了,要给出建议,或者是给出该脚本正确的使用方法)

使用argparse简化例程

import argparse
from caesar_encryption import encrypt

def caesar():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('-e', '--encrypt', action='store_true')
group.add_argument('-d', '--decrypt', action='store_true')
parser.add_argument('text', nargs='*')
parser.add_argument('-k', '--key', type=int, default=1)
args = parser.parse_args()

text_string = ' '.join(args.text)
key = args.key
if args.decrypt:
key = -key
cyphertext = encrypt(text_string, key)
print(cyphertext)

if __name__ == '__main__':
caesar()

Python中的argparse模块能自动地解析命令行参数,并且处理相应的输入异常,比如:

>>> python caesar_script_using_argparse.py --encode My message
usage: caesar_script_using_argparse.py [-h] [-e|-d] [-k KEY] [text [text ...]]
caesar_script_using_argparse.py: error: unrecognized arguments: --encode

>>> python caesar_script_using_argparse.py --help
usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]

positional arguments:
text

optional arguments:
-h, --help show this help message and exit
-e, --encrypt
-d, --decrypt
-k KEY, --key KEY

关于argparse更多的用法请参考Python的官方文档。

进度条控件

当Python脚本中有遍历操作时,使用进度条控件可以让我们知道程序运行的进度。Python中有tqdm模块提供了进度条控件,使用方式如下:

from tqdm import tqdm

for key in tqdm(range(26)):
pass

click 能做得更好!

幸运的是,有个 Python 库能提供与 argparse 同样的功能(甚至还能提供更多),它的代码风格更优雅。这个库的名字叫 click。

这里是凯撒加密脚本的第三版,使用了 click:

import click

from caesar_encryption import encrypt

@click.command()
@click.argument('text', nargs=-1)
@click.option('--decrypt/--encrypt', '-d/-e')
@click.option('--key', '-k', default=1)
def caesar(text, decrypt, key):
text_string = ' '.join(text)
if decrypt:
key = -key
cyphertext = encrypt(text_string, key)
click.echo(cyphertext)

if __name__ == '__main__':
caesar()

注意现在参数和选项都在修饰器里定义,定义好的参数直接作为函数参数提供。

我来解释一下上面代码中的一些地方:

  • 脚本参数定义中的nargs参数指定了该参数期待的单词的数目(一个用引号括起来的字符串算一个单词)。默认值是1。这里nargs=-1允许接收任意数目的单词。
  • --encrypt/--decrypt这种写法可以定义完全互斥的选项(类似于argparse中的add_mutually_exclusive_group函数),它将产生一个布尔型参数。
  • click.echo是该库提供的一个工具函数,它的功能与print相同,但兼容Python 2和Python 3,还有一些其他功能(如处理颜色等)。

添加一些隐秘性

这个脚本的参数(被加密的消息)应当是最高机密。而我们却要求用户直接在终端里输入文本,使得这些文本被记录在命令历史中,这不是很讽刺吗?

解决方法之一就是使用隐藏的提示。或者可以从输入文件中读取文本,对于较长的文本来说更实际一些。或者可以干脆让用户选择。

输出也一样:用户可以保存到文件中,也可以输出到终端。这样就得到了凯撒脚本的最后一个版本:

import click

from caesar_encryption import encrypt

@click.command()
@click.option(
'--input_file',
type=click.File('r'),
help='File in which there is the text you want to encrypt/decrypt.'
'If not provided, a prompt will allow you to type the input text.',
)
@click.option(
'--output_file',
type=click.File('w'),
help='File in which the encrypted / decrypted text will be written.'
'If not provided, the output text will just be printed.',
)
@click.option(
'--decrypt/--encrypt',
'-d/-e',
help='Whether you want to encrypt the input text or decrypt it.'
)
@click.option(
'--key',
'-k',
default=1,
help='The numeric key to use for the caesar encryption / decryption.'
)
def caesar(input_file, output_file, decrypt, key):
if input_file:
text = input_file.read()
else:
text = click.prompt('Enter a text', hide_input=not decrypt)
if decrypt:
key = -key
cyphertext = encrypt(text, key)
if output_file:
output_file.write(cyphertext)
else:
click.echo(cyphertext)

if __name__ == '__main__':
caesar()

这个版本有什么新东西吗?

首先,注意到我给每个参数选项都加了个help参数。由于脚本变得复杂了,help参数可以给脚本的行为添加一些文档。运行结果如下:

> python caesar_script_v2.py --help
Usage: caesar_script_v2.py [OPTIONS]
Options:
--input_file FILENAME File in which there is the text you want to encrypt/decrypt. If not provided, a prompt will allow you to type the input text.
--output_file FILENAME File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed.
-d, --decrypt / -e, --encrypt Whether you want to encrypt the input text or decrypt it.
-k, --key INTEGER The numeric key to use for the caesar encryption / decryption.
--help Show this message and exit.

两个新的参数:input_file 和 output_file,类型均为 click.File。该库能够用正确的模式打开文件,处理可能的错误,再执行函数。例如:

> python caesar_script_v2.py --decrypt --input_file wrong_file.txt
Usage: caesar_script_v2.py [OPTIONS]
Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory

正像help文本中解释的那样,如果没有提供input_file,就使用click.promp让用户直接在提示符下输入文本,在加密模式下这些文本是隐藏的。如下所示:

> python caesar_script_v2.py --encrypt --key 2
Enter a text: **************
yyy.ukectc.eqo

破解密文!

现在设想你是个黑客:你要解密一个用凯撒加密过的密文,但你不知道秘钥是什么。

最简单的策略就是用所有可能的秘钥调用解密函数 25 次,阅读解密结果,看看哪个是合理的。

但你很聪明,而且也很懒,所以你想让整个过程自动化。确定解密后的 25 个文本哪个最可能是原始文本的方法之一,就是统计所有这些文本中的英文单词的个数。这可以使用 PyEnchant 模块实现:

import click
import enchant

from caesar_encryption import encrypt

@click.command()
@click.option(
'--input_file',
type=click.File('r'),
required=True,
)

@click.option(
'--output_file',
type=click.File('w'),
required=True,
)

def caesar_breaker(input_file, output_file):
cyphertext = input_file.read()
english_dictionnary = enchant.Dict("en_US")
max_number_of_english_words = 0
for key in range(26):
plaintext = encrypt(cyphertext, -key)
number_of_english_words = 0
for word in plaintext.split(' '):
if word and english_dictionnary.check(word):
number_of_english_words += 1
if number_of_english_words > max_number_of_english_words:
max_number_of_english_words = number_of_english_words
best_plaintext = plaintext
best_key = key
click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
output_file.write(best_plaintext)

if __name__ == '__main__':
caesar_breaker()


貌似运行得很不错,但别忘了,好的命令行程序还有个规则需要遵守:

示例中的文本包含104个单词,因此该脚本需要大约5秒才能解密。这很正常,因为它需要检查所有25个秘钥,每个秘钥都要检查104个单词是否出现在英文字典中。

假设你要解密的文本包括10^5个但IC,那么就要花费50秒才能输出结果,用户可能会非常着急。

因此我建议这种任务一定要显示进度条。特别是,显示进度条还非常容易实现。

下面是个显示进度条的例子:

import click
import enchant

from tqdm import tqdm

from caesar_encryption import encrypt

@click.command()
@click.option(
'--input_file',
type=click.File('r'),
required=True,
)

@click.option(
'--output_file',
type=click.File('w'),
required=True,
)

def caesar_breaker(input_file, output_file):
cyphertext = input_file.read()
english_dictionnary = enchant.Dict("en_US")
best_number_of_english_words = 0
for key in tqdm(range(26)):
plaintext = encrypt(cyphertext, -key)
number_of_english_words = 0
for word in plaintext.split(' '):
if word and english_dictionnary.check(word):
number_of_english_words += 1
if number_of_english_words > best_number_of_english_words:
best_number_of_english_words = number_of_english_words
best_plaintext = plaintext
best_key = key
click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
output_file.write(best_plaintext)

if __name__ == '__main__':
caesar_breaker()

你发现区别了吗?可能不太好找,因为区别真的很小,只有四个字母:tqdm。

tqdm 是 Python 库的名字,也是它包含的类的名字。只需用它包裹一个可迭代的东西,就能显示出进度条:

for key in tqdm(range(26)):

这样就能显示出非常漂亮的进度条。我都不敢相信这是真的。



另外,click也提供类似的显示进度条的工具(click.progress_bar),但我觉得它的外观不太容易懂,而且要写的代码也多一些。

我希望这篇文章能让你在改进开发者的体验上多花点时间。

参考:
https://zhuanlan.zhihu.com/p/55467001

举报

相关推荐

0 条评论