正则表达式的单词边界表达式

34

例如,我有以下字符串"one two(three) (three) four five",我想将"(three)"替换为"(four)",但不影响单词内的部分。我该如何实现?

基本上,我想执行一个正则表达式替换,并最终得到以下字符串:

"one two(three) (four) four five"

我尝试了以下正则表达式,但它没有起作用:

@"\b\(three\)\b"
基本上,我正在编写一些搜索和替换代码,并为用户提供常规的匹配大小写、匹配整个单词等选项。在这种情况下,用户选择匹配整个单词,但我不知道被搜索的文本是什么。

任何位于括号(或)两侧的内容都将自动成为单词边界,因为它不在两个单词字符之间。 - Gareth
5个回答

68

你的问题源于对 \b 的实际含义的误解。不可否认地,这并不明显。

\b\(three\)\b 无法匹配你的输入字符串中的“three”的原因如下:

  • \b 表示:一个单词字符和一个非单词字符之间的边界。
  • 字母 (例如 a-z) 被视为单词字符
  • 标点符号,如(被视为非单词字符

下面是您的输入字符串,稍微拉长一点,并标出了\b匹配的位置:

 o n e   t w o ( t h r e e )   ( t h r e e )   f o u r   f i v e
↑     ↑ ↑     ↑ ↑         ↑     ↑         ↑   ↑       ↑ ↑       ↑

正如您在这里看到的,"two" 和 "(three)" 之间有一个 \b,但在第二个 "(three)" 前面没有。
故事的寓意是什么?如果你要搜索的不仅仅是一个单词(一串字母),那么“全词搜索”就没有太多意义了。由于你的搜索字符串中有标点符号(括号),所以它并不是一个“单词”。如果你只搜索由单词字符组成的单词,那么\b会做你期望的事情。
当然,您可以使用不同的正则表达式来匹配字符串,只有在被空格包围或出现在字符串的开头或结尾时才匹配:
(^|\s)\(three\)(\s|$)

然而,这样的问题是,如果您搜索“three”(不带括号),它将无法找到“(three)”中的一个,因为它周围没有空格,即使它实际上是一个完整的单词。
我认为大多数文本编辑器(包括Visual Studio)只会在您的搜索字符串实际上以单词字符开头和/或结尾时使用\b。
var pattern = Regex.Escape(searchString);
if (Regex.IsMatch(searchString, @"^\w"))
    pattern = @"\b" + pattern;
if (Regex.IsMatch(searchString, @"\w$"))
    pattern = pattern + @"\b";

那样的话,即使你选择“仅整个单词”,他们也会找到“(三)”。

可能这听起来没有意义,但这就是我想让它工作的方式。你有任何想法我该怎么做吗?基本上,我想模仿 Visual Studio 中的查找和替换功能。 - CroweMan
@CroweMan:你自相矛盾。你说,“我不想替换‘两个(三个)’”,但是Visual Studio却要替换。 - Timwi
非常感谢。你是个明星! - CroweMan
1
请注意 \b 样式边界。 - tchrist

9

这里有一段简单的代码,你可能会感兴趣:

    string pattern = @"\b" + find + @"\b";
    Regex.Replace(stringToSearch, pattern, replace, RegexOptions.IgnoreCase);

源代码:snip2code - C#: 替换句子中的确切单词


注意:正如接受的答案中所提到的,这只适用于“查找”是仅由“单词”(\w)字符组成的字符串。它不适用于此问题,其中查找是“(three)”。 - ToolmakerSteve

1

了解一下 单词边界 的匹配情况:

单词边界可以出现在以下三个位置:

  • 如果第一个字符是单词字符,则出现在字符串中第一个字符之前。
  • 如果最后一个字符是单词字符,则出现在字符串中最后一个字符之后。
  • 出现在字符串中两个字符之间,其中一个是单词字符,另一个不是单词字符。

所以,你的 \b\(three\)\b 正则表达式确实可以工作,但并不是你期望的方式。它不能匹配 In (three) yearsIn(three) yearsIn (three)years 中的 (three),但是在 In(three)years它能够匹配,因为在 n( 以及 )y 之间存在单词边界。

在这种情况下,您可以使用动态适应性单词边界,这些构造确保仅在预期时匹配整个单词(请参见我的 "Dynamic adaptive word boundaries" YT 视频,以更好地理解这些构造)。

在 C# 中,它可以写成:

@"(?!\B\w)\(three\)(?<!\w\B)"

简而言之:
  • (?!\B\w) - 仅在紧随单词字符的字符后面需要左侧的单词边界
  • \(three\)
  • (?<!\w\B) - 仅在先前的字符是单词字符时,才要求右侧的单词边界。
如果您的搜索短语可能包含空格,并且您需要首先匹配更长的替代方案,则可以从列表中动态构建模式。
var phrases = new List<string> { @"(one)", @".two.", "[three]" };
phrases = phrases.OrderByDescending(x => x.Length).ToList();
var pattern = $@"(?!\B\w)(?:{string.Join("|", phrases.Select(z => Regex.Escape(z)))})(?<!\w\B)";

通过使用类似于(?!\B\w)(?:\[three]|\(one\)|\.two\.)(?<!\w\B)的匹配模式,您可以匹配到预期结果。请参见C#演示正则表达式演示


0

我最近在 JavaScript 中遇到了类似的问题,尝试仅将以 "$" 字符开头的术语作为单独的词匹配,例如,如果 $hot = 'FUZZ',则:

"some $hot $hotel bird$hot pellets" ---> "some FUZZ $hotel bird$hot pellets"

正则表达式/\b\$hot\b/g(我的第一个猜测)之所以不起作用,与原问题中括号不匹配的原因相同——作为非单词字符,它们之前没有空格或字符串开头的单词/非单词边界。
然而,正则表达式/\B\$hot\b/g确实匹配,这表明@timwi的优秀示例中未标记的位置与\B术语匹配。对我来说,这并不直观,因为") ("不是由正则表达式单词字符组成的。但我想,既然\B是\b类的反转,它就不必是单词字符,它只需要不是-不是-单词字符 :)

-1

正如Gopi所说,但(理论上)只捕获(three)而不是two(three)

string input = "one two(three) (three) four five";

string output = input.Replace(" (three) ", " (four) ");

当我测试时,我得到:"one two(three) (four) four five"。请记住,空格也是一个字符串字符,因此它也可以被替换。如果我这样做:

//use same input
string output = input.Replace(" ", ";");

我会得到one;two(three);(three);four;five"


问题在于用户正在输入文本到查找和替换框中,并且他们选择了“匹配整个单词”。因此,我需要使用像正则表达式这样的智能工具,而不能仅仅在表达式前后添加一个“ ”,因为后面的字符可能是“,”或其他字符。 - CroweMan

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