从 Grep RegEx 中捕获组

Posted

技术标签:

【中文标题】从 Grep RegEx 中捕获组【英文标题】:Capturing Groups From a Grep RegEx 【发布时间】:2010-12-25 21:29:33 【问题描述】:

我在sh (Mac OSX 10.6) 中有这个小脚本来查看文件数组。 Google 目前已停止提供帮助:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

到目前为止(显然,对你来说,shell 专家)$name 仅包含 0、1 或 2,这取决于 grep 是否发现文件名与提供的内容匹配。 我想要捕获括号内的内容 ([a-z]+) 并将其存储到变量中

如果可能,我希望只使用grep。如果没有,请不要使用 Python 或 Perl 等。sed 或类似的东西 - 我是 shell 新手,想从 *nix 纯粹主义者的角度来攻击它。

另外,作为一个超酷的奖励,我很好奇如何在 shell 中连接字符串?我捕获的组是存储在$name中的字符串“somename”,我想在它的末尾添加字符串“.jpg”,我可以cat $name '.jpg'吗?

如果你有时间,请解释一下发生了什么。

【问题讨论】:

grep 真的 比 sed 更纯粹的 unix 吗? 啊,不是故意的。我只是希望可以使用我在这里专门尝试学习的工具找到解决方案。如果使用grep 无法解决,那么sed 会很棒,如果可以使用sed 解决。 我应该在那个 btw 上放一个:) ... 嘘,今天我的脑子太炸了哈哈。 @martinclayton 这将是一个有趣的论点。我确实认为 sed,(或者准确地说是 ed)会更​​旧(因此更纯粹?也许?) unix,因为 grep 从 ed 表达式 g(lobal)/re(gular expression)/p(rint) 派生它的名称。 【参考方案1】:

我相信只用 grep 是不可能的

对于 sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

不过,我会尝试一下奖金:

echo "$name.jpg"

【讨论】:

不幸的是,sed 解决方案不起作用。它只是打印出我目录中的所有内容。 更新了,如果不匹配会输出一个空行,所以一定要检查一下 现在只输出空行! 这个 sed 有问题。第一组捕获括号包含所有内容。当然 \2 将一无所有。 它适用于一些简单的测试用例... \2 得到内部组【参考方案2】:

给您的建议 - 您可以使用参数扩展来删除从最后一个下划线开始的名称部分,并且在开头类似:

f=001_abc_0za.jpg
work=$f%_*
name=$work#*_

那么name 的值将是abc

请参阅 Apple developer docs,向前搜索“参数扩展”。

【讨论】:

这不会检查 ([a-z]+)。 @levislevis - 这是真的,但是,正如 OP 所评论的那样,它确实做了需要做的事情。【参考方案3】:

这对于纯 grep 来说是不可能的,至少一般情况下是不可能的。

但如果您的模式合适,您可以在管道中多次使用grep,首先将您的行缩减为已知格式,然后仅提取您想要的位。 (尽管像cutsed 这样的工具在这方面要好得多)。

假设您的模式更简单一点:[0-9]+_([a-z]+)_ 您可以这样提取:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

第一个grep 将删除与您的整体模式不匹配的所有行,第二个grep(指定--only-matching)将显示名称的字母部分。这只适用于模式是合适的:“alpha 部分”足够具体,可以提取出您想要的内容。

(旁白:我个人会使用grep + cut 来实现您的目标:echo $name | grep pattern | cut -d _ -f 2。这让cut 通过在分隔符_ 上拆分来将行解析为字段,并且仅返回字段 2(字段编号从 1 开始)。

Unix 哲学是拥有做一件事的工具,并且把它做好,并将它们结合起来完成不平凡的任务,所以我认为grep + sed 等是一种更 Unixy 的做事方式事情:-)

【讨论】:

for f in $files; do name=echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'| cut -d _ -f 2; 啊哈! 我不同意那个“哲学”。如果您可以在不调用外部命令的情况下使用 shell 的内置功能,那么您的脚本的性能会快很多。有些工具在功能上重叠。例如 grep 和 sed 和 awk。它们都进行字符串操作,但 awk 比它们更突出,因为它可以做的更多。实际上,所有这些命令链,例如上面的双 grep 或 grep+sed 都可以通过使用一个 awk 进程来缩短。 @ghostdog74:这里没有任何论据,将许多微小的操作链接在一起通常比在一个地方完成所有操作效率低,但我坚持我的断言,即 Unix 哲学是许多工具一起工作。例如,tar 只是存档文件,它不压缩它们,并且因为它默认输出到 STDOUT,所以您可以使用 netcat 将其通过网络传输,或者使用 bzip2 等进行压缩。在我看来,这加强了约定和一般性Unix 工具应该能够在管道中协同工作的精神。 cut 很棒——感谢您的提示!至于工具与效率的争论,我喜欢链接工具的简单性。 grep 的 o 选项的道具,很有帮助【参考方案4】:

如果您使用 Bash,您甚至不必使用 grep

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="$BASH_REMATCH[1]"
        echo "$name.jpg"    # concatenate strings
        name="$name.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

最好将正则表达式放在变量中。如果按字面意思包含,某些模式将不起作用。

这使用=~ 这是 Bash 的正则表达式匹配运算符。匹配结果保存到名为$BASH_REMATCH 的数组中。第一个捕获组存储在索引 1 中,第二个(如果有)存储在索引 2 中,依此类推。索引 0 是完全匹配。

您应该知道,如果没有锚,此正则表达式(以及使用 grep 的正则表达式)将匹配以下任何示例以及更多示例,这可能不是您想要的:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

要消除第二个和第四个示例,请使您的正则表达式如下:

^[0-9]+_([a-z]+)_[0-9a-z]*

表示字符串必须以一位或多位数字开始。克拉代表字符串的开头。如果您在正则表达式的末尾添加一个美元符号,如下所示:

^[0-9]+_([a-z]+)_[0-9a-z]*$

那么第三个示例也将被删除,因为点不在正则表达式中的字符之间,而美元符号表示字符串的结尾。请注意,第四个示例也使匹配失败。

如果你有 GNU grep(我想,大约是 2.5 或更高版本,当添加了 \K 运算符时):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

\K 运算符(可变长度后视)导致前面的模式匹配,但不包括结果中的匹配。等价的固定长度是 (?<=) - 该模式将包含在右括号之前。如果量词可能匹配不同长度的字符串(例如+*2,4),则必须使用\K

(?=) 运算符匹配固定或可变长度的模式,称为“前瞻”。它也不在结果中包含匹配的字符串。

为了使匹配不区分大小写,使用了(?i) 运算符。它会影响跟随它的模式,因此它的位置很重要。

可能需要根据文件名中是否有其他字符来调整正则表达式。您会注意到,在这种情况下,我展示了一个在捕获子字符串的同时连接字符串的示例。

【讨论】:

在这个答案中,我想投票支持“最好将正则表达式放入变量中。如果按字面意思包含某些模式将无法正常工作。” "最好将正则表达式放在一个变量中。如果按字面包含某些模式将不起作用。" - 为什么会这样?有办法解决吗? @FrancescoFrassinelli:一个例子是包含空格的模式。转义很尴尬,而且您不能使用引号,因为这会迫使它从正则表达式转换为普通字符串。正确的方法是使用变量。在作业过程中可以使用引号使事情变得更简单。 @Brandon:确实有效。你使用的是什么版本的 Bash?告诉我你在做什么不起作用,也许我可以告诉你原因。 @mdelolmo:我的回答包括有关grep 的信息。它也被 OP 接受并获得了很多支持。感谢您的反对。【参考方案5】:

如果你有 bash,你可以使用扩展通配符

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

【讨论】:

这看起来很有趣。您能否对它进行一些解释?或者,如果您愿意,链接到一个特别有见地的资源来解释它?谢谢!【参考方案6】:

这是一个使用 gawk 的解决方案。这是我发现我需要经常使用的东西,所以我为它创建了一个函数

function regex1  gawk 'match($0,/'$1'/, ary) print ary['$2:-'1'']'; 

使用只是做

$ echo 'hello world' | regex1 'hello\s(.*)'
world

【讨论】:

好主意,但似乎不适用于正则表达式中的空格 - 它们需要替换为 \s。你知道怎么解决吗?【参考方案7】:

我意识到这个答案已经被接受,但从“严格 *nix 纯粹主义者的角度”看来,适合这项工作的工具是 pcregrep,这似乎不是尚未提及。换行试试:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

到以下:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

只获取捕获组1的内容。

pcregrep 工具利用了您已在 grep 中使用的所有相同语法,但实现了您需要的功能。

-o 参数的工作方式与grep 版本相同,但它也接受pcregrep 中的数字参数,表示您要显示哪个捕获组。

使用此解决方案,脚本所需的更改最少。您只需用另一种模块化实用程序替换一个模块化实用程序并调整参数。

有趣的注意事项:您可以使用多个 -o 参数来返回多个捕获组,按照它们在行上出现的顺序。

【讨论】:

pcregrepMac OS X 中默认不可用,这是 OP 使用的 我的pcregrep 似乎不理解-o 后面的数字:“-o1”中的未知选项字母'1'。在查看@987654333 时也没有提及该功能@ @WAF 抱歉,我想我应该在评论中包含该信息。我在 Centos 6.5 上,pcgrep 版本显然很旧:7.8 2008-09-05. 是的,非常有帮助,例如echo 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456 pcregrep 8.41(在Ubuntu 16.03 上安装apt-get install pcregrep)无法识别-Ei 开关。不过,没有它它也能完美运行。在 macOS 上,pcregrep 通过@anishpatel 上面提到的homebrew(也是 8.41)安装,至少在 High Sierra 上,-E 开关也无法识别。【参考方案8】:

我更喜欢pythonperl 这一行命令,它们通常都包含在主要的Linux 发行版中

echo $'
<a href="http://***.com">
</a>
<a href="http://google.com">
</a>
' |  python -c $'
import re
import sys
for i in sys.stdin:
  g=re.match(r\'.*href="(.*)"\',i);
  if g is not None:
    print g.group(1)
'

以及处理文件:

ls *.txt | python -c $'
import sys
import re
for i in sys.stdin:
  i=i.strip()
  f=open(i,"r")
  for j in f:
    g=re.match(r\'.*href="(.*)"\',j);
    if g is not None:
      print g.group(1)
  f.close()
'

【讨论】:

+1 用于多行 python 程序,我觉得这是在很多系统上执行此操作的相当标准的方式,这也是内联但比标准 bash 工具灵活得多。【参考方案9】:
str="1w 2d 1h"
regex="([0-9])w ([0-9])d ([0-9])h"
if [[ $str =~ $regex ]]
then
    week="$BASH_REMATCH[1]"
    day="$BASH_REMATCH[2]"
    hour="$BASH_REMATCH[3]"
    echo $week --- $day ---- $hour
fi

输出: 1 --- 2 ---- 1

【讨论】:

【参考方案10】:

我在使用带有perl 的捕获组的正则表达式方面取得了巨大成功,例如

for f in 123_abc_123.jpg 123_xyz_432.jpg
do
    echo $f
    echo $f | perl -ne 'if (/[0-9]+_([[a-z]+)_[0-9a-z]*/)  print $1 . "\n" '
done

输出:

123_abc_123.jpg
abc
123_xyz_432.jpg
xyz

因此perl 中的 if-regex 条件将同时过滤掉所有不匹配的行,对于那些匹配的行,它将应用您可以使用 $1 访问的捕获组, $2, ... 分别,

【讨论】:

我希望我能在一周前找到这个。很好用,谢谢!!!

以上是关于从 Grep RegEx 中捕获组的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式 [REGEX] - 替换/替换 - 捕获组 1 和 2 中的内容

具有多个捕获组的 R 中的正则表达式组捕获

Java Regex替换为捕获组

如何在 grep 中使用非捕获组?

有没有办法让 Regex.Match 只提供预期的捕获组?

如何仅获取给定的捕获组 <regex> c++