如何使用grep、正则表达式或perl提取符合某一模式的字符串

120

我有一个文件,看起来像这样:

    <table name="content_analyzer" primary-key="id">
      <type="global" />
    </table>
    <table name="content_analyzer2" primary-key="id">
      <type="global" />
    </table>
    <table name="content_analyzer_items" primary-key="id">
      <type="global" />
    </table>

我需要提取紧随name=引号内的任何内容,即content_analyzercontent_analyzer2content_analyzer_items

我在Linux上进行此操作,因此可以使用sed、perl、grep或bash解决方案。


6
不用害羞,欢迎来到这里! - Benoit
9
我认为不将链接 https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454 放在这里是错误的。 - Christoffer Hammarström
1
感谢大家提供的有用评论。我为XML格式不正确而道歉。我删除了一些标签以简化它。 - wrangler
8个回答

221

由于您需要匹配内容但不包括在结果中(必须匹配name=",但它不是所需结果的一部分),因此需要某种形式的零宽度匹配或组捕获。这可以通过以下工具轻松完成:

Perl

使用Perl,您可以使用n选项逐行循环并打印捕获组的内容(如果匹配成功):

perl -ne 'print "$1\n" if /name="(.*?)"/' filename

GNU grep

如果你有一个更好的版本的grep,比如GNU grep,你可能会发现有-P选项可用。这个选项将启用类似Perl的正则表达式,使你能够使用\K,这是一个简写的向后查找。它将重置匹配位置,因此它之前的任何内容都是无宽度的。

grep -Po 'name="\K.*?(?=")' filename

o 选项使 grep 只打印匹配到的文本,而不是整行。

Vim - 文本编辑器

另一种方法是直接使用文本编辑器。使用 Vim,其中一种实现此目的的方法是删除没有 name= 的行,然后从结果行中提取内容:

:v/.*name="\v([^"]+).*/d|%s//\1

标准 grep

如果由于某种原因你无法访问这些工具,使用标准的 grep 命令也可以实现类似功能。但是,由于没有预搜索功能,稍后可能需要进行一些清理:

grep -o 'name="[^"]*"' filename

关于保存结果的注释

在上面的所有命令中,结果将被发送到stdout。重要的是要记住,您可以通过添加管道将它们保存到文件中:

> result

到命令的结尾。


12
在GNU grep中的查找(Lookarounds):grep -Po '.*name="\K.*?(?=".*)' - Dennis Williamson
3
我为什么会生气?没有.*,你可以使用grep -Po '(?<=name=").*?(?=")'\K可以用作简写,但只有当其左侧的匹配长度可变时才真正需要它。在这种情况下,使用环视的原因是相当明显的。非贪婪操作看起来更加整洁([^"]*.*?相比),而且你不必重复锚定字符。我不知道速度如何。我认为这很大程度上取决于上下文。希望这有所帮助。 - Dennis Williamson
+1 为命令描述。如果您能更新您的答案并解释正则表达式中的 "[...]" 部分,我们将不胜感激。 - lreeder
@lreeder 谢谢。这是一个字符类,当它以 ^ 开头时,意味着它匹配除了其内容之外的任何字符。因此,[^"] 表示每个不是引号的字符。我没有在后面的答案中使用它,而是选择了未准备好的版本 .*?。前面的版本是贪婪的,所以我使用该类来匹配除引号之外的所有内容,并打算在第一个引号处停止,这与“非贪婪”地匹配任何东西直到引号相同。希望这有助于理解,如果需要更好的澄清,请告诉我。 - sidyll
在OS X上,只需通过Homebrew安装grep并使用它来代替默认的工具即可。它应该可以正常工作。 - Sebastián Barschkis
显示剩余6条评论

6
正则表达式应该是:
.+name="([^"]+)"

然后分组将在 \1 中进行。

5

应该使用HTML解析器来实现这个目的,而不是使用正则表达式。一个利用HTML::TreeBuilder的Perl程序:

程序

#!/usr/bin/env perl

use strict;
use warnings;

use HTML::TreeBuilder;

my $tree = HTML::TreeBuilder->new_from_file( \*DATA );
my @elements = $tree->look_down(
    sub { defined $_[0]->attr('name') }
);

for (@elements) {
    print $_->attr('name'), "\n";
}

__DATA__
<table name="content_analyzer" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
  <type="global" />
</table>

输出

content_analyzer
content_analyzer2
content_analyzer_items

5

3
请注意,例子中提供的格式不够规范(例如 <type="global"),因此大多数 XML 解析器会报错并停止运行。 - bvr

2

以下是使用HTML Tidy和xmlstarlet的解决方案:

htmlstr='
<table name="content_analyzer" primary-key="id">
<type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
<type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
<type="global" />
</table>
'

echo "$htmlstr" | tidy -q -c -wrap 0 -numeric -asxml -utf8 --merge-divs yes --merge-spans yes 2>/dev/null |
sed '/type="global"/d' |
xmlstarlet sel -N x="http://www.w3.org/1999/xhtml" -T -t -m "//x:table" -v '@name' -n

2
这可以做到:
perl -ne 'if(m/name="(.*?)"/){ print $1 . "\n"; }'

1

糟糕,sed命令必须在tidy命令之前执行:

echo "$htmlstr" | 
sed '/type="global"/d' |
tidy -q -c -wrap 0 -numeric -asxml -utf8 --merge-divs yes --merge-spans yes 2>/dev/null |
xmlstarlet sel -N x="http://www.w3.org/1999/xhtml" -T -t -m "//x:table" -v '@name' -n

0

如果你的xml(或一般文本)结构是固定的,最简单的方法是使用cut。对于你的特定情况:

echo '<table name="content_analyzer" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer2" primary-key="id">
  <type="global" />
</table>
<table name="content_analyzer_items" primary-key="id">
  <type="global" />
</table>' | grep name= | cut -f2 -d '"'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接