编辑推荐:
本技巧(tip)解释了基于字节的ASCII字符串函数(ASCII string functions)与基于Unicode码点的函数(Unicode code-point-based functions)之间的差异。该技巧对以下用户最为有益:1)分析包含多语种和
本技巧(tip)解释了基于字节的ASCII字符串函数(ASCII string functions)与基于Unicode码点的函数(Unicode code-point-based functions)之间的差异。该技巧对以下用户最为有益:1)分析包含多语种和语标文字字符、表情符号(emoji)及特殊符号的非ASCII文本数据的用户;2)从事非结构化文本和文档重度文本挖掘的用户;3)在字节级别剥离传输日志、签名文件和二进制文件的用户。
在14版之前,Stata只是存储接收到的任意字节,并根据操作系统的活动代码页(code page)(例如Windows-1252、ISO-8859-1)进行解释。因此,同一个8位序列在Windows和macOS/Linux中可能被不同地解释。在这种遗留编码体系下,每个字节精确地表示一个字符,无论是普通ASCII范围0-127还是扩展ASCII范围128-255。由于字节与字符码点之间的一一对应关系,Stata 14之前的字符串函数是与编码无关的函数,它们操作的是字节而非语言字符。这些函数被设计用于分析字节中的位序列,因此不会检查该字节是否属于ASCII范围或构成多字节流(multibyte stream)的一部分。
Stata 14发布时改进了字符串架构,采用UTF-8作为默认文本编码标准,从而实现了完整的Unicode兼容性。UTF-8编码的字节序列被转换为Unicode码点,支持几乎所有已记录的现存或死亡文字、符号、数学运算符、乐谱、表情符号等,但受字体支持和操作系统支持的限制。Unicode意识促使引入一组新的字符串函数,这些函数将字符串分析为有序的Unicode码点列表,具有连贯的多字节序列,而不是像基于字节的ASCII函数假设的那样分析为独立字节流。所有Unicode字符串函数的名称都以字母u开头。
**解读《Stata tip 168: ASCII bytes and Unicode code points》**
**1. 研究背景、问题提出与研究意义**
在Stata 14之前,软件的字符串函数完全基于字节(byte)运作,并依赖操作系统当前代码页(code page)解释文本。这导致同一个8位二进制序列在Windows、macOS或Linux上可能被解释为不同字符,造成跨平台文本处理的混乱。在遗留编码体系下,一个字节恰好对应一个字符,因此旧版字符串函数实质上是“编码无关”的字节操作工具,它们不关心所处理的字节是否属于ASCII范围,也不理解多字节字符序列的存在。随着文本数据日益包含非拉丁字母、表情符号、数学符号等Unicode字符,这种字节级处理方式已无法满足需求。Stata 14引入了默认的UTF-8编码,并新增一组以字母`u`开头的Unicode字符串函数,用于按“字符码点”而非“字节”来分析字符串。然而,许多用户仍不清楚新旧函数之间的根本区别,常常误用导致数据损坏或结果错误。本文正是为了系统阐述这两类函数的差异,帮助用户根据具体任务(人类语言分析 vs. 机器字节处理)选择正确的工具。
**2. 研究内容、结论与意义**
研究人员通过构造一个同时包含希腊字母、普通空格和Ogham空格的示例字符串,对ASCII字节字符串函数与Unicode码点字符串函数进行了逐项对比,涵盖字符串长度、反转、空白字符处理、变量名转换、匹配与替换等操作。研究得出明确结论:Unicode字符串函数适用于自然语言文本的“人类视角”分析,例如姓名、地址、访谈记录等;ASCII函数则适用于字节级的“机器视角”分析,例如剥离文件签名、解析字节序标记和魔数。即使数据仅包含ASCII字符,研究人员仍建议优先考虑Unicode函数,因为它们在正则表达式等高级功能上更具优势。该结论同样适用于Mata中的字符串函数。这一研究为Stata用户在多语言文本处理中避免常见陷阱提供了重要指导,具有很高的实用价值。该论文发表于《The Stata Journal》。
**3. 主要技术方法**
研究人员采用解释性示例与函数对比方法,未涉及实证数据或样本队列。具体技术手段包括:利用希腊问候语“καλημ?ρα, π?? ε?σαι;”及句子中夹杂的三个Ogham空格作为测试字符串;使用Stata的显示命令和`uchar()`、`ustrunescape()`、`ustrtohex()`函数查看码点及其十六进制表示;使用`tobytes()`函数展示字符串在内存中的字节序列;并成对调用`strlen()/ustrlen()`、`strreverse()/ustrreverse()`、`strtrim()/ustrtrim()`、`subinstr()/usubinstr()`等函数,比较二者输出结果。
**4. 研究结果**
**4.1 UTF-8编码(UTF-8 encoding)**
研究人员详细讲解了UTF-8的编码规则:ASCII码点0-127以单字节`0xxxxxxx`表示,因此每个合法ASCII文件自动兼容UTF-8;其余码点分别以2字节(`110xxxxx`起始)、3字节(`1110xxxx`起始)或4字节(`11110xxx`起始)编码,所有续字节均以`10xxxxxx`起始。这种设计保证了ASCII字节不会出现在多字节序列中,但这也意味着基于字节的旧函数无法理解多字节字符。实验表明,常见的带重音拉丁字母大多占2字节,而部分预组合字母、表情符号及非拉丁文字占3或4字节,因此非纯ASCII文本会占用更多内存。
**4.2 字符串长度(String lengths)**
通过示例字符串测试,`ustrlen()`计数得到32个Unicode码点,而`strlen()`计数得到60字节,因为希腊字母每字符占2字节,Ogham空格占3字节。在变量名转换中,`strtoname()`按32字节截断,`ustrtoname()`按32个码点截断,且后者会将Ogham空格识别为空白并替换为下划线,而前者仅替换ASCII空格。在截取子串时,`substr()`按字节截取,若截断位置落在多字节字符中间(如只取希腊字母η的第一字节),则返回Unicode替换字符“”;`usubstr()`则按完整码点截取,避免该问题。
**4.3 反转字符串(Reversing strings)**
`ustrreverse()`按码点顺序反转字符串,结果正确;而`strreverse()`按字节反转,导致原字符串末尾的Ogham空格(字节序列`11100001 10011010 10000000`)被逆转成`10000000 10011010 11100001`,其中两个字节因无法单独编码而显示为替换字符,随后与下一组字节重新组合,甚至意外形成缅甸字母Ya的编码序列。通过`tobytes()`可以清晰观察到这一字节级反转过程,证实了字节操作会彻底破坏多字节字符的完整性。
**4.4 空白字符(Whitespaces)**
`strtrim()`、`strltrim()`和`strrtrim()`仅移除ASCII空格(码点32),而`ustrtrim()`、`ustrltrim()`、`ustrrtrim()`根据Unicode的白空间属性(White_Space)识别全部25种空白字符,例如Ogham空格(码点5760)和表意空格(码点12288)。类似地,`wordcount()`和`word()`仅按ASCII空格分词,`ustrwordcount()`和`ustrword()`按所有Unicode空白字符分词。此外,`stritrim()`用于压缩字符串内部连续多个ASCII空格,但没有对应的Unicode版本,需要借助Unicode正则表达式替换函数,并指定空格属性`\s`才能实现相同效果。
**4.5 字符串替换(String substitution)**
`subinstr()`按字节替换,`usubinstr()`按Unicode码点替换。由于UTF-8的设计,任何ASCII单字节不会出现在多字节序列中,因此用二者替换完整码点时结果通常一致。然而,`subinstr()`不验证替换后的字节序列是否为合法UTF-8,例如它可以把任意字符替换为十进制值131(二进制`10000011`)这种孤立续字节,从而生成无效字符串;`usubinstr()`则会验证参数,如果无效则返回替换字符“”(U+FFFD)。这展示了两者在安全性和严格性上的本质区别。
**5. 讨论与结论**
原文没有单独的讨论部分,结论集中指出:Unicode字符串函数适合以“人类视角”处理自然语言文本,如姓名、地址、访谈记录、医生笔记等;ASCII函数适合以“机器视角”分析底层字节流,如剥离文件签名、检查字节序标记和魔数。即使数据仅含ASCII字符,也建议考虑切换到Unicode函数,以充分利用其更丰富的正则表达式功能。该分析同样适用于Mata中的字符串函数。研究人员强调,正确区分这两类函数是可靠文本处理的前提。