【问题标题】:How can I output only captured groups with sed?如何使用 sed 仅输出捕获的组?
【发布时间】:2011-02-16 04:21:01
【问题描述】:

有没有办法告诉sed 只输出捕获的组?

例如,给定输入:

This is a sample 123 text and some 987 numbers

和模式:

/([\d]+)/

我能否以反向引用格式化的方式仅获得 123 和 987 输出?

【问题讨论】:

  • 注意,组捕获需要sed 使用-E 标志打开扩展正则表达式。
  • 另请注意,sed -E 适用于 Max OSX 和 FreeBSD。如果您使用的是 GNU 发行版(或在 Git Bash 或 WSL 中),sed -r 也可以使用。如果您担心跨平台兼容性,请首选-E

标签: regex sed


【解决方案1】:

Sed 有多达九种记忆模式,但您需要使用转义括号来记忆正则表达式的部分内容。

有关示例和更多详细信息,请参阅 here

【讨论】:

  • sed -e 's/version=\(.+\)/\1/' input.txt 这仍然会输出整个 input.txt
  • @Pablo,在您的模式中,您必须编写 \+ 而不是 +。而且我不明白为什么人们只将-e 用于一个 sed 命令。
  • 使用sed -e -n 's/version=\(.+\)/\1/p' input.txt 见:mikeplate.com/2012/05/09/…
  • 我建议使用sed -E 来使用看起来更接近 Perl/Java/JavaScript/Go/任何风格的所谓“现代”或“扩展”正则表达式。 (与grep -Eegrep 比较。)默认语法有那些奇怪的转义规则,被认为是“过时的”。有关两者之间差异的更多信息,请运行man 7 re_format
【解决方案2】:

试试

sed -n -e "/[0-9]/s/^[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\).*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/p"

我在cygwin下得到了这个:

$ (echo "asdf"; \
   echo "1234"; \
   echo "asdf1234adsf1234asdf"; \
   echo "1m2m3m4m5m6m7m8m9m0m1m2m3m4m5m6m7m8m9") | \
  sed -n -e "/[0-9]/s/^[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\)[^0-9]*\([0-9]*\).*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/p"

1234
1234 1234
1 2 3 4 5 6 7 8 9
$

【讨论】:

    【解决方案3】:

    你可以使用 grep

    grep -Eow "[0-9]+" file
    

    【讨论】:

    • @ghostdog74:完全同意你的看法。如何让 greo 仅输出捕获的组?
    • @Michael - 这就是o 选项存在的原因 - unixhelp.ed.ac.uk/CGI/man-cgi?grep : -o, --only-matching 仅显示匹配 PATTERN 的匹配行部分
    • @Bert F:我理解匹配部分,但它不是捕获组。我想要的是这样 ([0-9]+).+([abc]{2,3}) 所以有 2 个捕获组。我只想通过反向引用或其他方式输出捕获组。
    • 你好迈克尔。您是否设法通过 grep 提取了第 n 个捕获的组?
    • @Pablo:grep 只输出匹配的内容。要给它多个组,请使用多个表达式:grep -Eow -e "[0-9]+" -e "[abc]{2,3}" 我不知道你怎么能要求这两个表达式除了来自前一个 grep 的管道之外的一行(如果任一模式匹配不止一次,这仍然无法工作在一条线上)。
    【解决方案4】:

    让这个工作的关键是告诉sed 排除你不想输出的内容并指定你想要的内容。

    string='This is a sample 123 text and some 987 numbers'
    echo "$string" | sed -rn 's/[^[:digit:]]*([[:digit:]]+)[^[:digit:]]+([[:digit:]]+)[^[:digit:]]*/\1 \2/p'
    

    这说:

    • 不要默认打印每一行 (-n)
    • 排除零个或多个非数字
    • 包括一位或多位数字
    • 排除一个或多个非数字
    • 包括一位或多位数字
    • 排除零个或多个非数字
    • 打印替换 (p)

    一般来说,在sed 中,您使用括号捕获组并使用反向引用输出您捕获的内容:

    echo "foobarbaz" | sed 's/^foo\(.*\)baz$/\1/'
    

    将输出“bar”。如果您使用-r(OS X 为-E)进行扩展正则表达式,则无需转义括号:

    echo "foobarbaz" | sed -r 's/^foo(.*)baz$/\1/'
    

    最多可以有 9 个捕获组及其反向引用。后面的引用按组出现的顺序编号,但它们可以按任何顺序使用并且可以重复:

    echo "foobarbaz" | sed -r 's/^foo(.*)b(.)z$/\2 \1 \2/'
    

    输出“a bar a”。

    如果你有 GNU grep(它也可以在 BSD 中工作,包括 OS X):

    echo "$string" | grep -Po '\d+'
    

    或变体,例如:

    echo "$string" | grep -Po '(?<=\D )(\d+)'
    

    -P 选项启用 Perl 兼容正则表达式。请参阅man 3 pcrepatternman 3 pcresyntax

    【讨论】:

    • 请注意,OSX Mountain Lion 不再支持 grep 中的 PCRE。
    • 附带说明,Solaris 9 不支持 grep -o 选项。此外,Solaris 9 不支持 sed -r 选项。 :(
    • 请您的系统管理员安装 gsed。你会惊讶于几个甜甜圈会给你带来什么......
    • 注意你可能需要在'('和')'前加上'\',我不知道为什么。
    • @lumbric:如果您指的是sed 示例,如果您使用-r 选项(或-E 对于OS X,IIRC),您不需要转义括号。区别在于基本正则表达式和扩展正则表达式 (-r)。
    【解决方案5】:

    我相信问题中给出的模式仅作为示例,目标是匹配 任何 模式。

    如果您有一个带有 GNU 扩展的 sed 允许在模式空间中插入换行符,一个建议是:

    > set string = "This is a sample 123 text and some 987 numbers"
    >
    > set pattern = "[0-9][0-9]*"
    > echo $string | sed "s/$pattern/\n&\n/g" | sed -n "/$pattern/p"
    123
    987
    > set pattern = "[a-z][a-z]*"
    > echo $string | sed "s/$pattern/\n&\n/g" | sed -n "/$pattern/p"
    his
    is
    a
    sample
    text
    and
    some
    numbers
    

    这些示例使用 tcsh(是的,我知道它的 shell 错误)和 CYGWIN。 (编辑:对于 bash,删除集合和 = 周围的空格。)

    【讨论】:

    • @Joseph:谢谢,但是,根据我的任务,我觉得 grep 更自然,就像 ghostdog74 建议的那样。只需要弄清楚如何让 grep 只输出捕获组,而不是整个匹配。
    • 只是一个注释,但加号“+”表示“一个或多个”,这样就无需在模式中重复自己。所以,“[0-9][0-9]*”会变成“[0-9]+”
    • @RandomInsano:为了使用+,您需要转义它或使用-r 选项(-E 对于 OS X)。您也可以使用\{1,\}(或-r-E,无需转义)。
    【解决方案6】:

    这不是 OP 要求的(捕获组),但您可以使用以下方法提取数字:

    S='This is a sample 123 text and some 987 numbers'
    echo "$S" | sed 's/ /\n/g' | sed -r '/([0-9]+)/ !d'
    

    提供以下内容:

    123
    987
    

    【讨论】:

      【解决方案7】:

      放弃使用 Perl

      既然sed 不切实际,我们就扔掉毛巾,用Perl,至少它是LSBgrep GNU 扩展不是:-)

      • 打印整个匹配部分,不需要匹配组或后视:

        cat <<EOS | perl -lane 'print m/\d+/g'
        a1 b2
        a34 b56
        EOS
        

        输出:

        12
        3456
        
      • 每行单个匹配,通常是结构化数据字段:

        cat <<EOS | perl -lape 's/.*?a(\d+).*/$1/g'
        a1 b2
        a34 b56
        EOS
        

        输出:

        1
        34
        

        向后看:

        cat <<EOS | perl -lane 'print m/(?<=a)(\d+)/'
        a1 b2
        a34 b56
        EOS
        
      • 多个字段:

        cat <<EOS | perl -lape 's/.*?a(\d+).*?b(\d+).*/$1 $2/g'
        a1 c0 b2 c0
        a34 c0 b56 c0
        EOS
        

        输出:

        1 2
        34 56
        
      • 每行有多个匹配项,通常是非结构化数据:

        cat <<EOS | perl -lape 's/.*?a(\d+)|.*/$1 /g'
        a1 b2
        a34 b56 a78 b90
        EOS
        

        输出:

        1 
        34 78
        

        向后看:

        cat EOS<< | perl -lane 'print m/(?<=a)(\d+)/g'
        a1 b2
        a34 b56 a78 b90
        EOS
        

        输出:

        1
        3478
        

      【讨论】:

      • 问题的结尾你没有得到什么:“with sed”?
      • @Moonchild Google 员工不在乎。
      • 我发现这很有用。并不是所有的命令行正则表达式问题都需要用 sed 来解决。
      【解决方案8】:

      运行数字

      此答案适用于任何数量的数字组。示例:

      $ echo 'Num123that456are7899900contained0018166intext' \
         | sed -En 's/[^0-9]*([0-9]{1,})[^0-9]*/\1 /gp'
      
      123 456 7899900 0018166
      

      扩展答案。

      有没有办法告诉 sed 只输出捕获的组?

      是的。用捕获组替换所有文本:

      $ echo 'Number 123 inside text' \
         | sed 's/[^0-9]*\([0-9]\{1,\}\)[^0-9]*/\1/'
      
      123
      
      s/[^0-9]*                           # several non-digits
               \([0-9]\{1,\}\)            # followed by one or more digits
                              [^0-9]*     # and followed by more non-digits.
                                     /\1/ # gets replaced only by the digits.
      

      或使用扩展语法(减少反引号并允许使用 +):

      $ echo 'Number 123 in text' \
         | sed -E 's/[^0-9]*([0-9]+)[^0-9]*/\1/'
      
      123
      

      为避免在没有数字的情况下打印原始文本,请使用:

      $ echo 'Number xxx in text' \
         | sed -En 's/[^0-9]*([0-9]+)[^0-9]*/\1/p'
      
      • (-n) 默认不打印输入。
      • (/p) 仅在替换完成后打印。

      并匹配多个数字(并打印它们):

      $ echo 'N 123 in 456 text' \
        | sed -En 's/[^0-9]*([0-9]+)[^0-9]*/\1 /gp'
      
      123 456
      

      这适用于任何位数的运行:

      $ str='Test Num(s) 123 456 7899900 contained as0018166df in text'
      $ echo "$str" \
         | sed -En 's/[^0-9]*([0-9]{1,})[^0-9]*/\1 /gp'
      
      123 456 7899900 0018166
      

      这与grep命令非常相似:

      $ str='Test Num(s) 123 456 7899900 contained as0018166df in text'
      $ echo "$str" | grep -Po '\d+'
      123
      456
      7899900
      0018166
      

      关于\d

      和模式:/([\d]+)/

      Sed 无法识别“\d”(快捷方式)语法。 [0-9] 上面使用的 ascii 等价物并不完全等价。唯一的替代解决方案是使用字符类:'[[:digit:]]`。

      选择的答案使用这样的“字符类”来构建解决方案:

      $ str='This is a sample 123 text and some 987 numbers'
      $ echo "$str" | sed -rn 's/[^[:digit:]]*([[:digit:]]+)[^[:digit:]]+([[:digit:]]+)[^[:digit:]]*/\1 \2/p'
      

      该解决方案仅适用于(完全)两个数字运行。

      当然,由于答案是在 shell 中执行的,我们可以定义几个变量来缩短答案:

      $ str='This is a sample 123 text and some 987 numbers'
      $ d=[[:digit:]]     D=[^[:digit:]]
      $ echo "$str" | sed -rn "s/$D*($d+)$D+($d+)$D*/\1 \2/p"
      

      但是,正如已经解释过的,使用s/…/…/gp 命令更好:

      $ str='This is 75577 a sam33ple 123 text and some 987 numbers'
      $ d=[[:digit:]]     D=[^[:digit:]]
      $ echo "$str" | sed -rn "s/$D*($d+)$D*/\1 /gp"
      75577 33 123 987
      

      这将涵盖重复的数字运行和编写短(er)命令。

      【讨论】:

      • 在阅读高票接受的答案后感到惊讶,我向下滚动以写下它的狭窄范围并实际解决问题的精神。我应该猜到有人会在几年前做到这一点。这解释得很好,是真正的正确答案。
      • 这有点老套,不能很好地概括。这种方法的问题是模式[^0-9]*([0-9]+)[^0-9]* 需要设计成永远不会越过另一个匹配的边界。这对于这个例子来说没问题,但是对于不能在逐个字符的基础上工作的复杂搜索查询,必须将实际所需的匹配组 (whatever) 包围起来并不是很实际,它的前向查找和反向查找否定。
      • 它还需要捕获不属于捕获组的所有内容
      【解决方案9】:

      你可以使用ripgrep,它似乎也是简单替换的sed替代,像这样

      rg '(\d+)' -or '$1'
      

      其中 ripgrep 使用 -o--only matching-r--replace 仅输出带有 $1 的第一个捕获组(引用以避免 shell 将其解释为变量)两次,因为两个匹配。

      【讨论】:

        【解决方案10】:

        我想举一个更简单的例子,关于“使用 sed 仅输出捕获的组”

        我有/home/me/myfile-99并希望输出文件的序列号:99

        我的第一次尝试是:

        echo "/home/me/myfile-99" | sed -r 's/myfile-(.*)$/\1/'
        # output: /home/me/99
        

        为了完成这项工作,我们还需要在捕获组中捕获不需要的部分:

        echo "/home/me/myfile-99" | sed -r 's/^(.*)myfile-(.*)$/\2/'
        # output: 99
        

        *) 请注意,sed 没有\d

        【讨论】:

          【解决方案11】:

          您需要在第二个命令中包含整行来打印组,但您不需要对第一个通配符进行分组。这也可以:

          echo "/home/me/myfile-99" | sed -r 's/.*myfile-(.*)$/\1/'
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-02-11
            • 1970-01-01
            • 1970-01-01
            • 2023-03-07
            • 1970-01-01
            • 2016-08-22
            • 2017-01-13
            • 1970-01-01
            相关资源
            最近更新 更多