清理错误的 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
是否有更好或更标准的方法从错误的 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"))
}
问题是 1
是 Latin-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 上试试这个。
由于用户数据格式错误,我的 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
是否有更好或更标准的方法从错误的 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"))
}
问题是 1
是 Latin-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 上试试这个。