Compromise 实战配方指南:从可变性陷阱到 PII 脱敏的 17 个开箱即用场景

原创2026-09-23 00:00:101,830 阅读
文章标签:NLP人工智能

Compromise 实战配方指南:从可变性陷阱到 PII 脱敏的 17 个开箱即用场景

本指南以 docs/recipes.md 为骨架,系统梳理 Compromise(modest natural-language processing)在日常开发中最常用的 17 个实战配方:从必须牢记的「可变性规则」、命名实体提取、动词时态改写、查找替换、PII 脱敏、名词单复数、数字处理,到匹配语法、句子过滤、否定、缩略词展开、文本归一化、结构化 JSON 输出、自定义词表、插件编写与 match 调试。每个片段都可在当前仓库中真实运行,并配有源码级佐证;读完你将能够直接复制这些代码解决实际 NLP 任务,并理解其背后的可变性(mutability)与选择(selection)机制。

所有示例默认你已经完成导入:

import nlp from 'compromise'

建议先阅读 concepts.md 了解核心概念,尤其是下文第一条「可变性规则」——它是大多数踩坑的根源。

可变性规则(务必先读)

Compromise 的变换方法(如 toPastTense()、replace()、toPlural())会就地修改底层文档(in place),而方法返回的 View 只是「被选中的子集」,不是整篇文档。因此正确做法是:从最初的 doc 上读取结果:

let doc = nlp('I walk to work')
doc.verbs().toPastTense()        // 就地修改 doc;返回值只是 "walked" 这个选择
doc.text()                       // 'I walked to work'   ✅ 从 doc 读取

// ⚠️ 常见错误 —— 这里的 .text() 只取到了被选中的片段:
nlp('I walk to work').verbs().toPastTense().text()   // 'walked work'  (不是整个句子)

想在不污染原文档的前提下变换一份副本,使用 .clone():

let doc = nlp('I walk')
let past = doc.clone().verbs().toPastTense().text()   // 'walked'
doc.text()                                            // 'I walk'  (原封不动)

这一设计贯穿全部变换类 API。从源码结构看,各子选择器(如 nouns/api/api.js 中的 Nouns、numbers/numbers/api.js 中的 Numbers)都是 View 的子类,通过 map/replaceWith 等操作直接在共享的 document 上改写,再返回一个持有新指针的视图——这正是「返回的是选择、改动的是全文」的根源。

提取命名实体(人物、地点、组织)

let doc = nlp('Mary met Dr. John Smith in Paris.')
doc.people().out('array')        // ['Mary', 'Dr. John Smith']
doc.places().out('array')        // ['Paris.']
nlp('Google and Mary went to Paris.').topics().out('array')  // ['Google', 'Paris.', 'Mary']

.topics() = 人物 + 地点 + 组织 的并集。实体识别依赖 data/lexicon 中的姓名、城市、国家等词表(如 people.js、places.js、organizations.js)以及 topics 插件的组合逻辑。

变换动词时态

let doc = nlp('I walk to work')
doc.verbs().toPastTense();   doc.text()   // 'I walked to work'
doc.verbs().toFutureTense(); doc.text()   // 'I will walk to work'

时态方法同样存在于 .sentences() 上,可对整个句子进行改写。实现位于 verbs/api/api.js,其中 toPast、toPresent、toFuture 分别是三个时态方法的别名;否定变换由 conjugate/toNegative.js 完成。此外还有 toGerund()、toInfinitive()、toPastParticiple() 等动词形态方法,可配合 verbs 测试 查看各类动词的变换预期。

查找与替换

let doc = nlp('I love cats')
doc.replace('cats', 'dogs')      // 按模式进行搜索-替换
doc.text()                       // 'I love dogs'

// 替换某个选择匹配到的内容:
let d2 = nlp('the cat sat')
d2.match('#Noun').replaceWith('dog')
d2.text()                        // 'the dog sat'

replace() 接受与 match() 相同的模式语法(见下文的「匹配一个模式并取出片段」),replaceWith() 则直接改写当前选择。相关实现可参考 change/api 与 match 插件;replace-sub 等边界行为见 tests/one/change/replace-sub.test.js。

脱敏 / 匿名化 PII

let doc = nlp('Mary called John')
doc.people().replaceWith('███')
doc.text()                       // '███ called ███'

内置的 .redact() 可一次性移除人物、地点、组织、邮箱、电话号码等敏感信息:

nlp('Mary joined Google today. Call (800) 555-0000 or email alice@example.com.')
  .redact().text()
// '██████████ joined ██████████ today. Call ██████████ or email ██████████.'

实现位于 redact/redact.js(通过 redact/plugin.js 挂载为 View.prototype.redact)。两点说明:

  • 默认开启的类别包括 people、places、organizations、acronyms、money、percentages、fractions、emails、phoneNumbers、atMentions、urls;而 properNouns、dates、numbers、pronouns 默认关闭(置为 false 即跳过)。可通过选项关闭某一类,例如 doc.redact({ people: false })。
  • 注意:当前仓库中 organizations 默认是开启的(redact-organizations 测试里 Mary joined Google today 会把 Google 一并替换),这与 recipes.md 中"默认不脱敏组织"的旧描述不同——如果你使用的是当前版本,组织名会被默认脱敏;文档的历史说明可作参考。脱敏时文本被替换为 ██████████(可通过第二参数自定义),并打上 Redacted 标签且保留原词性标签(keep=true),见 tests/three/redact.test.js。

名词复数化 / 单数化

let doc = nlp('one good dog')
doc.nouns().toPlural();   doc.text()   // 'one good dogs'

let d2 = nlp('three turnovers')
d2.nouns().toSingular();  d2.text()    // 'three turnover'

nouns() 返回 Nouns 视图(见 nouns/api/api.js),除了 toPlural/toSingular 还提供 isPlural()、isSingular()、adjectives()、parse()、conjugate()(给出 Singular/Plural 词形)等方法。复数规则引擎在 pairs(如 Plural.js),匹配测试见 tests/three/nouns/toPlural.test.js。

处理数字

nlp('it cost twelve dollars').numbers().get()           // [12]
nlp('five hundred').numbers().toNumber().text()         // '500'   (文字 → 数字)
nlp('it is 5 km').numbers().toText().text()             // 'five'  (数字 → 文字)

数字视图还支持 .greaterThan()、.lessThan()、.between()、.add()、.subtract()(别名 .minus()、.plus()、isBetween),以及 .set()、.isEqual()(别名 .equals())、.increment()、.decrement()、.isOrdinal()、.isCardinal()、.toLocaleString()(别名 .toNice())等,全部定义在 numbers/numbers/api.js。货币、分数、百分比也有各自的子选择:.money()、.fractions()、.percentages()(后两者与数字共用 Numbers 类),View.prototype.values 是 numbers 的别名。

匹配一个模式并取出片段

let doc = nlp('the price of milk is high')
doc.match('price of [<thing>.]').groups('thing').text()   // 'milk'

方括号 <a href="https://link.gitcode.com/i/b1bf0ba8ac75550d0ff7123be9909c57" target="_blank"> ... ] 圈定捕获片段,<name> 为其命名,. 表示任意一个词,然后用 .groups('thing') 取回。完整的模式语言(词性标签 #Tag、正则、*/+ 量词、or/not、前后查找等)见 [match-syntax.md,实现位于 1-one/match,详尽的模式测试在 tests/two/match 与 tests/one/match 下。

判断文本是否包含某内容(聊天机器人式路由)

nlp('the deal is closed').has('#Determiner #Noun')   // true
nlp('I love cats').has('love #Plural')               // true

.has() 返回布尔值且永不修改文档,非常适合做意图路由/关键词命中判断。其内部基于匹配引擎实现(见 1-one/match 的 api),因此支持完整的 match 语法。

过滤句子

nlp('I am here. Are you ok?').questions().out('array')        // ['Are you ok?']
nlp('I like cats. Dogs are loud.').sentences().if('#Plural').out('array')
// ['I like cats.', 'Dogs are loud.']   —— 只保留含复数名词的句子

.questions() 通过 sentences/questions.js 识别问句(sentences().isQuestion() 是同义方法);.sentences().if(pattern) 按模式过滤句子。完整的句子 API(isExclamation()、toPastTense()、toNegative() 等)见 sentences/api.js。

否定一个句子

let doc = nlp('he is happy')
doc.sentences().toNegative()
doc.text()                       // 'he is not happy'

句子级否定由 verbs/api/conjugate/toNegative.js 驱动,会依据动词形态插入否定词;相关预期见 tests/three/verbs/toNegative.test.js。

展开缩略词

let doc = nlp("she isn't here")
doc.contractions().expand()
doc.text()                       // 'she is not here'

缩略词处理拆分为两层:一层的收缩/展开在 1-one/contraction-one(compute 与 model),更复杂的规则在 2-two/contraction-two。测试见 tests/two/contractions/expand.test.js 与 contract.test.js。

归一化混乱文本

nlp('I  LOVE   Café!!').normalize().text()   // 'I LOVE Cafe!'

.normalize({ ... }) 支持选项控制空白、大小写、unicode 变体、标点、缩略词等处理维度,归一化逻辑集中在 3-three/normalize(methods.js 定义各规则,api.js 暴露入口),预置与自定义选项的测试见 tests/three/normalize。

导出结构化数据

nlp('big cats').json()[0].terms.map(t => t.text)   // ['big', 'cats']
nlp('hi there').json({ offset: true })[0].offset   // { index: 0, start: 0, length: 8 }

.json() 接受若干开关,返回带 terms 等字段的数组;offset: true 会附带每个词在原文中的位置信息(index/start/length),便于做高亮或审计。其他可用标志包括 tags、normal、reduced 等,完整说明见 api.md;输出实现位于 1-one/output。

教它认识新词

解析时传入 lexicon 对象(仅对本次调用生效):

nlp('kermit waved', { kermit: 'FirstName' }).people().out('array')   // ['kermit']

全局注册,使用 nlp.addWords()(值必须是有效标签,标签列表见 tags.md):

nlp.addWords({ frodo: 'FirstName', gandalf: 'FirstName' })
nlp('frodo met gandalf').people().out('array')   // ['frodo', 'gandalf']

词表机制由 1-one/lexicon 插件实现(methods 提供查找逻辑),仓库内置词表在 data/lexicon/index.js。除了 addWords,还可用 doc.tag() 直接给已解析的词打标签(见 tests/one/lexicon/lexicon.test.js)。

编写插件(添加自己的方法)

nlp.plugin({
  // 向词表添加单词
  words: { kermit: 'FirstName' },
  // 向标签图添加新标签
  tags: { Muppet: { isA: 'Person' } },
  // 添加可链式调用的新方法
  api: (View) => {
    View.prototype.exclaim = function () {
      return this.post('!')   // 在每个匹配后追加 '!'
    }
  },
})

let doc = nlp('hello there')
doc.match('there').exclaim()
doc.text()                       // 'hello there!'

插件形状(words/tags/api/model/compute 等)的完整说明见 concepts.md,View.prototype 的扩展方式见 extend.js。仓库 plugins 目录下有大量真实插件可供参考:日期 plugins/dates、统计 plugins/stats、维基百科 plugins/wikipedia、语音 plugins/speech 等——写插件前先看看它们如何组织 plugin.js 与 api.js。

当 .match() 匹配不到东西时

按以下顺序排查:

  1. 确认标签真实存在——见 tags.md。#Name、#Location、#Adj 都不是标签;人物应是 #Person,地点是 #Place,形容词是 #Adjective(完整标签集也可在 src/2-two/preTagger/tagSet 中查看)。
  2. 匹配不会跨句子边界——模式只能在一个句子内部匹配,需要跨句请先合并或对单句逐一匹配。
  3. 用 doc.debug() 打印每个词实际被标注的标签——可直观看到词性标注结果,快速定位是分词还是打标环节出了问题。
  4. 精确词只能匹配字面单词——想匹配所有屈折形态(如 run/runs/ran/running),使用 {root} 语法(见 match-syntax.md)。

更多匹配陷阱与负向断言、贪婪捕获等高级用法,可结合 tests/two/match 下的 not.test.js、greedy-capture.test.js 等用例对照学习。


附:本文配方与源码对应关系速查

配方 主要实现位置 相关测试
可变性 / 变换 API src/API/View.js、src/1-one/change tests/one/change
实体提取 src/3-three/topics + data/lexicon tests/three/topics.test.js
动词时态 / 否定 src/3-three/verbs/api/api.js tests/three/verbs
查找替换 src/1-one/match、src/1-one/change tests/two/match
PII 脱敏 src/3-three/redact/redact.js tests/three/redact.test.js
名词单复数 src/3-three/nouns/api/api.js、data/pairs tests/three/nouns
数字处理 src/3-three/numbers/numbers/api.js tests/three/numbers
句子过滤 / 问句 src/3-three/sentences tests/three/sentences
归一化 src/3-three/normalize tests/three/normalize
结构化输出 src/1-one/output tests/two/output/json.test.js
自定义词表 src/1-one/lexicon tests/one/lexicon
插件机制 src/API/extend.js、plugins tests/three/plugin.test.js

这些配方覆盖了日常文本处理的高频需求:聊天机器人意图判断、内容清洗与归一化、日志/用户内容的 PII 脱敏、结构化信息抽取、文本风格改写(时态、否定、单复数)。遇到新需求时,先想清楚「我要读结果还是改文档」,再用 match() 定位、用对应子视图变换,最后用 json()/out('array') 取出结构化结果即可。

登录后查看全文
compromise