无效字符的字节到字符串转换

3
我需要解析UDP数据包,但这些数据包可能无效或包含一些错误。我想在字节转换为字符串后,将无效的字符替换为 . ,以显示数据包的内容。
我该如何做?这是我的代码:
func main() {
   a := []byte{'a', 0xff, 0xaf, 'b', 0xbf}
   s := string(a)
   s = strings.Replace(s, string(0xFFFD), ".", 0)

   fmt.Println("s: ", s) // I would like to display "a..b."
   for _, r := range s {
      fmt.Println("r: ", r)
   }
   rs := []rune(s)
   fmt.Println("rs: ", rs)
}
3个回答

5
你可以使用 strings.ToValidUTF8() 来解决这个问题:

ToValidUTF8 函数将 s 字符串中每个无效的 UTF-8 字节序列替换为一个指定的替换字符串(replacement string)并返回一个新的字符串。

它似乎正是你需要的。测试一下:

a := []byte{'a', 0xff, 0xaf, 'b', 0xbf}
s := strings.ToValidUTF8(string(a), ".")
fmt.Println(s)

输出结果(在Go Playground中尝试):

a.b.

我使用了“似乎”这个词,因为你可以看到,在ab之间只有一个点:因为可能存在2个字节,但是只有一个无效的序列。

请注意,你可以避免[]byte => string 转换,因为有一个操作并返回[]bytebytes.ToValidUTF8()等效函数:

a := []byte{'a', 0xff, 0xaf, 'b', 0xbf}
a = bytes.ToValidUTF8(a, []byte{'.'})
fmt.Println(string(a))

输出将是相同的。请在 Go Playground 上尝试此代码。

如果你担心多个(无效序列)字节缩成一个点,那么请继续阅读。

还要注意,为了检查可能包含文本的任意字节片段,你可以简单地使用hex.Dump(),它会生成以下输出:

a := []byte{'a', 0xff, 0xaf, 'b', 0xbf}
fmt.Println(hex.Dump(a))

输出:

00000000  61 ff af 62 bf                                    |a..b.|

以下是您期望的输出 a..b.,还有其他(有用的)数据,例如十六进制偏移量和字节的十六进制表示。

为了获得更好的输出效果,请使用稍长一些的输入进行尝试:

a = []byte{'a', 0xff, 0xaf, 'b', 0xbf, 50: 0xff}
fmt.Println(hex.Dump(a))

00000000  61 ff af 62 bf 00 00 00  00 00 00 00 00 00 00 00  |a..b............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 ff                                          |...|

请在Go Playground上进行尝试。


ab之间只有一个“点”- 如果想看精确的字节偏移量,这是误导人的。我认为原帖的作者实际上只是想要可打印(和不可打印)的ASCII字节。使用UTF-8技巧会使事情过于复杂化。 - colm.anseo
@colm.anseo 是的,也许吧。但是字符á是可打印的,并且具有代码225:它甚至适合单个字节!然而,在UTF-8中,它占用2个字节。如果您为单个字符打印这2个字节,那么这也可能会产生误导。为了获得准确的偏移量,应优先使用hex.Dump() - icza
同意。OP 的目标是进行 UDP 数据包检查 - 所以只想清楚地划分信号与标记字节。 - colm.anseo

5
你的方法存在的根本问题是将 []byte 类型转换为 string 后结果中没有任何 U+FFFD:这种类型转换只是逐字地从源复制字节到目标。
与 byte 切片一样,Go 语言中的字符串也不必包含 UTF-8 编码的文本;它们可以包含任何数据,包括与文本无关的不透明二进制数据。
但是对于字符串的某些操作——即 将它们转换为[]rune使用range迭代它们 ——确实将字符串解释为UTF-8编码的文本。 这正是你被绊倒的地方:你的range调试循环试图解释字符串,每次尝试解码正确编码的代码点失败时,range就会产生一个替换字符U+FFFD
再次强调,通过类型转换获得的字符串不包含您想要用正则表达式替换的字符。
至于如何将您的数据制作成有效的UTF-8编码字符串,您可以采用两步法:
  1. 将字节片转换为字符串——就像您已经做的那样。
  2. 使用任何一种将字符串解释为UTF-8的方法——在此过程中将动态出现的U+FFFD替换掉——当您正在迭代时。
类似这样:
var sb strings.Builder
for _, c := range string(b) {
  if c == '\uFFFD' {
    sb.WriteByte('.')
  } else {
    sb.WriteRune(c)
  }
}
return sb.String()

关于性能的注释:由于将[]byte转换为string会复制内存——因为字符串是不可变的,而切片不是——因此对于处理大量数据和/或在紧密处理循环中工作的代码来说,类型转换的第一步可能是浪费资源的。
在这种情况下,值得使用encoding/utf8包的DecodeRune函数,该函数适用于字节切片。 其文档中的示例可以轻松地改编为上述循环所需的内容。

另请参见:从字符串中删除无效的UTF-8字符


1
U+FFFD 是常量 utf8.RuneError 的值,该常量用于告诉调用者输入不是正确的 utf8 编码数据。https://pkg.go.dev/unicode/utf8#pkg-constants - user4466350

5

@kostix的回答是正确的,非常清楚地解释了从字符串扫描Unicode符文的问题。

只是补充一下:如果您的意图仅查看ASCII范围内(可打印字符<127)的字符,并且您真的不关心其他Unicode代码点,则可以更加直接:

// create a byte slice with the same byte length as s
var bs = make([]byte, len(s))

// scan s byte by byte :
for i := 0; i < len(s); i++ {
    switch {
    case 32 <= s[i] && s[i] <= 126:
        bs[i] = s[i]

    // depending on your needs, you may also keep characters in the 0..31 range,
    // like 'tab' (9), 'linefeed' (10) or 'carriage return' (13) :
    // case s[i] == 9, s[i] == 10, s[i] == 13:
    //   bs[i] = s[i]

    default:
        bs[i] = '.'
    }
}


fmt.Printf("rs: %s\n", bs)

这个函数将为您提供接近于“hexdump -C”中的“text”部分的内容。

playground


很好的观点,实际上;+1。 - kostix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接