特殊字符(夏威夷 'Okina)导致奇怪的字符串行为

Posted

技术标签:

【中文标题】特殊字符(夏威夷 \'Okina)导致奇怪的字符串行为【英文标题】:Special character (Hawaiian 'Okina) leads to weird string behavior特殊字符(夏威夷 'Okina)导致奇怪的字符串行为 【发布时间】:2019-08-22 14:55:54 【问题描述】:

Hawaiian quote 在与字符串函数结合使用时在 T-SQL 中有一些奇怪的行为。这里发生了什么?我错过了什么吗?其他角色会遇到同样的问题吗?

SELECT UNICODE(N'ʻ') -- Returns 699 as expected.

SELECT REPLACE(N'"ʻ', '"', '_') -- Returns "ʻ, I expected _ʻ

SELECT REPLACE(N'aʻ', 'a', '_') -- Returns aʻ, I expected _ʻ

SELECT REPLACE(N'"ʻ', N'ʻ', '_') -- Returns __, I expected "_

SELECT REPLACE(N'-', N'ʻ', '_') -- Returns -, I expected -

另外,例如在LIKE 中使用时会很奇怪:

DECLARE @table TABLE ([Name] NVARCHAR(MAX))
INSERT INTO
    @table
VALUES
    ('John'),
    ('Jane')

SELECT
    *
FROM
    @table
WHERE
    [Name] LIKE N'%ʻ%' -- This returns both records. I expected none.

【问题讨论】:

很好的问题有助于说明 Unicode 的缺点它们背后的历史。这应该是计算机历史博物馆的一部分! @PanagiotisKanavos 我不是想把它拖出来,但我不明白这是 Unicode 的一个缺点。 Unicode 提供了更准确地捕捉大量语言变体中的许多复杂性的能力。微软对它的实现可能不如 ICU,但它比忽略某些语言的标记要好得多,这些标记会改变它们之前和/或之后字符的含义和/或行为。我认为这个问题说明了 Unicode 的惊人成就(是的,还有随之而来的巨大学习曲线????)。 【参考方案1】:

夏威夷语引号在 T-SQL 中与字符串函数一起使用时会出现一些奇怪的行为。 ... 其他角色也有同样的问题吗?

一些事情:

    这不是夏威夷语“引号”:它是影响发音的“glottal stop”。 这不是“奇怪”的行为:这不是您所期望的。

    这种行为并不是一个“问题”,虽然是的,还有其他角色表现出类似的行为。例如,以下字符(上方的 U+02DA 环)的行为会略有不同,具体取决于它位于字符的哪一侧:

    SELECT REPLACE(N'a˚aa' COLLATE Latin1_General_100_CI_AS, N'˚a',  N'_'); -- Returns a_a
    SELECT REPLACE(N'a˚aa' COLLATE Latin1_General_100_CI_AS, N'a˚',  N'_'); -- Returns _aa
    

现在,使用 SQL Server 2008 或更高版本的任何人都应该使用 100(或更高)级别的排序规则。他们在 100 系列中添加了很多排序权重和大写/小写映射,这些映射不在 90 系列中,或者没有编号的系列,或者大部分过时的 SQL Server 排序规则(名称以 SQL_ 开头的排序规则)。

这里的问题不在于它不等同于任何其他字符(在二进制排序规则之外),实际上它确实等同于另一个字符 (U+0312 Combining Turned Comma Above):

;WITH nums AS
(
  SELECT TOP (65536) (ROW_NUMBER() OVER (ORDER BY @@MICROSOFTVERSION) - 1) AS [num]
  FROM   [master].sys.all_columns ac1
  CROSS JOIN   [master].sys.all_columns ac2
)
SELECT nums.[num] AS [INTvalue],
       CONVERT(BINARY(2), nums.[num]) AS [BINvalue],
       NCHAR(nums.[num]) AS [Character]
FROM   nums
WHERE  NCHAR(nums.[num]) = NCHAR(0x02BB) COLLATE Latin1_General_100_CI_AS;
/*
INTvalue    BINvalue    Character
699         0x02BB      ʻ
786         0x0312      ̒
*/

问题在于这是一个“间距修饰符”字符,因此它会附加到它之前或之后的字符并修改其含义/发音,具体取决于您正在处理的修饰符字符。

根据Unicode Standard, Chapter 7 (Europe-I),第 7.8 节(修饰符字母),第 323 页(文档而非 PDF):

7.8 修饰字母

修饰符字母,在 Unicode 标准中使用的意义上,是通常与其他字母相邻书写并以某种方式修改其用法的字母或符号。它们没有正式组合标记(gc = Mn 或 gc = Mc),也没有以图形方式与它们修改的基本字母组合。他们本身就是基础角色。它们修改其他字母的意义更多的是它们的语义使用问题。它们通常倾向于像变音符号一样发挥作用,表示字母发音的变化,或以其他方式区分字母的用途。通常,这种变音符号修饰适用于修饰字母之前的字符,但修饰字母有时可能会修饰后面的字符。有时,修饰字母可能只是单独代表它自己的声音。 ...

间距修饰字母:U+02B0–U+02FF

拼音用法。 此块中的大部分修饰字母是拼音修饰符,包括国际音标覆盖所需的字符。在许多情况下,修饰字母用于表示相邻字母的发音在某些方面有所不同——因此得名“修饰符”。它们还用于标记重音或音调,或者可能只是代表它们自己的声音。

  下面的例子应该有助于说明。我正在使用 100 级排序规则,它需要区分重音(即名称包含 _AS):

SELECT REPLACE(N'ʻ'    COLLATE Latin1_General_100_CI_AS, N'ʻ',   N'_'); -- Returns _
SELECT REPLACE(N'ʻa'   COLLATE Latin1_General_100_CI_AS, N'ʻ',   N'_'); -- Returns _a
SELECT REPLACE(N'ʻaa'  COLLATE Latin1_General_100_CI_AS, N'ʻ',   N'_'); -- Returns _aa
SELECT REPLACE(N'aʻaa' COLLATE Latin1_General_100_CI_AS, N'ʻ',   N'_'); -- Returns __aa

SELECT REPLACE(N'ʻaa'  COLLATE Latin1_General_100_CI_AS, N'ʻa',  N'_'); -- Returns ʻ__
SELECT REPLACE(N'aʻaa' COLLATE Latin1_General_100_CI_AS, N'ʻa',  N'_'); -- Returns aʻ__

SELECT REPLACE(N'aʻaa' COLLATE Latin1_General_100_CI_AS, N'aʻ',  N'_'); -- Returns _aa
SELECT REPLACE(N'aʻaa' COLLATE Latin1_General_100_CI_AS, N'aʻa', N'_'); -- Returns _a

SELECT REPLACE(N'aʻaa' COLLATE Latin1_General_100_CI_AS, N'a',   N'_'); -- Returns aʻ__
SELECT REPLACE(N'אʻaa' COLLATE Latin1_General_100_CI_AS, N'א',   N'_'); -- Returns אʻaa
SELECT REPLACE(N'ffʻaa' COLLATE Latin1_General_100_CI_AS, N'ff',   N'_'); -- Returns ffʻaa
SELECT REPLACE(N'ffaa'  COLLATE Latin1_General_100_CI_AS, N'ff',   N'_'); -- Returns _aa



SELECT CHARINDEX(N'a', N'aʻa' COLLATE Latin1_General_100_CI_AS); -- 3
SELECT CHARINDEX(N'a', N'aʻa' COLLATE Latin1_General_100_CI_AI); -- 1



SELECT 1 WHERE N'a' = N'aʻ' COLLATE Latin1_General_100_CI_AS; -- (0 rows returned)
SELECT 2 WHERE N'a' = N'aʻ' COLLATE Latin1_General_100_CI_AI; -- 2

如果您需要以忽略其预期语言行为的方式处理此类字符,那么是的,您必须使用二进制排序规则。在这种情况下,请使用最新级别的排序规则,并使用BIN2 而不是BIN(假设您使用的是 SQL Server 2005 或更高版本)。含义:

SQL Server 2000:Latin1_General_BIN SQL Server 2005:Latin1_General_BIN2 SQL Server 2008、2008 R2、2012、2014 和 2016:Latin1_General_100_BIN2 SQL Server 2017 和更新版本:Japanese_XJIS_140_BIN2

如果你好奇我为什么提出这个建议,请参阅:

Differences Between the Various Binary Collations (Cultures, Versions, and BIN vs BIN2)

并且,有关排序规则/Unicode/编码/等的更多信息,请访问:Collations Info

【讨论】:

非常彻底的答案。希望我能投票两次。 一个真正的多语言鹏洛客。我仍然对您在 2017+ 年使用 Japanese_XJIS_140_BIN2 的建议感到困惑。我在你的(优秀的)文章中没有看到对它的引用。 @TT。嗨,谢谢:)。推理在第二部分,标题为“不同版本”。基本上,大写/小写映射比版本 100 排序规则多 211 个。而且,只有日本排序规则有 140 版设置,否则我会选择 Latin1_General_140_* 如果存在这样的事情...... @HoneyBadger 谢谢!我很欣赏这种情绪:-)。【参考方案2】:

我无法提供详细的答案,但我可以提供满足您期望的解决方案。

这与排序规则有关,但我不确定为什么 Windows 排序规则会给出意外的结果。如果您使用二进制排序规则,您会得到预期的结果(请参阅所罗门的优秀答案,了解要使用哪个 BIN):

SELECT REPLACE(N'aʻ' COLLATE Latin1_General_BIN, N'a', N'_') 

返回

DECLARE @table TABLE ([Name] NVARCHAR(MAX))
INSERT INTO
    @table
VALUES
    (N'John'),
    (N'Jane'),
    (N'Hawaiʻi'),
    (N'Hawai''i'),
    (NCHAR(699))

SELECT
    *
FROM
    @table
WHERE
    [Name] like N'%ʻ%' COLLATE Latin1_General_BIN

返回:

Hawaiʻi
ʻ

您可以使用以下代码(改编自 @SolomonRutzky (source) 的代码)检查哪种排序规则符合您的期望。它评估所有排序规则的SELECT REPLACE(N'"ʻ', N'ʻ', N'_')) = '"_'

DECLARE @SQL NVARCHAR(MAX) = N'DECLARE @Counter INT = 1;';

SELECT @SQL += REPLACE(N'
  IF((SELECT REPLACE(N''"ʻ'' COLLATE Name, N''ʻ'', N''_'')) = ''"_'')
  BEGIN
    RAISERROR(N''%4d.  Name'', 10, 1, @Counter) WITH NOWAIT;
    SET @Counter += 1;
  END;
', N'Name', col.[name]) + NCHAR(13) + NCHAR(10)
FROM   sys.fn_helpcollations() col
ORDER BY col.[name]

--PRINT @SQL;
EXEC (@SQL);

【讨论】:

This should give a hint ,加上misuse of the character 用于其他语言的音译。只有二​​进制排序顺序将此字符识别为单独的字符 @PanagiotisKanavos ,关于“只有二进制排序顺序将此字符识别为单独的字符”:这是不正确的。也不是问题。 Unicode 允许某些字符根据上下文表现出不同的行为,因此单独测试它们不会显示它们的真实行为。详情请见my answer。另外,HoneyBadger,请参阅我的答案末尾关于使用哪种二进制排序规则的注释:-)。保重... @SolomonRutzky 我使用 your SQL 脚本发现只有二进制排序规则才能识别字符 - any 二进制排序规则,而与语言无关。 没有返回非二进制排序规则,因此根据该脚本,只有二进制排序规则可以工作 @PanagiotisKanavos 请尊重他人,不要吝啬(我没有对你无礼),并在争论我所说的之前阅读我的回答。它解释了这个角色发生了什么以及为什么 HoneyBadger 对我的查询的改编只解决了这种情况的一个方面。我假设您的意思是此答案中的查询,因为我的答案中的查询显示了我在说什么。而且,如果说“不考虑语言”意味着您认为文化可能适用于 Unicode 的二进制比较,那么请阅读我的答案末尾链接的帖子以了解为什么不是。 @SolomonRutzky 我并不吝啬,只是简单地回答了我的发现。识别字符的排序规则是all BINx 排序规则。顺便说一句,不错的脚本,它可以在类似的问题上节省 很多 时间。至于实际问题,第一个链接显示that the character is considered significant in Hawaiian, although it wasn't in the past。 Wikipedia article 与您的回答并不矛盾,它解释了导致此问题和其他类似问题的混淆

以上是关于特殊字符(夏威夷 'Okina)导致奇怪的字符串行为的主要内容,如果未能解决你的问题,请参考以下文章

为啥html,xml的特殊符号转义不用斜杠,而要用& quot ;这样的奇怪形式?

腻子中串行连接的字符集编码

使用 vigenere 密码加密时出现奇怪的错误

Oracle更新表字段时内容中含有特殊字符&的解决方法

Oracle更新表字段时内容中含有特殊字符&的解决方法

编译时添加了奇怪的字符,导致错误