【问题标题】:Issue with using While loop in bash script (to split a file into multiple files)在 bash 脚本中使用 While 循环的问题(将文件拆分为多个文件)
【发布时间】:2016-10-13 19:57:22
【问题描述】:

我需要从一个文件中读取数据并插入到多个文件中(每个文件的大小小于 3mb,文件大小可以不同)。重要的是 - 代理的记录不应该被拆分到多个文件中。我在 UNIX bash 脚本的 While 循环中执行所有这些操作。

Input.csv
        Src,AgentNum,PhoneNum
        DWH,Agent_1234,phone1  
        NULL,NULL,phone2  
        NULL,NULL,phone3 
        DWH,Agent_5678,phone1 
        NULL,NULL,phone2 
        NULL,NULL,phone3
        DWH,Agent_9999,phone1 
        NULL,NULL,phone2 
        NULL,NULL,phone3

Desired Output -

Output1.csv (less than 3MB)
        Src,AgentNum,PhoneNum
        DWH,Agent_1234,phone1  
        NULL,NULL,phone2  
        NULL,NULL,phone3

Output2.csv (less than 3MB)
        Src,AgentNum,PhoneNum
        DWH,Agent_5678,phone1 
        NULL,NULL,phone2 
        NULL,NULL,phone3
        DWH,Agent_9999,phone1 
        NULL,NULL,phone2 
        NULL,NULL,phone3

Bash Shell 脚本

#!/bin/bash
BaseFileName=$(basename $FileName | cut -d. -f1)
Header=`head -1 $FileName`
MaxFileSize=$(( 3 * 1024 * 1024 ))

    sed 1d $FileName | 
    while read -r line
    do
        echo $line >> ${BaseFileName}_${FileSeq}.csv

        MatchCount=`echo $line | grep -c -E '^.DWH'`

        if [[ $MatchCount -eq 1 ]]
        then
            FileSizeBytes=`du -b ${BaseFileName}_${FileSeq}.csv | cut -f1`
            if [[ $FileSizeBytes -gt $MaxFileSize ]] 
            then
                #Add a header record to each file
                sed -i "1i ${Header}" ${BaseFileName}_${FileSeq}.csv
                FileSeq=$((FileSeq + 1))
            fi
        fi
    done 

它几乎可以正常工作,除了 1)它没有按预期拆分记录(代理的一些记录被拆分到多个文件中) 2)它只为第一个输出文件插入标题记录。 3)太慢了,一个10MB的文件需要3分钟。实际上我有一个 3GB 的文件。

有人可以建议我在哪里做错了。 有没有更好的方法来处理这个问题?

【问题讨论】:

  • 很多更好的方法来处理它,是的。在这里使用du 效率非常低,因为您可以只在内部维护一个字节计数器。
  • echo $line 只是普通的越野车。见BashPitfalls#14
  • 另外,修复shellcheck.net 发现的错误 - 通常,作为在此处提问之前的练习。
  • (并且将>> 放在echos 上是低效的——这意味着您每次想要写一行时都重新打开输出文件)。
  • 顺便说一句——要清楚,每行前面的标签是文字吗? (如果没有,为什么在正则表达式中DWH 前面有一个通配符?)

标签: linux bash shell unix sed


【解决方案1】:

这是一个粗略的尝试——它没有纯awk 解决方案那么快,但它比你已经拥有的要快得多,很多

#!/bin/bash

# two external parameters: input file name, and max size in bytes (default to 3MB)
InputFile=$1
MaxFileSize=${2:-$(( 3 * 1024 * 1024 ))}

BaseName=${InputFile%.*} # strip extension
Ext=${InputFile##*.}     # store extension
FileSeq=0                # start output file at sequence 0

# redirect stdin from the input file, stdout to the first output file
exec <"$InputFile" || exit
exec >"${BaseName}.${FileSeq}.${Ext}" || exit

# read the header; copy it to the first output file, and initialize CurFileSize
IFS= read -r Header || exit
printf '%s\n' "$Header" || exit
CurFileSize=$(( ${#Header} + 1 ))

# ...then loop over our inputs, and copy appropriately
while IFS= read -r line; do
  if [[ $line = DWH,* ]] && (( CurFileSize > MaxFileSize )); then
    (( FileSeq++ ))
    exec >"${BaseName}.${FileSeq}.${Ext}" || exit
    printf '%s\n' "$Header" || exit
    CurFileSize=$(( ${#Header} + 1 ))
  fi
  printf '%s\n' "$line" || exit
  (( CurFileSize += ${#line} + 1 ))
done

值得注意的变化:

  • 根本没有调用任何外部工具。没有sed,没有basename,没有du,没有grep。任何时候你写$()``,都会有非常重要的性能成本;除非无法避免,否则不应在紧密循环中使用这些构造——并且当使用 POSIX sh 标准的 ksh 或 bash 扩展时,它们实际上是无法避免的,这种情况很少见。
  • 仅当需要打开新的输出文件时才调用重定向。我们不是每次想写一行时都使用&gt;&gt;"$filename",而是每次需要启动新的输出文件时使用exec &gt;"$filename"
  • 在参数扩展期间始终使用引号,除非在字符串拆分或通配被其他语法明确禁止的情况下。不这样做可能会损坏您的文件(例如,用当前目录中的文件列表替换*;用空格替换制表符等)。如有疑问,请引用更多。
  • POSIX 标准对使用printf '%s\n' 的定义比echo 更好——请参阅the standard definition for echo,尤其是应用程序使用部分。
  • 我们正在明确地进行错误处理。也可以使用set -e,但也可以使用substantial caveats

测试过程和输出如下:

$ cat >input.csv <<'EOF'
Src,AgentNum,PhoneNum
DWH,Agent_1234,phone1
NULL,NULL,phone2
NULL,NULL,phone3
DWH,Agent_5678,phone1
NULL,NULL,phone2
NULL,NULL,phone3
DWH,Agent_9999,phone1
NULL,NULL,phone2
NULL,NULL,phone3
EOF

$ ./splitCSV input.csv 100  ## split at first boundary after 100 bytes

$ cat input.0.csv
Src,AgentNum,PhoneNum
DWH,Agent_1234,phone1
NULL,NULL,phone2
NULL,NULL,phone3
DWH,Agent_5678,phone1
NULL,NULL,phone2
NULL,NULL,phone3

$ cat input.1.csv
Src,AgentNum,PhoneNum
DWH,Agent_9999,phone1
NULL,NULL,phone2
NULL,NULL,phone3

【讨论】:

  • 非常感谢!!用于脚本和有价值的 cmets。快速提问 - 实际上,在我的输入文件中,evey 代理记录以“DWH”开头,我如何在 if 条件下使用它?
  • [[ $line = DWH,* ]] 还没有充分测试这种情况吗?
  • 如果我按原样运行您的脚本,它只是复制文件(而不是进行拆分)。我的猜测是它可能与第一列中 DWH 周围的双引号有关。请指教。
  • 哦——不清楚双引号是不是字面的;我以为您是在使用它们来将您要询问的字符串与其他英语文本分隔开来,因此字母 DWH 就是现在/曾经存在的内容。以后请使用反引号,即。 "DWH",表示 StackOverflow 文本格式中的文字字符串。无论如何,在这种情况下,它将是[[ $line = '"DWH",'* ]]
  • 它现在就像一个魅力。你刚刚拯救了我的一天。非常感谢!!!您对使用 BashPitfalls 和 shellcheck.net 的建议也很有帮助。
猜你喜欢
  • 2013-03-10
  • 1970-01-01
  • 1970-01-01
  • 2021-09-22
  • 2020-02-17
  • 2015-05-20
  • 2014-10-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多