如何更有效地在bash中过滤文本数据

问题描述 投票:0回答:5

我有需要使用 bash 脚本过滤的数据文件,请参阅数据示例:

name=pencils
name=apples
value=10
name=rocks
value=3
name=tables
value=6
name=beds
name=cups
value=89

我需要像apples=10那样对名称值对进行分组,如果当前行以名称开头,下一行以名称开头,则应完全省略第一行。所以结果文件应该如下所示:

apples=10
rocks=3
tables=6
cups=89

我提出了这个简单的解决方案,它可以工作,但速度非常慢,需要 5 分钟才能完成 2000 行的文件。

VALUES=$(cat input.txt)
for x in $VALUES; do
  if [[ -n $(echo $x | grep 'name=') ]]; then
    name=$(echo $x | sed "s/name=//")
  elif [[ -n $(echo $x | grep 'value=') ]]; then
    value=$(echo $x | sed "s/value=//")
    echo "${name}=${value}" >> output.txt
  fi
done

我知道这种任务不太适合bash,但是脚本已经写好了,这只是其中的一小部分。

如何在 bash 中优化此任务?

bash
5个回答
4
投票

不要在子 shell 中运行任何命令,这会大大减慢脚本速度。您可以在当前 shell 中执行所有操作。

#! /bin/bash
while IFS== read k v ; do
    if [[ $k == name ]] ; then
        name=$v
    elif [[ $k == value ]] ; then
        printf '%s=%s\n' "$name" "$v"
    fi
done

2
投票

您可以进行三种简单的优化,这将大大加快脚本速度,而无需重新思考。

1.将
for
替换为
while read

input.txt
加载到字符串中,然后使用
for x in $VALUES
循环该字符串,速度很慢。它需要将整个文件读入内存,即使此任务可以以流式方式完成,一次读取一行。

for line in $(cat file)
的常见替代品是
while read line; do ... done < file
。事实证明,循环是复合命令,就像我们习惯的普通单行命令一样,复合命令可以有
<
>
重定向。将文件重定向到循环意味着在循环期间,stdin 来自文件。因此,如果您在循环内调用
read line
,那么每次迭代都会读取一行。

while IFS= read -r x; do
  if [[ -n $(echo $x | grep 'name=') ]]; then
    name=$(echo $x | sed "s/name=//")
  elif [[ -n $(echo $x | grep 'value=') ]]; then
    value=$(echo $x | sed "s/value=//")
    echo "${name}=${value}" >> output.txt
  fi
done < input.txt

2.在循环外重定向输出

不仅仅是可以重定向输入。我们可以对

>> output.txt
重定向做同样的事情。在这里您将看到最大的加速。当
>> output.txt
位于循环内部时,每次迭代都必须打开和关闭
output.txt
,这非常慢。将其移至外部意味着只需打开一次。快得多、快得多。

while IFS= read -r x; do
  if [[ -n $(echo $x | grep 'name=') ]]; then
    name=$(echo $x | sed "s/name=//")
  elif [[ -n $(echo $x | grep 'value=') ]]; then
    value=$(echo $x | sed "s/value=//")
    echo "${name}=${value}"
  fi
done < input.txt > output.txt

3. shell字符串处理

最后一项改进是使用更快的字符串处理。调用

grep
需要每次分叉一个子进程,只是为了进行简单的字符串分割。如果我们可以仅使用 shell 结构来进行字符串分割,那么速度会快很多。好吧,既然我们已经切换到
read
,那么这很容易。
read
可以做的不仅仅是阅读整行;它还可以在变量
$IFS
(字段间分隔符)的分隔符上进行拆分。

while IFS='=' read -r key value; do
  case "$key" in
    name) name="$value";;
    value) echo "$name=$value";;
  fi
done < input.txt > output.txt

进一步阅读


1
投票

如果您有性能问题,请根本不要使用 bash。使用文本处理工具,例如

awk
:

$ awk -F= '{name = $2} $1 == "value" {print name "=" $2}' data.txt 
apples=10
rocks=3
tables=6
cups=89

说明:

-F=
定义字段分隔符为字符
=
。仅当一行的第一个字段 (
$1
) 等于字符串
value
时,才会执行第一个块。它打印变量
name
,后跟字符
=
和第二个字段 (
$2
)。第二个块在每一行上执行,并将第二个字段 (
$2
) 存储在变量
name
中。

通常,如果您的输入与您显示的内容相似,则应自动跳过第一行。否则,我们可以使用

NR
变量的测试显式排除它,该变量的值是行号,从 1:

开始
awk -F= 'NR != 1 && $1 == "value" {print name "=" $2}
         NR != 1 {name = $2}' data.txt

所有这些都适用于您显示的输入,但不适用于您拥有其他类型的行或多个

value=...
连续行的输入。如果您确实想测试名称/值对是否位于连续的两行上,我们还需要更多内容。例如,测试第一个字段是否为
name
并使用另一个变量
n
来存储最后遇到的
name=...
行的行号。通过所有这些测试,我们现在可以以稍微更直观的顺序放置这 2 个块(但相反的顺序也是一样的):

awk -F= 'NR != 1 && $1 == "name" {name = $2; n = NR}
         NR != 1 && NR == n+1 && $1 == "value" {print name "=" $2}' data.txt

0
投票

使用 awk 可能有一个更优雅的解决方案,但你可以:

awk 'BEGIN{RS="\n?name=";FS="\nvalue="} {if($2) printf "%s=%s\n",$1,$2}' inputs.txt

RS="\n?name="
表示记录分隔符是
name=

FS="\nvalue="
表示每条记录的字段分隔符是
value=

if($2)
表示仅在第二个字段存在时才继续打印


0
投票

为什么要为 IFS 烦恼呢?想想 JGE(足够好)和 KISS(保持简单,愚蠢)——shell 变量扩展无论如何都可以完成这项工作...

#!/usr/bin/env bash

# Keep shellcheck happy
declare line='' name=''

while read line ; do
  case "$line" in
    name=*)  name="${line%=*}"        ;;
    value=*) echo "$name=${line#*=}" ;;
  esac
done < input.txt > output.txt

...并且看不到子外壳;-)

© www.soinside.com 2019 - 2024. All rights reserved.