【问题标题】:Insert a date in a column using awk使用 awk 在列中插入日期
【发布时间】:2015-04-24 23:45:36
【问题描述】:

我正在尝试格式化 csv 列中的日期。

输入类似于:28 April 1966

我想要这个输出:1966-04-28

可以通过以下代码获取:

date -d "28 April 1966" +%F

所以现在我想混合 awk 和这段代码来格式化整个列,但我不知道如何。

编辑:

输入示例:(分隔符“|”实际上是制表符)

1 | 28 April 1966
2 | null
3 | null
4 | 30 June 1987 

预期输出:

1 | 1966-04-28
2 | null
3 | null
4 | 30 June 1987

【问题讨论】:

  • 嗯,是的,可以使用 awk。但是您的输入文件看起来如何? mktime 可以提供帮助,例如 stackoverflow.com/a/6820700/1983854
  • 其他脚本语言应该没问题。我只需要正确格式化它才能导入数据库
  • 请注意,我必须在数十亿个条目上运行它...

标签: bash date awk


【解决方案1】:

一个简单的方法是

awk -F '\\| ' -v OFS='| ' '{ cmd = "date -d \"" $3 "\" +%F 2> /dev/null"; cmd | getline $3; close(cmd) } 1' filename

即:

{
  cmd = "date -d \"" $3 "\" +%F 2> /dev/null"  # build shell command
  cmd | getline $3                             # run, capture output
  close(cmd)                                   # close pipe
}
1                                              # print

这是有效的,因为如果日期无效,date 不会在其标准输出中打印任何内容,因此getline 失败并且$3 不会更改。

注意事项:

  1. 对于非常大的文件,这将在这些 shell 中生成大量 shell 和进程(每行一个)。这可能会显着拖累性能。
  2. 小心代码注入。如果 CSV 文件来自不可靠的来源,这种方法很难防御攻击者,您最好还是走很长的路,手动解析日期使用 gawk 的 mktimestrftime

EDIT re:comment:要将制表符用作分隔符,可以将命令更改为

awk -F '\t' -v OFS='\t' '{ cmd = "date -d \"" $3 "\" +%F 2> /dev/null"; cmd | getline $3; close(cmd) } 1' filename

编辑回复:评论 2: 如果性能令人担忧,那么为每一行生成进程并不是一个好方法。在这种情况下,您必须手动进行解析。例如:

BEGIN {
  OFS = FS

  m["January"  ] =  1
  m["February" ] =  2
  m["March"    ] =  3
  m["April"    ] =  4
  m["May"      ] =  5
  m["June"     ] =  6
  m["July"     ] =  7
  m["August"   ] =  8
  m["September"] =  9
  m["October"  ] = 10
  m["November" ] = 11
  m["December" ] = 12
}

$3 !~ /null/ {
  split($3, a, " ")
  $3 = sprintf("%04d-%02d-%02d", a[3], m[a[2]], a[1])
}
1

把它放在一个文件中,比如foo.awk,然后运行awk -F '\t' -f foo.awk filename.csv

【讨论】:

  • 我会删除我的答案,因为它与你的相同,但我今天已经从这件事中删除了太多:(
  • 呃,你实在是太快了…… close(cmd) 有什么用?日期无论如何都会退出,不是吗?
  • 非常感谢您的快速回答;)只有一件事:我没有“|”作为分隔符,而是“\t”(制表符)。请问可以编辑吗?? :D
  • @Camusensei 管道在记录之间持续存在,因此如果您为每一行打开一个管道但从不关闭它们,您将面临文件描述符用完的风险。见gnu.org/software/gawk/manual/html_node/…
  • @Piloumpicou 只需将FSOFS 设置为\t 即可。见编辑。
【解决方案2】:

这应该适用于您给定的输入

awk -F'\\|' -vOFS="|" '!/null/{cmd="date -d \""$3"\" +%F";cmd | getline $3;close(cmd)}1' file

输出

| 1 |1966-04-28
| 2 | null
| 3 | null
| 4 |1987-06-30

【讨论】:

    【解决方案3】:

    我建议使用支持解析日期的语言,例如 perl:

    $ cat file
    1       28 April 1966
    2       null
    3       null
    4       30 June 1987
    $ perl -F'\t' -MTime::Piece -lane 'print "$F[0]\t", 
      $F[1] eq "null" ? $F[1] : Time::Piece->strptime($F[1], "%d %B %Y")->strftime("%F")' file
    1       1966-04-28
    2       null
    3       null
    4       1987-06-30
    

    Time::Piece 核心模块允许您使用strftime 的标准格式说明符来解析和格式化日期。如果第二个字段不为“null”,则此解决方案将输入拆分为制表符并修改格式。

    这种方法比使用system 调用或调用子进程要快得多,因为一切都在本机 perl 中完成。

    【讨论】:

    • 我总是喜欢使用适当解析器的解决方案,很高兴知道 Perl 的 Time::Piece 可以优雅地处理 Epoch 之前的日期——我发现 gawk 的 strftime 不能。 :P
    【解决方案4】:

    这是在纯 BASH 中执行此操作并避免从 awk 调用 systemgetline 的方法:

    while IFS=$'\t' read -ra arr; do 
       [[ ${arr[1]} != "null" ]] && arr[1]=$(date -d "${arr[1]}" +%F)
       printf "%s\t%s\n" "${arr[0]}" "${arr[1]}"
    done < file
    
    1       1966-04-28
    2       null
    3       null
    4       1987-06-30
    

    【讨论】:

    • 纯 bash 很好,但您仍然多次调用昂贵的“日期”。来自 awk 的 system() 或 |getline 并不昂贵,没有理由不使用它们。
    • system() 或 |getline from awk 并不昂贵date 很昂贵?仅供参考 getline 最终调用 date 的次数与此 BASH 解决方案一样多。
    • 从 shell 调用 date 或在 awk 中通过 system 之间的区别在这里并不重要。这种方法与当前接受的答案之间的主要区别是您使用while read 循环来读取文件,而不是 awk,因此如果有的话,这可能会更慢。
    • @TomFenech:不,我从来没有真正声称这会更快(但仍然认为它不会比 awk 慢)。我刚刚回答了一个错误的假设,即此解决方案调用date 命令比awk 解决方案更多。
    • @TomFenech:我使用time 进行了快速测试,这个解决方案的运行速度总是比awk 快,我能理解原因。 awk 必须在内部运行 while 循环以循环遍历文件,并且 BASH 是比 awk 更低级别的语言,因此代码在 BASH 中似乎做得更多,但速度更快。
    【解决方案5】:

    只有一个日期调用,不会出现代码注入问题,请参见以下内容:

    此脚本将日期(使用 awk)提取到一个临时文件中,使用一次“日期”调用对其进行处理,然后将结果合并回来(使用 awk)。

    代码

    awk -F '\t' 'match($3,/null/) { $3 = "0000-01-01" } { print $3 }' input > temp.$$
    date --file=temp.$$ +%F > dates.$$
    awk -F '\t' -v OFS='\t' 'BEGIN {
                               while ( getline < "'"dates.$$"'" > 0 )
                               {
                                  f1_counter++
                                  if ($0 == "0000-01-01") {$0 = "null"}
                                  date[f1_counter] = $0
                               }
                             }
                             {$3 = date[NR]}
                             1' input.$$
    

    单线使用 bash 进程重定向(无临时文件):

    inputfile=/path/to/input
    awk -F '\t' -v OFS='\t' 'BEGIN {while ( getline < "'<(date -f <(awk -F '\t' 'match($3,/null/) { $3 = "0000-01-01" } { print $3 }' "$inputfile") +%F)'" > 0 ){f1_counter++; if ($0 == "0000-01-01") {$0 = "null"}; date[f1_counter] = $0}}{$3 = date[NR]}1' "$inputfile"
    

    详情

    它的使用方法如下:

    # configuration
    input=/path/to/input
    temp1=temp.$$
    temp2=dates.$$
    output=output.$$
    # create the sample file (optional)
    #printf "\t%s\n" $'1\t28 April 1966' $'2\tnull' $'3\tnull'  $'4\t30 June 1987' > "$input"
    # Extract all dates
    awk -F '\t' 'match($3,/null/) { $3 = "0000-01-01" } { print $3 }' "$input" > "$temp1"
    # transform the dates
    date --file="$temp1" +%F > "$temp2"
    # merge csv with transformed date
    awk -F '\t' -v OFS='\t' 'BEGIN {while ( getline < "'"$temp2"'" > 0 ){f1_counter++; if ($0 == "0000-01-01") {$0 = "null"}; date[f1_counter] = $0}}{$3 = date[NR]}1' "$input" > "$output"
    # print the output
    cat "$output"
    # cleanup
    rm "$temp1" "$temp2" "$output"
    #rm "$input"
    

    注意事项

    • 使用“0000-01-01”作为无效(空)日期的临时占位符
    • 代码应该比其他调用“date”的方法快很多次,但它读取输入文件两次。

    【讨论】:

      猜你喜欢
      • 2019-06-29
      • 1970-01-01
      • 1970-01-01
      • 2011-12-20
      • 1970-01-01
      • 2015-06-09
      • 1970-01-01
      • 2014-09-05
      • 2017-04-09
      相关资源
      最近更新 更多