登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go bufio.Scanner 如何按空行分隔多段文本

来源:17golang原创

时间:2026-10-16 10:56:35 392浏览 收藏

Go 的 bufio.Scanner 默认使用 ScanLines,每次 Scan() 取得一行文本;遇到空行时,Text() 得到空字符串。它不会自动把多行合并成段落,所以“按空行分隔多段文本”要在扫描循环外维护一个当前段缓冲区:普通行追加,空行提交,连续空行忽略,循环结束再提交一次最后一段。

官方文档:https://pkg.go.dev/bufio#Scanner

下面的写法适合配置文件片段、邮件正文、Markdown 简化解析等场景。本文只处理段落边界,不把超长 token 的缓冲修复混进来。

一、先明确 Scanner 返回什么

Scanner 负责从 io.Reader 取数据,分隔规则由 SplitFunc 决定。默认的 ScanLines 去掉行尾换行符,但仍然可以返回空行;因此段落并不是 Scanner 的内置结果,而是由业务代码根据空行重新聚合出来的。

Go Scanner 从 io.Reader 到段落结果的静态结构说明图
图1:Go Scanner 按行读取并聚合段落的结构说明图,重点看输入扫描、空行 token 与当前段缓冲区的边界;这是静态说明图,不是运行截图。

二、用当前段缓冲区聚合普通行

最小实现是保留一个 current 切片。普通行直接追加,提交时用换行重新拼接。这样不会把每次 Scan 的结果误认为一个完整段落,也能保留段内原有的行结构。

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)

// readParagraphs 按空行聚合段落,并把底层读取错误返回给调用方。
func readParagraphs(r io.Reader) ([]string, error) {
    scanner := bufio.NewScanner(r)
    // 显式写出默认规则,让段落判断建立在“逐行”语义上。
    scanner.Split(bufio.ScanLines)

    var paragraphs []string
    var current []string

    // flush 只提交非空段,连续空行不会制造空字符串段落。
    flush := func() {
        if len(current) == 0 {
            return
        }
        paragraphs = append(paragraphs, strings.Join(current, "\n"))
        current = current[:0]
    }

    for scanner.Scan() {
        line := scanner.Text()
        // 这里只判断空行,普通行原样保存,不改写正文空格。
        if line == "" {
            flush()
            continue
        }
        current = append(current, line)
    }

    // 最后一段可能没有尾随空行,必须在 Scan 结束后补交。
    flush()
    if err := scanner.Err(); err != nil {
        return nil, fmt.Errorf("扫描文本失败: %w", err)
    }
    return paragraphs, nil
}

这里的关键不是 strings.Join,而是 flush 的调用位置:空行触发一次,EOF 再触发一次。current[:0] 复用切片容量,但不会把已经追加到 paragraphs 的字符串内容清掉。

三、遇到空行时提交段落

空行提交的是“当前已经收集到的段”,不是空行本身。输入中有两个或更多连续空行时,第一次空行会提交当前段,后续空行只会命中空缓冲区并返回,因此结果不会凭空多出空段。

如果业务需要保留段落内部的换行,上面的 strings.Join(current, "\n") 就可以直接使用;如果只关心段落文本,也可以在提交时改用空格拼接,但那会改变原文的换行语义。

四、在 EOF 和 Err 分别收尾

Scan() 返回 false 既可能表示正常 EOF,也可能表示读取错误。先调用一次 flush 可以覆盖“最后一段没有空行结尾”的正常情况;随后检查 scanner.Err(),避免把半截输入误当成完整结果。

普通行、空行、连续分隔、EOF 与 scanner.Err 的段落边界说明图
图2:段落聚合的输入边界说明图,比较严格空行、空白行、连续分隔、EOF 与 scanner.Err;这是结果语义说明图,不是运行截图。

如果读取源在中途返回错误,函数应该保留错误信息并交给上层决定是否丢弃已收集的段落。示例选择返回 nil 和包装后的错误,表达“这次结果不完整”;如果业务允许部分结果,则可以把 paragraphs 与错误同时返回,但要在接口文档中写清楚。

五、按输入规则选择空行判断

严格空行与只包含空格或制表符的行不是同一个规则。示例使用 line == "",适合只有真正空行才分段的格式。如果上游文本把空白行也当作分隔符,可以把条件替换为 strings.TrimSpace(line) == "",但要接受它会把只含空格的正文行当成分隔线。

// isSeparator 根据输入协议决定“空行”的定义。
func isSeparator(line string, blankSpaceSeparates bool) bool {
    if blankSpaceSeparates {
        // 业务允许空格、制表符单独作为段落分隔。
        return strings.TrimSpace(line) == ""
    }
    // 严格模式只接受长度为零的行。
    return line == ""
}

选择规则时不要顺手调用 TrimSpace 改写每一行。更稳妥的做法是只用它参与分隔判断,真正追加到当前段的仍然保留原始 line。

常见误区

  • 把每次 Scan() 当成段落:默认规则返回的是行,不是段。
  • 只在遇到空行时 flush:没有尾随空行的最后一段会丢失。
  • 忽略 scanner.Err():读取失败时可能把不完整文本当成成功结果。
  • 无条件 TrimSpace:会同时改变正文中的前后空格和分隔判断,应该分开处理。
  • 把连续空行追加为结果:flush 前先判断当前缓冲区,才能保持段落结果紧凑。

速查表

输入情况处理动作结果
普通行追加到 current继续收集当前段
严格空行flush current提交已有段落
连续空行空 current 时跳过不产生空段
EOF最后再 flush 一次保留无尾随空行的段落
scanner.Err 非 nil返回错误不把不完整输入当成功

相关问题

Scanner 能否直接按段落返回?可以自定义 SplitFunc,但段落规则和错误边界会一起进入切分函数;只需要按空行聚合时,先用 ScanLines 再在业务层 flush 更容易维护。

空行后还要保留分隔符吗?如果输出只需要段落文本,通常不需要;如果要重建原文,应额外记录分隔符数量,而不是把它们静默丢弃。

文本可能包含超长单行怎么办?那是 Scanner token 上限问题,应单独评估 Scanner.Buffer 或 bufio.Reader,不要和空行聚合规则混在一起。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>