用 R 读 Excel,日期列打开一看全是 44562、44635 这样的数字——数据是从某个业务系统导出的,或者同事把日期列"选择性粘贴 → 仅数值"过一遍,就会出现这种情况。
这不是文件坏了:Excel 内部本来就用数字记日期,只是单元格套了个"日期格式"的皮;导出的文件把皮弄丢了,R 读到的就是裸数字。
本文用一组实测数据演示两种还原方法,以及三个连一些老手都会踩的坑。
先复现问题。测试文件有 4 列:正常日期列、系统导出的序列号列、文本型序列号列:
library(readxl)
dat <- read_excel("samples.xlsx")
str(dat)
tibble [4 x 4] (S3: tbl_df/tbl/data.frame)
$ sample_id : chr [1:4] "S001" "S002" "S003" "S004"
$ collect_date: POSIXct[1:4], format: "2022-01-01" "2022-03-15" ...
$ export_date : num [1:4] 44562 44635 44520 44742
$ date_text : chr [1:4] "44562" "44635" "44520" "44742"
看得非常清楚:collect_date 是真日期(POSIXct),而 export_date 被读成了 num(数字),date_text 是 chr(文本)。两种情况解法略有不同。
先搞懂:44562 凭什么是 2022-01-01?
Excel 的日期本质是序列号:1900 年日期系统的第 1 天是 1900-01-01(序列号 1),之后每天加 1。2022-01-01 恰好是第 44562 天。
那还原时 origin(起点)应该填什么?网上很多文章写 origin = "1900-01-01",这是错的,会差两天。原因是个历史遗留 bug:Excel 为了兼容 Lotus 1-2-3,把 1900 年错误地当成了闰年——日历里根本不存在 1900-02-29 这一天,但序列号给它留了位置。于是 1900-03-01 之后的日期,序列号实际等于"距 1899-12-30 的天数"。验证一下:
as.Date(1, origin = "1899-12-30") # 序列号 1 对应哪天?
# [1] "1899-12-31" # 不是 1900-01-01!差的那天就是虚构的 1900-02-29
as.Date(44562, origin = "1899-12-30")
# [1] "2022-01-01" # 1900-03-01 之后的所有日期,这个 origin 都是对的
origin = "1899-12-30"。唯一的例外是 1900 年 1、2 月的历史数据会差一天,但实际工作里几乎碰不到。
解法一:as.Date + origin(基础写法,不依赖任何包)
# 数值型序列号,直接转
dat$export_date <- as.Date(dat$export_date, origin = "1899-12-30")
# 文本型数字(chr),要先转数值再转日期
dat$date_text <- as.Date(as.numeric(dat$date_text), origin = "1899-12-30")
dat$export_date
# [1] "2022-01-01" "2022-03-15" "2021-11-20" "2022-06-30"
解法二:janitor::excel_numeric_to_date(推荐,不用记 origin)
如果你装了 tidyverse 生态的 janitor 包,一行搞定,函数名就是自我解释:
janitor::excel_numeric_to_date(dat$export_date)
# [1] "2022-01-01" "2022-03-15" "2021-11-20" "2022-06-30"
它内部处理了 origin 问题,还能用 include_time = TRUE 保留时间。openxlsx 包也有等价的 convertToDate(),效果相同。
坑 1:col_types = "date" 这个偏方,一半灵一半不灵
网上有人建议读的时候直接指定 col_types = "date"。我实测(readxl 1.4.3)的结果是:数值型序列号真能被救回来,但会刷一排警告;文本型数字直接变 NA,数据无声丢失:
dat2 <- read_excel("samples.xlsx",
col_types = c("text", "date", "date", "date"))
# Warning: Coercing numeric to date in C2 / R2C3
# Warning: Expecting date in D2 / R2C4: got '44562' <-- 文本列变 NA
结论:应急可以(列全是数值型序列号时),但不能当通用解法——它救不了文本列,而且返回的是 POSIXct 日期时间而不是 Date。想稳妥,还是读完之后用解法一或解法二显式转换。
坑 2:转出来的日期差了整整 4 年?文件是 1904 日期系统
如果套用 origin = "1899-12-30" 后日期明显不对劲(差 4 年左右),说明这个文件是老版 Mac Excel 生成的 1904 日期系统(Excel for Mac 2011 及更早版本的默认设置)。对照实测:
as.Date(44562, origin = "1904-01-01")
# [1] "2026-01-02" # 同一序列号,差出 4 年多
遇到这种情况把 origin 换成 "1904-01-01" 即可。现在的 Excel(Windows / Mac 统一后)默认都是 1900 系统,这个坑主要存在于十年前的老文件。
怎么确认一列数字确实是日期序列号
快速判断法:现代日期(1970 ~ 2030 年)对应的序列号范围大约是 25569 ~ 47847。如果你那列"看起来像乱码的数字"落在这个区间、而且业务上确实应该有日期列,基本可以断定是序列号。
完整可跑代码
library(readxl)
dat <- read_excel("samples.xlsx")
# 数值型序列号 -> 日期
dat$export_date <- as.Date(dat$export_date, origin = "1899-12-30")
# 文本型 -> 先数值再日期
dat$date_text <- as.Date(as.numeric(dat$date_text), origin = "1899-12-30")
str(dat) # 两列都变成了 Date
扩展阅读
- 日期转回来之后怎么格式化、怎么计算?见 R 日期处理速查:as.Date 格式符、Excel 序列号还原、Date 与 POSIXct 怎么选
- 读进来的文本列要清洗?见 R 字符串处理速查:stringr 的提取 / 替换 / 拆分与正则常用写法
- 系列全部文章:R 语言教程全集(23 篇导航)