Go 如何按行读取(大)文件?尝试 bufio 包提供的几种方式

嗨,大家好!我是波罗学。本文是系列文章 Go 技巧第十七篇,系列文章查看:Go 语言技巧。

本文将介绍 Go 如何按行读取文件,基于此会逐步延伸到如何按块读取文件。

引言

我们将要介绍的按行读取文件的方式其实是非常适合处理超大文件。

按行读取文件相较于一次性载入,有着很多优势,如内存效率高、处理速度快、实时性高、可扩展性强和灵活度高等,特别是当遇到处理大文件时,这些优势会更加明显。

稍微展开说下各个优势吧。

内存效率高,因为是按行读取,处理完一行就会丢弃,内存占用将大大减少。

处理速度快,主要体现在逐行处理时,因为无需等待全量数据,能更快开始,而且如果无顺序要求,还可并行计算以最大化利用计算资源,进一步提升处理速度。

实时性高,因为按行读取,无需一次加载全量数据,自然有 实时性高 的特点,这对于处理实时流数据,如日志数据,非常有用。

可扩展性强,按行读取这种方式,不仅仅适用于小文件,大文件同样使用,有了统一的处理方式,即使未来数据量膨胀,也易于扩展。

灵活度高,因为是一行行的处理,如果想停止,随时可以。如果继续之前的流程,我们只要重新启动,从之前的位置继续处理即可。

按行读取其实只是按块读取的一种特殊形式(分隔符是 \n),自然地,上述的优势也同样适用于按块读取文件。

本文的重点在于如何使用 GO 实现按行读取,基于的是标准库的 bufio.Readerbufio.Scanner

正式进入主题吧。

准备一个文本文件

我们先准备一个文本文件 example.txt,内容如下:

This post covers the Golang Interface. Let’s dive into it.

Duck Typing

To understand Go’s interfaces, it’s crucial to grasp the Duck Typing concept.

So, what’s Duck Typing?

基于 bufio.Reader

Go 中的按行读取文件,首先可通过 bufio 提供的 Reader 类型实现。

使用 Reader.ReadLine

Reader 中有一个名为 ReadLine 的方法,顾名思义,它的作用就是按行读取文件的。

演示代码:

file, err := os.Open("example.txt")
if err != nil {
	panic(err)
}
defer func() { _ = file.Close() }()

reader := bufio.NewReader(file)
for {
	line, _, err := reader.ReadLine() // 按行读取文件
	if err == io.EOF { // 用于判断文件是否读取到结尾
		break
	}
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", line)
}

重点就是那句 line, _, err := reader.ReadLine(),返回值的第一值是读取的内容,第三个值是错误信息。

执行与输出:

$ go run main.go
This post covers the Golang Interface. Let’s dive into it.

Duck Typing

To understand Go’s interfaces, it’s crucial to grasp the Duck Typing concept.

So, what’s Duck Typing?

和我们预期的一样,输出了完整的文本信息。

要提醒的是,ReadLine 读取的内容不包括行尾符(如 “\r\n” 或 “\n”)。也就是说,当读取到一行数据时,要自行处理可能的行尾符差异,尤其是在处理来自不同操作系统的文本数据时。

还有,ReadLine 省略的第二个参数,名为 isPrefix,它表示是否是前缀的意思,如果 isPrefix 为 true 表示返回的 line 被截断了,而截断原因很可能是行的内容大小大于缓冲区。我们可以在初始化时通过 bufio.NewReaderSize(rd io.Reader, size int) 调整默认缓冲区大小。

不过,这并非最优的解法。

使用 Reader.ReadString

解决大行读取被截断的问题,还可用 bufio.Reader 的另外一个方法 ReadString 解决。

它与 ReadLine 类似,不过在单个 buffer 不足以容纳单行内容时,它会多次读取,直到找到目标分割符,合并多次读取的内容。

示例代码:

reader := bufio.NewReader(file)
for {
	line, err := reader.ReadString('\n')
	if err == io.EOF {
		break
	}
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s\n", line)
}

重点就是那句 reader.ReadString('\n'),它的入参是分割符(delim),即 ‘\n’,而返回值分别读取内容(line)和错误(err)。

相较于 ReadLineReadString 显然是更加灵活,无大行读取被截断的问题,而且分割符也可自定义。但只支持单一字节的分割符自定义,还不够完美,如我们想按多个字符(如 .|, 等等)分割文本,或者按照大小分块读取,就没有那么方便了。

我们继续引入另一个 Go 标准提供的按行读取文件的方案,即 bufio.Scanner

使用 bufio.Scanner

为了由浅入深地介绍 bufio.Scanner 的使用,我们还是先从 bufio.Scanner 实现按行读取讲起吧。

一个示例代码了解 bufio.Scanner 的基本使用。

// 创建文件的扫描器,用于逐行读取文件
scanner := bufio.NewScanner(file)
// 循环,直到文件结束
for scanner.Scan() {
    // 处理每行的内容:打印
    fmt.Println(scanner.Text())
}

// 最后,检查扫描过程中是否有错误发生
if err := scanner.Err(); err != nil {
    panic(err)
}

这个例子中,我们基于打开的文件描述符 file,创建了一个 bufio.Scanner 变量 scanner,它通过 scanner.Scan() 逐行扫描文件和 scanner.Text() 从 buffer 中获取扫描内容,直到结束。

毫无疑问,相对于 bufio.Reader,以上通过 bufio.Scanner 实现的代码简洁很多,而且,错误处理也是集中在 for 循环完成后统一进行。

如何读取大行?

bufio.Scanner 如何处理特别长的行呢?

默认情况下,bufio.Scanner 初始缓冲区是 4KB,而最大 token 大小是 64KB,即无法处理超过 64KB 的行。

来自源码中的定义,如下所示:

// `MaxScanTokenSize` 可定义 buffer 中 token 的最大 size,
// 除非用户通过 `Scanner.Buffer` 显式修改
// 缓冲区初始大小和 token 最大 size,
// 实际的最大标记大小可能会更小,因为
// 缓冲区可能需要包含例如换行符之类的内容。
MaxScanTokenSize = 64 * 1024
// 缓冲区的初始大小 
startBufSize = 4096 

bufio.Scanner 中提供了 Scanner.Buffer() 方法可用于调整默认的缓冲区。

示例代码:

const maxCapacity = 1024 * 1024  // 例如,1MB,可读取任何 1MB 的行。
buf := make([]byte, maxCapacity) // 初始缓冲大小 1MB,无需多次扩容
scanner.Buffer(buf, maxCapacity)

scanner 扫描前,加上这段代码,会重新设置缓冲区,将初始缓冲大小和最大容易都设置为 1MB,这样就可以处理异常长的大行(size <= 1MB)了,而且由于初始缓冲区大小就是最大容量,也无需多次扩容缓冲。

缓冲区逻辑

为了更好理解上面的缓冲区配置,我简单介绍下 bufio.Scanner 是的 Scan 文件读取逻辑以及缓冲区是如何用的。

bufio.Scanner 内部有一个 s.buf 缓冲区,当我们调用 scannder.Scan 方法时,它会尝试用 io.Reader(即示例中的 file 文件描述符)中读取一个缓存大小的内容。它的具体实现是在 bufio.ScannerScan 方法中。如果当缓冲区大小不足以容纳一个完整的 token,Scanner 会自动增加缓冲区的大小。

接下来,让我们实现 bufio.Scanner 按单词读取。

扩展思路

如果每次都读取这么大块的一整行,和一次载入没有什么区别,这明显已经失去了开头介绍的一行行读取的优势了。

除了直接读取整行,是否还有什么更好的方法处理大行呢?

我们可以尝试解放一些思路,是否还有其他方式定义一次读取内容呢?我们只要保证读取的内容有实际含义即可,如按一句话,一个单词或者固定的块大小的切割,而非是纠结于是不是一整行。

分割规则定义

在正式介绍切割规则前,先说明下什么是完整 token。前面一直在说 token,如 MaxScanTokenSize 定义的就是 token 最大 size。

token 定义其实就是对一次读取内容的定义,如一行文本,一个单词,或者一个固定大小的块。相对于特定分隔符,分割规则更加灵活,可以定义任意的分割方式。

bufio.Scanner 是一个非常灵活的工具,它提供了自定义切割文本规则的函数 - Scanner.Split

// 参数
//  data []byte: 未处理数据的初始子串,当前需要处理的输入数据。
//  atEOF bool: 一个标志,如果为 true,则表示没有更多数据可处理。
// 返回值
//  advance int: 需要在输入中前进多少以到达下一个标记的起始位置。
//  token []byte: 要返回给用户的内容(如果有)。
//  err error: 扫描过程中遇到的错误。
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

它的返回是分别读取内容的长度、读取的内容和错误信息。

默认情况下,Scanner 按行分割(ScanLines)。

scanner.Split(bufio.ScanLines) // 默认配置,按行读取

我们可以通过自定义的 Split 函数改变这个默认行为,如按单词分割。

示例代码:

const input = "This is a test. This is only a test."
scanner := bufio.NewScanner(strings.NewReader(input))

// 设置分割函数为按单词分割
scanner.Split(bufio.ScanWords)

// 逐个读取单词
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading input:", err)
}

输出:

This
is
a
test.
This
is
only
a
test.

现在,无论多大的文件,我们都可以通过巧妙定义切割方式来避免一次性读取的缺点了。

我之前利用 whispwer 识别油管视频的字幕,有些视频的内容非常长,超长字幕,都在一行。现在我就可以通过如句号、问号、感叹号分割即可。现在,我要做的定义这样一个 ScanSentences 函数。

示例代码:

func ScanSentences(data []byte, atEOF bool) (advance int, token []byte, err error) {
	// 如果我们处于 EOF 并且有数据,则返回剩余的数据
	if atEOF && len(data) > 0 {
		return len(data), data, nil
	}

	// 定义一个查找任意句子结束符的函数
	findSentenceEnd := func(data []byte) int {
		// 检查每个可能的句子结束符
		endIndex := -1
		for _, sep := range []byte{'.', '?', '!'} {
			if i := bytes.IndexByte(data, sep); i >= 0 {
        // 选择最小的 index 作为句子结尾
				if i < endIndex || endIndex == -1 {
					endIndex = i
				}
			}
		}
		return endIndex
	}

	// 使用新的查找逻辑来查找句子结束位置
	if i := findSentenceEnd(data); i >= 0 {
		// 返回找到的句子(包括句子结束符),以及下一个 token 的起始位置
		return i + 1, data[:i+1], nil
	}

	return 0, nil, nil
}

我们写个 main 函数测试下 ScanSentences 的正确性吧。

示例代码:

func main() {
	const input = "This is a test. This is only a test. Is this a test? \n" +
		"Wow, what a brilliant test! Thanks for your help."
	scanner := bufio.NewScanner(strings.NewReader(input))
	scanner.Split(ScanSentences)
	for scanner.Scan() {
		text := scanner.Text()
		fmt.Printf("%s\n", strings.TrimSpace(text))
	}

	if err := scanner.Err(); err != nil {
		panic(err)
	}
}

执行输出:

$ go run main.go
This is a test.
This is only a test.
Is this a test?
Wow, what a brilliant test!
Thanks for your help.

或者按照固定大小分批读取文件,SplitBatchSize 示例代码:

// ScanBatchSize 返回一个 bufio.SplitFunc 函数,该函数按照固定的大小分割数据。
// 如果数据大小不足一个完整的批次,并且已经到达 EOF,则返回剩余的数据。
func ScanBatchSize(batchSize int) bufio.SplitFunc {
	return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		// 如果数据大小达到或超过批次大小,或者在 EOF 时有剩余数据
		if len(data) >= batchSize || (atEOF && len(data) > 0) {
			// 如果当前批次大小超过剩余数据大小,则只返回剩余数据
			if len(data) < batchSize {
				return len(data), data[:], nil
			}
			// 否则,返回一个完整批次的大小和数据
			return batchSize, data[:batchSize], nil
		}

		// 如果没有足够的数据并且没有到达 EOF,需要更多数据来形成一个完整的批次
		if !atEOF {
			return 0, nil, nil
		}

		// 处理到达 EOF 但没有剩余数据的情况
		return 0, nil, nil
	}
}

SplitBatchSize 是一个闭包,它的返回值是我们期待的 SplitFunc。我们可传递参数配置每次读取内容的大小。具体可自行测试,这里就演示了。

不得不说

到这里,我还是想再提一点,每次从文件中读取内容大小是由传入系统调用 read() 函数时传入参数 buf 大小决定的,而不是由所谓按行还是按块确定的。按行按块是基于读取出来的二次处理的结果。

之所以要提这点,因为我之前看到一些文章说,按块相比按行读取减少了读取的次数。

结论

本文详细介绍了在 Go 中如何使用 bufio.Reader 和 bufio.Scanner 按行或按块读取文件,通过利用 GO 的标准库能力,我们有了更加灵活、高效处理大型文本文件的策略。

最后,感谢阅读,希望本文对你有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/404432.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日OJ题_二叉树dfs⑥_力扣257. 二叉树的所有路径

目录 力扣257. 二叉树的所有路径 解析代码 力扣257. 二叉树的所有路径 257. 二叉树的所有路径 难度 简单 给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输…

电路设计(27)——交通信号灯的multisim仿真

1.功能要求 使用数字芯片设计一款交通信号灯&#xff0c;使得&#xff1a; 主干道的绿灯时间为60S&#xff0c;红灯时间为45S 次干道的红灯时间为60S&#xff0c;绿灯时间为45S 主、次干道&#xff0c;绿灯的最后5S内&#xff0c;黄灯闪烁 使用数码管显示各自的倒计时时间。 按…

go-zero微服务入门教程

go-zero微服务入门教程 本教程主要模拟实现用户注册和用户信息查询两个接口。 准备工作 安装基础环境 安装etcd&#xff0c; mysql&#xff0c;redis&#xff0c;建议采用docker安装。 MySQL安装好之后&#xff0c;新建数据库dsms_admin&#xff0c;并新建表sys_user&#…

Springboot--整合定时任务quartz--集群篇

文章目录 前言一、quartz 的集群&#xff1a;1.1 服务集群带来的定时任务问题&#xff1a;1.2 服务集群定时任务解决思路&#xff1a; 二、quartz 集群实现&#xff1a;2.1 引入jar2.2 配置文件&#xff1a;2.3 定义quartz 数据源&#xff1a;2.4 集群测试&#xff1a;2.4.1 定…

介绍 CI / CD

目录 一、介绍 CI / CD 1、为什么要 CI / CD 方法简介 1、持续集成 2、持续交付 3、持续部署 2、GitLab CI / CD简介 3、GitLab CI / CD 的工作原理 4、基本CI / CD工作流程 5、首次设置 GitLab CI / CD 6、GitLab CI / CD功能集 一、介绍 CI / CD 在本文档中&#x…

【Pytorch深度学习开发实践学习】B站刘二大人课程笔记整理lecture07多维输入

lecture07多维输入 课程网址 Pytorch深度学习实践 部分课件内容&#xff1a; import torch import numpy as npxy np.loadtxt(diabetes.csv.gz, delimiter,, dtypenp.float32) x_data torch.from_numpy(xy[:,:-1]) #第一列开始最后一列不要 y_data torch.from_numpy(…

【Python_Zebra斑马打印机编程学习笔记(一)】实现标贴预览的两种方式

实现标贴预览的两种方式 实现标贴预览的两种方式前言一、调用 Labelary Online ZPL Viewer API 方法实现标贴预览功能1、Labelary Online ZPL Viewer API 案例介绍2、生成 PNG 格式3、Parameters 二、通过 zpl 的 label.preview() 方法实现标贴预览功能1、实现步骤2、代码示例 …

gitlab,从A仓库迁移某个工程到B仓库,保留提交记录

从A仓库&#xff0c;拷贝 git clone --bare ssh://git192.168.30.88:22/framework/platform.git 在B仓库新建工程&#xff0c;注意&#xff1a;一定要去掉默认的生成README文件进入platform.git 文件夹下&#xff0c;推送到B仓库 git push --mirror ssh://git192.168.30.100…

怎么用sora赚第一桶金?

&#x1f31f;解锁文字变视频的强大功能&#xff01;&#x1f31f; ✨欢迎来到 Sora Cand&#xff0c;一个革命性的网站&#xff0c;利用 OpenAI 的 Sora 模型帮你把文字变成酷炫的视频&#xff01;✨ 想象一下&#xff0c;你的文字从纸上跳出来&#xff0c;变成引人入胜的视觉…

全志T527国产核心板及米尔配套开发板批量上市!

2023年12月&#xff0c;米尔电子联合战略合作伙伴全志科技&#xff0c;率先业内发布了国产第一款T527核心板及开发板。这款高性能、高性价比、八核A55的国产核心板吸引了广大客户关注&#xff0c;为积极响应客户需求&#xff0c;米尔基于全志T527核心板现已批量上市&#xff0c…

RabbitMQ 部署方式选择

部署模式 RabbitMQ支持多种部署模式&#xff0c;可以根据应用的需求和规模选择适合的模式。以下是一些常见的RabbitMQ部署模式&#xff1a; 单节点模式&#xff1a; 最简单的部署方式&#xff0c;所有的RabbitMQ组件&#xff08;消息存储、交换机、队列等&#xff09;都运行在…

Java项目:21 基于SSM实现的图书借阅管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 基于SSM实现的图书借阅管理系统设计了两个角色&#xff0c;分别是管理员、用户&#xff0c;在数据表user中以ident字段区分&#xff0c;为1表示管理员…

Math方法,以及三角函数计算

abs(x) 返回参数的绝对值 var xMath.abs(-5) //5floor(x) 向下舍入为最接近的整数。 var xMath.floor(2.1) //2ceil(x) 向上舍入为最接近的整数。 var xMath.ceil(2.1) //3fround(x) 最接近的&#xff08;32 位单精度&#xff09;浮点表示。 var xMath.fround(2.60) //2.59…

企业动态|上海航空工业集团殷舜晖部长一行到访同创永益

1月24日上午&#xff0c;中国商飞上海航空工业集团采购中心殷舜晖部长一行4人到访同创永益北京总部。同创永益COO马青山、营销副总经理刘翔、总经办主任田东陪同参观&#xff0c;并介绍了公司的发展历程与近年来的突出成绩。 在随后的会议中&#xff0c;马青山向殷舜晖部长一行…

AppBox快速开发框架(开源)开发流程介绍

目前很多低代码平台都是基于Web用拖拽方式生成界面&#xff0c;确实可以极大的提高开发效率&#xff0c;但也存在一些问题&#xff1a; 大部分平台灵活性不够&#xff0c;特殊需求需要较大的自定义开发&#xff1b; 解析json配置的执行效率不是太高&#xff1b; 大部分平台缺…

统计图雷达图绘制方法

统计图雷达图绘制方法 常用的统计图有条形图、柱形图、折线图、曲线图、饼图、环形图、扇形图。 前几类图比较容易绘制&#xff0c;饼图环形图绘制较难。 还有一种雷达图的绘制也较难&#xff0c;今提供雷达图的绘制方法供参考。 本方法采用C语言的最基本功能&#xff1a; &am…

k8s(2)

目录 一.二进制部署k8s 常见的K8S安装部署方式&#xff1a; k8s部署 二进制与高可用的区别 二.部署k8s 初始化操作&#xff1a; 每台node安装docker&#xff1a; 在 master01 节点上操作; 准备cfssl证书生成工具:&#xff1a; 执行脚本文件&#xff1a; 拉入etcd压缩包…

【目标检测新SOTA!v7 v4作者新作!】YOLO v9 思路复现 + 全流程优化

YOLO v9 思路复现 全流程优化 提出背景&#xff1a;深层网络的 信息丢失、梯度流偏差YOLO v9 设计逻辑可编程梯度信息&#xff08;PGI&#xff09;&#xff1a;使用PGI改善训练过程广义高效层聚合网络&#xff08;GELAN&#xff09;&#xff1a;使用GELAN改进架构 对比其他解法…

Airtest-Selenium实操小课③:下载可爱猫猫图片

1. 前言 那么这周我们看看如何实现使用Airtest-Selenium实现自动搜索下载可爱的猫猫图片吧~ 2. 需求分析和准备 整体的需求大致可以分为以下步骤&#xff1a; 打开chrome浏览器 打开百度网页 搜索“可爱猫猫图片” 定位图片元素 创建存储图片的文件夹 下载可爱猫猫图片…

SpringBoot中Redis缓存的使用

目录 1 前言 2 实现方法 2.1 查询数据时 2.2 修改数据 1 前言 对于一些不常改变&#xff0c;但又经常查询的数据&#xff0c;我们可以使用Redis缓存&#xff0c;来缓解数据库的压力&#xff0c;其中的逻辑如下&#xff1a; 2 实现方法 2.1 查询数据时 一般在控制类查询方…