摘要:CSV 是数据分析中最通用的数据交换格式。本文系统梳理 R 语言中处理 CSV 文件的完整方法,涵盖 base R 的read.csv/write.csv、readr包的read_csv、data.table的fread/fwrite,并针对编码、分隔符、缺失值、大文件性能、类型推断等常见坑给出可直接运行的代码示例。
一、为什么 CSV 在 R 中如此重要
CSV(Comma-Separated Values)以纯文本存储表格数据,几乎被所有数据库、Excel、BI 工具和编程语言支持。在 R 的数据分析流程中,CSV 通常承担两个角色:
- 数据入口:从业务系统、爬虫或同事那里拿到的原始数据,最常见的形式就是 CSV。
- 数据出口:分析结果、模型输出、报表中间结果,用 CSV 交付最不容易出问题。
R 生态中处理 CSV 的方案主要有三套,各有适用场景:
| 方案 | 核心函数 | 依赖 | 特点 |
|---|---|---|---|
| base R | read.csv/write.csv | 无 | 开箱即用,速度一般,默认行为容易踩坑 |
| readr | read_csv/write_csv | tidyverse | 速度快、类型推断稳定、不自动改列名 |
| data.table | fread/fwrite | data.table | 极快,适合百万行以上大文件 |
二、准备示例数据
为了让后面的示例可以直接运行,我们先构造一份包含中文、缺失值和日期字段的示例数据,并写入磁盘。
# 构造示例数据 set.seed(42) df <- data.frame( id = 1:6, name = c("张三", "李四", "王五", "赵六", "钱七", "孙八"), city = c("北京", "上海", "广州", "深圳", "杭州", "成都"), score = c(88.5, 92.0, NA, 76.5, 95.0, 81.0), enroll_date = as.Date(c("2024-01-15", "2024-02-20", "2024-03-05", "2024-04-11", "2024-05-30", "2024-06-18")), stringsAsFactors = FALSE ) 写入工作目录 write.csv(df, "students.csv", row.names = FALSE, fileEncoding = "UTF-8") file.exists("students.csv")注意row.names = FALSE:如果不加这个参数,write.csv会额外写出一列行号,读回来时多出一列X,这是新手最常遇到的困惑之一。
三、使用 base R 读写 CSV
3.1 read.csv 的基本用法
# 最基础的读取 df1 <- read.csv("students.csv", fileEncoding = "UTF-8") str(df1) head(df1)read.csv实际上是read.table的封装,默认参数为header = TRUE、sep = ","、stringsAsFactors = FALSE(R 4.0.0 之后)。
3.2 常用参数详解
df2 <- read.csv( file = "students.csv", header = TRUE, # 第一行是否为列名 sep = ",", # 分隔符 quote = "\"", # 引号字符 dec = ".", # 小数点符号 na.strings = c("", "NA"), # 哪些字符串视为缺失值 colClasses = c( # 显式指定列类型,避免推断错误 id = "integer", name = "character", city = "character", score = "numeric", enroll_date = "Date" ), nrows = 100, # 只读前 100 行,用于快速预览 skip = 0, # 跳过开头若干行 comment.char = "", # 关闭注释符识别,可提速 fileEncoding = "UTF-8" ) str(df2)几个值得记住的实践要点:
colClasses显式声明类型,既避免类型推断错误,也能显著提升读取速度。comment.char = ""关闭注释解析,对大文件有明显加速效果。na.strings要覆盖业务中真实出现的空值表示,例如"NULL"、"-"、"N/A"。
3.3 处理中文编码问题
Windows 下 Excel 导出的 CSV 常为 GBK 编码,直接读取会出现乱码。可以这样处理:
# 方式一:读取时指定编码 df_gbk <- read.csv("students_gbk.csv", fileEncoding = "GBK") 方式二:先探测编码再读取 raw <- readBin("students_gbk.csv", "raw", n = 2000) guess <- stringi::stri_enc_detect(raw) guess[[1]]$Encoding 方式三:读取后转换列内容 df_gbk$name <- iconv(df_gbk$name, from = "GBK", to = "UTF-8")建议统一约定:内部流程一律使用 UTF-8,只在读取外部文件时做一次转换。
3.4 写入 CSV
# 基础写入 write.csv(df, "out_base.csv", row.names = FALSE, fileEncoding = "UTF-8") 更细粒度控制 write.table( df, file = "out_table.csv", sep = ",", quote = TRUE, # 字符列加引号,避免逗号冲突 na = "", # 缺失值写为空字符串 row.names = FALSE, col.names = TRUE, fileEncoding = "UTF-8" ) 追加写入(注意:不会自动写表头) write.table(df[1:2, ], "out_append.csv", sep = ",", row.names = FALSE, col.names = TRUE, fileEncoding = "UTF-8") write.table(df[3:4, ], "out_append.csv", sep = ",", row.names = FALSE, col.names = FALSE, append = TRUE, fileEncoding = "UTF-8")四、使用 readr 读写 CSV
readr是 tidyverse 的读取组件,返回tibble,打印友好,且不会自动把字符串转成因子、不会给列名加X前缀。
4.1 read_csv 基本用法
library(readr) df3 <- read_csv("students.csv", show_col_types = FALSE) class(df3) print(df3)read_csv会输出列类型推断结果,便于快速核对。若想关闭提示,使用show_col_types = FALSE。
4.2 显式指定列类型
df4 <- read_csv( "students.csv", col_types = cols( id = col_integer(), name = col_character(), city = col_character(), score = col_double(), enroll_date = col_date(format = "%Y-%m-%d") ), na = c("", "NA", "NULL"), locale = locale(encoding = "UTF-8") ) str(df4)col_types支持简写字符串,例如"iccdD"分别对应 integer、character、character、double、Date,适合列很多的宽表。
4.3 分块读取大文件
# 只读前 1000 行做探索 df_peek <- read_csv("big_data.csv", n_max = 1000, show_col_types = FALSE) 跳过前若干行 df_skip <- read_csv("big_data.csv", skip = 5, show_col_types = FALSE) 只读指定列 df_cols <- read_csv( "big_data.csv", col_select = c(id, score), show_col_types = FALSE )4.4 写入 CSV
library(readr) 默认 UTF-8,缺失值写为 NA write_csv(df, "out_readr.csv") 缺失值写为空字符串 write_csv(df, "out_readr_na.csv", na = "") 追加写入 write_csv(df[1:3, ], "out_readr_append.csv") write_csv(df[4:6, ], "out_readr_append.csv", append = TRUE) 写入时保留日期格式 write_csv(df, "out_readr_date.csv", na = "")五、使用 data.table 处理大文件
当文件达到百万行级别,fread和fwrite通常是 R 中最快的选择,且能自动识别分隔符与编码。
5.1 fread 读取
library(data.table) dt <- fread("students.csv", encoding = "UTF-8") class(dt) str(dt) 常用参数 dt2 <- fread( "students.csv", sep = "auto", # 自动识别分隔符 header = TRUE, na.strings = c("", "NA"), select = c("id", "name", "score"), # 只读指定列 nrows = 1000, encoding = "UTF-8" )5.2 fwrite 写入
# 快速写入 fwrite(dt, "out_dt.csv") 指定分隔符与缺失值表示 fwrite(dt, "out_dt_na.csv", sep = ",", na = "", quote = "auto") 追加写入 fwrite(dt[1:3, ], "out_dt_append.csv") fwrite(dt[4:6, ], "out_dt_append.csv", append = TRUE)5.3 三种方案性能对比
library(microbenchmark) library(readr) library(data.table) bench <- microbenchmark( base = read.csv("students.csv", fileEncoding = "UTF-8"), readr = read_csv("students.csv", show_col_types = FALSE), dt = fread("students.csv", encoding = "UTF-8"), times = 20 ) print(bench)在小文件上差异不明显,但数据量上升到几十万行后,fread与read_csv通常比read.csv快数倍。实际选型建议:
- 临时脚本、无额外依赖要求:用 base R。
- tidyverse 工作流、需要稳定类型推断:用
readr。 - 大文件、批量处理、追求吞吐:用
data.table。
六、常见问题与解决方案
6.1 多出一列 X
# 原因:写入时未设置 row.names = FALSE # 解决一:写入时修正 write.csv(df, "students.csv", row.names = FALSE) 解决二:读取时忽略第一列 df <- read.csv("students.csv")[, -1]6.2 分隔符不是逗号
# 分号分隔(欧洲常见) df_semi <- read.csv("data_semicolon.csv", sep = ";") 制表符分隔 df_tsv <- read.delim("data.tsv", sep = "\t") readr 中自动识别 df_auto <- read_delim("data_semicolon.csv", delim = ";")6.3 字段内含逗号或换行
# 正确做法:写入时加引号,读取时保持 quote 默认值 write.csv(df, "quoted.csv", row.names = FALSE, quote = TRUE) df_q <- read.csv("quoted.csv", quote = "\"")6.4 数值被读成字符
# 常见原因:列中混有 "1,234" 这类千分位或单位符号 df_bad <- read.csv("bad_numeric.csv", colClasses = "character") df_bad$amount <- as.numeric(gsub("[,\\s]", "", df_bad$amount)) summary(df_bad$amount)6.5 日期列读成字符
df <- read.csv("students.csv", fileEncoding = "UTF-8") df$enroll_date <- as.Date(df$enroll_date, format = "%Y-%m-%d") class(df$enroll_date) readr 中直接声明 df <- read_csv("students.csv", col_types = cols(enroll_date = col_date(format = "%Y-%m-%d")), show_col_types = FALSE)6.6 大文件内存不足
library(data.table) 只读需要的列,能显著降低内存占用 dt <- fread("big_data.csv", select = c("id", "score")) 分块处理 chunk_size <- 100000 con <- file("big_data.csv", "r") header <- readLines(con, n = 1) repeat { chunk <- read.csv(con, nrows = chunk_size, header = FALSE, col.names = strsplit(header, ",")[[1]]) if (nrow(chunk) == 0) break 在这里对 chunk 做聚合或过滤 } close(con)七、一个完整的实战流程
下面把前面的知识点串起来,完成一次「读取 → 清洗 → 聚合 → 导出」的完整流程。
library(readr) library(dplyr) 1. 读取 raw <- read_csv( "students.csv", col_types = cols( id = col_integer(), name = col_character(), city = col_character(), score = col_double(), enroll_date = col_date(format = "%Y-%m-%d") ), na = c("", "NA"), show_col_types = FALSE ) 2. 清洗 clean <- raw %>% filter(!is.na(score)) %>% mutate( score_level = case_when( score >= 90 ~ "优秀", score >= 80 ~ "良好", TRUE ~ "待提升" ), enroll_month = format(enroll_date, "%Y-%m") ) 3. 聚合 summary_tbl <- clean %>% group_by(city, score_level) %>% summarise( cnt = n(), avg_score = round(mean(score), 2), .groups = "drop" ) %>% arrange(desc(cnt)) print(summary_tbl) 4. 导出 write_csv(summary_tbl, "summary_by_city.csv", na = "") write_csv(clean, "clean_students.csv", na = "")这个流程体现了几个关键习惯:读取时显式声明类型、清洗阶段处理缺失值、聚合结果单独落盘,便于后续复用与审计。
八、最佳实践清单
- 写入必加
row.names = FALSE,避免多出无意义的行号列。 - 统一使用 UTF-8 编码,读取外部文件时显式指定
fileEncoding或locale。 - 显式声明列类型,用
colClasses或col_types替代自动推断。 - 明确缺失值表示,用
na.strings/na参数覆盖业务中的空值写法。 - 大文件先预览再全量读,用
nrows/n_max或select控制内存。 - 不要用
attach和隐式类型转换,保持数据流可读可复现。 - 导出结果时保留一份原始数据,便于回溯与重跑。
九、小结
R 中处理 CSV 并不复杂,难点在于编码、类型推断和缺失值这三类细节。base R 足够应付日常小文件;readr提供了更稳定的默认行为和更好的 tidyverse 集成;data.table则是大文件场景的性能首选。把本文的示例代码跑一遍,再结合自己的业务数据调整参数,基本就能覆盖绝大多数 CSV 读写需求。