Unicode 字符类别助手






4.43/5 (8投票s)
2005年9月10日
9分钟阅读

47197

742
用于根据类别和块生成和测试 Unicode 范围的工具。
引言
这个工具的主要用途是生成用于正则表达式字符类的字符块列表。比如,您想匹配任何实现 "\w" 匹配类似 [-a-zA-Z_0-9] 的所有语言的字母。这在两个方面都存在问题,因为它只匹配英文字母,并且它匹配非字母字符(即使在支持 Unicode 的引擎上这仍然是个问题)。或者您想匹配字母,但只在某些语言中匹配,例如,只在拉丁语中,而不是俄语或阿拉伯语。这个小工具将告诉您需要使用什么,并通过告诉您哪些输入字符匹配,哪些不匹配,帮助您快速测试结果。
这也可以作为一个参考/学习工具,用于快速查找一组字符属于哪个类别,或者一个区域/类别(组合)包含什么,因为它能生成块列表中的字符,或者告诉您输入字符的 Unicode 位置和类别。
背景
我最初的问题很简单:JavaScript 的 RegExp
对象不支持 Unicode 类别(例如 `/[p{Ll}]/`)。“没问题,我可以自己定义它们”,我想。于是我开始研究各种官方 Unicode 文档并进行搜索……肯定有人做过这个!我偶然发现了 这篇博文,里面有一个 C# 代码片段可以做到这一点。这个脚本不是一个糟糕的开始(感谢作者),但除了我很快发现了一个 bug,并且需要确保没有更多 bug,我还想了解所有类别的整体情况,而不是查看几十个 PDF……所以,我写了这个小程序。
使用 GUI
需要注意的是,默认情况下,每次更改都会更新块列表。您可以通过取消选中“自动更新”来禁用此功能。同样,配置文本框会在每次更改时尝试验证其数据。如果无效,它将变为红色,并使用最后一个有效值或默认值。一旦有效,它将恢复为黑色(或任何颜色)并生效。请注意,我想快速编写这个程序,并没有做太多花哨的事情……例如,如果字体不好,它将恢复为默认字体。如果一个范围在边界处无效,将不会使用任何范围。基本上,如果一个文本框变成红色;在使用结果之前,请将其变回黑色,否则结果可能会是错误的。
第一部分是关于一些文本显示选项:更改字体和脚本(也称为代码页)可能会让您显示更多不常见的字符,前提是您已正确配置了它们。第二行是影响块列表生成方式的选项,大多数时候应将其保留原样,也许除了在前缀中添加额外的“\”以转义语言中的斜杠含义,以便在字符串字面量中使用,或者将格式更改为“x2”,以便字符可以用 2 位十六进制数字指定。请注意,后者是唯一一个在无效时不会变红的文本框……它应该使用“x4”,但没有进行检查,所以更改它时要小心。
接下来是类别……使用两个字母的代码以节省空间,但工具提示会告诉您每个类别的完整名称。您想做的第一件事是选择您感兴趣的类别。如果没有选中任何类别,**获取代码** 将起作用。所有复选框现在都有三种状态:不确定(“灰色”)表示类别被“忽略”……具体解释如下。
接下来是“边界”部分……一个 Unicode 范围列表(仅 4 个十六进制字符),其中低位和高位由连字符(“-”)分隔,并且不同的范围可选地由逗号(“,”)分隔。
- **限制** 意味着即使字符匹配已选类别,也只包含这些边界内的字符。
- **排除** 意味着即使字符匹配其他条件,位于这些边界内的字符也将不被包含。
- **忽略** 意味着在其他条件都相同的情况下,生成器会优先选择一个较小的块而不包含被忽略的字符,但如果只有被忽略的字符分隔了匹配的字符,则不会打破这个块。将类别设置为不确定状态与将该类别的块放在 **忽略** 中效果相同。忽略未分配的字符(Cn, OtherNotAssigned)可以在某些设置下大大简化块列表(Ll、Lu、Lt 组合从 735 个字符减少到 389 个字符),这也是我添加这个选项的原因……当然,它不能保证您的块能正确匹配未来的 Unicode 版本,但忽略它们也无法保证(这保证了新分配的字符不会被匹配)。
还有一个组合框,您可以在其中输入 Unicode 范围或选择预定义的范围(取自官方块“本地”列表,请参阅下面的 **链接** 部分),然后单击相应的按钮将其添加到边界。
**生成块** 在 **自动更新** 被选中时,与更改任何类别或边界的作用相同:根据各种选项更新主文本框中的块列表。
**生成字符** 将使用选定的选项(类别和边界)生成一个新的块列表,然后将主文本框设置为所有块匹配的字符列表。**忽略被忽略的字符** 复选框决定了是否打印块匹配但被设置为忽略的字符。请注意,如果包含 \u0000(空字符),则会跳过它,因为它无法打印,并且似乎会阻止其余字符的显示。
**获取代码** 只会使用文本框的内容。它会将文本框中的每个字符转换为一行,包含字符、其十六进制和十进制的 Unicode 值以及其类别名称。但在执行此操作之前,它会用其等效字符替换所有十进制实体(例如,“$”,或“&36;”)和 Unicode 转义(默认前缀“\u” + 2 到 4 位十六进制数字,例如“\u0024”或“\u24”)(对于前面所有示例,例如“$”)。这对于同时处理块列表和字符列表很有用,但主要用作快速参考,用于检查字符的 Unicode 或反之,或者如果您不确定如何匹配您感兴趣的字符……
**匹配字符** 将根据选项生成一个新的块列表,并将其与文本框内容进行匹配。运行此项可以验证您是否选择了正确的选项,并且块包含您想要的一切,不多也不少。
**测试** 完全不查看文本框。它会从块生成一个 Regex,并用它匹配每个字符,以确保所有应该匹配的都匹配了,而且不多也不少。它帮助我找到了块生成逻辑中的一个 bug。在开始使用结果之前,请单击它,这会让您感觉更安心。
**恢复文本** 将简单地将文本框恢复到上次生成的值(计算恢复文本)。如果自动更新阻碍了您,或者想快速在视图之间切换,请使用此功能。用户输入不会影响此功能,请使用 Ctrl-z 来撤销手动更改……
使用块
假设您想确保某个内容是“单词”。这是一个比看起来更难定义的概念,但我们将举一个简单的例子:在 JavaScript 中,“Ll”、“Lu”和“Lt”的组合(为简单起见,忽略“Cn”)。打开 CharCatHelper
并选择正确的选项。您应该会得到以下列表
\u0041-\u005a\u0061-\u007a\u00aa\u00b5\u00ba\
u00c0-\u00d6\u00d8-\u00f6\u00f8-\u01ba\u01bc-\u01bf
\u01c4-\u02ad\u0386\u0388-\u0481\u048c-\u0556\
u0561-\u0587\u10a0-\u10c5\u1e00-\u1fbc\u1fbe\u1fc2-\u1fcc
\u1fd0-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ffc\u207f\u2102\u2107\
u210a-\u2113\u2115\u2119-\u211d\u2124\u2126
\u2128\u212a-\u212d\u212f-\u2131\u2133\u2134\u2139\
ufb00-\ufb17\uff21-\uff3a\uff41-\uff5a
您可以直接在字面量正则表达式中使用此列表,但为了重用和保持复杂模式的可读性,我们将把该组定义为字符串常量,并在 RegExp
中使用标识符。在 **代码前缀** 中添加额外的“\”来转义 JS 引擎的斜杠含义。
const UNICODE_LETTERS =
"\\u0041-\\u005a\\u0061-\\u007a\\u00aa\
\\u00b5\\u00ba\\u00c0-\\u00d6\
\\u00d8-\\u00f6\\u00f8-\\u01ba\\u01bc-\\u01bf\
\\u01c4-\\u02ad\\u0386\\u0388-\\u0481\\u048c-\\u0556\
\\u0561-\\u0587\\u10a0-\\u10c5\\u1e00-\\u1fbc\\u1fbe\
\\u1fc2-\\u1fcc\\u1fd0-\\u1fdb\\u1fe0-\\u1fec\
\\u1ff2-\\u1ffc\\u207f\\u2102\\u2107\\u210a-\\u2113\
\\u2115\\u2119-\\u211d\\u2124\\u2126\\u2128\
\\u212a-\\u212d\\u212f-\\u2131\\u2133\\u2134\\u2139\
\\ufb00-\\ufb17\\uff21-\\uff3a\\uff41-\\uff5a";
var isWord = new RegExp("^[" + UNICODE_LETTERS + "]+$");
if (isWord.test("Официальный"))
alert("Официальный is a word !");
*行末的额外“\”只是为了转义换行符,以防止页面水平滚动。*
使用代码
如果您想在此 GUI 之外使用该代码,我已经将代码分成了区域,并且块列表函数位于最后一个区域,“Block Generator”:getBlock()
、getBlocks()
和 isInBoundaries()
。一切都很直接,看起来像这样
// int[] boundaries: even numbers are block
// starts, uneven block ends
// returns true if charNum is in any
// block, false otherwise
bool isInBoundaries(int[] boundaries, int charNum)
{
if (boundaries == null)
return false;
for (int i=0; i < boundaries.Length; i+=2)
{
if (charNum >= boundaries[i] &&
harNum <= boundaries[i+1])
{
return true;
}
}
return false;
}
// Formats and returns an escaped
// character or character range
string getBlock(int blockStart, int blockEnd,
string prefix, string separator,
string format)
{
if (blockStart == blockEnd)
{ // Single character match
return prefix + blockStart.ToString(format);
}
else if (blockStart + 1 == blockEnd)
{ // Two consecutive chars only;
// don't add separator
return prefix + blockStart.ToString(format) +
prefix + blockEnd.ToString(format);
}
else
{ // It's a character block
return prefix + blockStart.ToString(format) +
separator + prefix +
blockEnd.ToString(format);
}
}
// Returns a list of blocks matching
// specified categories and boundaries
// Modify the two main conditionals
// (checkState(cat) X= CheckState.X)
// to use this outside the GUI...
string getBlocks(string prefix, string separator,
string format, int[] boundaries,
int[] excludes, int[] ignores)
{
string blockList = "";
int blockStart = -1;
int blockEnd = -1;
int lastBlockEnd = -1;
for (int charNum = 0;
charNum <= char.MaxValue; charNum++)
{
char c = Convert.ToChar(charNum);
UnicodeCategory cat =
char.GetUnicodeCategory(c);
// Replace this with whatever
// way you select categories
if (checkState(cat) == CheckState.Checked
&& (boundaries == null ||
isInBoundaries(boundaries, charNum))
&& (excludes == null ||
!isInBoundaries(excludes, charNum)))
{
if (blockStart == -1)
{ // New block, set low boundary
blockStart = charNum;
}
blockEnd = charNum;
}
else if (lastBlockEnd < blockEnd // Block isn't
// already saved
&& (checkState(cat) !=
CheckState.Indeterminate // And char isn't
// in an ignored
// category
&& (ignores == null ||
!isInBoundaries(ignores, charNum)))) // Or block
{
// No match so our block is done, save it
blockList += getBlock(blockStart, blockEnd,
prefix, separator, format);
// Remember last saved position
// and reset counters
lastBlockEnd = blockEnd;
blockStart = blockEnd = -1;
}
}
// If FFFF is included or ignored...
if (lastBlockEnd < blockEnd)
{
blockList += getBlock(blockStart, blockEnd,
prefix, separator, format);
}
return blockList;
}
链接
本地列表取自 UNIDATA/Blocks.txt(unicode.org),版本 4.1.0(截至 UCCH 1.1)。
历史
- 版本 1.1.1
- 优化了 Get Chars,速度提高了 3-4 倍,并且可以在可接受的时间内(我的电脑上不到 2 秒)生成“Lo”。
Get Codes 速度更快,如果您愿意,可以随意构建所有字符的表。
- 修复了十进制实体匹配:忘了“#”,哎呀……将其设为可选。
- 为 Get Codes 表添加了标题列和十进制值行。
- 添加了垂直滚动条。
- 优化了 Get Chars,速度提高了 3-4 倍,并且可以在可接受的时间内(我的电脑上不到 2 秒)生成“Lo”。
- 版本 1.1
- 所有类别复选框现在都有三种状态:不确定表示类别被忽略。
- 添加了忽略块。
- 添加了一个组合框,其中包含 Unicode 4.1.0 的块名称,以及将它们添加到任何边界的按钮。
- 添加了“恢复”按钮,该按钮会将主文本框恢复到上一个(非用户)值。
- 字符打印不再尝试打印 \u0000,因为它会导致整个程序失败……
- 边界在变得无效时现在会重置为 null。
- Get Codes 将首先将十进制实体和 Unicode 转义转换为字符。
- 将代码分成区域并进行了一些注释。
- 一些其他小的 UI 更改(例如,结果文本框现在可以通过单击来“重置”)。
我在这上面花的时间比预期的要长,这很可能是最后一个版本,尽管还有很多改进空间……但如果您认为某些功能确实不起作用,请发布您的问题,我会尝试修复。希望有人觉得这有用。