这是一个通过原始测试的模式,同时也解决了 Vitali Ponomar 对 manji 答案括号的评论。
^.*?[.!?](?:\s|$)(?!.*\))
这使用negated lookahead 来有效地说:
- 从头开始并匹配任意字符,任意次数,但尽可能少地匹配仍然允许以下内容成立。
- 我们看到以下字符之一:
. 或 ? 或 !
- 后跟:空格字符或行尾
-
不是后跟任何导致
) 右括号字符的内容。
这利用了这样一个事实,即只要括号组是平衡的,我们就知道括号组在哪里结束。因此,如果由于用户输入或处理不当等原因导致句子格式错误,它可能会失败。
您可以通过断言“句首”标记必须包含大写字符来添加一定程度的保护。
^.*?[.!?](?:\s[A-Z]|$)(?!.*\))
这是可取的原因是,在大多数程序中,在连接它们之前将字符串大写要比确保括号在其中正确平衡要容易得多。
请注意,因为 OP 接受了使用 non-capturing 组的答案,例如 (?:foo),所以我也使用了一个。这将导致“句子开头”标记包含在匹配中。您可能想要也可能不想要这个,这取决于您是仅依赖空格字符还是我添加的大小写检查。
我的建议是不包含它,您可以使用 lookahead 来代替,例如 (?=foo)。
^.*?[.!?](?=\s[A-Z]|$)(?!.*\))
现在我们没有在匹配中包含 cruft,让我们来处理第一句后只有一个空格的情况:
^.*?[.!?](?=\s[A-Z]|\s?$)(?!.*\))
现在用这个相当不错的模式进行一些测试:
太好了。但仍有一些地方会出现这种情况。例如:引号。句子很复杂!要做到这一点,您确实需要考虑给定语言的整个标点符号规则,然后提出一种算法,该算法不会假设每个人都会始终完美地遵循它们,并在不引入奇怪匹配的情况下使某些部分成为可选部分。一旦你沿着这条路走下去,你最终会得到一个长而难以理解的表达式,其中包含大量 greed operators(? 问号的某些用法)。
最后,它主要取决于程序的输入是什么样的,它来自哪里,以及在对它应用复杂的模式匹配之前你可以对它进行多好的预处理。通常,对更小、更简单的模式进行多次传递虽然性能较差,但更可靠、更易读。一种是删除或删除您不关心的内容(如换行符或其他空白字符),然后一种是删除可能的恶意输入痕迹,......等。随着输入的简化,慢慢变得更加复杂。