如何将 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

具有 FNR、NR 和行的更复杂的 awk 功能

问题仍然是是否需要所有三个: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.
自 1998 年以来,Moshe 一直参与 Linux 社区,帮助举办 Linux“安装聚会”。 自 1999 年以来,他一直在编写 Python 程序,并为核心 Python 解释器做出了贡献。 Moshe 在这些术语出现之前就一直是 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])

知识共享许可协议本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.