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

Non greedy (reluctant) regex matching in sed?

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

所以来自:

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

我想要:

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

(有或没有牵引斜杠,没关系)

我已经尝试过:

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

和(逃离非贪婪量词)

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

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


基本的或扩展的posix/gnu regex都不能识别非贪婪量词;您需要一个更晚的regex。幸运的是,此上下文的PerlRegex非常容易获得:

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


在这种特定的情况下,您可以在不使用非贪婪regex的情况下完成任务。

尝试这个非贪婪的regex [^/]*,而不是.*?

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


对于SED,我通常通过搜索除分隔符之外的任何内容来实现非贪婪搜索,直到分隔符:

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

输出:

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

这是:

  • 不输出-n
  • 搜索、匹配图案、替换并打印s///p
  • 使用;搜索命令分隔符而不是/使键入s;;;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'

如果要在域后也包含反斜杠,请在组中再添加一个反斜杠以记住:

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

输出:

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


SED不支持"非贪婪"运算符。

必须使用"[]"运算符从匹配中排除"/"。

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

另外,不需要反斜杠"/"。


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

还有所有其他的雷吉克斯口味!

  • 查找表达式的第一个匹配项:

    • posix-ere(使用-r选项)

      Regex:

      1
      (EXPRESSION).*|.

      塞德:

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

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

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

      它是如何工作的?

      这个regex得益于一个交替的|。在每个位置,引擎都将查找交替的第一个边(我们的目标),如果它与交替的第二个边不匹配,该边有一个点.与下一个直接字符匹配。

      enter image description here

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

    • POSIX BRE

      Regex:

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

      塞德:

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

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

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

      这一个类似于ERE版本,但没有涉及到变更。这就是全部。在每一个位置,引擎都试图匹配一个数字。

      enter image description here

      如果找到,则消耗并捕获后面的其他数字,其余的行立即匹配,否则,因为*表示它跳过第二个捕获组\(\([0-9]\{1,\}\).*\)*,到达一个点.,以匹配单个字符,这个过程继续进行。

  • 查找第一个出现的分隔表达式:

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

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

    输入字符串:

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

    -ede:end

    -sde:start

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

    输出:

    1
    start block #1 end

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

    enter image description here

    然后将结果传递给第二个regex \(\(start.*\)*.\)*,与上面的posix-bre版本相同。它匹配一个字符如果开始分隔符start不匹配,否则它将匹配并捕获开始分隔符并匹配其余字符。

    enter image description here

  • 直接回答你的问题

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

    • ede:[^:/]\/

    • sde:http:

    用途:

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

    输出:

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


    不止一个字符的非贪婪解决方案

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

    所以一个好的解决方案包括两个步骤,假设您可以在输入中保留一个您不期望的惟一单词,比如top_sekrit

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

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

    当然,使用更简单的输入,您可以使用更小的单词,甚至可以使用单个字符。

    嗯!


    这可以使用CUT来完成:

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

    SED-Christoph Sieghart的非贪婪匹配

    在SED中获得非贪婪匹配的技巧是匹配所有字符,不包括终止匹配的字符。我知道,这是一个轻而易举的人,但我浪费了宝贵的时间,毕竟shell脚本应该是快速而简单的。因此,如果其他人可能需要它:

    贪婪匹配

    1
    2
    % echo"foobar" | sed 's/<.*>//g'
    bar

    非贪婪匹配

    1
    2
    % echo"foobar" | sed 's/<[^>]*>//g'
    foobar

    另一种不使用regex的方法是使用字段/分隔符方法eg

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


    以东十一〔37〕当然有它的位置,但这不是其中之一!

    正如迪伊所指出的:只要使用cut。在这种情况下,它更简单,更安全。下面是一个使用bash语法从URL中提取各种组件的示例:

    1
    2
    3
    4
    5
    6
    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-)

    给你:

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

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

    (全部贷记DEE)


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

    1
    sed -r -e":loop" -e 's|(http://.+)/.*|\1|' -e"t loop"
    • -R:使用扩展regex(用于+和无括号括号)
    • ":loop":定义名为"loop"的新标签
    • -E:向SED添加命令
    • "T loop":如果替换成功,则跳转回标签"loop"

    这里唯一的问题是它还将剪切最后一个分隔符("/"),但是如果您确实需要它,您仍然可以在"循环"完成后将其放回原处,只需在上一个命令行末尾附加此附加命令:

    1
    -e"s,$,/,"

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

    更新:macos x上的-e,gnu-sed中的-r。


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


    因为您特别声明要使用SED(而不是Perl、Cut等),所以请尝试分组。这会绕过可能无法识别的非贪婪标识符。第一组是协议(即"http://"、"https://"、"tcp://"等)。第二组是域:

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

    如果您不熟悉分组,请从这里开始。


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


    这就是如何使用sed对多个字符串进行可靠的非贪婪匹配。假设您想将每个foo...bar更改为,例如这个输入:

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

    应成为该输出:

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

    为此,您可以将foo和bar转换为单个字符,然后在它们之间使用这些字符的负数:

    1
    2
    $ 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

    在上面:

  • s/@/@A/g; s/{/@B/g; s/}/@C/g正在将{}转换为输入中不存在的占位符字符串,因此这些字符可用于将foobar转换为。
  • s/foo/{/g; s/bar/}/gfoobar分别转换为{}
  • s/{[^{}]*}/<&>/g正在执行我们想要的操作—将foo...bar转换为
  • s/}/bar/g; s/{/foo/g{}转换回foobar
  • s/@C/}/g; s/@B/{/g; s/@A/@/g正在将占位符字符串转换回其原始字符。
  • 请注意,上面的内容并不依赖于输入中没有的任何特定字符串,因为它在第一步中生成这些字符串,也不关心要匹配的任何特定regexp的哪个出现,因为您可以在表达式中使用{[^{}]*}尽可能多的次数来隔离所需的实际匹配和/或与seds。数字匹配运算符,例如仅替换第二个匹配项:

    1
    2
    $ 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进行此操作:

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

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

    我喜欢有时对超复杂的regex使用vi,因为(1)perl i sdeaddilling,(2)vim有一个非常高级的regex引擎,和(3)我已经在我的日常使用编辑文档中非常熟悉viregex。


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

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


    另一个SED版本:

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

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


    以下是您可以使用两步方法和awk进行的操作:

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

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

    希望有帮助!


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