
处理大型csv文件时,直接加载全部内容到内存以随机选取行会造成性能瓶颈和内存溢出。本文将介绍如何利用水塘抽样(reservoir sampling)算法,在go语言中以单次遍历、固定内存消耗的方式,从任意大小的csv文件中高效地随机抽取指定数量的行,避免全文件加载,并提供详细的go语言实现示例。
在Go语言中处理CSV文件时,encoding/csv包提供了强大的解析能力。然而,当面对TB级别甚至更大的CSV文件时,常见的csv.NewReader(file).ReadAll()方法会将整个文件内容加载到内存中,这不仅耗时巨大,还极易导致内存溢出(OOM)。特别是当需求是随机抽取文件中的若干行进行分析或测试时,这种全量加载的方式显得非常低效且不切实际。
尽管Go的os.File支持Seek操作以实现文件内的随机访问,但对于CSV文件而言,"随机行"的定义并非简单的字节偏移。不同行的长度可能不一致,这意味着我们无法直接通过计算随机字节偏移来定位到行的起始位置。若要实现行的随机访问,通常需要预先遍历文件构建一个行偏移索引,但这又回到了全量遍历的起点,并且索引本身也可能占用大量内存。
因此,我们需要一种能够在不预知文件总行数、不加载全部内容的情况下,实现单次遍历即可随机抽取指定行数的算法。
水塘抽样是一种经典算法,用于在不知道数据流总长度的情况下,从数据流中均匀地随机抽取k个样本。它的核心思想是维护一个大小为k的“水塘”(reservoir),并以特定概率替换水塘中的元素。
立即学习“go语言免费学习笔记(深入)”;
算法步骤(以选取 k 个样本为例):
为什么有效? 该算法保证了数据流中的每个元素被选中并保留在最终水塘中的概率都是 k/N(其中 N 是数据流的总长度)。对于流中的第 i 个元素(i >= k),它被选中的概率是 k/i。一旦被选中,它被后续元素替换的概率会逐渐降低,最终达到均匀分布。
下面我们将使用Go语言实现一个 ReadRandomCSVLines 函数,它利用水塘抽样算法从大型CSV文件中抽取指定数量的随机行。
package main
import (
"encoding/csv"
"fmt"
"io"
"math/rand"
"os"
"time"
)
// ReadRandomCSVLines 使用水塘抽样算法从CSV文件中读取指定数量的随机行。
// 它不会将整个文件加载到内存中,适用于处理大型CSV文件。
// filePath: CSV文件路径
// numSamples: 希望抽取的随机行数
// 返回值: 抽取的随机行([][]string 格式),或错误信息
func ReadRandomCSVLines(filePath string, numSamples int) ([][]string, error) {
if numSamples <= 0 {
return nil, fmt.Errorf("numSamples 必须是正整数")
}
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("无法打开文件 %s: %w", filePath, err)
}
defer file.Close()
reader := csv.NewReader(file)
// 如果CSV文件包含标题行,可以在这里读取并跳过它:
// _, err = reader.Read()
// if err != nil && err != io.EOF {
// return nil, fmt.Errorf("无法读取CSV标题行: %w", err)
// }
// 初始化水塘,预分配容量以减少扩容开销
reservoir := make([][]string, 0, numSamples)
var linesRead int64 = 0 // 记录已读取的总行数,使用int64以支持超大文件
// 初始化随机数生成器,使用当前时间作为种子以确保每次运行结果不同
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
record, err := reader.Read() // 读取下一条CSV记录
if err == io.EOF {
break // 文件读取完毕
}
if err != nil {
return nil, fmt.Errorf("读取CSV记录失败: %w", err)
}
linesRead++ // 已读取行数加一
if linesRead <= int64(numSamples) {
// 如果已读取行数小于或等于目标样本数,直接填充水塘
reservoir = append(reservoir, record)
} else {
// 对于后续行,以 numSamples/linesRead 的概率决定是否替换水塘中的一个元素
// 生成一个 [0, linesRead-1] 之间的随机整数
j := r.Int63n(linesRead)
// 如果 j 小于 numSamples,则替换水塘中索引为 j 的元素
if j < int64(numSamples) {
reservoir[j] = record
}
}
}
return reservoir, nil
}
func main() {
// 创建一个虚拟的CSV文件用于测试
dummyData := `id,name,city,age
1,Alice,New York,30
2,Bob,London,24
3,Charlie,Paris,35
4,David,Berlin,29
5,Eve,Tokyo,22
6,Frank,Sydney,41
7,Grace,Rome,28
8,Heidi,Madrid,33
9,Ivan,Beijing,27
10,Judy,Moscow,31
11,Kyle,Dubai,26
12,Liam,Toronto,38
13,Mia,Seoul,25
14,Noah,Cairo,32
15,Olivia,Rio,23
16,Peter,Bangkok,36
17,Quinn,Warsaw,21
18,Rachel,Lisbon,29
19,Sam,Dublin,34
20,Tina,Vienna,26
`
filePath := "large_dummy.csv"
err := os.WriteFile(filePath, []byte(dummyData), 0644)
if err != nil {
fmt.Println("创建虚拟文件失败:", err)
return
}
defer os.Remove(filePath) // 程序结束时清理虚拟文件
// 示例1: 抽取 5 条随机行
numSamples1 := 5
fmt.Printf("--- 尝试从 %s 中抽取 %d 条随机行 ---\n", filePath, numSamples1)
randomLines1, err := ReadRandomCSVLines(filePath, numSamples1)
if err != nil {
fmt.Println("读取随机行失败:", err)
return
}
fmt.Println("抽取的随机行 (示例1):")
for i, line := range randomLines1 {
fmt.Printf("%d: %v\n", i+1, line)
}
fmt.Printf("实际抽取行数: %d\n\以上就是Go语言中高效读取大型CSV文件的随机行:水塘抽样法实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号