← 中文资源

简体中文版本

纯文本句子边界切分

按句末标点切分纯文本,正确保留常见缩写、小数以及紧跟在句号后的引号。

阅读英文原文

功能说明

这个切分器面向没有 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()