脚本是重复解决问题的有效方法,而 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
具有 FNR、NR 和行的更复杂的 awk 功能
问题仍然是是否需要所有三个: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 条评论