PAYATHON 2026

如何在 Ruby 2 中移除无效字节并保留有效 UTF-8

技术老齐

结论

将字符串标记为 UTF-8,再以 UTF-16LE 为中间编码进行转码。转成 UTF-16LE 时,Ruby 会校验输入的 UTF-8 序列,并用空字符串替换无效部分,然后再将结果转回 UTF-8。

这种方法不依赖 String#scrub,适用于需要同时兼容 Ruby 1.9.3 和 Ruby 2.6.10 的代码:

def remove_invalid_utf8(string)
  utf8 = string.dup
  utf8.force_encoding(Encoding::UTF_8)

  cleaned = utf8.encode(
    Encoding::UTF_16LE,
    Encoding::UTF_8,
    invalid: :replace,
    undef: :replace,
    replace: ''
  )

  cleaned.encode(Encoding::UTF_8)
end

使用示例

源文件编码、IRB 或 JRuby 对字符串字面量的处理可能影响测试,因此可以直接用字节构造字符串:

bytes = [76, 246, 115, 195, 169, 228, 190, 141]
str = bytes.pack('C*')
str.force_encoding(Encoding::UTF_8)

cleaned = remove_invalid_utf8(str)

p cleaned
p cleaned.bytes.to_a
p cleaned.valid_encoding?

预期结果:

"Lsé侍"
[76, 115, 195, 169, 228, 190, 141]
true

十进制字节 246,即十六进制的 F6,会被删除。é 对应的 C3 A9 和 侍 对应的 E4 BE 8D 则会保留。

为什么直接调用 encode('UTF-8') 可能无效

如果字符串已经标记为 UTF-8,目标编码也是 UTF-8,Ruby 的某些版本或实现可能不会真正执行转码。这样一来,invalid: :replace 就没有机会处理无效序列:

str.encode('UTF-8', invalid: :replace, replace: '')

使用 UTF-16LE 作为中间编码,可以让 Ruby 实际解析并校验输入的 UTF-8 序列:

可能包含错误的 UTF-8
        ↓ 校验并删除无效序列
      UTF-16LE
        ↓
      UTF-8

这里使用明确指定字节序的 UTF-16LE,可以省去 UTF-16 可能涉及的字节序标记处理。

从文件读取时的写法

如果文件本应是 UTF-8 编码,但可能混入错误字节,可以先以二进制方式读取,再明确标记为 UTF-8:

str = File.open('./errf.txt', 'rb') { |file| file.read }
str.force_encoding(Encoding::UTF_8)

cleaned = remove_invalid_utf8(str)

p cleaned.bytes.to_a
p cleaned.valid_encoding?

force_encoding 只会修改字符串的编码标签,不会转换、删除或重排字节。这里应将字符串标记为 Encoding::UTF_8,不能先标记为 ASCII 再转码,否则 é 和 侍 的多字节序列也会被视为无法转换的数据。

Ruby 2.x 的简化写法

较新的 Ruby 2.x 提供了 String#scrub,可以直接删除无效的 UTF-8 序列:

cleaned = str.scrub('')

Ruby 1.9.3 没有这个方法。如果同一份代码必须同时支持 Ruby 1.9.3 和 2.6.10,可以使用前面的 UTF-16LE 中间转码方案,也可以先判断当前环境是否支持 String#scrub:

def remove_invalid_utf8(string)
  utf8 = string.dup
  utf8.force_encoding(Encoding::UTF_8)

  return utf8.scrub('') if utf8.respond_to?(:scrub)

  cleaned = utf8.encode(
    Encoding::UTF_16LE,
    Encoding::UTF_8,
    invalid: :replace,
    undef: :replace,
    replace: ''
  )

  cleaned.encode(Encoding::UTF_8)
end

这段代码按 UTF-8 的编码规则删除无效序列,并非根据固定的“非法字节列表”逐字节过滤。一个字节是否合法,取决于它所在的位置以及与前后字节的组合,因此字节黑名单无法覆盖所有情况。

备注:内容仅供参考。