清理错误的 UTF-8 字符串

Sanitizing bad UTF-8 strings

由于用户数据格式错误,我的 gRPC 服务未能发送请求。结果是 HR 用户数据有一个错误的 UTF-8 字符串并且 gRPC 无法对其进行编码。我将错误字段缩小到这个字符串:

"Gr1gory Smith" // Gr�gory Smith  (this is coming from an LDAP source)

所以我想要一种方法来清理这些包含错误 UTF-8 编码的输入。

unicode/utf8 标准包中没有看到任何明显的清理功能,这是我第一次天真的尝试:

func naïveSanitizer(in string) (out string) {
    for _, rune := range in {
        out += string(rune)
    }
    return
}

输出:

Before: Valid UTF-8? false  Name: 'Gr�gory Smith' Byte-Count:  13
After:  Valid UTF-8? true   Name: 'Gr�gory Smith' Byte-Count:  15

Playground version

是否有更好或更标准的方法从错误的 UTF-8 字符串中挽救尽可能多的有效数据?


我在这里暂停的原因是因为在迭代字符串时遇到错误的(第 3 个)字符,utf8.ValidRune(rune) returns true: https://play.golang.org/p/_FZzeTRLVls

所以我的后续问题是,迭代一个字符串 - 一次一个符文 - 符文值是否始终有效?即使底层源字符串编码格式不正确?


编辑:

澄清一下,此数据来自 LDAP 源:500K 用户记录。在这 500K 条记录中,只有 15(十五)条记录,即 ~0.03% return uf8.ValidString(...) of false.

正如@kostix 和@peterSO 所指出的,如果从另一种 编码(例如Latin-1)转换为UTF-8,这些值可能是有效的。将这一理论应用于这些离群样本:

https://play.golang.org/p/9BA7W7qQcV3

Name:     "Jean-Fran\u00e7ois Smith" : (good UTF-8) :            : Jean-François Smith
Name:                   "Gr\xe9gory" : (bad  UTF-8) : Latin-1-Fix: Grégory
Name:               "Fr\xe9d\xe9ric" : (bad  UTF-8) : Latin-1-Fix: Frédéric
Name:                 "Fern\xe1ndez" : (bad  UTF-8) : Latin-1-Fix: Fernández
Name:                     "Gra\xf1a" : (bad  UTF-8) : Latin-1-Fix: Graña
Name:                     "Mu\xf1oz" : (bad  UTF-8) : Latin-1-Fix: Muñoz
Name:                     "P\xe9rez" : (bad  UTF-8) : Latin-1-Fix: Pérez
Name:                    "Garc\xeda" : (bad  UTF-8) : Latin-1-Fix: García
Name:                  "Gro\xdfmann" : (bad  UTF-8) : Latin-1-Fix: Großmann
Name:                     "Ure\xf1a" : (bad  UTF-8) : Latin-1-Fix: Ureña
Name:                    "Iba\xf1ez" : (bad  UTF-8) : Latin-1-Fix: Ibañez
Name:                     "Nu\xf1ez" : (bad  UTF-8) : Latin-1-Fix: Nuñez
Name:                     "Ba\xd1on" : (bad  UTF-8) : Latin-1-Fix: BaÑon
Name:                  "Gonz\xe1lez" : (bad  UTF-8) : Latin-1-Fix: González
Name:                    "Garc\xeda" : (bad  UTF-8) : Latin-1-Fix: García
Name:                 "Guti\xe9rrez" : (bad  UTF-8) : Latin-1-Fix: Gutiérrez
Name:                      "D\xedaz" : (bad  UTF-8) : Latin-1-Fix: Díaz
Name:               "Encarnaci\xf3n" : (bad  UTF-8) : Latin-1-Fix: Encarnación

解决您的问题。 1是Unicode码位的八进制值é.

package main

import "fmt"

func main() {
    fmt.Println(string(rune(0351)))
    fullname := "Grégory Smith" // "Gr1gory Smith"
    fmt.Println(fullname)
}

游乐场:https://play.golang.org/p/WigFZk3iSK1

输出:

é
Grégory Smith

您可以通过丢弃无效符文来改进您的 "sanitiser":

package main

import (
    "fmt"
    "strings"
)

func notSoNaïveSanitizer(s string) string {
    var b strings.Builder
    for _, c := range s {
        if c == '\uFFFD' {
            continue
        }
        b.WriteRune(c)
    }
    return b.String()
}

func main() {
    fmt.Println(notSoNaïveSanitizer("Gr1gory Smith"))
}

Playground.

问题是 1Latin-1 中的字符 é。

@PeterSO 指出它也恰好位于 Unicode 的 BMP 中的相同位置,这是正确的,但 Unicode 不是一种编码,并且你的数据应该是编码的,所以我认为你只是有一个错误的假设关于数据的编码,它不是 UTF-8,而是 Latin-1(或与拉丁重音字母兼容的东西)。

所以我会确认您确实在处理 Latin-1(或其他),如果是, golang.org/x/text/encoding 提供完整的工具,用于将旧编码重新编码为 UTF-8(或其他编码)。

(附带说明,您最好不要碰巧明确要求您的数据源为您提供 UTF-8 编码的数据。)

Go 1.13 引入了 strings.ToValidUTF8(),所以 sanitizer() 应该只是:

func sanitize(s string) string {
    return strings.ToValidUTF8(s, "")
}

我什至认为它不值得拥有它自己的功能。在 Go Playground.

上试用

如果您的输入恰好是一个字节切片,您可以使用类似的bytes.ToValidUTF8()函数。

另请注意,如果您不只是想在没有踪迹的情况下丢弃输入中的某些数据,则可以在调用 strings.ToValidUTF8() 时使用任何替换字符(或多个字符),例如:

return strings.ToValidUTF8(in, "❗")

Go Playground 上试试这个。