sed中的非贪婪(勉强)正则表达式匹配?


406

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

来自:

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

我想要:

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

(无论是否带有斜杠,都没有关系)

我努力了:

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

和(转义非贪婪量词)

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

但我似乎无法使非贪婪量词(?)正常工作,因此最终总是匹配整个字符串。


54
旁注:如果用“ |”分隔正则表达式,则不必转义“ /”。实际上,大多数人用“ |”定界 而不是“ /”,以避免出现“栅栏”。
AttishOculus

12
@AttishOculus sed中替换表达式中's'之后的第一个字符是定界符。因此是's ^ foo ^ bar ^'或's!foo!bar!' 也可以
Squidly'2

1
对于扩展的正则表达式,请使用 sed -E 's...。仍然,没有勉强的运营商。
Ondra参观Žižka

不是回答问题标题,而是在这种特定情况下简单的cut -d'/' -f1-3作品。
Petr Javorik '19

Answers:


421

基本的或扩展的Posix / GNU正则表达式都不能识别非贪婪的量词;您需要稍后的正则表达式。幸运的是,在这种情况下,Perl正则表达式非常容易获得:

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

12
为了做到这一点,使用选项-pi -e
realnice 2013年

11
神圣的烟雾,我不敢相信它是有效的:-)现在唯一令人讨厌的是我的脚本具有Perl依赖项:-(从
好的

6
@Freedom_Ben:IIRC perl需要通过POSIX
MestreLion

4
@ dolphus333:“基本的或扩展的Posix / GNU regex都不能识别非贪婪的量词”表示“您不能在sed中使用非贪婪的量词”。
混乱

3
@Sérgio,这就是您要执行的操作的方式,这在中是不可能的sed,使用的语法基本上与sed
chaos

250

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

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

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

3
如何使用此技术使sed匹配非贪婪的短语?
user3694243

6
不幸的是你不能;看到混乱的答案
丹尼尔H,

非常感谢...因为在许多Linux发行版中,perl不再是默认安装库!
st0ne


@DanielH实际上,可以根据要求使用此技术非贪婪地匹配短语。以足够的精度编写任何一种模式都可能会有些痛苦。例如,在URL查询中解析键值分配时,可能需要使用来查看架构分配([^&=#]+)=([^&#]*)。在某些情况下,不能肯定以这种方式无法正常工作,例如,在解析其主机部分的URL和路径名时,使用最终斜杠(假定为可选斜杠)被排除在捕获范围之外:^(http:\/\/.+?)/?$
Thomas Urban

121

使用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/

8
关于最近的编辑:括号是一种括弧字符,因此将其称为括号是正确的,特别是如果像作者一样在单词后面加上实际字符。另外,它是某些区域性中的首选用法,因此用您自己区域性中的首选用法替换它似乎有点不礼貌,尽管我敢肯定这不是编辑器想要的。就个人而言,我认为最好使用纯描述性名称,例如圆括号方括号尖括号
艾伦·摩尔

2
是否可以用字符串替换分隔符?
Calculemus 2014年

37

sed不支持“非贪婪”运算符。

您必须使用“ []”运算符将“ /”排除在匹配之外。

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

PS不需要反斜杠“ /”。


并不是的。如果定界符可能是许多可能的字符之一(仅表示数字字符串),则否定匹配可能会变得越来越复杂。很好,但是可以选择使。*非贪婪
gesell '16

1
这个问题更笼统。这些解决方案适用于URL,但不适用于(例如)我去除尾随零的用例。s/([[:digit:]]\.[[1-9]]*)0*/\1/显然不会适合1.20300。但是,由于最初的问题是关于URL的,因此应在公认的答案中提及它们。
丹尼尔·H

33

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

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

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

    • POSIX ERE(使用-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]+).*。但是秩序也很重要。

      在此处输入图片说明

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

    • POSIX BRE

      正则表达式:

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

      塞德:

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

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

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

      此版本类似于ERE版本,但不涉及更改。就这样。在每个单个位置,引擎都会尝试匹配一个数字。

      在此处输入图片说明

      如果找到了,则会消耗并捕获其他后面的数字,并立即匹配其余的行,否则由于*意味着 更多或零,它将跳过第二个捕获组\(\([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

    在此处输入图片说明

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

    在此处输入图片说明


直接回答您的问题

使用方法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/

注意:这不适用于相同的定界符。


3)在建议使用regex101之类的网站进行演示时,请注意,由于语法和功能差异,它并不总是适用于cli工具
Sundeep

1
@Sundeep谢谢。我把所有这些引号都变成了单引号。我还考虑了最左边最长的比赛规则。但是,在平等方面,in sed和其他遵循相同标准顺序的引擎确实很重要。所以echo 'foo 1' | sed -r 's/.|([0-9]+).*/\1/g'没有比赛,但是echo 'foo 1' | sed -r 's/([0-9]+).*|./\1/g'有。
revo

@Sundeep还对分隔表达式的变通方法不适用于相同的开始和结束定界符,我为此添加了注释。
revo

会发生什么,当不同的交替从同一位置开始并具有相同的长度,猜测会遵循从左到右的顺序像其他引擎..需要仰视如果在手册中描述伟大的一点
森迪普•

:虽然有这里有一个奇怪的情况下stackoverflow.com/questions/59683820/...
森迪普•

20

针对多个字符的非贪婪解决方案

该线程确实很旧,但我认为人们仍然需要它。假设您想杀死一切,直到第一次出现HELLO。你不能说[^HELLO]...

因此,一个不错的解决方案涉及两个步骤,假设您可以保留输入中不期望的唯一单词,例如say 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!


4
为了使它更好,在无法使用未使用字符的情况下很有用:1.用真正未使用的WORD替换该特殊字符,2.用特殊字符替换结尾序列,3.以特殊字符结尾的搜索,4 。替换特殊字符,5.替换特殊WORD。例如,您想要<hello>和</ hello>之间的贪婪运算符:
Jakub,2014年

3
在这里示例:echo“查找:<hello> fir〜st <br>是</ hello> <hello> sec〜ond </ hello>” | sed -e“ s,〜,VERYSPECIAL,g” -e“ s,</ hello>,〜,g” -e“ s,。*查找:<hello>([^〜] *)。*,\ 1 ,“ -e” s,\〜,</ hello>,“ -e” s,VERYSPECIAL,〜,“
雅库布(Jakub)

2
我同意。不错的解决方案。我想改写一下评论:如果您不能依靠〜未使用,请先使用s /〜/ VERYspeciaL / g替换其当前出现的位置,然后执行上述技巧,然后使用s / VERYspeciaL /〜/ g返回原始的〜
ishahak 2014年

1
我倾向于在这种情况下使用稀有的“变量”,因此`,我会使用<$$>(因为它$$在shell中扩展为您的进程ID,尽管您必须使用双引号而不是单引号,并且可能会破坏您的正则表达式的其他部分),或者,如果可以使用unicode,则类似的东西<∈∋>
亚当·卡兹

在某些时候,您必须问自己为什么不只使用perlpython其他某种语言。perl在一行中以不太脆弱的方式做到了这一点
ArtOfWarfare

18

sed-Christoph Sieghart的非贪婪匹配

在sed中获得非贪婪匹配的技巧是匹配除终止匹配的字符以外的所有字符。我知道,这很容易,但是我浪费了宝贵的时间,毕竟,shell脚本应该是快速而简单的。因此,如果有人可能需要它:

贪婪匹配

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

非贪婪匹配

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

17

这可以使用cut来完成:

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

9

不使用正则表达式的另一种方法是使用字段/定界符方法,例如

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

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)



3

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

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


4
不,不是。至少不是GNU sed。
Michel de Ruiter

7
更广泛地说,它-E是BSD sed以及OS X 所独有的。手册页的链接。-r确实将扩展的正则表达式带到了GNU,sed如@stephancheg的更正中所述。当使用跨'nix分布的已知可变性的命令时要当心。我了解到这很困难。
2012年

1
如果要使用sed,这是正确的答案,并且最适用于初始问题。
2013年

8
GNU sed的-r选项仅根据Appendix A Extended regular expressions信息文件和一些快速测试来更改转义规则。它实际上并没有添加非贪婪的限定词(GNU sed version 4.2.1至少。)
eichin 2013年

1
GNU sed -E一段时间以来一直被认为是未记录的选项,但是在4.2.2.177版本中,文档已进行更新以反映这一点,所以-E现在两者都很好。
本杰明·W。17年

3

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

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

唯一的问题是它还会剪切最后一个分隔符('/'),但是如果您确实需要它,您仍然可以在“循环”完成后简单地将其放回去,只需在上一个命令的末尾附加此附加命令即可。命令行:

-e "s,$,/,"

2

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

回声“ http://www.suon.co.uk/product/1/7/3/” | sed“ s | ^ \(。* // \)\([^ /] * \)。* $ | \ 1 \ 2 |”

如果您不熟悉分组,请从此处开始。


1

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


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/}/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 正在将占位符字符串转换回其原始字符。

请注意,上面的内容不依赖于输入中不存在的任何特定字符串,因为它在第一步中制造了这样的字符串,也不在乎您想要匹配的任何特定正则表达式的出现位置,因为您可以{[^{}]*}根据需要使用多次在表达式中隔离所需的实际匹配项和/或使用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

1

尚未看到此答案,所以这是您可以使用vi或的方法vim

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

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

我喜欢使用vi有时超级复杂的正则表达式,因为:(1)Perl是奄奄一息,(2),Vim有一个非常先进的正则表达式引擎,和(3)我已经非常熟悉vi我的一天到一天的使用正则表达式编辑文件。


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

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


4
因此您会遇到贪婪的match /home/one/two/three///home/one/two/three/four/myfile.txtfour/home/one/two/three/four
:,

0

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


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

希望有帮助!


0

另一个sed版本:

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

它匹配/后跟一个字母数字字符(因此不能再加上一个正斜杠)以及其余字符,直到行尾为止。之后,它什么也不替换(即删除它。)


1
我想应该是"[[:alnum:]]",不是"[:alphanum:]"
oli_arborum
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.