【问题标题】:Split file into multiple files one row at a time将文件拆分为多个文件,一次一行
【发布时间】:2021-03-04 21:45:37
【问题描述】:

我有一个日志文件(大约 50K 行),格式为:

email1@gmail.com:address0:some_details0
email2@gmail.com:address1:some_details1
email1@yahoo.com:address2:some_details2
email2@yahoo.com:address3:some_details3

我正在尝试读取此文件并将其拆分为两个文件夹(gmail.com 和 yahoo.com),然后将每一行写入以电子邮件 ID 命名的唯一文件。我下面的代码有效,但速度很慢。有人可以帮我让它更快更有效吗?将不胜感激。

#/bin/sh
grep -hv -P "[^[:ascii:]]" * |
awk -F":" '
    {
        if ($1 ~ /^[[:alnum:]_.+-]+@[[:alnum:]_.-]+\.[[:alnum:]]/ && NF>1 && $NF!="")
        {
            split($1, arr, "@")
            system("mkdir -p "tolower(arr[2]))
            print $0 >> tolower(arr[2])"/"tolower(arr[1])
        }
    }'

PS:正则表达式是确保电子邮件地址有效的基本检查。我没有做过重的检查。起初我认为正则表达式使我的代码变慢,但并非如此。即使没有正则表达式,代码也非常慢。我认为 I/O 使这变慢了。我们如何改进?

【问题讨论】:

    标签: bash awk grep cut


    【解决方案1】:

    像这样:

    awk -F'[@:]' '{system("mkdir -p \047"$2"\047");f=$2"/"$1;print>>f;close(f)}' file
    

    -F'[@:]' 将输入字段分隔符设置为@:,从而使用以下记录:

    email1 gmail.com address0 some_details0
    email2 gmail.com address1 some_details1
    email1 yahoo.com address2 some_details2
    email2 yahoo.com address3 some_details3
    

    当记录像这样拆分时,输出文件名只是第二个字段 + '/' + 第一个字段。 print >> $2"/"$1 会将当前记录附加到该文件中。如果它不存在,awk 将创建它。

    close(f) 使用它来确保当输入文件包含(太多)不同的域并因此输出文件时我们不会用完文件描述符。

    【讨论】:

      【解决方案2】:

      它主要是生成一个新的子shell,每个输入行调用一次 mkdir,这使您的代码运行如此缓慢。改为这样做:

      filename = tolower(arr[1])
      dirname = tolower(arr[2])
      if ( !seen[dirname]++ ) {
          system("mkdir -p \047" dirname "\047")
      }
      print > (dirname "/" filename)
      

      所以你只生成一个子shell 来为每个目录调用一次 mkdir。

      请注意,除非您使用 GNU awk,否则当您创建大约十几个输出文件时,您会遇到“打开的文件过多”错误,即使使用 GNU awk,您拥有的输出文件越多,它也会变得越慢开放,因此也可能会影响您的代码性能。常见的解决方案是先按电子邮件地址对输入文件进行排序,然后在每次电子邮件地址(新的输出文件名)更改时关闭当前输出文件。

      鉴于此,这就是我真正编写程序的方式:

      #!/usr/bin/env bash
      
      grep -hv -P '[^[:ascii:]]' "${@:--}" |
      sort -t':' -k1,1 -s |
      awk -F':' '
          !($1 ~ /^[[:alnum:]_.+-]+@[[:alnum:]_.-]+\.[[:alnum:]]/ && NF>1 && $NF!="") { next }
          { curr = tolower($1) }
          curr != prev {
              close(out)
              split(curr, arr, /@/)
              filename = arr[1]
              dirname = arr[2]
              if ( !seen[dirname]++ ) {
                  system("mkdir -p \047" dirname "\047")
              }
              out = dirname "/" filename
              prev = $1
          }
          { print > out }
      '
      

      我将上面的 GNU 排序用于-s 用于“稳定排序”,如果您没有该排序并且关心输出中保留给定电子邮件地址的输入行的相对顺序,还有其他方法可以处理它,例如awk -v OFS=':' '{print NR, $0}' | sort -t':' -k2,2 -k1,1n | cut -d':' -f2-.

      【讨论】:

      • 你好@Ed。感谢您再次提供帮助。我收到一个错误:grep: Invalid character class name
      • 啊,这就是你在 grep 调用中使用 -P 的原因。我刚刚复制了你的代码,但删除了-P,因为我没有看到它有什么用处,我现在把它加回来了。
      猜你喜欢
      • 1970-01-01
      • 2016-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多