
本文介绍如何使用自定义 bufio.SplitFunc 让 Go 的 bufio.Scanner 正确解析 PDF 规范兼容的混合行结尾(CR、LF 或 CRLF),弥补标准 ScanLines 对单个 \r 的支持缺失。
本文介绍如何使用自定义 `bufio.splitfunc` 让 go 的 `bufio.scanner` 正确解析 pdf 规范兼容的混合行结尾(cr、lf 或 crlf),弥补标准 `scanlines` 对单个 `\r` 的支持缺失。
Go 标准库的 bufio.Scanner 默认通过 bufio.ScanLines 分割行,但它仅支持“可选的 \r + 必需的 \n”(即 \n 或 \r\n),不支持单独的 \r(Mac OS 9 风格或部分 PDF 文件中的行结束符)。而 PDF 规范明确允许三种行终结方式:\r、\n 或 \r\n。因此,需实现一个更灵活的分割函数。
下面是一个生产就绪的 SplitFunc 实现,它能准确识别任意一种合法行结尾,并返回不含终止符的行内容:
import (
"bytes"
"bufio"
)
// ScanPDFLines 是一个 bufio.SplitFunc,支持 \r、\n 和 \r\n 作为行结束符
func ScanPDFLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
// 空数据且已到 EOF:无内容可返回
if atEOF && len(data) == 0 {
return 0, nil, nil
}
// 查找第一个 \r 或 \n 的位置
if i := bytes.IndexAny(data, "\r\n"); i >= 0 {
if data[i] == '\n' {
// 情况1:遇到 \n → 行以 \n 结束(可能前面有 \r,但已被包含在 [0:i] 中)
return i + 1, data[0:i], nil
}
// 情况2:遇到 \r
advance = i + 1
// 检查是否为 \r\n 组合:\r 后紧跟 \n
if len(data) > i+1 && data[i+1] == '\n' {
advance += 1
}
return advance, data[0:i], nil
}
// 未找到行结束符
if atEOF {
// 已到文件末尾 → 返回剩余全部内容作为最后一行(无终止符)
return len(data), data, nil
}
// 尚未到达 EOF,且当前 buffer 中无行结束符 → 请求更多数据
return 0, nil, nil
}
使用方式简洁直观:
file, _ := os.Open("document.pdf")
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(ScanPDFLines)
for scanner.Scan() {
line := scanner.Text() // 自动 UTF-8 解码;如需原始字节,用 scanner.Bytes()
// 处理 line...
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
✅ 关键优势:
- 完全兼容 PDF 规范(ISO 32000)对行终结的定义;
- 零内存拷贝(
token直接切片自data,无需append或copy); - 正确处理跨 buffer 边界的行结尾(如
\r在当前 chunk 末尾,\n在下个 chunk 开头——bufio.Scanner会自动合并缓冲区并重试分割); - 支持空行和末尾无终止符的文件。
⚠️ 注意事项:
-
ScanPDFLines不处理 Unicode 换行符(如U+2028LINE SEPARATOR),PDF 中不使用,故无需扩展; - 若需保留原始行结尾用于校验或重写,请改用
scanner.Bytes()并自行截断,或改写token逻辑; -
bufio.Scanner默认每行上限 64KB,超长行会报错;如需支持超长 PDF 行,调用scanner.Buffer(make([]byte, 64*1024), 1 提升容量。
综上,通过自定义 SplitFunc,你无需从头实现流式字节解析,即可在保持 Scanner 易用性的同时,精准满足 PDF 等协议对行终结的宽松要求。











