如何将 awk 脚本移植到 Python

将 awk 脚本移植到 Python 更多的是关于代码风格,而不是直译。
120 位读者喜欢这篇文章。
Woman sitting in front of her laptop

kris krüg

脚本是重复解决问题的有效方法,而 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 来构建一个迭代器,该迭代器循环遍历所有行并保持持久计数。

如果您需要 FNRNR 的等效项,这里有一个更复杂的循环

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

问题仍然是您是否需要全部三个:FNRNRline。如果您确实需要,使用一个三元组(其中两个项目是数字)可能会导致混淆。命名参数可以使此代码更易于阅读,因此最好使用 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 的 $1parts[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! 如果您有任何问题,请在评论中告诉我。

接下来阅读
标签
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe 自 1998 年以来一直参与 Linux 社区,帮助举办 Linux “安装派对”。 他自 1999 年以来一直从事 Python 编程,并为核心 Python 解释器做出了贡献。 Moshe 在 DevOps/SRE 这些术语出现之前就一直是 DevOps/SRE,非常关心软件可靠性、构建可重现性等。

6 条评论

目前一切都很好。
Awk 具有模式-动作的编码方式,这也很好地映射到 Python。

很棒的文章。 我认为,当输入分布在多个文件时,Python 比 Awk 有一个非常强大的用例。 在 Awk 中,输入源之间的关联并非易事。

你可以说我老派,但在我看来,AWK 的最佳替代品仍然是 Perl。 但我明白,现在没人再学 Perl 了。

我还在考虑学习 Perl。

现在大家的共识是什么? Perl 5 还是 6?

回复 ,作者 dirk dierickx (未验证)

谢谢 Moshe,文章写得很好。
我已经使用 awk 一段时间了,并且同意将脚本移植到 Python 的好处。
如果能看到一些性能比较,那就很有趣了。

嗨,我认为您还应该提及 Python 标准库中的 “fileinput”。 它可以为您计算行号,如果您愿意,它可以从命令行参数自动获取文件名,它甚至可以对文本文件进行 Perl 风格的就地编辑(带有可选备份)。

with fileinput.input() as f
for line in f
parts = line.rstrip("\n").split()
if parts
print(parts[0])

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.