在sed中非贪婪(不情愿)的正则表达式匹配?

时间:2009-07-09 10:47:31

标签: regex sed pcre greedy regex-greedy

我正在尝试使用sed清理网址行以仅提取域名。

所以来自:

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

我想:

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

(有或没有火车斜线,没关系)

我试过了:

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

和(逃避非贪婪量词)

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

但我似乎无法使非贪婪量词工作,所以它总是最终匹配整个字符串。

24 个答案:

答案 0 :(得分:393)

基本或扩展的Posix / GNU正则表达式都不承认非贪心量词;你需要一个后来的正则表达式。幸运的是,Perl正则表达式非常容易获得:

perl -pe 's|(http://.*?/).*|\1|'

答案 1 :(得分:221)

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

试试这个非贪婪的正则表达式[^/]*而不是.*?

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

答案 2 :(得分:110)

使用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://[^/]*\)表示搜索并记住http://后跟除/之外的任何字符并记住您找到的内容
  • 我们要搜索直到结束域名,请停留在下一个/上,因此请在结尾处添加另一个/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/

答案 3 :(得分:36)

sed不支持“非贪婪”操作员。

您必须使用“[]”运算符从匹配项中排除“/”。

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

P.S。没有必要反斜杠“/".

答案 4 :(得分:25)

sed

中模拟懒惰(非贪婪)量词

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

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

    • POSIX ERE (使用-r选项)

      正则表达式:

      (EXPRESSION).*|.
      

      桑达:

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

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

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

      如何运作

      此正则表达式可以从替换|中受益。在每个位置,引擎将查找交替的第一侧(我们的目标),如果不匹配,则具有点.的交替的第二侧匹配下一个直接字符。

        

      enter image description here

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

    • POSIX BRE

      正则表达式:

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

      桑达:

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

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

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

      这个版本与ERE版本相同,但没有涉及更改。就这样。在每个单一位置,引擎会尝试匹配一个数字。

        

      enter image description here

      如果找到,则消耗并捕获其他后续数字,否则立即匹配其余行,否则*表示 more或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
    

    -EDE: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

  3. 直接回答您的问题

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

    • EDE:[^:/]\/

    • SDE:http:

    用法:

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

    输出:

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

答案 5 :(得分:21)

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

这个帖子真的很老但我认为人们仍然需要它。 让我们说你想要杀死所有东西,直到第一次出现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!

答案 6 :(得分:16)

这可以使用cut:

完成
echo "http://www.suepearson.co.uk/product/174/71/3816/" | cut -d'/' -f1-3

答案 7 :(得分:15)

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

答案 8 :(得分:9)

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

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

答案 9 :(得分:5)

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)

答案 10 :(得分:4)

仍然有希望使用纯(GNU)sed来解决这个问题。尽管在某些情况下这不是通用解决方案,但您可以使用&#34;循环&#34;消除字符串中所有不必要的部分,如下所示:

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

这里唯一的问题是它还会删除最后一个分隔符(&#39; /&#39;),但是如果你真的需要它,你仍然可以简单地在&#34;循环后退回#34 ;完成后,只需在上一个命令行的末尾添加此附加命令:

-e "s,$,/,"

答案 11 :(得分:3)

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

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

答案 12 :(得分:3)

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

答案 13 :(得分:2)

因为您明确声明您正在尝试使用sed(而不是perl,cut等),请尝试分组。这避免了可能无法识别的非贪婪标识符。第一组是协议(即'http://','https://','tcp://'等)。第二组是域名:

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

如果您不熟悉分组,请启动here

答案 14 :(得分:1)

我意识到这是一个旧条目,但有人可能会发现它很有用。 由于完整域名的总长度不得超过253个字符,因此替换。* with。\ {1,255 \}

答案 15 :(得分:1)

这是使用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/}/gfoobar分别转换为{}
  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正在将占位符字符串转换回其原始字符。

请注意,上面的内容并不依赖于输入中不存在的任何特定字符串,因为它在第一步中会制造此类字符串,也不在乎您想匹配哪个特定的正则表达式,因为可以使用{{ 1}}在表达式中根据需要进行多次,以隔离所需的实际匹配和/或使用seds数字匹配运算符,例如仅替换第二次出现:

{[^{}]*}

答案 16 :(得分:0)

如果您可以访问 gnu grep,则可以使用 perl 正则表达式:

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/

答案 17 :(得分:0)

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

您需要前缀到第 3 个 /,因此选择两次不包含 / 且跟随 / 的任意长度字符串,然后选择不包含 / 的任意长度字符串,然后匹配 / 跟随任意字符串,然后打印选择。这个想法适用于任何单个字符 delims。

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

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

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

这比一次吃炭要快得多。

如果之前成功匹配,则跳转到标签。在第一个分隔符之前的 / 处添加 \n。删除最多第一个\n。如果添加了\n,则跳转到末尾并打印。

如果有开始和结束分隔符,很容易删除结束分隔符,直到你到达你想要的第n-2个元素,然后做D技巧,结束分隔符后删除,如果不匹配则跳转删除,开始前删除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'

答案 18 :(得分:0)

@Daniel H(关于您对andcoz的答案的评论,尽管很久以前):删除尾随零可以使用

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

这是要明确定义匹配条件...

答案 19 :(得分:0)

还没有看到这个答案,所以这是使用vivim的方法:

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

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

我有时喜欢对超级复杂的正则表达式使用vi,因为(1)perl即将死去,(2)vim具有 very 高级正则表达式引擎,以及(3)我在日常使用情况编辑文档中已经非常熟悉vi正则表达式。

答案 20 :(得分:0)

这是你可以用两步法和awk做的事情:

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

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

希望有所帮助!

答案 21 :(得分:0)

另一个sed版本:

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

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

答案 22 :(得分:0)

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

答案 23 :(得分:0)

echo "/home/one/two/three/myfile.txt" | sed 's|\(.*\)/.*|\1|'

不要打扰,我在另一个论坛上得到了它:)

相关问题