Unicode NFD 规范与俄日韩等文字命名文件的字素分解现象
版权声明:原创文章,未经授权,请勿转载
当 Windows
接收来自 macOS
、iOS
以俄语、日语、韩语、瑞典语等字符命名的文件时,可能会出现一种名为 "字素分解" 的现象:
看起来就像是: 韩文被拆分成一个个符号或“字母”,并且将这个被拆解的字符串粘贴到 Chrome、Edge 浏览器以后还可以得到正确的显示!
出现这个问题,主要是 macOS
、iOS
与 Windows
文件系统所采用的 Unicode
归一化(Normalization
)规范不同所导致。
关于
Normalization
的翻译大多数文档中为 正规化,但私以为归一化翻译更优
简单的说,就是在 Unicode
中会存在 同一个字符(视觉上的) 可能具有不同的代码点的现象,这就导致了两个看起来一模一样的文件路径,按照字符串逐字节的方式对比,会出现不匹配的情况。
例如:
- 字符 Å,可以被编码为: U+00C5(标准名:LATIN CAPITAL LETTER A WITH RING ABOVE),也可以被编码为: U+212B(标准名:ANGSTROM SIGN)。
- 日文 パ 可以被编码为: U+30d1(
パ
),也可以被编码为 U+30CF,U+309A (パ
)- 韩文 신 可以被编码为: U+C2E0(
신
),也可以被编码为 U+C2E0,U+1175,U+11AB (신
)
为了解决这个问题,Unicode
提出了 等价性、归一化 概念,即 Unicode
部分代码点之间,实际上是完全等价的。而对不同代码点表示的等价字符,能够在匹配时得到一致的结果,就需要通过归一化,将代码点转换统一的标准形式 [1] 。
正规形式有两种,一种叫完全组合(Fully Composed),即尽可能将多个码点替换为单个码点;另一种叫完全分解(Fully Decomposed),即尽可能将单个码点分解为多个码点。
其中最常见的两个就是:
- NFC(Normalization Form Canonical Composition)
- NFC 规范,将字符组合为标准的组合形式。
- NFC 规范,也是 大多数平台的文件系统采用的归一化规范,如:
Windows
和大多数Linux
发行版。 - 示例: 신(
신
)
- NFD(Normalization Form Canonical Decomposition)
而文章开头描述的问题,是因为 macOS
、iOS
的 HFS+
文件系统采用了 NFD
规范来存储的文件名,与 Windows 采用的规范 NFC
不同,因此导致的问题。
解决方案也挺简单,只需要将文件名的字符串转换为 NFC
规范即可,各位看官也可以自行在 Unicode Normalization Test 工具上测试。
NFC & NFD 规范转换
Python 示例
import unicodedata as ud
a = b'\xc2\xe0'.decode('utf-16-be')
b = b'\x11\x09\x11\x75\x11\xab'.decode('utf-16-be')
print(f'a:{a}, b:{b}, a==b:{a==b}') # 输出: a:신, b:신, a==b:False
print(f'a:{a}, b:{b}, a==b:{a==ud.normalize("NFC", b)}') # 输出: a:신, b:신, a==b:True
print(f'a:{a}, b:{b}, a==b:{b==ud.normalize("NFD", b)}') # 输出: a:신, b:신, a==b:True
Win32 C++ 示例
#include <format>
#include <iostream>
#include "windows.h"
inline std::wstring NormalizeWString(const std::wstring& string, std::string form = "NFC")
{
// Convert the form string to uppercase
for (char& c : form)
c = std::toupper(c);
// Determine the normalization form
NORM_FORM normForm = (form == "NFD") ? NormalizationD : NormalizationC;
// Get the required buffer size for the normalized string
int bufferSize = NormalizeString(normForm, string.c_str(), -1, nullptr, 0);
// Allocate buffer
std::wstring normalizedString;
normalizedString.resize(bufferSize);
// Normalize the string
NormalizeString(normForm, string.c_str(), -1, &normalizedString[0], bufferSize);
// Remove the null-terminator
return normalizedString.c_str();
}
int main(int argc, char* argv[])
{
// 两个字符串在Web浏览器中看起来可能没有区别,但实际上是完全不同的字符串,
// 这点可以复制到记事本中查看。
std::wstring nfc = L"신민아 조정석-나의 사랑 나의 신부 - 커플송";
std::wstring nfd = L"신민아 조정석-나의 사랑 나의 신부 - 커플송";
bool result1 = NormalizeWString(nfd, "NFC") == nfc;
bool result2 = NormalizeWString(nfc, "NFD") == nfd;
std::cout <<
std::format("nfc-len: {}, nfd-len: {}, result1: {}, result2: {}",
nfc.length(), nfd.length(), result1, result2)
<< std::endl;
return 0;
}
// 需要c++20, 并在编译选项中加入 "/utf-8" 否则会出现: 字符不能在当前代码页表示的错误
// 输出如下:
// nfc-len: 26, nfd-len: 51, result1: true, result2: true
Comments ()