功能说明
这个切分器面向没有 NLP 依赖的轻量场景,例如生成摘要、字幕分句、文档段落统计。它先压缩连续空白,再扫描句末标点,只有在标点后面跟着空格(或先跟一个右引号再跟空格)时才认为句子结束,因此不会把 3.14 这类小数拆开。
常见缩写表覆盖称谓、公司后缀和 e.g. 等写法;单个字母后跟句点也会被视为缩写,避免把姓名首字母当成句末。右引号、右括号会归入前一句,符合排版习惯。
关键代码
split_sentences 维护 start 和 index 两个位置指针。遇到句末标点时,cursor 先跳过可能的右引号,再检查空格;确认边界后把 start 到 cursor 的片段加入结果,并把 start 移到空格之后。_is_abbreviation 取句点前最多十六个字符,按空格切出最后一个单词,去掉左括号和引号后与缩写表比较。末尾剩余片段单独追加。
使用注意
该实现只处理英文标点,中文句号和省略号需要扩展字符集合。缩写表是静态的,遇到未收录的缩写(例如公司自定义简写)仍会误切,可以按业务补充。换行会被折叠成空格,因此原文中的段落边界丢失,需要段落信息时请先按空行切段。问号和感叹号不做缩写判断。URL 中的点号后通常没有空格,不会被切断,但以句点结尾的域名可能误判。
验证方式
示例段落应输出五句:第一句包含 Dr. 和 Inc. 两个缩写而不被拆开;第二句保留 12.5% 中的小数点;第四句把右引号留在 yes. 之后;第五句是结尾陈述。可以再加入 e.g. 与首字母缩写用例,确认它们不会产生额外句子。
代码示例
ABBREVIATIONS = frozenset(
{
"mr",
"mrs",
"ms",
"dr",
"prof",
"sr",
"jr",
"vs",
"etc",
"e.g",
"i.e",
"fig",
"no",
"inc",
"ltd",
}
)
CLOSERS = "\"')]}"
def _is_abbreviation(text: str, dot_index: int) -> bool:
window = text[max(0, dot_index - 16) : dot_index]
word = window.rsplit(" ", 1)[-1].lower().lstrip("(\"'")
return word in ABBREVIATIONS or (len(word) == 1 and word.isalpha())
def split_sentences(text: str) -> list[str]:
normalized = " ".join(text.split())
if not normalized:
return []
sentences: list[str] = []
start = 0
index = 0
while index < len(normalized):
char = normalized[index]
if char in ".!?":
cursor = index + 1
if cursor < len(normalized) and normalized[cursor] in CLOSERS:
cursor += 1
if cursor < len(normalized) and normalized[cursor] == " ":
if char == "." and _is_abbreviation(normalized, index):
index += 1
continue
sentences.append(normalized[start:cursor].strip())
start = cursor + 1
index = cursor
index += 1
tail = normalized[start:].strip()
if tail:
sentences.append(tail)
return [sentence for sentence in sentences if sentence]
def main() -> None:
paragraph = (
"Dr. Smith joined Acme Inc. in 2019. Revenue grew 12.5% that year! "
"Was it sustainable? The board said \"yes.\" Next year told another story."
)
for number, sentence in enumerate(split_sentences(paragraph), start=1):
print(f"{number}. {sentence}")
if __name__ == "__main__":
main()