从代码到代码风格,将awk脚本移植到Python
脚本是解决问题的有效方法,而awk是编写脚本的出色语言。 它特别擅长简单文本处理,并且它可以带您完成配置文件的某些复杂重写或目录中文件名的格式重新格式化。
到了某个时候,awk的局限性开始显现出来。它没有将文件分解为模块的实际概念,它、缺乏质量错误报告,并且缺少了现在被认为是语言工作原理的其他内容。当编程语言的这些丰富功能有助于维护关键脚本时,移植将是一个不错的选择。
我最喜欢的完美移植awk的现代编程语言是Python。
在将awk脚本移植到Python之前,通常值得考虑一下其原始上下文。 例如,由于awk的局限性,awk代码通常是从Bash脚本调用的,其中包括对其他命令行收藏夹(如sed,sort和gang)的一些调用。 最好将所有内容转换为一个一致的Python程序。 有时,脚本会做出过于宽泛的假设。 例如,即使实际上只运行一个文件,该代码也可能允许任意数量的文件。
在仔细考虑了上下文并确定了要用Python替代的东西之后,该编写代码了。
请记住以下Python代码:
with open(some_file_name) as fpin:
for line in fpin:
pass # do something with line
此代码将逐行循环遍历文件并处理这些行。
如果要访问行号(相当于awk的NR ),则可以使用以下代码:
with open(some_file_name) as fpin:
for nr, line in enumerate(fpin):
pass # do something with line
如果您需要能够遍历任意数量的文件同时保持行数的持续计数(例如awk的FNR ),那么此循环可以做到这一点:
def awk_like_lines(list_of_file_names):
def _all_lines():
for filename in list_of_file_names:
with open(filename) as fpin:
yield from fpin
yield from enumerate(_all_lines())
该语法使用Python的生成器和yield from来构建遍历所有行并保持持久计数的迭代器 。
如果你同时需要 FNR 和 NR,这里有一个更复杂的循环:
def awk_like_lines(list_of_file_names):
def _all_lines():
for filename in list_of_file_names:
with open(filename) as fpin:
yield from enumerate(fpin)
for nr, (fnr, line) in _all_lines:
yield nr, fnr, line
问题仍然是你是否需要所有三个功能:FNR,NR 和线。 如果你真的这样做,使用三元组,其中两个项目是数字可能会导致混淆。 命名参数可以使代码更容易阅读,所以最好使用一个数据类:
import dataclass
@dataclass.dataclass(frozen=True)
class AwkLikeLine:
content: str
fnr: int
nr: int
def awk_like_lines(list_of_file_names):
def _all_lines():
for filename in list_of_file_names:
with open(filename) as fpin:
yield from enumerate(fpin)
for nr, (fnr, line) in _all_lines:
yield AwkLikeLine(nr=nr, fnr=fnr, line=line)
你可能会想,为什么不从这个方法开始呢? 从其他地方开始的原因是,这几乎太复杂了。 如果您的目标是使通用库更容易将awk移植到Python,请考虑这样做。但是编写一个循环,使您能够准确地得到特定情况下所需要的内容更容易,也更容易理解(因此也更容易维护)。
一旦拥有与一行相对应的字符串,如果要转换awk程序,通常需要将其分解为多个字段。Python有几种方法可以做到这一点。 这将返回一个字符串列表,在任意数量的连续空格上分割该行:
line.split()
如果需要另一个字段分隔符,比如使用’:’ 和’;’,则需要 rstrip 方法来删除最后一个换行符:
line.rstrip("\n").split(":")
在执行以下操作之后,列表parts将具有分解的字符串:
parts = line.rstrip("\n").split(":")
这种拆分对于选择如何处理这些参数是有好处的,但是我们处于了一个错误的情况下。现在parts[0]将对应 awk 的 $1,parts[1]将对应 awk 的 $2,依此类推。之所以出现这种情况是因为awk从1开始计数“字段”,而Python从0开始计数。 在 awk 的 $0中是整个行——相当于 line.rstrip("\n") ,而且awk的NF (字段数)更容易作为len(parts)检索。
作为一个示例,让我们将《如何用 awk 删除文件中的重复行》中的一行代码转换为 Python。
最初的 awk 是:
awk '!visited$0++' your_file > deduplicated_file
“真正的” Python 转换是:
import collections
import sys
visited = collections.defaultdict(int)
for line in open("your\_file"):
did\_visit = visited[line]
visited[line] += 1
if not did\_visit:
sys.stdout.write(line)
然而,Python 比 awk 有更多的数据结构。 与其计算访问次数(我们不使用这个,除了知道我们是否看到了一行),为什么不记录被访问的行呢?
import sys
visited = set()
for line in open("your_file"):
if line in visited:
continue
visited.add(line)
sys.stdout.write(line)
Python社区提倡编写Pythonic代码,这意味着它遵循公认的代码风格。 更加Python化的方法将区分唯一性和输入/输出的关注点。 此更改将使对代码进行单元测试更加容易:
def unique_generator(things):
visited = set()
for thing in things:
if thing in visited:
continue
visited.add(things)
yield thing
import sys
for line in unique_generator(open("your_file")):
sys.stdout.write(line)
将所有逻辑从输入 / 输出代码中移除,可以提高代码的关注点分离和可用性和可测试性。
将awk脚本移植到Python时,通常是在考虑适当的Python代码风格时重新实现核心需求,而不是通过条件/操作对条件/操作进行笨拙的翻译。 考虑原始上下文并产生高质量的Python解决方案。 虽然有时候使用awk的Bash单行代码可以完成工作,但是Python编码是通往更易于维护的代码的途径。
另外,如果您正在编写awk脚本,我相信您也可以学习Python! 如果您有任何疑问,请告诉我。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。