使用 grep 或 sed 从日志中提取电子邮件地址

Extract email addresses from log with grep or sed

Jan 23 00:46:24 portal postfix/smtp[31481]: 1B1653FEA1: to=<wanted1918_ke@yahoo.com>, relay=mta5.am0.yahoodns.net[98.138.112.35]:25, delay=5.4, delays=0.02/3.2/0.97/1.1, dsn=5.0.0, status=bounced (host mta5.am0.yahoodns.net[98.138.112.35] said: 554 delivery error: dd This user doesn't have a yahoo.com account (wanted1918_ke@yahoo.com) [0] - mta1321.mail.ne1.yahoo.com (in reply to end of DATA command))
Jan 23 00:46:24 portal postfix/smtp[31539]: AF40C3FE99: to=<devi_joshi@yahoo.com>, relay=mta7.am0.yahoodns.net[98.136.217.202]:25, delay=5.9, delays=0.01/3.1/0.99/1.8, dsn=5.0.0, status=bounced (host mta7.am0.yahoodns.net[98.136.217.202] said: 554 delivery error: dd This user doesn't have a yahoo.com account (devi_joshi@yahoo.com) [0] - mta1397.mail.gq1.yahoo.com (in reply to end of DATA command))

我想从上面的邮件日志中提取 angular 方括号 < ... > 中包含的电子邮件地址,例如。 to=<wanted1918_ke@yahoo.com>wanted1918_ke@yahoo.com

我正在使用 cut -d' ' -f7 来提取电子邮件,但我很好奇是否有更灵活的方法。

使用 GNU grep,只需使用包含后视和前视的正则表达式:

$ grep -Po '(?<=to=<).*(?=>)' file
wanted1918_ke@yahoo.com
devi_joshi@yahoo.com

这是说:嘿,提取所有以 to=< 开头并后跟 > 的字符串。

您可以这样使用 awk

awk -F'to=<|>,' '{print }' the.log

我按 to=<>, 拆分行并打印第二个字段。

awk -F'[<>]' '{print }' file

wanted1918_ke@yahoo.com
devi_joshi@yahoo.com

只是为了显示 sed 替代方案(由于 -E,需要 GNU 或 BSD/macOS sed):

sed -E 's/.* to=<(.*)>.*//' file

请注意正则表达式必须如何匹配 整个 行,以便 capture-group 匹配(电子邮件地址)的替换产生 only 匹配。

A 稍微 更有效 - 但可能不太可读 - 变化是
sed -E 's/.* to=<([^>]*).*//' file


由于 BRE 所需的遗留语法(基本 正则表达式),POSIX-compliant 公式有点麻烦:

sed 's/.* to=<\(.*\)>.*//' file

的变体:

grep -Po ' to=<\K[^>]*' file

\K,它会丢弃匹配到该点的所有内容,不仅在语法上比 look-behind 断言更简单((?<=...),而且更灵活 - 它支持 变量-length 表达式 - 更快(尽管在许多 real-world 情况下这可能无关紧要;如果性能最重要:请参见下文)。


性能比较

以下是此页面上各种解决方案的性能比较。

请注意,这在许多用例中可能并不重要,但可以深入了解:

  • 各种标准实用程序的相对性能
  • 对于给定的实用程序,调整正则表达式会产生怎样的影响。

绝对值并不重要,但相对性能有望提供一些见解。请参阅生成这些数字的脚本的底部,这些数字是在 2012 年末的 27" iMac 运行 macOS 10.12.3 上获得的,使用通过复制问题中的示例输入创建的 250,000 行输入文件,平均每次运行 10 次的时间。

Mawk                            0.364s
GNU grep, \K, non-backtracking  0.392s
GNU awk                         0.830s
GNU grep, \K                    0.937s
GNU grep, (?>=...)              1.639s
BSD grep + cut                  2.733s
GNU grep + cut                  3.697s
BSD awk                         3.785s
BSD sed, non-backtracking       7.825s
BSD sed                         8.414s
GNU sed                         16.738s
GNU sed, non-backtracking       17.387s

几个结论:

  • 给定实用程序的具体实施很重要。
  • grep 通常是一个不错的选择,即使它需要与 cut
  • 结合使用
  • 调整正则表达式以避免回溯和 look-behind 断言会有所作为。
  • GNU sed 出奇地慢,而 GNU awk 比 BSD awk 快。奇怪的是,GNU sed.
  • 的(部分)non-backtracking 解决方案 较慢

这是生成上述时间的脚本;请注意,g 前缀的命令是 GNU 实用程序,它们是通过 Homebrew 安装在 macOS 上的;同样,mawk 是通过 Homebrew 安装的。

请注意,"non-backtracking" 仅部分 适用于某些命令。

#!/usr/bin/env bash

# Define the test commands.
test01=( 'BSD sed'                        sed -E 's/.*to=<(.*)>.*//' )
test02=( 'BSD sed, non-backtracking'      sed -E 's/.*to=<([^>]*).*//' )
# ---
test03=( 'GNU sed'                        gsed -E 's/.*to=<(.*)>.*//' )
test04=( 'GNU sed, non-backtracking'      gsed -E 's/.*to=<([^>]*).*//' )
# ---
test05=( 'BSD awk'                        awk  -F' to=<|>,' '{print }' )
test06=( 'GNU awk'                        gawk -F' to=<|>,' '{print }' )
test07=( 'Mawk'                           mawk -F' to=<|>,' '{print }' )
#--
test08=( 'GNU grep, (?>=...)'             ggrep -Po '(?<= to=<).*(?=>)' )
test09=( 'GNU grep, \K'                   ggrep -Po ' to=<\K.*(?=>)' )
test10=( 'GNU grep, \K, non-backtracking' ggrep -Po ' to=<\K[^>]*' )
# --
test11=( 'BSD grep + cut'                 "{ grep -o  ' to=<[^>]*' | cut  -d'<' -f2; }" )
test12=( 'GNU grep + cut'                 "{ ggrep -o ' to=<[^>]*' | gcut -d'<' -f2; }" )

# Determine input and output files.
inFile='file'
# NOTE: Do NOT use /dev/null, because GNU grep apparently takes a shortcut
#       when it detects stdout going nowhere, which distorts the timings.
#       Use dev/tty if you want to see stdout in the terminal (will print
#       as a single block across all tests before the results are reported).
outFile="/tmp/out.$$"
# outFile='/dev/tty'

# Make `time` only report the overall elapsed time.
TIMEFORMAT='%6R'

# How many runs per test whose timings to average.
runs=10

# Read the input file up to even the playing field, so that the first command
# doesn't take the hit of being the first to load the file from disk.
echo "Warming up the cache..."
cat "$inFile" >/dev/null

# Run the tests.
echo "Running $(awk '{print NF}' <<<"${!test*}") test(s), averaging the timings of $runs run(s) each; this may take a while..."
{
    for n in ${!test*}; do    
        arrRef="$n[@]"
        test=( "${!arrRef}" )
        # Print test description.
        printf '%s\t' "${test[0]}"
        # Execute test command.
        if (( ${#test[@]} == 2 )); then # single-token command? assume `eval` must be used.
          time for (( n = 0; n < runs; n++ )); do eval "${test[@]: 1}" < "$inFile" >"$outFile"; done
        else # multiple command tokens? assume that they form a simple command that can be invoked directly.
          time for (( n = 0; n < runs; n++ )); do "${test[@]: 1}" "$inFile" >"$outFile"; done
        fi
    done
} 2>&1 | 
  sort -t$'\t' -k2,2n | 
    awk -v runs="$runs" '
      BEGIN{FS=OFS="\t"} { avg = sprintf("%.3f", /runs); print , avg "s" }
    ' | column -s$'\t' -t