非贪婪(不情愿)正则匹配sed?

我试图使用sed来清理url行来提取域。

所以从:

http://www.suepearson.co.uk/product/174/71/3816/

我想要:

http://www.suepearson.co.uk/

(不管后面有没有斜杠,都没有关系)

我试过:

 sed 's|\(http:\/\/.*?\/\).*|\1|'

And(转义非贪婪量词)

sed 's|\(http:\/\/.*\?\/\).*|\1|'

但我似乎不能让非贪婪量词(?)工作,所以它总是匹配整个字符串。

292990 次浏览

在这种特定情况下,您可以在不使用非贪婪正则表达式的情况下完成工作。

试试这个非贪婪正则表达式[^/]*代替.*?:

sed 's|\(http://[^/]*/\).*|\1|g'

基本的和扩展的Posix/GNU regex都不能识别非贪婪的量词;你需要稍后的正则表达式。幸运的是,这个上下文的Perl regex非常容易获得:

perl -pe 's|(http://.*?/).*|\1|'
sed 's|(http:\/\/[^\/]+\/).*|\1|'

另一种方法,不使用正则表达式,是使用字段/分隔符方法,如

string="http://www.suepearson.co.uk/product/174/71/3816/"
echo $string | awk -F"/" '{print $1,$2,$3}' OFS="/"

Sed不支持“非贪婪”操作符。

你必须使用“[]”操作符来排除“/”匹配。

sed 's,\(http://[^/]*\)/.*,\1,'

附注:不需要反斜杠"/"。

sed -E将正则表达式解释为扩展(现代)正则表达式

更新:MacOS X为-E, GNU sed为-r。

这可以使用cut:

echo "http://www.suepearson.co.uk/product/174/71/3816/" | cut -d'/' -f1-3
echo "/home/one/two/three/myfile.txt" | sed 's|\(.*\)/.*|\1|'

别麻烦了,我在另一个论坛上看到的:)

我知道这是一个旧条目,但有人可能会发现它有用。 由于完整域名的总长度不超过253个字符,请将。*替换为。\{1,255 \}

使用sed,我通常通过搜索除分隔符以外的任何东西来实现非贪婪搜索,直到分隔符:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*\)/.*;\1;p'

输出:

http://www.suon.co.uk

这是:

  • 不输出-n
  • 搜索,匹配模式,替换和打印s/<pattern>/<replace>/p
  • 使用;搜索命令分隔符代替/,以便更容易键入s;<pattern>;<replace>;p
  • 记住括号之间的匹配\(\),之后可以通过\1\2
  • 匹配http://
  • 后面跟着括号中的[][ab/]表示ab/
  • []中的第一个^表示not,所以后面跟着除[]中的东西之外的任何东西
  • 所以[^/]意味着除了/字符以外的任何字符
  • *是重复前一个组,所以[^/]*表示除/之外的字符。
  • 到目前为止,sed -n 's;\(http://[^/]*\)意味着搜索并记住__abc1后面跟着除/之外的任何字符,并记住你找到了什么
  • 我们希望搜索到domain的末尾,因此在下一个/上停止,因此在结尾添加另一个/: sed -n 's;\(http://[^/]*\)/',但我们希望匹配域之后的其余行,因此添加.*
  • 现在在组1 (\1)中记住的匹配是域,因此将匹配的行替换为组\1中保存的内容并打印:sed -n 's;\(http://[^/]*\)/.*;\1;p'

如果你想在域名后面加上反斜杠,那么在组中再加一个反斜杠来记住:

echo "http://www.suon.co.uk/product/1/7/3/" | sed -n 's;\(http://[^/]*/\).*;\1;p'

输出:

http://www.suon.co.uk/

sed 's|\(http:\/\/www\.[a-z.0-9]*\/\).*|\1|也可以

sed当然有它的位置,但这不是其中之一!

正如Dee指出的:只需使用cut。在这种情况下,它要简单得多,也安全得多。下面是一个使用Bash语法从URL中提取各种组件的示例:

url="http://www.suepearson.co.uk/product/174/71/3816/"


protocol=$(echo "$url" | cut -d':' -f1)
host=$(echo "$url" | cut -d'/' -f3)
urlhost=$(echo "$url" | cut -d'/' -f1-3)
urlpath=$(echo "$url" | cut -d'/' -f4-)

给你:

protocol = "http"
host = "www.suepearson.co.uk"
urlhost = "http://www.suepearson.co.uk"
urlpath = "product/174/71/3816/"

正如你所看到的,这是一个更加灵活的方法。

(全部归功于Dee)

非贪婪的解决方案超过一个字符

这个线程真的很旧,但我认为人们仍然需要它。 假设你想杀死HELLO第一次出现之前的所有东西。你不能说[^HELLO]

因此,一个很好的解决方案包括两个步骤,假设你可以在输入中留出一个你不期望的唯一单词,比如top_sekrit

在这种情况下,我们可以:

s/HELLO/top_sekrit/     #will only replace the very first occurrence
s/.*top_sekrit//        #kill everything till end of the first HELLO

当然,对于一个简单的输入,你可以使用一个更小的单词,甚至可能是一个字符。

HTH !

因为您特别指出您正在尝试使用sed(而不是perl、cut等),所以请尝试分组。这就避免了非贪婪标识符可能无法被识别。第一组是协议(即协议)。'http://', 'https://', 'tcp://',等等)。第二组是域:

echo "http://www.suon.co.uk/product/1/7/3/" | sed "s|^\(.*//\)\([^/]*\).*$|\1\2|"

如果你不熟悉分组,可以从在这里开始。

另一个sed版本:

sed 's|/[:alnum:].*||' file.txt

它匹配/后面跟着一个字母数字字符(所以不是另一个正斜杠)以及其余字符,直到行尾。之后,它将什么也没有替换掉。删除它。)

使用纯(GNU) sed仍然有希望解决这个问题。尽管这不是一个通用的解决方案,在某些情况下,你可以使用“循环”来消除字符串中所有不必要的部分,就像这样:

sed -r -e ":loop" -e 's|(http://.+)/.*|\1|' -e "t loop"
  • -r:使用扩展的正则表达式(用于+和未转义的括号)
  • 定义一个名为"loop"的新标签
  • -e:在sed中添加命令
  • "t loop":如果有成功的替换,则跳回标记"loop"

这里唯一的问题是它也会切掉最后一个分隔符('/'),但如果你真的需要它,你仍然可以在“循环”结束后简单地把它放回去,只需要在前面的命令行末尾追加这个额外的命令:

-e "s,$,/,"

sed中模拟惰性(非贪婪)量词

以及所有其他正则表达式口味!

  1. 查找表达式的第一次出现:

    • POSIX之前(使用-r选项)

      正则表达式:

        (EXPRESSION).*|.
      

      对话:

        sed -r ‍'s/(EXPRESSION).*|./\1/g' # Global `g` modifier should be on
      

      示例(查找第一个数字序列)现场演示:

        $ sed -r 's/([0-9]+).*|./\1/g' <<< 'foo 12 bar 34'
      
        12
      

      它是如何工作的吗?

      此正则表达式受益于替换|。在每个位置,引擎都试图选择最长的匹配(这是POSIX标准,其他几个引擎也遵循这个标准),这意味着它会使用.,直到找到([0-9]+).*的匹配。但是秩序也很重要。

      enter image description here

      由于设置了全局标志,引擎尝试继续逐字符匹配,直到输入字符串或目标的末尾。一旦第一个也是唯一一个替换左侧的捕获组被匹配(EXPRESSION),其余的行也立即被消耗.*。我们现在在第一个捕获组中保持我们的价值。

    • < p > POSIX的信徒

      正则表达式:

        \(\(\(EXPRESSION\).*\)*.\)*
      

      对话:

        sed 's/\(\(\(EXPRESSION\).*\)*.\)*/\3/'
      

      示例(查找第一个数字序列):

        $ sed 's/\(\(\([0-9]\{1,\}\).*\)*.\)*/\3/' <<< 'foo 12 bar 34'
      
        12
      

      这是一个类似ERE版本,但不涉及交替。这是所有。在每个位置,引擎尝试匹配一个数字。

      enter image description here

      如果找到它,则消耗并捕获后面的其他数字,并且立即匹配该行的其余部分,否则,因为*意味着 more or zero,它跳过第二个捕获组\(\([0-9]\{1,\}\).*\)*,到达一个点.来匹配单个字符,这个过程继续。

  2. 查找分隔表达式的第一次出现:

    这种方法将匹配被分隔的字符串的第一次出现。我们可以称它为一根绳子。

    sed 's/\(END-DELIMITER-EXPRESSION\).*/\1/; \
    s/\(\(START-DELIMITER-EXPRESSION.*\)*.\)*/\1/g'
    

    输入字符串:

    foobar start block #1 end barfoo start block #2 end
    

    爱德:end

    sde: start

    $ sed 's/\(end\).*/\1/; s/\(\(start.*\)*.\)*/\1/g'
    

    输出:

    start block #1 end
    

    第一个正则表达式\(end\).*匹配并捕获第一个结束分隔符end,并用最近捕获的字符替换所有匹配 是结束分隔符。在这个阶段,我们的输出是:foobar start block #1 end.

    enter image description here

    然后将结果传递给第二个正则表达式\(\(start.*\)*.\)*,该正则表达式与上面的POSIX BRE版本相同。它匹配单个字符 如果开始分隔符start不匹配,否则它将匹配并捕获开始分隔符并匹配其余字符

    enter image description here


直接回答你的问题

使用方法#2(带分隔符的表达式),你应该选择两个合适的表达式:

  • < p >埃德:[^:/]\/

  • < p > SDE: http:

用法:

$ sed 's/\([^:/]\/\).*/\1/g; s/\(\(http:.*\)*.\)*/\1/' <<< 'http://www.suepearson.co.uk/product/174/71/3816/'

输出:

http://www.suepearson.co.uk/

注意:对于相同的分隔符,这将不起作用。

以下是你可以用两步方法和awk完成的事情:

A=http://www.suepearson.co.uk/product/174/71/3816/
echo $A|awk '
{
var=gensub(///,"||",3,$0) ;
sub(/\|\|.*/,"",var);
print var
}'
< p >输出: http://www.suepearson.co.uk < / p >

希望有帮助!

< a href = " http://0x2a。at/blog/2008/07/sed——non-greedy-matching/" rel="noreferrer">sed -non-greedy-matching by Christoph Sieghart

在sed中获得非贪婪匹配的技巧是匹配除终止匹配的字符外的所有字符。我知道,这很简单,但我在这上面浪费了宝贵的时间,毕竟shell脚本应该是快速而简单的。所以以防别人需要

贪婪匹配

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

非贪婪匹配

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

这是如何使用sed健壮地进行多字符字符串的非贪婪匹配。假设你想要将每个foo...bar更改为<foo...bar>,例如这个输入:

$ cat file
ABC foo DEF bar GHI foo KLM bar NOP foo QRS bar TUV

应该变成这样的输出:

ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

要做到这一点,你将foo和bar转换为单独的字符,然后在它们之间使用这些字符的反字符:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/g; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC <foo DEF bar> GHI <foo KLM bar> NOP <foo QRS bar> TUV

在上述:

  1. s/@/@A/g; s/{/@B/g; s/}/@C/g正在将{}转换为输入中不存在的占位符字符串,因此这些字符随后可用于将foobar转换为。
  2. s/foo/{/g; s/bar/}/g分别将foobar转换为{}
  3. s/{[^{}]*}/<&>/g正在执行我们想要的操作——将foo...bar转换为<foo...bar>
  4. s/}/bar/g; s/{/foo/g正在将{}转换回foobar
  5. s/@C/}/g; s/@B/{/g; s/@A/@/g正在将占位符字符串转换回原始字符。

请注意,上面的方法并不依赖于输入中不存在的任何特定字符串,因为它在第一步中就制造了这样的字符串,它也不关心你想要匹配的任何特定regexp的哪个出现,因为你可以在表达式中使用{[^{}]*}多次来隔离你想要的实际匹配和/或使用seds数值匹配操作符,例如只替换第二个出现:

$ sed 's/@/@A/g; s/{/@B/g; s/}/@C/g; s/foo/{/g; s/bar/}/g; s/{[^{}]*}/<&>/2; s/}/bar/g; s/{/foo/g; s/@C/}/g; s/@B/{/g; s/@A/@/g' file
ABC foo DEF bar GHI <foo KLM bar> NOP foo QRS bar TUV

还没有看到这个答案,所以这里是你如何用vivim来做到这一点:

vi -c '%s/\(http:\/\/.\{-}\/\).*/\1/ge | wq' file &>/dev/null

这将全局运行vi :%s替换(后面的g),如果没有找到模式,则避免引发错误(e),然后将结果更改保存到磁盘并退出。&>/dev/null可以防止GUI在屏幕上短暂闪烁,这可能很烦人。

我有时喜欢用vi来处理超级复杂的正则表达式,因为(1)perl正在消亡,(2)vim有一个非常高级正则表达式引擎,(3)在我日常使用的编辑文档中,我已经非常熟悉vi正则表达式。

@Daniel H(关于你对andcoz的回答的评论,虽然是很久以前的事了):删除后面的零

s,([[:digit:]]\.[[:digit:]]*[1-9])[0]*$,\1,g

it's about clearly defining the matching conditions ...

您还应该考虑没有匹配界限的情况。你是否想输出这一行。如果不匹配,我这里的示例不会输出任何内容。

你需要前缀到第三个/,所以选择两次字符串的任何长度不包含/和后面的/,然后字符串的任何长度不包含/,然后匹配/后面的任何字符串,然后打印选择。这个想法适用于任何单个的char delims。

echo http://www.suepearson.co.uk/product/174/71/3816/ | \
sed -nr 's,(([^/]*/){2}[^/]*)/.*,\1,p'

使用sed命令,您可以快速删除前缀或delim选择,如:

echo 'aaa @cee: { "foo":" @cee: " }' | \
sed -r 't x;s/ @cee: /\n/;D;:x'

这比一次吃焦肉快多了。

如果之前匹配成功,跳转到标签。在第一道线/之前加\n。移除到第一个\n。如果添加了\n,则跳转到结束并打印。

如果有开始和结束delim,很容易删除结束delim,直到你到达你想要的第n -2个元素,然后做D技巧,在结束delim后删除,如果不匹配跳转到删除,在开始delim和打印之前删除。这仅在开始/结束分隔成对出现时有效。

echo 'foobar start block #1 end barfoo start block #2 end bazfoo start block #3 end goo start block #4 end faa' | \
sed -r 't x;s/end//;s/end/\n/;D;:x;s/(end).*/\1/;T y;s/.*(start)/\1/;p;:y;d'

如果你有gnu grep,那么可以使用perl regex:

grep -Po '^https?://([^/]+)(?=)' <<< 'http://www.suepearson.co.uk/product/174/71/3816/'
http://www.suepearson.co.uk

或者,获取域使用的所有

grep -Po '^https?://([^/]+)\K.*' <<< 'http://www.suepearson.co.uk/product/174/71/3816/'
/product/174/71/3816/

下面的解决方案适用于匹配/使用multiply present(链式;串联;复合)HTML或其他标签。例如,我想编辑HTML代码以删除<span>标记,这是串联出现的。

问题: regular sed正则表达式贪婪地匹配从第一个到最后一个的所有标签。

解决方案:非贪婪模式匹配(在此线程的其他地方讨论;例如https://stackoverflow.com/a/46719361/1904943)。

例子:

echo '<span>Will</span>This <span>remove</span>will <span>this.</span>remain.' | \
sed 's/<span>[^>]*>//g' ; echo


This will remain.

解释:

  • s/<span>:找到<span>
  • [^>]:后面跟任何不是>的东西
  • *>:直到你找到>
  • //g:将任何这样的字符串替换为空。

齿顶高

我试图清理url,但我遇到了困难匹配/排除一个词- href -使用上面的方法。我简单地看了一下负环行(正则表达式来匹配不包含单词的行),但这种方法似乎过于复杂,并没有提供一个令人满意的解决方案。

我决定用`替换href(反勾),做正则表达式替换,然后用href替换`

例子(为便于阅读而格式化):

printf '\n
<a aaa h href="apple">apple</a>
<a bbb "c=ccc" href="banana">banana</a>
<a class="gtm-content-click"
data-vars-link-text="nope"
data-vars-click-url="https://blablabla"
data-vars-event-category="story"
data-vars-sub-category="story"
data-vars-item="in_content_link"
data-vars-link-text
href="https:example.com">Example.com</a>\n\n' |
sed 's/href/`/g ;
s/<a[^`]*`/\n<a href/g'


<a href="apple">apple</a>
<a href="banana">banana</a>
<a href="https:example.com">Example.com</a>

解释:基本如上所述。在这里,

  • s/href/` :用`替换href(反引号)
  • s/<a:查找URL的开始
  • [^`]:后面跟任何不是`的东西(反引号)
  • *` :直到你找到一个`
  • /<a href/g:将找到的每一个替换为<a href

由于这里也标记了PCRE,我们可以通过在正则表达式.*?中使用非惰性匹配来使用GNU grep,它将匹配与.*相对的第一个最近的匹配(它实际上是贪婪的,直到最后一次出现匹配)。

grep -oP '^http[s]?:\/\/.*?/' Input_file

< >强解释:< / >强使用grepoP选项,其中-P负责在这里启用PCRE正则表达式。在grep的主程序中,提到正则表达式,它匹配开始http/https,然后是://,直到下一次出现/,因为我们已经使用了.*?,它将在(http/https://).)之后查找第一个/它只打印匹配的部分。

不幸的是,如上所述,sed不支持这个。 为了克服这个问题,我建议使用次优方法(实际上甚至更好),使用vim类sed功能

.bash-profile中定义

vimdo() { vim $2 --not-a-term -c "$1"  -es +"w >> /dev/stdout" -cq!  ; }

这将创建无头vim来执行命令。

现在你可以这样做:

echo $PATH | vimdo "%s_\c:[a-zA-Z0-9\\/]\{-}python[a-zA-Z0-9\\/]\{-}:__g" -

来过滤掉$PATH中的python。

在vimdo中使用-来获得来自管道的输入。

而大多数语法是相同的。Vim具有更高级的特性,并且使用\{-}是非贪婪匹配的标准。看到help regexp