Python 脚本作为 Bash 实用脚本的替代方案
对于 Linux 用户来说,命令行是我们整个体验中备受推崇的一部分。与其他流行的操作系统不同,在这些操作系统中,命令行对于大多数经验丰富的资深人士来说都是一个可怕的主张,但在 Linux 社区中,命令行使用是受到鼓励的。与使用图形用户界面执行类似任务相比,命令行通常可以提供更优雅和高效的解决方案。
随着 Linux 社区对命令行的依赖性越来越强,UNIX shell(如 bash 和 zsh)已发展成为极其强大的工具,可以补充 UNIX shell 体验。借助 bash 和其他类似的 shell,可以使用许多强大的功能,例如管道、文件名通配符以及从名为脚本的文件中读取命令的能力。
让我们看一个真实的例子来演示命令行的强大功能。每次用户登录服务时,他们的用户名都会记录到一个文本文件中。对于此示例,让我们找出有多少唯一用户使用该服务。
以下示例中的一系列命令通过将较小的构建块链接在一起,展示了更复杂实用程序的强大功能
$ cat names.log | sort | uniq | wc -l
管道符号 (|) 用于将一个命令的标准输出传递到下一个命令的标准输入。在此示例中,cat names.txt
的输出被传递到 sort
命令。sort
命令的输出是文件中每行的字母顺序重新排列。随后,它通过管道传递到 uniq
命令,该命令删除任何重复的名称。最后,uniq
的输出被传递到 wc
命令。wc
是一个计数命令,并且设置了 -l
标志,它返回行数。这允许您将多个命令链接在一起。
然而,有时所需的功能可能会变得非常复杂,并且将命令链接在一起可能会变得笨拙。在这种情况下,shell 脚本是答案。shell 脚本是 shell 读取并按顺序执行的命令列表。Shell 脚本还支持一些编程语言基础知识,例如变量、流程控制和数据结构。Shell 脚本对于经常重复运行的批处理作业非常有用。不幸的是,shell 脚本也存在一些缺点
-
Shell 脚本很容易变得过于复杂且对于想要改进或维护它们的开发人员来说难以阅读。
-
通常,这些 shell 脚本的语法和解释器可能很笨拙且不直观。语法越笨拙,对于必须使用这些脚本的开发人员来说,可读性就越差。
-
代码通常在其他脚本中不可用。脚本之间的代码重用往往很困难,并且脚本往往非常特定于某个问题。
-
高级功能(如 HTML 解析或 HTTP 请求)的库不如现代编程和脚本语言那样容易获得。
这些问题会使 shell 脚本编写成为一项笨拙的任务,并且通常会导致大量开发人员时间的浪费。相反,Python 编程语言可以用作非常有能力的替代品。使用 Python 作为 shell 脚本的替代品有很多好处
-
Python 默认安装在所有主要的 Linux 发行版上。打开命令行并键入
python
将立即使您进入 Python 解释器。这种普遍性使其成为大多数脚本任务的明智选择。 -
Python 具有非常易于阅读和理解的语法。它的风格强调极简主义和简洁的代码,同时允许开发人员以适合 shell 脚本的简陋风格进行编写。
-
Python 是一种解释型语言,这意味着没有编译阶段。这使得 Python 成为脚本编写的理想语言。Python 还带有一个读取-求值-打印循环 (REPL),允许您以解释方式快速尝试新代码。这让开发人员可以尝试想法,而无需将完整的程序写到文件中。
-
Python 是一种功能齐全的编程语言。代码重用很简单,因为 Python 模块可以轻松导入并在任何 Python 脚本中使用。脚本可以轻松扩展或构建在其基础上。
-
Python 可以访问优秀的标准库和数千个第三方库,用于各种高级实用程序,例如解析器和请求库。例如,Python 的标准库包含 datetime 库,该库允许您将日期解析为您指定的任何格式,并轻松地将其与其他日期进行比较。
-
Python 可以是链条中的一个简单环节。Python 不应取代所有 bash 命令。编写以 UNIX 方式运行的 Python 程序(即,读取标准输入并写入标准输出)与编写 Python 替代现有 shell 命令(如 cat 和 sort)一样强大。
让我们在上文已解决的问题的基础上继续构建。除了已经完成的工作之外,让我们找出某个用户登录系统多少次。uniq
命令只是删除重复项,但不提供有关重复项数量的任何信息。可以使用 Python 脚本代替 uniq
作为链中的另一个命令。这是一个执行此操作的 Python 程序(在我的示例中,我将此文件称为 namescount.py)
#!/usr/bin/env python
import sys
if __name__ == "__main__":
# Initialize a names dictionary as empty to start with.
# Each key in this dictionary will be a name and the value
# will be the number of times that name appears.
names = {}
# sys.stdin is a file object. All the same functions that
# can be applied to a file object can be applied to sys.stdin.
for name in sys.stdin.readlines():
# Each line will have a newline on the end
# that should be removed.
name = name.strip()
if name in names:
names[name] += 1
else:
names[name] = 1
# Iterating over the dictionary,
# print name followed by a space followed by the
# number of times it appeared.
for name, count in names.iteritems():
sys.stdout.write("%d\t%s\n" % (count, name))
让我们看看这个 Python 脚本如何融入命令链。首先,它从通过 sys.stdin 对象公开的标准输入中读取输入。任何输出都写入 sys.stdout 对象,这就是标准输出在 Python 中的实现方式。Python 字典(在其他语言中通常称为哈希映射)用于获取从用户名到重复计数的映射。要获取所有用户的计数,请执行以下操作
$ cat names.log | python namescount.py
这显示了用户出现的次数以及用户名,并使用制表符作为分隔符。接下来要做的是按顺序显示使用系统最频繁的用户。这可以在 Python 级别完成,但让我们使用核心 UNIX 实用程序已经提供的实用程序来实现它。之前,我使用 sort
命令按字母顺序排序。如果为该命令提供了 -rn
标志,它将按数值降序对行进行排序。由于 Python 脚本打印到标准输出,因此您可以简单地将命令通过管道传递到 sort
并检索您想要的输出
$ cat names.log | python namescount.py | sort -rn
这是使用 Python 作为命令链一部分的功能示例。在这种情况下使用 Python 的优点如下
-
能够与 cat 和 sort 等工具链接。简单的实用程序(逐行读取文件和按数值排序文件)由经过尝试和信任的 UNIX 命令处理。这些命令也逐行读取,这意味着这些功能可以扩展到大型文件,并且速度非常快。
-
当链中需要进行一些繁重的工作时,可以编写一个非常清晰、简洁的 Python 脚本,该脚本可以完成它需要做的事情,然后将责任转移到链中的下一个环节。
-
它是一个可重用的模块,尽管此示例专门针对名称,但如果您向其提供任何包含重复行的输入,它将打印出每一行和重复次数。使 Python 代码模块化使您可以将其应用于各种场景。
为了演示以模块化和管道方式组合 Python 脚本的强大功能,让我们进一步扩展问题空间。让我们找到服务的前五名用户。head
是一个命令,允许您指定要显示的标准输入行的特定数量。将此添加到命令链会得到以下结果
$ cat names.log | python namescount.py | sort -rn | head -n 5
这仅打印前五名用户,而忽略其余用户。类似地,要获得使用服务最少的五个用户,您可以使用 tail
命令,该命令采用相同的参数。Python 命令打印到标准输出的结果允许您构建和扩展其功能。
为了演示此脚本的模块化,让我们再次更改问题空间。该服务还生成一个逗号分隔值 (CSV) 日志文件,其中包含电子邮件地址列表以及每个电子邮件地址对服务的评论。这是一个示例条目
"email@example.com", "This service is great."
任务是为服务提供一种方式,向评论频率最高的前十名用户发送感谢消息。首先,您需要一个可以读取和打印 CSV 数据特定列的脚本。Python 的标准库提供了 CSV 读取器。下面的 Python 脚本完成了此目标
#!/usr/bin/env python
# CSV module that comes with the Python standard library
import csv
import sys
if __name__ == "__main__":
# The CSV module exposes a reader object that takes
# a file object to read. In this example, sys.stdin.
csvfile = csv.reader(sys.stdin)
# The script should take one argument that is a column number.
# Command-line arguments are accessed via sys.argv list.
column_number = 0
if len(sys.argv) > 1:
column_number = int(sys.argv[1])
# Each row in the CSV file is a list with each
# comma-separated value for that line.
for row in csvfile:
print row[column_number]
此脚本可以解析 CSV 数据,并以纯文本形式返回作为命令行参数提供的列。它使用 print
而不是 sys.stdout.write
,因为 print
默认情况下使用标准输出作为其输出文件。
让我们将此脚本添加到链中。新脚本与其他脚本链接在一起,以使用下面列出的命令打印出电子邮件地址列表及其评论频率(假定 .csv 日志文件名为 emailcomments.csv,而新的 Python 脚本名为 csvcolumn.py)
$ cat emailcomments.csv | python csvcolumn.py |
↪python namescount.py | sort -rn | head -n 5
接下来,您需要一种发送电子邮件的方法。在 Python 标准函数库中,您可以导入 smtplib,这是一个允许您连接到 SMTP 服务器以发送邮件的模块。让我们编写一个简单的 Python 脚本,使用此库向已找到的前十名电子邮件地址中的每一个发送消息
#!/usr/bin/env python
import smtplib
import sys
GMAIL_SMTP_SERVER = "smtp.gmail.com"
GMAIL_SMTP_PORT = 587
GMAIL_EMAIL = "Your Gmail Email Goes Here"
GMAIL_PASSWORD = "Your Gmail Password Goes Here"
def initialize_smtp_server():
'''
This function initializes and greets the smtp server.
It logs in using the provided credentials and returns
the smtp server object as a result.
'''
smtpserver = smtplib.SMTP(GMAIL_SMTP_SERVER, GMAIL_SMTP_PORT)
smtpserver.ehlo()
smtpserver.starttls()
smtpserver.ehlo()
smtpserver.login(GMAIL_EMAIL, GMAIL_PASSWORD)
return smtpserver
def send_thank_you_mail(email):
to_email = email
from_email = GMAIL_EMAIL
subj = "Thanks for being an active commenter"
# The header consists of the To and From and Subject lines
# separated using a newline character
header = "To:%s\nFrom:%s\nSubject:%s \n" % (to_email,
from_email, subj)
# Hard-coded templates are not best practice.
msg_body = """
Hi %s,
Thank you very much for your repeated comments on our service.
The interaction is much appreciated.
Thank You.""" % email
content = header + "\n" + msg_body
smtpserver = initialize_smtp_server()
smtpserver.sendmail(from_email, to_email, content)
smtpserver.close()
if __name__ == "__main__":
# for every line of input.
for email in sys.stdin.readlines():
send_thank_you_mail(email)
此 Python 脚本支持联系任何 SMTP 服务器,无论是本地服务器还是远程服务器。为了易于使用,我包含了 Gmail 的 SMTP 服务器,并且它应该可以工作,前提是您为脚本提供了正确的 Gmail 凭据。该脚本使用 smtplib 中提供的函数发送邮件。这再次演示了在此级别使用 Python 的强大功能。像 SMTP 交互这样的事情在 Python 中很容易且可读。等效的 shell 脚本很混乱,并且此类库不易访问,如果它们存在的话。
为了将电子邮件发送给按评论频率排序的前十名用户,首先您必须隔离列名称输出中的电子邮件列。要在 Linux 中隔离特定列,您可以使用 cut
命令。在下面的示例中,命令以两个单独的链给出。为了易于使用,我将输出写入一个临时文件,该文件可以加载到第二个链中。这只是使过程更具可读性(用于发送邮件的 Python 脚本称为 sendemail.py)
$ cat emailcomments.csv | python csvcolumn.py |
↪python namescount.py | sort -rn > /tmp/comment_freq
$ cat /tmp/comment_freq | head -n 10 | cut -f2 |
↪python sendemail.py
这显示了 Python 作为 bash 命令链中的实用程序的真正强大之处。编写接受来自标准输入的输入并将任何数据写入标准输出的脚本,允许开发人员快速轻松地将此类命令链接在一起,链中的链接通常是 Python 程序。这种设计服务于一个目的的小应用程序的理念非常符合此处使用的命令流程。
通常,在命令行上使用的 Python 脚本中,使用参数在用户运行特定命令时为用户提供选项。例如,head
命令采用 -n
参数,该参数采用其后的数字,并且仅打印该行数。提供给 Python 脚本的每个参数都通过 sys.argv
数组公开,可以通过首先导入 sys
来访问该数组。下面的代码显示了如何将单个单词作为参数。此程序是一个简单的加法器,它接受两个数字参数并将它们相加,然后将结果打印给用户。但是,这种接受命令行参数的格式相当基本。很容易犯错误,例如,将两个字符串(如 hello 和 world)传递给此命令,您将开始收到错误
#!/usr/bin/env python
import sys
if __name__ == "__main__":
# The first argument of sys.argv is always the filename,
# meaning that the length of system arguments will be
# more than one, when command-line arguments exist.
if len(sys.argv) > 2:
num1 = long(sys.argv[1])
num2 = long(sys.argv[2])
else:
print "This command takes two arguments and adds them"
print "Less than two arguments given."
sys.exit(1)
print "%s" % str(num1 + num2)
值得庆幸的是,Python 有许多模块可以处理命令行参数。我个人最喜欢的是 OptionParser。OptionParser 是标准库提供的 optparse 模块的一部分。OptionParser 允许您使用命令行参数执行一系列非常有用的操作
-
如果未提供特定参数,则指定默认值。
-
它支持参数标志(存在或不存在)和带值的参数 (-n 10000)。
-
它支持传递参数的不同格式,例如,-n=100000 和 -n 100000 之间的差异。
让我们使用 OptionParser 来增强发送邮件的脚本。原始脚本中有很多变量是硬编码到位的,例如 SMTP 详细信息和用户的登录凭据。在下面提供的代码中,命令行参数用于传入这些变量
#!/usr/bin/env python
import smtplib
import sys
from optparse import OptionParser
def initialize_smtp_server(smtpserver, smtpport, email, pwd):
'''
This function initializes and greets the SMTP server.
It logs in using the provided credentials and returns the
SMTP server object as a result.
'''
smtpserver = smtplib.SMTP(smtpserver, smtpport)
smtpserver.ehlo()
smtpserver.starttls()
smtpserver.ehlo()
smtpserver.login(email, pwd)
return smtpserver
def send_thank_you_mail(email, smtpserver):
to_email = email
from_email = GMAIL_EMAIL
subj = "Thanks for being an active commenter"
# The header consists of the To and From and Subject lines
# separated using a newline character.
header = "To:%s\nFrom:%s\nSubject:%s \n" % (to_email,
from_email, subj)
# Hard-coded templates are not best practice.
msg_body = """
Hi %s,
Thank you very much for your repeated comments on our service.
The interaction is much appreciated.
Thank You.""" % email
content = header + "\n" + msg_body
smtpserver.sendmail(from_email, to_email, content)
if __name__ == "__main__":
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option("--email", dest="email",
help="email to login to smtp server")
parser.add_option("--pwd", dest="pwd",
help="password to login to smtp server")
parser.add_option("--smtp-server", dest="smtpserver",
help="smtp server url", default="smtp.gmail.com")
parser.add_option("--smtp-port", dest="smtpserverport",
help="smtp server port", default=587)
options, args = parser.parse_args()
if not (options.email or options.pwd):
parser.error("Must provide both an email and a password")
smtpserver = initialize_smtp_server(options.stmpserver,
options.smtpserverport, options.email, options.pwd)
# for every line of input.
for email in sys.stdin.readlines():
send_thank_you_mail(email, smtpserver)
smtpserver.close()
此脚本显示了 OptionParser 的实用性。它为命令行参数提供了一个简单易用的界面,允许您为每个命令行选项定义某些属性。它还允许您指定默认值。如果未提供某些参数,它允许您抛出特定错误。
那么你学到了什么?与其用一个 Python 脚本替换一系列 bash 命令,不如让 Python 只在中间做繁重的工作通常更好。这允许更模块化和可重用的脚本,同时也利用了 Python 提供的所有功能。使用 stdin 作为文件对象允许 Python 读取输入,该输入从其他命令通过管道传递给它,而写入 stdout 允许它继续通过管道系统传递信息。像这样组合信息可以创建一些非常强大的程序。我在此处给出的示例都适用于记录到文件的虚构服务。
作为一个真实的例子,最近我一直在处理千兆字节的 CSV 文件,我一直在使用 Python 脚本将其转换为包含 SQL 命令的文件以插入信息。为了理解我在此处关注的数据类型,我为单个表运行了数据,该脚本花费了 23 小时执行并生成了一个 20GB 大小的 SQL 文件。以本文描述的方式使用 Python 脚本的优势在于,不需要将整个文件读入内存。这意味着可以一次处理一行 20GB+ 的整个文件。此外,当每个步骤(读取、排序、操作和写入)都分为这些逻辑步骤时,更容易考虑问题。保证每个作为 UNIX 类环境核心实用程序一部分的命令都是高效且稳定的,这有助于整个体验更加稳定和安全。
另一个好处是没有硬编码的文件被读入。通常,拥有传递字符串而不是文件概念的灵活性非常强大。例如,如果通过某个文件的 20,000 行,脚本中断,则可以使用 tail
而不是从头开始重新运行脚本,仅从脚本失败的行开始读取。
Python 在 shell 中有很多方面超出了本文的范围,例如 os 模块和 subprocess 模块。os 模块是一个标准库函数,它包含许多关键的操作系统级操作,例如列出目录和声明文件,以及一个出色的子模块 os.path,用于处理规范化目录路径。subprocess 模块允许 Python 程序运行系统命令和其他高级操作,例如处理上面描述的在 Python 代码中在生成的进程之间进行管道传输。如果您打算进行任何 Python shell 脚本编写,那么这两个库都值得查看。