Golang中如何计算固定宽度文件的UTF8字符数

Golang中如何计算固定宽度文件的UTF8字符数 如果您曾处理过扩展字符,无疑会遇到 len(someString) 引发的问题。因为在大多数情况下,当您想要获取字符串的长度时,您想要的是(或多或少)字符串中的字符数,而不是字节数。这段代码:

str := "Ñ世界"
fmt.Println("len =", len(str))
fmt.Println("runes =", utf8.RuneCountInString(str))

会产生以下输出:

len = 8
runes = 3

您可能熟悉这篇优秀的文章(这应该是每位 Go 开发者的必读文章;在某些时候,它会派上用场):

go.dev

Go 中的字符串、字节、符文和字符 - Go 编程语言

介绍 Go 中字符串的工作原理以及如何使用它们。

在我的案例中,我一直在处理固定宽度的订单上传,其中宽度是由符文的数量决定的,而不是字节。目前,我采用类似以下的方法:

func processRow(row string) error {
	rowLen := utf8.RuneCountInString(row)
	// 为了测试目的,我们假设
	// 此行有 3 个字段。每个字段宽 1 个字符。
	if rowLen != 3 {
		return fmt.Errorf("invalid row length: %v", rowLen)
	}
	// 获取所有符文
	runes := make([]rune, rowLen)
	i := 0
	for _, v := range row {
		runes[i] = v
		i++
	}
	// 根据宽度构建列
	col1 := string(runes[0:1])
	col2 := string(runes[1:2])
	col3 := string(runes[2:3])
	fmt.Printf("Row columns: %v, %v, %v.", col1, col2, col3)
	return nil
}

将字符串转换为符文切片,然后再将这些符文转换回字符串,这似乎效率不高。我知道这是过早的优化,但我只是好奇是否有其他人处理过类似的问题。如果有,您是如何处理的?


更多关于Golang中如何计算固定宽度文件的UTF8字符数的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

我对你正在做的事情很感兴趣,但我还不确定你具体想优化什么。由于UTF-8是一种可变宽度编码,因此无法在不扫描并计算可变大小的码点的情况下,直接索引到UTF-8编码字符串中的特定符文。如果你需要索引到特定符文,我想了解更多关于这个问题的背景信息,因为你可能希望将字符串作为 []rune 切片来处理。

func main() {
    fmt.Println("hello world")
}

更多关于Golang中如何计算固定宽度文件的UTF8字符数的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


您是在寻找 utf8.DecodeRuneInString 吗?

cols := [3]string

for rowIndex, colIndex := 0, 0; rowIndex < len(row); colIndex++ {
    _, n := utf8.DecodeRuneInString(row[rowIndex:])
    cols[colIndex] = row[rowIndex:rowIndex+n]
    rowIndex += n
}

请查看这里的 norm 包:norm package - golang.org/x/text/unicode/norm - Go Packages。你可以使用规范化形式 C 或规范化形式 KC 将文本规范化为通用形式( (a + ̈) 和 ä 都应规范化为 ä)。

另请参阅:UAX #15: Unicode Normalization Forms

我也使用过 utf8.DecodeRuneInString,但我的理解是,在这种情况下它与 for range 循环是相同的。根据我上面链接的文档:

除了 Go 源代码是 UTF-8 这一基本事实外,Go 真正特殊对待 UTF-8 的方式只有一种,那就是在字符串上使用 for range 循环。

我们已经看到了常规 for 循环会发生什么。相比之下,for range 循环在每次迭代中解码一个 UTF-8 编码的符文。每次循环时,循环的索引是当前符文的起始位置(以字节为单位),而码点就是它的值。

因此,根据我设计的 processRow 函数,这个

func main() {
	processRow("Ñ世界")
}

… 产生以下输出:

Row columns: Ñ, 世, 界.

这没问题。我想我原本希望不必解码符文,而可以直接根据符文索引而不是字节索引来切片我的字符串。我认为真正的问题是:我处理的列宽是基于符文计数而不是字节计数,这有点奇怪。大多数存储引擎处理的是字节计数,但大多数用户考虑的是符文/字符计数(这本身就很复杂,因为“字符”的构成并不像我曾经认为的那样简单明了!)。

我想我不会再试图过早地优化这个问题了。我已经有了一个足够有效的解决方案。只是想再次确认,没有我不知道的、更优越的思考方式。

在处理固定宽度文件时,确实需要按符文(rune)计数而不是字节数。以下是更高效的实现方式:

func processRow(row string) error {
    // 直接使用符文迭代器处理字段
    var col1, col2, col3 strings.Builder
    count := 0
    
    for _, r := range row {
        switch count {
        case 0:
            col1.WriteRune(r)
        case 1:
            col2.WriteRune(r)
        case 2:
            col3.WriteRune(r)
        }
        count++
    }
    
    if count != 3 {
        return fmt.Errorf("invalid row length: %v", count)
    }
    
    fmt.Printf("Row columns: %v, %v, %v.", 
        col1.String(), col2.String(), col3.String())
    return nil
}

对于更通用的解决方案,可以这样处理任意数量的字段:

func splitFixedWidth(row string, fieldWidths []int) ([]string, error) {
    var result []string
    var current strings.Builder
    runeCount := 0
    fieldIndex := 0
    
    for _, r := range row {
        current.WriteRune(r)
        runeCount++
        
        if fieldIndex < len(fieldWidths) && runeCount == fieldWidths[fieldIndex] {
            result = append(result, current.String())
            current.Reset()
            runeCount = 0
            fieldIndex++
        }
    }
    
    // 检查是否还有剩余字符
    if current.Len() > 0 {
        return nil, fmt.Errorf("extra characters after last field")
    }
    
    if fieldIndex != len(fieldWidths) {
        return nil, fmt.Errorf("insufficient characters for all fields")
    }
    
    return result, nil
}

// 使用示例
func main() {
    row := "Ñ世界"
    fields, err := splitFixedWidth(row, []int{1, 1, 1})
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("Row columns: %v, %v, %v\n", fields[0], fields[1], fields[2])
}

如果字段宽度相同,可以使用更简洁的版本:

func splitEqualWidth(row string, width, numFields int) ([]string, error) {
    result := make([]string, 0, numFields)
    runes := []rune(row)
    
    if len(runes) != width*numFields {
        return nil, fmt.Errorf("invalid row length")
    }
    
    for i := 0; i < len(runes); i += width {
        result = append(result, string(runes[i:i+width]))
    }
    
    return result, nil
}

这些方法避免了创建中间符文切片,直接处理符文迭代,内存效率更高。对于性能关键场景,可以预分配缓冲区:

func processRowEfficient(row string) error {
    if utf8.RuneCountInString(row) != 3 {
        return fmt.Errorf("invalid row length")
    }
    
    // 预分配符文数组
    runes := [3]rune{}
    i := 0
    for _, r := range row {
        runes[i] = r
        i++
    }
    
    // 直接使用数组切片
    col1 := string(runes[0:1])
    col2 := string(runes[1:2])
    col3 := string(runes[2:3])
    
    fmt.Printf("Row columns: %v, %v, %v.", col1, col2, col3)
    return nil
}

我对你正在做的事情很感兴趣,但还不确定你具体想优化什么。

我更想知道其他人是如何处理这个问题的。这是为一个拥有B2B门户的国际客户做的。他们正试图实现现代化,但他们业务中的所有东西都运行在AS400上;他们的客户也是如此。所以,我们的第一步是创建一个部署到三大云提供商之一的Web门户。

我们允许通过这个门户上传订单,其中一种格式是固定宽度的。但是——客户认为“宽度”或多或少就是“字符数”,这有点复杂。我举了一个简单的例子,但考虑一下这个:

func main() {
	str := "ä"
	for _, v := range str {
		fmt.Println(string(v))
	}
}

… 输出:

a
̈

因为“符文”(rune)或多或少映射到UTF8码点,而表示那个字符需要2个码点。我链接的那个Go博客有一个非常好的解释(加粗部分是我的强调):

到目前为止,我们在使用“字节”和“字符”这两个词时一直非常小心。部分原因是字符串保存的是字节,部分原因是“字符”的概念有点难以定义。Unicode标准使用术语“码点”来指代由单个值表示的项目。码点U+2318,十六进制值为2318,代表符号⌘。(关于该码点的更多信息,请参见其Unicode页面。)

举一个更平淡的例子,Unicode码点U+0061是小写拉丁字母‘A’:a。

但是小写带重音符的字母‘A’,à呢?那是一个字符,它也是一个码点(U+00E0),但它有其他表示形式。例如,我们可以使用“组合”重音符码点U+0300,并将其附加到小写字母a(U+0061)上,以创建相同的字符à。通常,一个字符可以由许多不同的码点序列表示,因此也由不同的UTF-8字节序列表示。

因此,计算中的字符概念是模糊的,或者至少是令人困惑的,所以我们谨慎使用它。为了使事情可靠,有一些规范化技术可以保证一个给定的字符总是由相同的码点表示,但这个主题目前离题太远。后续的博客文章将解释Go库如何处理规范化。

“码点”有点拗口,所以Go为这个概念引入了一个更短的术语:符文(rune)。这个术语出现在库和源代码中,其含义与“码点”完全相同,只是有一个有趣的补充。

我目前没有具体的问题或难题。只是想知道其他人做了什么。看起来Unicode®标准附件#29中的字素簇(“用户感知的字符”)或多或少解决了“用户感知意义上的字符是什么?”这个问题。有一个模块实现了这个:

GitHub - rivo/uniseg: Unicode Text Segmentation, Word Wrapping, and String...

GitHub - rivo/uniseg: Unicode Text Segmentation, Word Wrapping, and String…

Unicode文本分割、自动换行和Go中的字符串宽度计算 - GitHub - rivo/uniseg: Go中的Unicode文本分割、自动换行和字符串宽度计算

关于我最初提到的将字符串转换为符文切片再转换回字符串似乎效率低下的评论,我认为我对内存分配的担忧是荒谬的。我对迭代符文与重新切片字符串进行了一些基准测试。后者(虽然不那么符合人体工程学)更快,但这几乎肯定无关紧要。

回到顶部