脚本是重复解决问题的有效方法,而 awk 是一种出色的脚本编写语言。它尤其擅长简单的文本处理,可以帮助您完成复杂的配置文件重写或目录中文件名的重新格式化。
何时从 awk 迁移到 Python
然而,在某些时候,awk 的局限性开始显现。它没有将文件分解为模块的实际概念,缺乏高质量的错误报告,并且缺少其他现在被认为是语言工作原理基础的东西。当编程语言的这些丰富功能有助于维护关键脚本时,移植成为一个不错的选择。
我最喜欢的现代编程语言,非常适合移植 awk,是 Python。
在将 awk 脚本移植到 Python 之前,通常值得考虑其原始上下文。例如,由于 awk 的局限性,awk 代码通常从 Bash 脚本调用,并包含对其他命令行常用工具(如 sed、sort 等)的调用。最好将其全部转换为一个连贯的 Python 程序。有时,脚本会做出过于宽泛的假设;例如,代码可能允许任意数量的文件,即使在实践中只运行一个文件。
在仔细考虑上下文并确定要用 Python 替换的内容后,就该编写代码了。
标准的 awk 到 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
Python 中类似 awk 的多文件行为
如果您需要能够迭代任意数量的文件,同时保持行号的持久计数(如 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
更复杂的 awk 功能,带有 FNR、NR 和 line
问题仍然是您是否需要全部三个:FNR、NR 和 line。如果您确实需要,使用一个三元组(其中两个项目是数字)可能会导致混淆。命名参数可以使此代码更易于阅读,因此最好使用 dataclass
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 字段
一旦您有一个对应于行的字符串,如果您正在转换 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) 检索。
在 Python 中移植 awk 字段
例如,让我们将 “如何使用 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)
编写 Pythonic 的 awk 代码
Python 社区提倡编写 Pythonic 代码,这意味着它遵循一种普遍认可的代码风格。 更 Pythonic 的方法将分离唯一性和输入/输出的关注点。 这种更改将使单元测试您的代码更容易
def unique_generator(things):
visited = set()
for thing in things:
if thing in visited:
continue
visited.add(thing)
yield thing
import sys
for line in unique_generator(open("your_file")):
sys.stdout.write(line)
将所有逻辑从输入/输出代码中分离出来可以更好地分离关注点,并提高代码的可用性和可测试性。
结论:Python 可能是一个不错的选择
将 awk 脚本移植到 Python 通常更多的是关于重新实现核心需求,同时考虑适当的 Pythonic 代码风格,而不是逐条件/动作地进行死板的翻译。 考虑到原始上下文并生成高质量的 Python 解决方案。 虽然有时带有 awk 的 Bash 单行命令可以完成工作,但 Python 编码是通往更易于维护的代码的道路。
另外,如果您正在编写 awk 脚本,我相信您也可以学习 Python! 如果您有任何问题,请在评论中告诉我。
6 条评论