Python程式設計師的主要工作是寫入命令列程序,也就是直接在終端機運行的腳本。隨著專案規模成長,我們希望創建有效的命令列接口,透過提供不同的參數,解決不同的問題,而不是每次修改原始程式碼。
為了實現這個目標,我總結了四個原則,希望對大家有所幫助:
讓我們將這些規則應用於一個具體的案例:一個使用Caesar cipher加密和解密訊息的腳本。
假設我們寫了一個encrypt函數,如下所示。現在要建立一個的腳本來加密和解密訊息。
腳本允許使用者選擇:模式(加密或解密),金鑰。前者的預設值是加密,後者的預設值是1。這一切都透過命令列參數實現。
def encrypt(plaintext, 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
腳本需要先取得命令列參數的值,讓我們先用最簡單的sys.argv實作。
sys.argv是一個列表,包含了使用者在執行腳本時輸入的所有參數(包括腳本名字本身)。
在終端機輸入以下指令:
> python caesar_script.py --key 23 --decrypt my secret message pb vhfuhw phvvdjh
sys.argv清單包含:
['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']
為了取得參數值,需要遍歷參數列表,尋找一個'--key' (或'-k' )來取得金鑰值,並尋找一個'--decrypt' 取得模式。
import sys from caesar_encryption import encryp 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] 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()
程式碼遵循我們一開始提出的原則:
有一個預設鍵值和一個預設模式
處理基本錯誤(不提供輸入文字或未知參數)
在參數錯誤或在不帶參數的情況下呼叫腳本時,列印簡潔的提示訊息
> python caesar_script_using_sys_argv.py Usage: python caesar.py [ --key <key> ] [ --encrypt|decrypt ] <text>
但是這個版本的腳本相當長(39行,不包括加密函數),而且程式碼非常醜陋。
是否有更好的方法來解析命令列參數?
argparse是用來解析命令列參數的Python標準函式庫模組。
修改腳本,使用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 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
腳本的第7行到第13行程式碼定義了命令列參數,但它們不是很優雅:太冗長且程式化,我們可以用更緊湊和聲明性的方式完成。
幸運的是有一個三方庫click用於創建命令行接口,它不僅提供比argparse更多的功能, 而且代碼風格更漂亮。用click取代argparse,繼續優化腳本。
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定義互斥的選項 ,最後以布林值傳遞給程式。
click.echo是click庫提供的基礎函數,功能類似於print,但提供更強大的功能,例如調整列印到控制台的文字的顏色。
命令列參數接收的值是將被加密的最高機密訊息,所以如果要求用戶直接在終端中輸入純文本,可能會引發安全疑慮。
一種更安全的方法是使用隱藏提示,或從本機檔案讀取文字 ,這對於長文字來說更加實用。
這個想法同樣適用於輸出:使用者可以將其儲存到檔案中,或在終端機中列印出來。讓我們繼續優化腳本。
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()
由於腳本變得更複雜,我們建立了參數文件(透過定義click.option裝飾器的help參數實作),詳細解釋參數的功能,效果如下。
> python caesar_script_v2.py --help Usage: caesar_script_v2.py [OPTIONS] Options: --input_file FILENAMEFile 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, --encryptWhether you want to encrypt the input text or decrypt it. -k, --key INTEGERThe numeric key to use for the caesar encryption / decryption. --help Show this message and exit.
我們有兩個新的參數input_file和output_file,類型是click.File,click會用正確的模式開啟檔案並處理可能發生的錯誤。例如找不到檔案:
> 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
如果未提供input_file,則我們用click.prompt,在命令列建立提示窗口,讓使用者直接輸入文本,該提示對於加密模式將是隱藏的。效果如下:
> 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:nn{best_plaintext[:1000]}...') output_file.write(best_plaintext) if __name__ == '__main__': caesar_breaker()
示例中的文本包含10^4个单词,因此该脚本需要大约5秒才能解密。这很正常,因为它需要检查所有25个秘钥,每个秘钥都要检查10^4个单词是否出现在英文字典中。
假设你要解密的文本包括10^5个单词,那么就要花费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:nn{best_plaintext[:1000]}...') output_file.write(best_plaintext) if __name__ == '__main__': caesar_breaker()
这里使用了tqdm库,tqdm.tqdm类可以将任何可迭代对象转化为一个进度条。click也提供了类似的接口来创建进度条(click.progress_bar),但我觉得它不如tqdm好用。
以上是使用click創建完美的Python命令列程序的詳細內容。更多資訊請關注PHP中文網其他相關文章!