【问题标题】:How to check if a UTF-8 string starts with an 'a'如何检查 UTF-8 字符串是否以“a”开头
【发布时间】:2017-11-22 03:19:59
【问题描述】:

我有一个 UTF-8 字符串,它以 null 结尾的 const char* 形式给出。我想知道这个字符串的第一个字母本身是否是a。以下代码

bool f(const char* s) {
  return s[0] == 'a';
}

错误,因为字符串的第一个字母(字形簇)可能是 à - 由 2 个 unicode 标量值组成:a`。所以这个非常简单的问题似乎很难回答,除非你知道字素簇是如何产生的。

不过,许多库解析 UTF-8 文件(例如 YAML 文件),因此应该能够回答此类问题。但这些库似乎并不依赖于 Unicode 库。

所以我的问题是:

  • 如何编写检查字符串是否以字母a开头的代码?

  • 假设第一个问题没有简单的答案,解析器(如 YAML 解析器)如何在无法回答此类问题的情况下解析文件?

【问题讨论】:

  • 为什么那些库需要了解字素簇?文本序列化倾向于根据代码点来定义。无法想象 YAML 是个例外。
  • 解析和验证 utf-8 以及正确解释它是有区别的。前者容易,后者不那么容易。
  • 将 UTF-8 解码为 Unicode,normalize it 使用 NFC 或 NFKC 以减少组合标记,然后根据需要测试标准化数据。

标签: c unicode utf-8 grapheme


【解决方案1】:

这根本不重要。

考虑:这个字符串是有效的 JSON 吗?

"̀"

(这是字节序列22 cc 80 22。)

您似乎认为它不是:因为 JSON 字符串应该以 "(引号)开头,而是以 (引号 + 组合重音符号)开头。

唯一合理的回答是您的想法有误:文本序列化是根据代码点定义的。仅考虑用于处理自然语言和编辑文本。

这当然被认为是有效的 JSON。

>>> json.loads(bytes.fromhex('22cc8022'))
'̀'

【讨论】:

  • 我认为文本序列化是根据字符(或字形簇)定义的,而不是根据代码点定义的。你的例子,字符串很清楚。我会认为它不是一个有效的字符串。这就是为什么我非常困惑。现在一切都清楚了。
  • 作为一个相关的问题,正则表达式是如何工作的? "^a.*" 是否匹配 "a + (COMBINING GRAVE ACCENT)"?
  • @InsideLoop:是的,确实匹配。
  • @InsideLoop:“字符(或图形簇)”不正确;这些不是遥不可及的概念。 Unicode 字符大致相当于代码点(有一些技术考虑;并非所有代码点都对应于字符等)。
  • @R:好的。 Swift(来自 Apple)以相同的方式使用字符和字素簇。他们可能错了。
【解决方案2】:

如何编写检查字符串是否以字母 a 开头的代码?

对此没有简单的答案。要回答这个问题,您需要测试代码点的 Unicode CCC 属性。如果它不为零,那么它是一个组合字符。

当然,C 没有这样的 API。

解析器(如 YAML 解析器)如何在无法回答此类问题的情况下解析文件。

这不是他们需要回答的问题。为什么?因为他们从不问。

如果 YAML 正在读取一个键,那么它会一直读取到名称终止字符(如 :)。 Unicode 组合字符不能组合这样的字符,YAML 规范并不关心: 的另一侧是否有组合字符。如果它看到一个:,那么它就知道它已经到了名字的末尾,而在它之前的所有东西都是一个键。

如果它正在读取一个文本字符串,那么它同样会继续读取,直到它读取一个终止字符或字符序列。

使用大多数文本格式解析文本是基于针对某些终止条件的正则表达式匹配(或类似的东西)。也就是说,字符串可以是某些字符集中的任何一个(或者,除了某些字符集之外的所有字符),直到终止字符。

【讨论】:

  • 谢谢。我认为它正在寻找字符(或字素簇):,而不是代码点:。现在,我对这些概念有了更好的理解。谢谢。
【解决方案3】:

这是一个检查 utf8 字符串是否以字母 'a' 开头的代码?

bool f(const char* s) {

        if (s[0] == 'a') return true;

        if (strlen(s) >= 2 && s[0] == '\xc3') {
                char s1 = s[1];
                if (s1 == '\x80') return true; // LATIN CAPITAL LETTER A WITH GRAVE
                if (s1 == '\x81') return true; // LATIN CAPITAL LETTER A WITH ACUTE
                if (s1 == '\x82') return true; // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
                if (s1 == '\x83') return true; // LATIN CAPITAL LETTER A WITH TILDE
                if (s1 == '\x84') return true; // LATIN CAPITAL LETTER A WITH DIAERESIS
                if (s1 == '\x85') return true; // LATIN CAPITAL LETTER A WITH RING ABOVE

                if (s1 == '\xa0') return true; // LATIN SMALL LETTER A WITH GRAVE
                if (s1 == '\xa1') return true; // LATIN SMALL LETTER A WITH ACUTE
                if (s1 == '\xa2') return true; // LATIN SMALL LETTER A WITH CIRCUMFLEX
                if (s1 == '\xa3') return true; // LATIN SMALL LETTER A WITH TILDE
                if (s1 == '\xa4') return true; // LATIN SMALL LETTER A WITH DIAERESIS
                if (s1 == '\xa5') return true; // LATIN SMALL LETTER A WITH RING ABOVE
        }
        return false;
}

【讨论】:

  • 我正在寻找仅当第一个字素簇为“a”时才返回 true 的代码。这不是这段代码的作用。
  • 哎呀。我误解了。那么你的原始代码是正确的。
【解决方案4】:

s[0] == 'a' 是第一个字符 是否为a 的正确测试。如果字符串包含à 的分解版本,那将是两个字符,a 和组合坟墓。直到 Apple 决定在所有地方强制执行 NFD,这基本上都不是问题,因为希望 à 本身被视为字符/字母的人会将其作为一个字符输入,而希望将其作为 a 的人会输入带有标记的将输入为两个。是的,这违背了规范等效的 Unicode 意图,但规范等效的 Unicode 意图在很大程度上违背了用户的期望和意图(更不用说现有的文本和文本处理模型)。

如果你真的想检查第一个字符是a 并且后面没有任何组合标记,这应该可以:

wchar_t tmp = WEOF;
mbrtowc(&tmp, s+1, MB_LEN_MAX, &(mbstate_t){0});
if (tmp && wcwidth(tmp)==0) {
    /* character following 'a' is a combining mark */
}

这取决于 POSIX wcwidth 函数,但您可以找到它的可移植版本或根据 Unicode 表编写自己的版本(实际上您可以编写一个更简单的函数,只检查组合状态,而不是东亚宽度属性)。

要回答您关于解析器的第二个问题,他们没有任何理由知道或关心您关注的问题。 yaml、json 等文件格式不受规范等价性影响(至少不在解析级别;存储在文件中的 内容,应用程序将解释这些内容,可能会受到影响)。一个不同的 Unicode 字符序列的字符串,即使它在规范上是等价的,也是一个比较不相等的 不同的字符串

【讨论】:

    猜你喜欢
    • 2019-08-24
    • 2015-04-15
    • 2018-09-03
    • 2012-02-06
    • 2023-01-01
    • 1970-01-01
    • 2011-05-04
    • 2010-12-01
    相关资源
    最近更新 更多