为何Go读取IMDb的title.basics.tsv时csv.Reader行数远少于预期?
我来帮你分析下这个问题——你遇到的情况其实是csv.Reader和bufio.Scanner对「行」的定义不一样导致的,本质是CSV规范和物理行计数的区别:
问题根源
wc -l和bufio.Scanner都是按物理行(也就是文件里的换行符\n/\r\n)来计数的,而csv.Reader是严格遵循CSV/TSV规范解析记录的。
IMDb的TSV数据里,部分字段(比如电影标题)可能包含换行符,并且这些字段会被引号包裹。根据CSV规范,引号内的换行符属于字段内容的一部分,而不是记录的分隔符。这时候csv.Reader会把跨了多个物理行的同一记录合并成一个,所以统计出的记录数自然会比物理行数少——你看到的5901463是实际的TSV记录数,而7434601是物理行数,两者都是正确的,只是统计维度不同。
你用bufio.Scanner+strings.Split得到每行都是9个元素,说明这些物理行本身都是完整的TSV行,不存在格式错误,只是部分记录被拆成了多行存储。
两种解决方案
根据你的需求,有两种处理方式:
方案1:遵循TSV规范解析(保留字段内的换行)
如果你的目标是正确解析TSV数据(比如保留标题里的换行符),那csv.Reader的行为是完全正常的。你可以验证一下,读取到的记录中确实存在包含换行符的字段:
// 在csv读取循环中添加这段代码查看 record, err := r.Read() if err != nil { fmt.Printf("CSV read error: %v\n", err) break } for idx, field := range record { if strings.Contains(field, "\n") || strings.Contains(field, "\r\n") { fmt.Printf("Record %d, field %d contains newline: %q\n", i, idx, field) break } }
方案2:按物理行读取(和wc行数一致)
如果你就是需要和wc统计的行数保持一致,按物理行处理每一行,那可以放弃直接用csv.Reader读取整个文件,改用bufio.Scanner读取每行后单独解析:
package main import ( "bufio" "encoding/csv" "fmt" "log" "os" "strings" ) func main() { f, err := os.Open(`C:\path\to\data.tsv`) if err != nil { log.Fatalf("Couldn't open TSV file: %v\n", err) } defer f.Close() scanner := bufio.NewScanner(f) physicalLineCount := 0 for scanner.Scan() { line := scanner.Text() // 单独解析每行,处理引号内的制表符 r := csv.NewReader(strings.NewReader(line)) r.Comma = '\t' r.LazyQuotes = true fields, err := r.Read() if err != nil { fmt.Printf("Warning: failed to parse line %d: %v\n", physicalLineCount+1, err) continue } // 这里可以处理解析后的fields physicalLineCount++ } if err := scanner.Err(); err != nil { log.Fatalf("Scanner error: %v\n", err) } fmt.Printf("Physical line count: %v\n", physicalLineCount) // 7434601 }
这种方式既可以正确处理引号内的制表符(避免strings.Split直接拆分的错误),又能得到和wc一致的行数,因为每行都被单独解析,不会合并跨物理行的记录。
总结
- 如果你需要的是TSV记录数,用
csv.Reader的结果是对的; - 如果你需要的是物理行数,用
bufio.Scanner+逐行解析的方式更合适。
内容的提问来源于stack exchange,提问作者Tabular

