# string.wordsSegmenter

基于 Windows 自带的 `Windows.Data.Text.WordsSegmenter`，对文本执行本地、离线分词。C# 桥接层通过 `dotNet.uwpCompiler` 编译，aardio 接口只返回普通数组和表，不暴露 .NET / WinRT 代理对象。

## 运行与构建

- 运行：Windows 10 或更新版本、系统自带的 .NET Framework 4.x。
- 普通使用不需要 Windows SDK、不调用编译器，也不下载词典。不依赖 `mmseg` 或其他第三方分词库。
- DLL 已预编译，并通过 `$` 嵌入库代码；发布 EXE 时不必另行复制此 DLL。
- 仅修改 C# 源码并重建 DLL 时需要 Windows SDK，见下方“重建”。

## 快速开始

```aardio
import string.wordsSegmenter;

// 省略语言或传 null，使用 zh-CN；也可指定 en-US、ja-JP、und 等。
var segmenter,err = string.wordsSegmenter("zh-Hans-CN");
if(!segmenter){
    print(err);
    return;
}

var words,err = segmenter.getWords("我爱北京和上海，Hello world! 👋");
if(words === null){
    print(err);
    return;
}

print(words);
print("实际分词规则：", segmenter.resolvedLanguage);
```

在测试电脑（Windows 10 19045）上，以上文本返回：

```text
["我", "爱", "北京", "和", "上海", "Hello", "world", "👋"]
```

Windows 版本及可用语言规则可能影响断词结果。此库保留 Windows 返回的词序、重复项、大小写及符号处理方式，不另做停用词过滤、词性标注或自定义词典处理。词列表不包含所有分隔符，不应通过拼接词列表还原全文。

## 创建分词器与语言

`string.wordsSegmenter(language)` 与 `string.wordsSegmenter.create(language)` 等价。成功返回分词器，失败返回 `null,err`。

| 属性 | 含义 |
| --- | --- |
| `language` | 只读，创建时请求的语言标记 |
| `resolvedLanguage` | 只读，Windows 实际使用的分词规则语言 |

- 默认 `zh-CN` 是本库的默认值，**不是自动检测文本语言**。
- `und` 表示语言中立规则，对中文等无空格语言通常不如专用规则。
- 合法但无对应专用规则的标记可能回退为 `und`，创建成功不代表该语言有专用分词器。
- 空字符串、非字符串或非法语言标记返回错误；切换语言请创建新的分词器。
- 同一分词器可重复处理多段文本，不必每次重新创建。

## API

| 方法 | 返回值 |
| --- | --- |
| `getWords(text)` | UTF-8 词文本的纯数组 |
| `getTokens(text)` | 包含位置、长度和替代形式的纯表数组 |
| `getTokenAt(text,startIndex)` | 包含指定位置的词，或该位置之后的第一个词 |

三个方法的 `text` 参数均接受字符串，`null` 按空文本处理；其他类型返回 `null,err`。`getWords` / `getTokens` 对空文本返回 `[]`。方法调用失败返回 `null,err`，而 `getTokenAt` 在合法位置没有后续词时返回 `null` 且没有错误。

创建分词器约需 16 毫秒，请复用。在测试电脑（Windows 11）上，19 个词的中文句子调用 `getWords` 或 `getTokens` 各约 0.5 毫秒，500 个词的 `getTokens` 约 5 毫秒。

### 词信息

`getTokens` 的每项和 `getTokenAt` 的返回对象均包含：

| 字段 | 类型与含义 |
| --- | --- |
| `text` | Windows 返回的词文本，UTF-8 字符串，可能已归一化 |
| `startPosition` | 在完整原文中的 **0 起始 UTF-16 代码单元**位置 |
| `length` | 原文片段的 **UTF-16 代码单元**长度 |
| `alternateForms` | 替代形式的纯数组，没有时为 `[]` |

每个替代形式包含 `alternateText`、`startPosition`、`length`、`normalizationFormat`。位置仍然相对完整原文，而不是相对当前词。`normalizationFormat` 为 Windows 返回的枚举名称字符串：`NotNormalized`、`Number`、`Currency`、`Date` 或 `Time`。

替代形式可能覆盖比当前词更多、更少或相同的原文，其文本可能是数字、日期等的归一化值，不保证等于原文子串。本库不修正 Windows 的识别或归一化结果。

`text` 本身也可能被 Windows 归一化。例如测试电脑的中文规则会把 `e` + 组合重音返回为 `é`，但 `length` 仍为原文的 2 个 UTF-16 单位。因此不能用 `text` 的长度替代 `length`；需要原样文本时请按 `startPosition` 和 `length` 从输入中提取。

```aardio
import string.wordsSegmenter;
var segmenter,err = string.wordsSegmenter("en-US");
if(!segmenter){ print(err); return; }

var tokens,err = segmenter.getTokens("Hello, world!");
if(tokens === null){ print(err); return; }
for i,token in tokens {
    print(token.text, token.startPosition, token.length, token.alternateForms);
}
```

### 索引规则与查词

**索引不是 aardio 的 UTF-8 字节索引，也不是 Unicode 码点索引。** 英文字符及 BMP 汉字通常各占 1 个 UTF-16 单位，`👋`、`𠀀` 等非 BMP 字符占 2 个单位；组合字符或复合 emoji 可能占更多单位。

`getTokenAt` 的 `startIndex` 必须是非负整数并小于原文的 UTF-16 长度。等于长度、空文本的索引 0、负数、小数及错误类型都返回 `null,err`。

```aardio
import string.wordsSegmenter;
var segmenter,err = string.wordsSegmenter("en-US");
if(!segmenter){ print(err); return; }

// "hello, world! " 的索引 5 是逗号：查到后面的 world。
var token,err = segmenter.getTokenAt("hello, world! ",5);
if(token) print(token.text,token.startPosition,token.length); // world,7,5
else if(err) print(err);

// 索引 12 合法，但之后无词：null，无错误。
var missing,err = segmenter.getTokenAt("hello, world! ",12);
print(missing,err);
```

如需根据位置提取原文，先转 UTF-16，再按字节范围切片：

```aardio
import string.wordsSegmenter;
var segmenter,err = string.wordsSegmenter("en-US");
if(!segmenter){ print(err); return; }

var text = "👋 中文 abc";
var tokens,err = segmenter.getTokens(text);
if(tokens === null){ print(err); return; }
var wide = string.fromto(text,65001,1200);
for i,token in tokens {
    var firstByte = token.startPosition * 2 + 1;
    var lastByte = (token.startPosition + token.length) * 2;
    var part = raw.slice(wide,firstByte,lastByte);
    print(string.fromto(part,1200,65001));
}
```

提取、查词和定位时必须使用分词时的同一份原文；不要预先改变换行、大小写或空白后再套用旧位置。

## 在线程中使用

分词器含有闭包和 .NET 对象，不应跨 aardio 线程传递。在每个工作线程中导入库并创建自己的分词器，只跨线程传递字符串和返回的普通数据。

```aardio
var words,err = thread.invokeAndWait(function(text){
    import string.wordsSegmenter;
    var segmenter,err = string.wordsSegmenter("zh-CN");
    if(!segmenter) return null,err;
    return segmenter.getWords(text);
},"中文分词示例");
print(words,err);
```

## 重建与文件布局

库根目录：`~/lib/string/wordsSegmenter/`

- `_.aardio`：公开接口及智能提示。
- `.res/wordsSegmenter.dll`：预编译的 AnyCPU 桥接程序集。
- `.res/WordsSegmenter.cs`：C# 5.0 源码。
- `.res/build.aardio`：使用 `dotNet.uwpCompiler` 重建 DLL。
- `.res/test.aardio`：无界面自动测试，返回 `util.testRunner` 报告。

在安装 Windows SDK 后运行 `.res/build.aardio` 即可重建。该文件也支持命令行 `-winmd` 指定 `Windows.winmd` 的绝对路径。输出位置固定为上述库资源目录，不受当前工程目录影响。重建后在新的运行会话中重新导入库，避免沿用已加载的程序集。

本库用 `$` 内嵌的字节加载 DLL，不锁定文件；但如果当前进程的某个线程按路径加载过旧 DLL，旧文件会被锁定到进程退出。此时 `build.aardio` 会先把旧文件改名为 `wordsSegmenter.dll.*.old`（被锁定的文件仍可改名），再生成新文件；这些旧文件在以后重建时自动删除。

修改 C# 接口时，返回给 aardio 的数据只能是一维类型化数组（`string[]`、`int[]`）；需要返回多个数组时用 `object[]` 包装，aardio 端读取其 `Value` 属性一次转换。不要每项返回一个 `object[]`：嵌套数组在 aardio 中是 .NET 代理，逐元素跨界调用，500 个词约需 0.86 秒；现在的扁平布局约 5 毫秒。布局说明见 `WordsSegmenter.cs` 中 `GetTokenData` 的注释。

重建只生成本库的 `wordsSegmenter.dll`，不依赖也不修改 WinRT 范例目录中的 `aardio.WindowsRuntime.dll`。

完整控制台示例位于本目录的 `example.aardio`。

## 原生 API 参考

微软文档可按下列 API 全名检索：

- `Windows.Data.Text.WordsSegmenter`
- `Windows.Data.Text.WordsSegmenter.GetTokenAt`
- `Windows.Data.Text.WordSegment`
- `Windows.Data.Text.TextSegment`
- `Windows.Data.Text.AlternateWordForm`
