忽略重音符号比较字符串
了解如何使用 JavaScript 规范化和 Intl.Collator 在比较字符串时忽略变音符号
简介
在开发支持多语言的应用时,经常需要比较包含重音符号的字符串。例如,用户搜索 "cafe" 时,应该能找到 "café" 的结果。检查用户名 "Jose" 时,也应匹配 "José"。标准的字符串比较会将它们视为不同的字符串,但你的应用逻辑需要将它们视为相同。
JavaScript 提供了两种方法来解决这个问题。你可以对字符串进行规范化并去除重音符号,或者使用内置的排序 API,根据特定的敏感度规则比较字符串。
什么是重音符号
重音符号是加在字母上方、下方或穿过字母的符号,用于改变发音或含义。这些符号称为变音符号。常见示例包括 "é" 上的尖音符、"ñ" 上的波浪号和 "ü" 上的分音符。
在 Unicode 中,这些字符可以有两种表示方式:一种是用单一代码点表示完整字符,另一种是用多个代码点将基础字母与独立的重音符号组合。例如,字母 "é" 可以用 U+00E9 表示,也可以用 "e" (U+0065) 加上组合尖音符 (U+0301) 表示。
何时在比较时忽略重音符号
搜索功能是最常见的需要忽略重音符号比较的场景。用户在输入查询时通常不会输入重音符号,但期望能找到包含重音字符的内容。例如,搜索 "Muller" 应该能找到 "Müller"。
在用户输入校验时也需要这种能力,比如检查用户名、电子邮件地址或其他标识符是否已存在。你希望防止 "maria" 和 "maría" 注册为两个不同的账户。
在进行不区分大小写的比较时,通常还需要同时忽略重音符号。当检查两个字符串是否匹配而不考虑大小写时,通常也希望忽略重音差异。
使用规范化去除重音符号
第一种方法是将字符串转换为规范化形式,将基础字母和重音符号分离,然后移除重音符号。
Unicode 规范化会将字符串转换为标准形式。NFD(规范分解)形式会将组合字符分解为基础字母和组合符号。例如,字符串 "café" 会变成 "cafe",后面跟一个组合的重音符号字符。
规范化后,可以使用正则表达式移除组合符号。Unicode 范围 U+0300 到 U+036F 包含了组合附加符号。
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const text1 = 'café';
const text2 = 'cafe';
const normalized1 = removeAccents(text1);
const normalized2 = removeAccents(text2);
console.log(normalized1 === normalized2); // true
console.log(normalized1); // "cafe"
这种方法可以得到没有重音符号的字符串,便于使用标准等值运算符进行比较。
你可以将此方法与小写转换结合,实现不区分大小写和重音的比较。
function normalizeForComparison(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
const search = 'muller';
const name = 'Müller';
console.log(normalizeForComparison(search) === normalizeForComparison(name)); // true
当你需要存储或索引规范化后的字符串以便高效搜索时,这种方法非常适用。
使用 Intl.Collator 比较字符串
第二种方法是使用 Intl.Collator API,它提供了可配置敏感度级别的本地化字符串比较功能。
Intl.Collator 对象会根据语言特定规则比较字符串。sensitivity 选项控制比较时哪些差异会被考虑。
"base" 敏感度级别会同时忽略重音符号和大小写差异。仅在重音或大小写不同的字符串会被视为相等。
const collator = new Intl.Collator('en', { sensitivity: 'base' });
console.log(collator.compare('café', 'cafe')); // 0 (equal)
console.log(collator.compare('Café', 'cafe')); // 0 (equal)
console.log(collator.compare('café', 'caff')); // -1 (first comes before second)
compare 方法在字符串相等时返回 0,第一个字符串排在第二个前面时返回负数,排在后面时返回正数。
你可以使用此方法进行等值判断或对数组进行排序。
const collator = new Intl.Collator('en', { sensitivity: 'base' });
function areEqualIgnoringAccents(str1, str2) {
return collator.compare(str1, str2) === 0;
}
console.log(areEqualIgnoringAccents('José', 'Jose')); // true
console.log(areEqualIgnoringAccents('naïve', 'naive')); // true
对于排序,你可以将 compare 方法直接传递给 Array.sort。
const names = ['Müller', 'Martinez', 'Muller', 'Márquez'];
const collator = new Intl.Collator('en', { sensitivity: 'base' });
names.sort(collator.compare);
console.log(names); // Groups variants together
Intl.Collator API 针对不同场景还提供了其他敏感度级别。
“accent” 级别会忽略大小写,但区分重音符号。例如,“Café” 等于 “café”,但不等于 “cafe”。
const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (equal)
console.log(accentCollator.compare('café', 'cafe')); // 1 (not equal)
“case” 级别会忽略重音符号,但区分大小写。例如,“café” 等于 “cafe”,但不等于 “Café”。
const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (equal)
console.log(caseCollator.compare('café', 'Café')); // -1 (not equal)
“variant” 级别会区分所有差异。这是默认行为。
const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (not equal)
选择归一化与排序规则
这两种方法都能实现对重音符号不敏感的比较,但各自有不同的特点。
归一化方法会生成不带重音符号的新字符串。当你需要存储或索引归一化版本时,建议使用此方法。搜索引擎和数据库通常会存储归一化文本以便高效检索。
Intl.Collator 方法在不修改字符串的情况下进行比较。当你需要直接比较字符串(如查重或排序列表)时,建议使用此方法。排序器会遵循特定语言的排序规则,而简单的字符串比较无法做到这一点。
性能考量因场景而异。创建排序器对象后复用,适合多次比较。归一化字符串适合一次归一化后多次比较。
归一化方法会永久移除重音信息。排序规则方法则在比较时保留原始字符串,并根据指定规则进行比较。
使用重音不敏感搜索过滤数组
常见用例是根据用户输入过滤数组项,忽略重音符号差异。
const products = [
{ name: 'Café Latte', price: 4.50 },
{ name: 'Crème Brûlée', price: 6.00 },
{ name: 'Croissant', price: 3.00 },
{ name: 'Café Mocha', price: 5.00 }
];
function searchProducts(query) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return products.filter(product => {
return collator.compare(product.name.slice(0, query.length), query) === 0;
});
}
console.log(searchProducts('cafe'));
// Returns both Café Latte and Café Mocha
对于子串匹配,归一化方法效果更佳。
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function searchProducts(query) {
const normalizedQuery = removeAccents(query.toLowerCase());
return products.filter(product => {
const normalizedName = removeAccents(product.name.toLowerCase());
return normalizedName.includes(normalizedQuery);
});
}
console.log(searchProducts('creme'));
// Returns Crème Brûlée
该方法会检查归一化后的产品名称是否包含归一化后的搜索查询作为子串。
处理文本输入匹配
在将用户输入与现有数据进行校验时,需要进行不区分重音符号的比较,以避免混淆和重复。
const existingUsernames = ['José', 'María', 'François'];
function isUsernameTaken(username) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return existingUsernames.some(existing =>
collator.compare(existing, username) === 0
);
}
console.log(isUsernameTaken('jose')); // true
console.log(isUsernameTaken('Maria')); // true
console.log(isUsernameTaken('francois')); // true
console.log(isUsernameTaken('pierre')); // false
这样可以防止用户创建仅在重音符号或大小写上与现有账户不同的账户名。
浏览器和环境支持
String.prototype.normalize 方法在所有现代浏览器和 Node.js 环境中都受支持。Internet Explorer 不支持该方法。
Intl.Collator API 在所有现代浏览器和 Node.js 版本中都受支持。Internet Explorer 11 仅部分支持。
这两种方法在当前的 JavaScript 环境下都能可靠运行。如果需要支持旧版浏览器,则需要使用 polyfill 或其他实现方式。
去除重音符号的局限性
某些语言使用变音符号来表示不同的字母,而不仅仅是重音变化。例如,土耳其语中的 "i" 和 "ı" 是不同的字母,德语中的 "ö" 也是独立的元音,而不是带重音的 "o"。
在这些情况下,去除重音符号会改变原意。请根据实际用例和目标语言,判断是否适合使用不区分重音符号的比较。
排序(collation)方法在这些场景下表现更好,因为它遵循特定语言的规则。在 Intl.Collator 构造函数中指定正确的 locale,可以确保比较方式符合文化习惯。
const turkishCollator = new Intl.Collator('tr', { sensitivity: 'base' });
const germanCollator = new Intl.Collator('de', { sensitivity: 'base' });
选择比较策略时,请始终考虑应用支持的语言。