13.9 UTF-8 库 (UTF-8 Library - Lua 5.3+) Lua 5.3+ UTF-8 库详解与实践 在 Lua 5.3 及更高版本中,为了更好地支持 Unicode 编码,特别是互联网上广泛使用的 UTF-8 编码,Lua 标准库引入了 库。这个库提供了一系列函数,用于处理 UTF-8 编码的字符串,使得 Lua 能够更有效地处理多语言文本,并避免在处理非 ASCII 字符时出现错误。 文章结构: UTF-8 编码简介及在 Lua 中的重要性 库概览:核心功能 库函数详解与实践 库的实际应用场景 总结与最佳实践 UTF-8 编码简介及在 Lua 中的重要性 什么是 UTF-8?
在 Lua 5.3 及更高版本中,为了更好地支持 Unicode 编码,特别是互联网上广泛使用的 UTF-8 编码,Lua 标准库引入了 utf8 库。这个库提供了一系列函数,用于处理 UTF-8 编码的字符串,使得 Lua 能够更有效地处理多语言文本,并避免在处理非 ASCII 字符时出现错误。
文章结构:
UTF-8 编码简介及在 Lua 中的重要性
utf8 库概览:核心功能
utf8 库函数详解与实践
utf8.char (...)
utf8.charpattern
utf8.codes (s)
utf8.codepoint (s [, i])
utf8.len (s)
utf8.offset (s, n [, i])
utf8.nfdnormalize (s)
utf8 库的实际应用场景
总结与最佳实践
什么是 UTF-8?
UTF-8 (Unicode Transformation Format 8-bit) 是一种变长字符编码,能够表示 Unicode 标准中的所有字符。它使用 1 到 4 个字节来编码一个字符:
ASCII 字符 (U+0000 to U+007F): 使用 1 个字节,与 ASCII 编码完全兼容。
扩展拉丁字母、希腊字母、西里尔字母等 (U+0080 to U+07FF): 使用 2 个字节。
基本多文种平面 (BMP) 中的其他字符,包括汉字、日文、韩文等 (U+0800 to U+FFFF): 使用 3 个字节。
补充字符 (U+10000 to U+10FFFF): 使用 4 个字节。
UTF-8 的优点在于:
兼容性: 与 ASCII 兼容,现有的 ASCII 文本可以直接作为 UTF-8 文本处理。
通用性: 能够表示世界上几乎所有的文字。
效率: 对于英文和西欧语言文本,编码效率很高,因为常用字符只用 1 或 2 个字节。
自同步: UTF-8 编码具有一定的自同步能力,即使数据流中丢失了一些字节,也可以较快地恢复同步,减少错误传播。
UTF-8 在 Lua 中的重要性
在 Lua 5.3 之前,Lua 字符串本质上是字节序列,对编码没有明确的概念。这意味着在处理非 ASCII 字符时,例如汉字、日文等,如果简单地使用字符串操作函数 (如 string.len, string.sub),可能会得到错误的结果,因为这些函数是基于字节来操作的,而不是基于字符。
例如,一个汉字通常由 3 个字节表示在 UTF-8 编码中。如果使用 string.len 获取包含汉字的 UTF-8 字符串的长度,得到的是字节数,而不是字符数。同样,使用 string.sub 截取字符串时,如果截取位置不当,可能会截断一个 UTF-8 字符,导致乱码。
Lua 5.3 引入 utf8 库正是为了解决这些问题。通过 utf8 库,Lua 能够正确地识别和处理 UTF-8 字符,从而实现对多语言文本的有效支持。这对于开发需要处理国际化文本的应用,如 Web 应用、文本编辑器、游戏等,至关重要。
utf8 库概览:核心功能utf8 库提供了一系列函数,主要用于以下方面:
字符编码与解码: 将 Unicode 码点 (code point) 转换为 UTF-8 字符,或将 UTF-8 字符转换为 Unicode 码点。
字符迭代: 遍历 UTF-8 字符串中的每个字符。
字符长度计算: 正确计算 UTF-8 字符串中的字符数,而不是字节数。
字符偏移计算: 在 UTF-8 字符串中根据字符位置进行偏移。
UTF-8 字符串规范化: 将 UTF-8 字符串规范化为 NFD (Normalization Form Decomposition) 形式,用于文本比较和处理。
核心功能可以概括为:UTF-8 字符串的正确处理和操作。 使用 utf8 库的函数,可以确保在 Lua 中处理 UTF-8 字符串时,能够像处理 ASCII 字符串一样方便和可靠,同时避免因编码问题导致的错误。
utf8 库函数详解与实践接下来,我们将逐个详细介绍 utf8 库中的函数,并通过代码示例来演示它们的用法和效果。
utf8.char (...)功能: 接收零个或多个整数参数,每个参数代表一个 Unicode 码点,返回由这些码点对应的 UTF-8 字符组成的字符串。
语法: utf8.char (i1, i2, ...)
参数: 零个或多个整数,表示 Unicode 码点。有效的码点范围是 0 到 1114111 (十六进制 0x10FFFF)。
返回值: 一个 UTF-8 字符串,由输入的码点对应的字符组成。
代码实践:
-- 将 Unicode 码点转换为 UTF-8 字符 local char1 = utf8.char(65) -- 码点 65 是 'A' local char2 = utf8.char(20013) -- 码点 20013 是 '中' local char3 = utf8.char(0x1F600) -- 码点 0x1F600 是 (笑脸表情) print(char1) --> A print(char2) --> 中 print(char3) --> -- 组合多个码点 local str = utf8.char(72, 101, 108, 108, 111, 32, 20013, 20170) -- Hello 世界 print(str) --> Hello 世界 -- 错误示例:无效的码点 -- local invalid_char = utf8.char(1114112) -- 码点超出范围,会报错 -- print(invalid_char) -- 报错:invalid value
详解:
utf8.char 函数是将 Unicode 码点转换为 UTF-8 字符的关键函数。它可以接受多个码点作为参数,并将它们连接成一个 UTF-8 字符串。这使得我们可以方便地通过码点来构建包含各种 Unicode 字符的字符串。
需要注意的是,输入的码点必须是有效的 Unicode 码点,即在 0 到 1114111 (0x10FFFF) 的范围内。超出这个范围的码点会报错。
utf8.charpattern功能: 一个预定义的模式字符串,用于匹配 UTF-8 字符序列。
语法: utf8.charpattern
返回值: 字符串 "[^\0-\x7F\xC2-\xF4][\x80-\xBF]*"
代码实践:
-- 使用 utf8.charpattern 匹配 UTF-8 字符 local str = "Hello 世界 " -- 查找第一个 UTF-8 字符 local char_start, char_end = string.find(str, utf8.charpattern) print(string.sub(str, char_start, char_end)) --> H -- 循环遍历所有 UTF-8 字符 for char_start, char_end in string.gmatch(str, utf8.charpattern) do print(string.sub(str, char_start, char_end)) end --> H --> e --> l --> l --> o --> --> 世 --> 界 --> -->
详解:
utf8.charpattern 并不是一个函数,而是一个预定义的字符串模式。这个模式字符串可以用于 string.find, string.gmatch 等字符串匹配函数中,用来匹配 UTF-8 字符。
模式 "[^\0-\x7F\xC2-\xF4][\x80-\xBF]*" 的含义是:
[^\0-\x7F\xC2-\xF4]: 匹配一个非 ASCII 字符的起始字节。UTF-8 编码中,ASCII 字符的范围是 0x00-0x7F,多字节字符的起始字节范围是 0xC2-0xF4 (用于 2-4 字节字符)。 [^...] 表示取反,所以这里匹配的是非 ASCII 字符的起始字节。
[\x80-\xBF]*: 匹配零个或多个后续字节。UTF-8 编码中,多字节字符的后续字节范围是 0x80-0xBF。 * 表示零个或多个。
因此,这个模式可以匹配任何有效的 UTF-8 字符,无论是单字节的 ASCII 字符,还是多字节的非 ASCII 字符。
注意: utf8.charpattern 匹配的是 一个 UTF-8 字符。如果需要匹配多个 UTF-8 字符,或者更复杂的模式,需要根据具体需求构建更复杂的模式。
utf8.codes (s)功能: 返回一个迭代器函数,用于遍历 UTF-8 字符串 s 中的所有 Unicode 码点。
语法: utf8.codes (s)
参数: s: 要遍历的 UTF-8 字符串。
返回值: 一个迭代器函数。每次调用迭代器函数,返回字符串中下一个字符的起始字节位置和对应的 Unicode 码点。当字符串遍历完成时,迭代器函数返回 nil。
代码实践:
-- 使用 utf8.codes 遍历 UTF-8 字符串的码点 local str = "Hello 世界 " for byte_offset, codepoint in utf8.codes(str) do print(string.format("Byte offset: %d, Codepoint: %d, Char: %s", byte_offset, codepoint, utf8.char(codepoint))) end --> Byte offset: 1, Codepoint: 72, Char: H --> Byte offset: 2, Codepoint: 101, Char: e --> Byte offset: 3, Codepoint: 108, Char: l --> Byte offset: 4, Codepoint: 108, Char: l --> Byte offset: 5, Codepoint: 111, Char: o --> Byte offset: 6, Codepoint: 32, Char: --> Byte offset: 7, Codepoint: 20170, Char: 世 --> Byte offset: 10, Codepoint: 30028, Char: 界 --> Byte offset: 13, Codepoint: 32, Char: --> Byte offset: 14, Codepoint: 128512, Char:
详解:
utf8.codes 函数提供了一种遍历 UTF-8 字符串中每个字符的码点的有效方式。它返回一个迭代器函数,每次调用迭代器函数,都会返回下一个 UTF-8 字符的起始字节位置 (从 1 开始计数) 和对应的 Unicode 码点。
通过 utf8.codes 返回的迭代器,我们可以方便地访问字符串中每个字符的码点,并进行进一步的处理,例如字符分析、转换等。
注意: 返回的字节偏移量是相对于字符串的起始位置的,这在某些需要知道字符在字符串中字节位置的场景下很有用。
utf8.codepoint (s [, i])功能: 返回 UTF-8 字符串 s 中,从字节位置 i 开始的第一个字符的 Unicode 码点。如果 i 缺失,则默认为 1。
语法: utf8.codepoint (s [, i])
参数:
s: UTF-8 字符串。
i: 可选参数,表示起始字节位置。默认为 1。
返回值: 从位置 i 开始的第一个 UTF-8 字符的 Unicode 码点。如果给定的字节位置 i 不是一个 UTF-8 字符的起始字节,则返回 nil。
代码实践:
-- 获取指定位置的字符的码点 local str = "Hello 世界 " print(utf8.codepoint(str)) -- 默认从位置 1 开始,获取第一个字符 'H' 的码点 --> 72 print(utf8.codepoint(str, 7)) -- 从位置 7 开始,获取 '世' 的码点 --> 20170 print(utf8.codepoint(str, 14)) -- 从位置 14 开始,获取 '' 的码点 --> 128512 -- 错误示例:无效的起始位置 print(utf8.codepoint(str, 8)) -- 位置 8 是 '世' 的中间字节,不是起始字节,返回 nil --> nil
详解:
utf8.codepoint 函数用于获取 UTF-8 字符串中指定位置的字符的 Unicode 码点。它允许我们根据字节位置来访问字符串中的字符。
需要注意的是,utf8.codepoint 函数的起始位置参数 i 是 字节位置,而不是字符位置。而且,i 必须是一个 UTF-8 字符的 起始字节 的位置。如果 i 指向一个多字节字符的中间字节,则函数会返回 nil。
在处理 UTF-8 字符串时,如果需要根据字节位置来获取字符的码点,可以使用 utf8.codepoint 函数。
utf8.len (s)功能: 返回 UTF-8 字符串 s 中包含的 UTF-8 字符的数量。
语法: utf8.len (s)
参数: s: UTF-8 字符串。
返回值: 字符串 s 中 UTF-8 字符的数量。
代码实践:
-- 计算 UTF-8 字符串的字符长度 local str1 = "Hello" local str2 = "世界" local str3 = "Hello 世界 " print(utf8.len(str1)) --> 5 (ASCII 字符串,字符数和字节数相同) print(utf8.len(str2)) --> 2 (汉字字符串,每个汉字 3 个字节,但字符数是 2) print(utf8.len(str3)) --> 9 (混合字符串,正确计算字符数) -- 对比 string.len (字节长度) 和 utf8.len (字符长度) print(string.len(str3)) --> 16 (字节长度,而不是字符长度)
详解:
utf8.len 函数是 utf8 库中最常用的函数之一。它用于计算 UTF-8 字符串的 字符长度,而不是字节长度。这对于处理多语言文本非常重要,因为传统的 string.len 函数返回的是字节长度,对于非 ASCII 字符会得到错误的结果。
utf8.len 函数能够正确地识别 UTF-8 字符的边界,并计算出字符串中实际包含的字符数量。在需要知道 UTF-8 字符串的字符长度时,应该使用 utf8.len 而不是 string.len。
utf8.offset (s, n [, i])功能: 返回 UTF-8 字符串 s 中第 n 个字符的起始字节位置。起始搜索位置可以从字节位置 i 开始,如果 i 缺失,则默认为 1。
语法: utf8.offset (s, n [, i])
参数:
s: UTF-8 字符串。
n: 要查找的字符的序号。正数表示从前往后数,负数表示从后往前数。
i: 可选参数,表示起始搜索的字节位置。默认为 1。
返回值: 字符串 s 中第 n 个字符的起始字节位置。如果找不到第 n 个字符 (例如 n 超出字符串长度),则返回 nil。
代码实践:
-- 获取指定字符位置的字节偏移量 local str = "Hello 世界 " print(utf8.offset(str, 1)) -- 第 1 个字符 'H' 的起始字节位置 --> 1 print(utf8.offset(str, 7)) -- 第 7 个字符 '世' 的起始字节位置 --> 7 print(utf8.offset(str, -1)) -- 倒数第 1 个字符 '' 的起始字节位置 --> 14 print(utf8.offset(str, 9)) -- 第 9 个字符 '' 的起始字节位置 --> 14 print(utf8.offset(str, 10)) -- 第 10 个字符,超出字符串长度,返回 nil --> nil -- 从指定位置开始搜索 print(utf8.offset(str, 1, 7)) -- 从字节位置 7 开始,查找第 1 个字符 (相对于起始位置),即 '世' --> 7 print(utf8.offset(str, 2, 7)) -- 从字节位置 7 开始,查找第 2 个字符 (相对于起始位置),即 '界' --> 10
详解:
utf8.offset 函数用于根据字符序号来查找 UTF-8 字符串中对应字符的起始字节位置。这在需要根据字符位置进行字符串截取、替换等操作时非常有用。
参数 n 可以是正数或负数:
正数 n: 从字符串的开头开始计数,查找第 n 个字符。
负数 n: 从字符串的末尾开始计数,查找倒数第 abs(n) 个字符。 例如,n = -1 表示最后一个字符,n = -2 表示倒数第二个字符,以此类推。
可选参数 i 可以指定搜索的起始字节位置。这可以用于在字符串的某个部分开始查找字符。
注意: utf8.offset 返回的是 字节位置,而不是字符位置。 它的作用是帮助我们找到指定字符在字节序列中的起始位置,以便于后续的字节级别的操作。
utf8.nfdnormalize (s)功能: 接收一个 UTF-8 字符串 s,并返回其 NFD (Normalization Form Decomposition) 形式的规范化字符串。
语法: utf8.nfdnormalize (s)
参数: s: UTF-8 字符串。
返回值: s 的 NFD 规范化形式的 UTF-8 字符串。
代码实践:
-- UTF-8 字符串 NFD 规范化 local str1 = "你好" local str2 = "ni好" -- 'n' + combining i (U+0307) + 'hao' local str3 = utf8.nfdnormalize(str2) print(str1 == str2) --> false (视觉上相似,但字节序列不同) print(str1 == str3) --> false (规范化后仍然不同,因为 '你' 和 'ni' 不同) local str4 = "cafe\u0301" -- "café" 使用 combining acute accent (U+0301) local str5 = "café" -- "café" 使用 precomposed character 'é' (U+00E9) local str6 = utf8.nfdnormalize(str4) local str7 = utf8.nfdnormalize(str5) print(str4 == str5) --> false (字节序列不同) print(str6 == str7) --> true (规范化后字节序列相同,都分解为 'cafe' + combining accent) print(str6) --> café (显示上可能看起来一样,但内部表示不同) print(str7) --> café print(string.len(str4)) --> 6 (bytes) print(string.len(str5)) --> 5 (bytes) print(string.len(str6)) --> 6 (bytes) print(string.len(str7)) --> 6 (bytes) print(utf8.len(str4)) --> 4 (chars) print(utf8.len(str5)) --> 4 (chars) print(utf8.len(str6)) --> 4 (chars) print(utf8.len(str7)) --> 4 (chars)
详解:
utf8.nfdnormalize 函数用于将 UTF-8 字符串规范化为 NFD (Normalization Form Decomposition) 形式。
什么是 Unicode 规范化?
在 Unicode 中,有些字符可以用不同的码点序列来表示,例如 "é" 可以用预组合字符 U+00E9 表示,也可以用基本字符 "e" (U+0065) 加上组合字符 "́" (combining acute accent, U+0301) 表示。 这就导致了视觉上相同的字符,但在计算机内部表示可能不同。
Unicode 规范化旨在解决这个问题,它定义了几种规范化形式,将不同的码点序列统一转换为唯一的标准形式,以便于文本比较、搜索和处理。
NFD (Normalization Form Decomposition) 是一种规范化形式,它将所有组合字符分解为其基本字符和组合标记的序列。例如,"é" (U+00E9) 在 NFD 中会被分解为 "e" (U+0065) + "́" (U+0301)。
utf8.nfdnormalize 的作用:
utf8.nfdnormalize 函数将输入的 UTF-8 字符串转换为 NFD 形式。这在以下场景中很有用:
字符串比较: 在比较两个 UTF-8 字符串是否相等时,如果字符串中包含组合字符,直接比较字节序列可能会得到错误的结果。使用 NFD 规范化后,可以将视觉上相同的字符串转换为相同的字节序列,从而进行正确的比较。
文本搜索: 在进行文本搜索时,使用 NFD 规范化可以提高搜索的准确性,使得搜索结果能够匹配到不同表示形式的相同字符。
数据清洗: 在处理文本数据时,使用 NFD 规范化可以将文本数据统一化,方便后续的处理和分析。
注意: NFD 规范化可能会改变字符串的字节长度,但不会改变其字符长度 (使用 utf8.len 计算的字符长度)。
utf8 库的实际应用场景utf8 库在处理多语言文本的各种场景中都非常有用。以下列举一些常见的应用场景:
国际化 (i18n) 和本地化 (l10n): 开发支持多语言的应用时,utf8 库是处理不同语言文本的基础。例如,在 Web 应用中,处理用户输入的各种语言的文本,显示多语言的用户界面等。
文本处理和分析: 在文本编辑器、搜索引擎、自然语言处理 (NLP) 等应用中,需要对文本进行各种处理和分析,包括分词、字符计数、文本规范化等。utf8 库提供了处理 UTF-8 文本的基本工具。
数据验证和清洗: 在数据处理和清洗过程中,需要验证文本数据的编码格式,并进行规范化处理,以保证数据的质量和一致性。utf8 库可以用于 UTF-8 编码验证和 NFD 规范化。
游戏开发: 在游戏中,可能需要显示各种语言的文本,例如角色名称、对话、UI 文本等。utf8 库可以用于处理游戏中的多语言文本。
网络编程: 在网络通信中,UTF-8 是常用的字符编码格式。utf8 库可以用于处理网络传输的 UTF-8 文本数据。