cuo*_*glm 241 shell text-processing
在 POSIX shell 中使用while 循环来处理文本通常被认为是不好的做法吗?
正如Stéphane Chazelas 所指出的,不使用 shell 循环的一些原因是概念、可靠性、易读性、性能和安全性。
这个答案解释了可靠性和易读性方面:
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< "$InputFile"
Run Code Online (Sandbox Code Playgroud)
为了性能,从文件或管道读取时,while循环和读取非常慢,因为内置的read shell一次读取一个字符。
怎么样的概念和安全性方面?
Sté*_*las 328
是的,我们看到了许多事情,例如:
while read line; do
echo $line | cut -c3
done
Run Code Online (Sandbox Code Playgroud)
或者更糟:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
Run Code Online (Sandbox Code Playgroud)
(别笑,我见过很多这样的)。
通常来自shell脚本初学者。这些是你在命令式语言(如 C 或 python)中所做的事情的幼稚字面翻译,但这不是你在 shell 中做事的方式,而且这些例子非常低效,完全不可靠(可能导致安全问题),如果你曾经管理过为了修复大多数错误,您的代码变得难以辨认。
在 C 或大多数其他语言中,构建块仅比计算机指令高一级。你告诉你的处理器要做什么,然后下一步做什么。你用手拿着你的处理器并对其进行微观管理:你打开那个文件,你读取那么多字节,你这样做,你用它做那个。
Shell 是一种高级语言。有人可能会说它甚至不是一种语言。它们在所有命令行解释器之前。这项工作由您运行的那些命令完成,而 shell 只是为了编排它们。
Unix 引入的一项伟大功能是管道和所有命令默认处理的默认 stdin/stdout/stderr 流。
50 年来,我们没有找到比该 API 更好的方法来利用命令的力量并让它们协作完成任务。这可能是人们今天仍在使用 shell 的主要原因。
你有一个切割工具和一个音译工具,你可以简单地做:
cut -c4-5 < in | tr a b > out
Run Code Online (Sandbox Code Playgroud)
shell 只是在做管道(打开文件,设置管道,调用命令),当一切准备就绪时,它只是在没有 shell 做任何事情的情况下运行。这些工具可以同时高效地按照自己的节奏完成工作,并具有足够的缓冲,以免一个阻止另一个,它既漂亮又简单。
调用工具虽然有成本(我们将在性能点上开发它)。这些工具可能用 C 语言编写了数千条指令。必须创建一个进程,必须加载、初始化、清理、销毁进程并等待该工具。
调用cut就像打开厨房的抽屉,拿起刀,使用它,清洗它,擦干它,然后把它放回抽屉。当你这样做时:
while read line; do
echo $line | cut -c3
done < file
Run Code Online (Sandbox Code Playgroud)
就像对于文件的每一行,read从厨房抽屉中取出工具(非常笨拙,因为它不是为此设计的),阅读一行,清洗阅读工具,然后将其放回抽屉。然后安排了一个会议echo和cut工具,从抽屉得到他们,调用它们,把它们洗干净,擦干,将它们放回抽屉等。
其中的一些工具(read和echo)是建立在大多数炮弹,但很难使一个区别就在这里,因为echo和cut仍然需要在单独的进程中运行。
这就像切一个洋葱,但洗完你的刀,然后在每片之间把它放回厨房的抽屉里。
这里显而易见的方法是cut从抽屉里拿出你的工具,把你的整个洋葱切成薄片,然后在整个工作完成后把它放回抽屉里。
IOW,在 shell 中,尤其是处理文本时,您调用尽可能少的实用程序并让它们协作完成任务,而不是依次运行数千个工具,等待每个工具启动、运行、清理,然后再运行下一个。
进一步阅读布鲁斯的好答案。shell 中的低级文本处理内部工具(除了 for zsh)有限、繁琐,一般不适合一般的文本处理。
如前所述,运行一个命令是有代价的。如果该命令不是内置的,则成本很高,但即使它们是内置的,成本也很高。
shell 并没有被设计成那样运行,它们没有自称是高性能的编程语言。它们不是,它们只是命令行解释器。因此,在这方面几乎没有进行优化。
此外,shell 在不同的进程中运行命令。这些构建块不共享公共内存或状态。当你在 C 中做 a fgets()orfputs()时,那是 stdio 中的一个函数。stdio 为所有 stdio 函数的输入和输出保留内部缓冲区,以避免过于频繁地执行昂贵的系统调用。
相应的偶数内置 shell 实用程序 ( read, echo, printf) 不能这样做。read旨在阅读一行。如果它读取超过换行符,则意味着您运行的下一个命令将错过它。因此read必须一次读取一个字节(如果输入是常规文件,则某些实现会进行优化,因为它们读取块并返回,但这仅适用于常规文件,bash例如仅读取 128 字节块,这是仍然比文本实用程序少很多)。
在输出端相同,echo不能只是缓冲其输出,它必须立即输出,因为您运行的下一个命令不会共享该缓冲区。
显然,按顺序运行命令意味着您必须等待它们,这是一个小小的调度程序舞蹈,可以从外壳程序控制到工具,然后返回。这也意味着(与在管道中使用长时间运行的工具实例相反)您不能在可用时同时利用多个处理器。
在我的快速测试中,在该while read循环和(据称)等效的循环之间,cut -c3 < file我的测试中的 CPU 时间比率约为 40000(一秒与半天)。但即使您只使用 shell 内置函数:
while read line; do
echo ${line:2:1}
done
Run Code Online (Sandbox Code Playgroud)
(此处为bash),仍约为 1:600(一秒对 10 分钟)。
很难使代码正确。我给出的例子在野外经常看到,但它们有很多错误。
read是一个方便的工具,可以做很多不同的事情。它可以读取用户的输入,将其拆分为单词以存储在不同的变量中。 read line并没有读取一行输入的,也可能是在一个非常特殊的方式读取一行。它实际上从输入中读取单词,这些单词由这些单词分隔,$IFS并且可以使用反斜杠来转义分隔符或换行符。
使用默认值$IFS, 在如下输入上:
foo\/bar \
baz
biz
Run Code Online (Sandbox Code Playgroud)
read line将存储"foo/bar baz"到$line,而不是" foo\/bar \"您所期望的。
要阅读一行,您实际上需要:
IFS= read -r line
Run Code Online (Sandbox Code Playgroud)
这不是很直观,但这就是它的方式,请记住,shell 不应该这样使用。
对于echo. echo扩展序列。您不能将它用于任意内容,例如随机文件的内容。你需要printf这里。
当然,有一种典型的忘记引用每个人都会陷入的变量的情况。所以更多的是:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Run Code Online (Sandbox Code Playgroud)
现在,还有一些警告:
zsh,如果输入包含 NUL 字符,则它不起作用,而至少 GNU 文本实用程序不会有问题。printf也没有反映它们在退出状态下无法写入标准输出。如果我们想解决上面的一些问题,那就变成:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Run Code Online (Sandbox Code Playgroud)
这变得越来越不清晰。
通过参数将数据传递给命令或在变量中检索它们的输出还有许多其他问题:
-(或+有时)开头时作为选项expr,test...当您开始使用 shell变量和命令参数时,您就进入了一个雷区。
如果您忘记引用您的变量,忘记选项标记的结尾,在具有多字节字符的语言环境中工作(这些天的规范),您肯定会引入迟早会成为漏洞的错误。
待定
Bru*_*ger 47
就概念和易读性而言,shell 通常对文件感兴趣。它们的“可寻址单元”是文件,“地址”是文件名。Shell 具有各种测试文件存在、文件类型、文件名格式(从 globbing 开始)的方法。Shell 很少有用于处理文件内容的原语。Shell 程序员必须调用另一个程序来处理文件内容。
由于文件和文件名方向,正如您所指出的,在 shell 中进行文本操作确实很慢,而且还需要一种不清楚和扭曲的编程风格。
Lau*_*haw 28
有一些复杂的答案,为我们中间的极客提供了许多有趣的细节,但它确实非常简单 - 在 shell 循环中处理大文件太慢了。
我认为提问者对一种典型的 shell 脚本感兴趣,它可能会从一些命令行解析、环境设置、检查文件和目录以及更多的初始化开始,然后再开始其主要工作:通过一个大的面向行的文本文件。
对于第一部分 ( initialization),shell 命令很慢通常并不重要——它只运行几十个命令,可能还有几个短循环。即使我们低效地编写了那部分,完成所有初始化通常只需要不到一秒钟的时间,这很好 - 它只发生一次。
但是当我们开始处理可能有数千或数百万行的大文件时,shell 脚本为每一行花费一秒钟的时间(即使只有几十毫秒)是不好的,因为这可能会加起来几个小时。
这就是我们需要使用其他工具的时候,Unix shell 脚本的美妙之处在于它们让我们很容易做到这一点。
我们需要通过命令管道传递整个文件,而不是使用循环来查看每一行。这意味着,shell 只调用一次,而不是调用数千或数百万次命令。这些命令确实会有循环来逐行处理文件,但它们不是 shell 脚本,它们旨在快速高效。
Unix 有许多很棒的内置工具,从简单到复杂,我们可以用它们来构建我们的管道。我通常会从简单的开始,只有在必要时才使用更复杂的。
我还会尝试坚持使用大多数系统上可用的标准工具,并尽量保持我的使用可移植性,尽管这并不总是可行的。如果你最喜欢的语言是 Python 或 Ruby,也许你不会介意确保它安装在你的软件需要运行的每个平台上的额外工作:-)
简单的工具包括head, tail, grep, sort, cut, tr, sed, join(合并 2 个文件时)和awk单行工具等等。有些人可以用模式匹配和sed命令做些什么,真是太神奇了。
当它变得更复杂,并且您确实必须对每一行应用一些逻辑时,这awk是一个不错的选择 - 一个单行(有些人将整个 awk 脚本放在“一行”中,尽管这不是很可读)或在一个简短的外部脚本。
作为awk一种解释性语言(如您的 shell),它可以如此高效地进行逐行处理是令人惊奇的,但它是为此专门构建的,而且速度非常快。
然后还有Perl大量其他非常擅长处理文本文件的脚本语言,并且还带有许多有用的库。
最后,如果您需要最大的速度和高度的灵活性(尽管文本处理有点乏味),还有很好的旧 C。但是,为您遇到的每个不同的文件处理任务编写新的 C 程序可能是对时间的一种非常糟糕的利用。我经常使用 CSV 文件,所以我用 C 编写了几个通用实用程序,我可以在许多不同的项目中重用它们。实际上,这扩展了我可以从我的 shell 脚本调用的“简单、快速的 Unix 工具”的范围,因此我可以只通过编写脚本来处理大多数项目,这比每次编写和调试定制的 C 代码要快得多!
一些最后的提示:
export LANG=C,否则许多工具会将你的普通 ASCII 文件视为 Unicode,使它们慢得多export LC_ALL=C如果您想sort产生一致的排序,无论环境如何,也可以考虑设置!sort你的数据,那可能比其他所有东西都需要更多的时间(和资源:CPU、内存、磁盘),所以尽量减少sort命令的数量和它们正在排序的文件的大小F. *_*uri 15
Stéphane Chazelas的正确答案是基于外壳的每个文本操作委托给特定的二进制数据,像观grep,awk,sed和其他人。
作为 bash能够自己做很多事情,所以放下叉子可能会变得更快(甚至比运行另一个解释器来完成所有工作)。
例如,看看这篇文章:
和
测试和比较...
没有考虑用户输入和安全性!
不要在bash下编写 web 应用程序!!
但是对于许多服务器管理任务,可以使用bash代替shell,使用内置 bash 可能非常有效。
编写bin utils 之类的工具与系统管理不同。
所以不是同一个人!
当系统管理员必须知道shell,他们可以写原型通过使用他的最佳(和最有名的)工具。
如果这个新实用程序(原型)真的有用,其他一些人可以使用一些更合适的语言开发专用工具。
| 归档时间: |
|
| 查看次数: |
54743 次 |
| 最近记录: |