首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >被 UTF-16 编码教做人的一天

被 UTF-16 编码教做人的一天

作者头像
小洁忘了怎么分身
发布2026-04-27 12:25:16
发布2026-04-27 12:25:16
50
举报
文章被收录于专栏:生信星球生信星球

写在前面:最近跟着小洁老师做转录组分析,数据集用的是 GEO 上的 GSE183464,一个腹主动脉瘤(AAA)相关的 RNA-seq 数据集。 本来以为下载完数据,read.delim 读进来就完事了。结果第一步就结结实实地翻了车。今天把这个由文件编码引发的“血案”,以及两种截然不同的解决思路记录下来,希望能帮大家少走弯路。

下面是这次踩坑和填坑的全过程:

代码语言:javascript
复制

## 📦 第一步:批量解压数据

从 GEO 下载下来的原始文件是一堆 `.txt.gz` 压缩包,全部放在 `GSE183464_RAW` 文件夹里。先批量解压到 `data` 文件夹:

```r
library(R.utils)

if (!dir.exists("data")) {
  dir.create("data")
  message("已创建 data 文件夹")
}

gz_files <- list.files(path = "GSE183464_RAW", 
                       pattern = "\\.txt\\.gz$", 
                       full.names = TRUE)

for (file_path in gz_files) {
  txt_name <- sub("\\.gz$", "", basename(file_path))
  out_path <- file.path("data", txt_name)
  gunzip(file_path, destname = out_path, remove = FALSE, overwrite = TRUE)
  message(sprintf("成功解压: %s", txt_name))
}

message("🎉 全部文件解压完成!")

💡 这里有两个细节值得注意:

  • full.names = TRUE:返回带文件夹前缀的完整路径,不然后面找不到文件。
  • remove = FALSE:保留原始 .gz 文件,千万别把原始数据删掉了。

💥 第二步:读取数据——然后报错了

解压完,我用最普通的方式读文件:

代码语言:javascript
复制
a = read.delim("data/GSM5557972_AAA1.txt")

结果控制台刷出来一堆红字:

代码语言:javascript
复制
Warning: line 1 appears to contain embedded nulls
Warning: line 2 appears to contain embedded nulls
...
Error in make.names(col.names, unique = TRUE): 
  '<ff><fe>'多字节字符串有错误

看着这堆红色的报错,我整个人是懵的。文件明明在那里,大小也正常,这个 <ff><fe> 到底是个什么外星语?

求助小洁老师后,我尝试换了极其强大的 data.table::fread() 函数。

代码语言:javascript
复制
a = data.table::fread("data/GSM5557972_AAA1.txt")
## Error in data.table::fread("data/GSM5557972_AAA1.txt"): 文件编码是UTF-16,fread()不支持此编码。请将文件转换为UTF-8。

虽然它也报错了,但它给出了一个极其关键的提示:“文件编码是 UTF-16,fread() 不支持此编码。”

破案了!R 语言默认读取 UTF-8 编码,遇到 UTF-16 自然会把双字节里的空字节误认为是 null。


🤖 第一种解法:写个转换函数(笨办法)

小洁老师当即借助 AI 手搓一个 UTF-16 转 UTF-8 的函数。

1. 编写底层转换代码

代码语言:javascript
复制
convert_utf16_to_utf8 <- function(src, dst,
                                  endian = c("little", "big")) {
  endian  <- match.arg(endian)
  cs      <- if (endian == "little") "UTF-16LE"else"UTF-16BE"

  con_in  <- file(src, "rb")
  con_out <- file(dst, "wb")

# 第一次单独读取,检测并去掉 BOM
  first_buf <- readBin(con_in, "raw", n = 1e6)
if (identical(first_buf[1:2], as.raw(c(0xff, 0xfe)))) {
    first_buf <- first_buf[-(1:2)]  # 去掉 BOM 的两个字节
  }
  raw_out <- iconv(list(first_buf), from = cs, to = "UTF-8", toRaw = TRUE)[[1]]
  writeBin(raw_out, con_out)

# 剩余数据正常循环读取
while (length(buf <- readBin(con_in, "raw", n = 1e6))) {
    raw_out <- iconv(list(buf), from = cs, to = "UTF-8", toRaw = TRUE)[[1]]
    writeBin(raw_out, con_out)
  }

  close(con_in)
  close(con_out)
}

细节:BOM(字节顺序标记)只存在于文件最开头,所以第一块数据单独处理,检测到 ff fe 就去掉,后面的数据块正常读写就行。

2. 测试与基因顺序检查

小洁老师常说:批量操作之前,先用少量样本测试,这是习惯。 别急着批量跑,先测两个文件:

代码语言:javascript
复制
convert_utf16_to_utf8("data/GSM5557972_AAA1.txt", "test.txt")
n = read.delim("test.txt")

convert_utf16_to_utf8("data/GSM5557973_AAA2.txt", "test2.txt")
n2 = read.delim("test2.txt")

identical(n$gene_name, n2$gene_name)
#> [1] TRUE

为什么要检查 identical?因为后面要用 cbind 按列拼接所有文件,cbind 是不管行名对不对,直接硬拼的。如果两个文件的基因顺序不一样,拼完之后同一行放的根本不是同一个基因的数据,整张表就乱了,但 R 不会报错,你也不会发现!返回 TRUE,说明基因名顺序完全一致,可以放心批量了。

3. 批量转换所有文件

代码语言:javascript
复制
if (!dir.exists("data_utf8")) {
  dir.create("data_utf8")
  message("已创建 data_utf8 文件夹")
}

txt_files <- list.files(path = "data", pattern = "\\.txt$", full.names = TRUE)

for (src_path in txt_files) {
  file_name <- basename(src_path)
  dst_path  <- file.path("data_utf8", file_name)

tryCatch({
    convert_utf16_to_utf8(src = src_path, dst = dst_path, endian = "little")
    message(sprintf("成功转换: %s", file_name))
  }, error = function(e) {
    message(sprintf("转换失败 %s: %s", file_name, e$message))
  })
}
message("🎉 全部文件编码转换完成!")

这里用了 tryCatch,某一个文件失败了不会让整个循环停掉,会记录错误继续跑,最后再回头看哪个文件出了问题。


📖 第二种解法:老手的“降维打击”

小洁老师又带我找到一个更简单实用的方法。

“既然知道是 UTF-16,我们可以试试直接让 R 读这种编码格式”

我当时就惊呆了,还可以这么思考和解决问题,接着,她带着我扒了一遍 R 的官方帮助文档,给我演示了什么叫老手的“查字典”。

代码语言:javascript
复制
?read.delim
# 在文档的 Arguments(参数)里,找到了 fileEncoding 这个参数。
# 提示:“如果你想指定文件的编码,请看 file 函数帮助文档里的 Encoding 章节”

?file
# 找到 Encoding 这一大段。
# 提示:“输入流的编码名称,和传给 iconv 函数的名称是一样的。去看看 iconv 的帮助页面,了解你的电脑平台到底支持哪些编码名称”

?iconv
# 文档里提到了一个配套的函数叫做 iconvlist()。
# 这个函数的作用就是打印出你的 R 语言当前支持的所有编码名称。

iconvlist()
代码语言:javascript
复制
##   [1] "437"                     "850"                    
##   [3] "852"                     "855"                    
... (中间省略几百种编码) ...
## [303] "UTF-16"                  "UTF-16BE"               
## [305] "UTF-16LE"                "UTF-32"                 
...
## [373] "x-mac-ukrainian"         "x_Chinese-Eten"

上面显示了多种 R 原生支持的编码名称。在里面找了一圈,我们发现了 R 对 UTF-16 小端序的标准称呼:UTF-16LE

于是,原本那一大串复杂的转换代码,变成了一行极其优雅的参数:

代码语言:javascript
复制
# 下一步先挑前两个文件读进来,检查一下它们的基因顺序是不是完全一样
fs = dir("data", pattern = "\\.txt$")
n1 = read.delim(paste0("data/", fs[1]), fileEncoding = "UTF-16LE")

📊 两种思路大比拼

解决思路

代码复杂度

适用场景

核心收获

AI 转换函数

较高(需处理底层字节)

遇到 R 原生不支持的特殊编码

学会了 BOM 处理与 tryCatch 防御机制

R 原生参数

极低(仅需一行代码)

标准的 UTF-16 等常见编码

掌握了查阅官方文档、顺藤摸瓜的正确姿势


💡 写在最后

这次卡住我的,说白了就是一个编码问题。但在不知道的时候,真的完全没有方向感,盯着报错不知道从哪里下手。

我觉得这就是新手和有经验的人之间最大的差距——不是代码写得多漂亮,而是看到报错知道去哪里找答案。

慢慢来吧,每踩一个坑,就少一个坑。

如果你也遇到过类似的编码问题,欢迎评论区交流~

——生信技能树的学徒,还在努力中 🐢

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信星球 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 💥 第二步:读取数据——然后报错了
  • 🤖 第一种解法:写个转换函数(笨办法)
    • 1. 编写底层转换代码
    • 2. 测试与基因顺序检查
    • 3. 批量转换所有文件
  • 📖 第二种解法:老手的“降维打击”
  • 📊 两种思路大比拼
  • 💡 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档