编程 Harper 深度解剖:Automattic 开源的 Rust 离线语法检查器——从 LSP 架构、规则引擎到毫秒级 Lint 的隐私优先工程真相

2026-07-26 03:45:28 +0800 CST views 7

Harper 深度解剖:Automattic 开源的 Rust 离线语法检查器——从 LSP 架构、规则引擎到毫秒级 Lint 的隐私优先工程真相

一、背景:语法检查这门生意,凭什么要把你的文字上传到云端?

写英文文档、提交英文 commit message、写英文 README,几乎是每个程序员的日常。而市面上的语法检查工具,长期以来只有两条路可走:

第一条路:Grammarly 式的云端 SaaS。 你在编辑器里敲下的每一个字符,都会被实时发送到厂商的服务器上做分析。检查效果确实好,但代价是你的全部文字——包括还没发布的技术方案、内部邮件、商业计划——都流经了第三方的机器。对企业用户来说,这是一笔说不清道不明的「隐私税」。不少公司的安全部门明令禁止在内网机器上安装这类浏览器插件,原因不言自明。

第二条路:LanguageTool 式的本地 Java 服务。 开源、可以自部署,听起来很美。但实际跑过的人都知道:一个 JVM 进程起步就要吃掉数百 MB 到上 GB 的内存,冷启动慢,检查延迟在几百毫秒到秒级。把它挂在编辑器后台当常驻服务,风扇会先表达不满。

这两条路的共同问题是:语法检查作为一个「每敲一个字都要跑一遍」的高频任务,要么牺牲隐私换体验,要么牺牲资源换开源。

2026 年 7 月,一个叫 Harper 的项目冲上了 GitHub Trending——单日新增 876 星,总量突破 1.3 万星。它背后站着的是 Automattic(WordPress.com 母公司),核心卖点极其克制,就一句话:

完全离线、毫秒级响应、内存占用以 MB 计的语法检查器,用 Rust 写的。

没有云端 AI,没有数据回传,没有订阅制。这篇文章我们把它拆开看:Harper 是怎么用 Rust 把语法检查这件事做到「快到感觉不到存在」的,它的 20 多个 crate 是怎么组织的,LSP 集成是怎么落地的,以及——它到底适不适合你。

二、Harper 是什么:定位与边界

先给结论,Harper 的定位可以概括为三个关键词:

  1. 离线优先(Offline-first):所有分析在本地完成,二进制不发起任何网络请求。你的文字不出机器。
  2. 编辑器原生(Editor-native):通过 LSP(Language Server Protocol)接入 VS Code、Neovim、Helix、Zed、Emacs 等一切支持 LSP 的编辑器,语法错误像编译器报错一样以 diagnostic 的形式出现在你熟悉的界面里。
  3. 面向开发者的文档场景:它不只检查纯文本,还能理解 Markdown、HTML、LaTeX、AsciiDoc、Typst,甚至能钻进源代码里只检查注释和文档字符串——Python 的 docstring、Rust 的 ///、Java 的 Javadoc、Git 的 commit message,都在射程之内。

同样重要的是它的边界,这点官方说得很诚实:

  • 目前只支持英文(美式/英式/加拿大/澳大利亚等变体)。对中文用户来说,它解决的是「写英文文档/注释/commit」的场景,不是中文校对。
  • 它是规则驱动 + 词典驱动的检查器,不是大语言模型。它能抓拼写错误、常见语法错误、标点误用、重复词、大小写问题,但不会像 LLM 那样帮你重写整段话。换句话说:它是 linter,不是 copilot。

这个边界划得非常「Unix 哲学」:做一件事,把它做到极致快。

三、架构总览:一个 monorepo 里的 20+ 个 crate

Harper 仓库是典型的 Rust workspace monorepo,拆分了 20 多个 crate。看它的 crate 划分,基本就能读懂整个系统的分层思路:

harper/
├── harper-core          # 核心引擎:分词、词典、规则、Lint 全在这
├── harper-ls            # LSP 服务器,编辑器集成的入口
├── harper-cli           # 命令行工具,CI/脚本场景
├── harper.js            # WASM 绑定,浏览器/Node.js 里跑
├── harper-comments      # 从源代码里提取注释(基于 tree-sitter)
├── harper-html          # HTML 解析与文本提取
├── harper-latex         # LaTeX 文档支持
├── harper-typst         # Typst 文档支持
├── harper-literate-haskell / harper-swift / ...  # 各语言/格式适配
└── packages/            # VS Code 插件、Obsidian 插件、浏览器扩展等

这个分层有两个值得抄作业的设计决策:

决策一:核心引擎(harper-core)零 I/O、零平台依赖。

harper-core 不读文件、不发网络请求、不依赖操作系统特性,纯粹是「文本进、Lint 结果出」的函数库。这带来一个巨大的红利:同一份核心代码可以毫无修改地编译到三个目标——本地二进制(CLI/LSP)、WASM(浏览器/Node)、以及 Tauri 打包的桌面应用。写 Rust 的同学应该深有体会,把 I/O 推到边缘、让核心保持 pure,是跨平台复用的第一性原理。

决策二:格式适配器与核心引擎解耦。

Markdown、HTML、LaTeX、源码注释……每种格式一个独立 crate,它们的职责统一抽象为:把「带标记的文档」转换成「纯英文文本 + 位置映射」。核心引擎只面对干净的文本流,检查完之后再通过位置映射把 Lint 结果投影回原始文档的行列坐标。

这就是为什么 Harper 给 LaTeX 报错时,能精确高亮到 \textbf{...} 花括号里的那个单词,而不是把整行标红——位置映射(span mapping)是贯穿全链路的一等公民。

四、核心引擎拆解:从字符流到 Lint 的四级流水线

harper-core 内部是一条经典的分析流水线,我们逐级拆开。

4.1 第一级:分词(Tokenization)

输入的文本首先被切分成 Token 流。Harper 的 Token 不是 NLP 里的「词向量」那一套,而是带精确位置信息的轻量结构,概念上长这样:

// 概念示意(以官方仓库实际定义为准)
pub struct Token {
    pub span: Span,        // 在原文中的字节/字符区间
    pub kind: TokenKind,   // 词、标点、空白、数字、URL、emoji……
}

pub enum TokenKind {
    Word(WordMetadata),    // 单词,附带词性等元数据
    Punctuation(Punctuation),
    Number(f64, Option<NumberSuffix>),
    Space(usize),
    Newline(usize),
    Url,
    Hostname,
    EmailAddress,
    Unlintable,            // 被格式解析器标记为「别碰」的区域
    // ...
}

注意那个 Unlintable——这是格式适配层和核心引擎之间的关键契约。Markdown 里的代码块、LaTeX 里的公式、HTML 的 <script> 内容,都会被适配器打上 Unlintable 标记。核心引擎看得见它们(保持位置连续性),但所有规则都会自动跳过。相比「先把代码块删掉再检查」的粗暴做法,这种「标记而非删除」的策略让位置映射简单了一个数量级。

4.2 第二级:词典与拼写检查

拼写检查是语法检查器的地基,Harper 在这里做了两件很「工程」的事。

其一:词典采用 Hunspell 风格的「词根 + 词缀规则」压缩存储。

英文单词的屈折变化极其规律:run → runs / running / runner。如果把每个变体都存一遍,词典体积会爆炸。Harper 沿用了 Hunspell 生态成熟的 affix 方案——词典里只存词根和一组词缀标记,运行时按规则展开:

# 词典条目示意
run/RGS    # R=可加-er, G=可加-ing, S=可加-s

启动时全部展开进内存索引,13 万+ 词根膨胀成几十万有效词形,内存占用依然控制在几十 MB 以内——因为存的是紧凑的结构化数据,不是字符串的裸堆积。

其二:拼写建议用编辑距离 + 频率权重排序。

当遇到词典外的单词,Harper 会在有限编辑距离(通常是 1~2)内搜索候选词。这里有个经典性能陷阱:朴素做法是拿错词和全词典逐一算 Levenshtein 距离,13 万词根一趟下来毫秒级预算直接爆掉。业界的标准解法是把词典组织成 trie/FST,在遍历过程中带着「编辑距离预算」剪枝,把搜索空间从 O(N) 压到接近 O(前缀分支数)。Harper 的实现思路同样是索引化搜索而非线性扫描,这是它能把「敲一个字母就重新检查一遍」变成可行方案的地基之一。

4.3 第三级:规则引擎(Linters)

拼写之上是语法规则。Harper 内置了数百条规则,每条规则实现统一的 Linter trait:

// 概念示意
pub trait Linter {
    fn lint(&mut self, document: &Document) -> Vec<Lint>;
    fn description(&self) -> &str;
}

pub struct Lint {
    pub span: Span,                    // 问题区间
    pub lint_kind: LintKind,           // 拼写/语法/风格/大小写……
    pub suggestions: Vec<Suggestion>,  // 修复建议(可自动应用)
    pub message: String,               // 给人看的解释
    pub priority: u8,
}

规则大致分三类:

  1. 模式规则:最常见的一类,本质是「Token 序列模式匹配 → 替换建议」。比如 an 后面跟辅音开头的词、a 后面跟元音开头的词、could of 应为 could have、重复词 the the。Harper 内部为这类规则做了一套表达式/模式 DSL,新规则往往十几行代码就能落地——翻一下仓库的 PR 列表,社区贡献的规则占了相当大的比例,门槛低是规则库能滚起来的直接原因。
  2. 结构规则:需要看句子级结构的,比如句首大小写、句号后空格数、括号/引号闭合。
  3. 词典联动规则:依赖词性元数据的,比如 they're / their / there 混用检测——这需要知道上下文里期望的是代词还是副词。

所有规则被注册进一个 LintGroup,统一调度、统一开关。用户可以在配置里按规则名精细控制启停,这直接映射到 LSP 的配置能力(后面讲)。

4.4 第四级:结果投影与建议应用

Lint 产出的 span 是「纯文本坐标系」的,最后一步通过格式适配层建立的映射表投影回原始文档坐标。同时每个 Suggestion 都是结构化的(替换/删除/插入 + 目标区间),这让「一键修复」在 LSP 里能直接转换成 workspace edit,编辑器点一下 code action 就应用——而不是给你一段文字建议让你自己改。

五、多格式支持的工程细节:tree-sitter 立大功

Harper 最讨程序员喜欢的功能,是只检查代码注释。这件事的难度在于:每种语言的注释语法都不一样,块注释、行注释、docstring、嵌套注释……自己写解析器是死路。

Harper 的选择是站在 tree-sitter 的肩膀上。tree-sitter 是 GitHub 开源的增量解析框架,几乎所有主流语言都有现成的 grammar。harper-comments 的工作流程是:

  1. 用对应语言的 tree-sitter grammar 解析源文件,得到语法树;
  2. 遍历语法树,摘出所有 comment 节点;
  3. 剥掉注释符号(///* */#""")和常见的装饰(比如每行开头的 *);
  4. 把剩下的自然语言文本连同位置映射喂给 harper-core。

这里有个容易被忽略的细节处理:注释里经常混着代码标识符(someVariableName)、路径(src/main.rs)、URL。如果按普通英文检查,满屏都是误报。Harper 的分词器会把 camelCase/snake_case 标识符、路径、URL 识别为专门的 TokenKind,规则层自动跳过。误报率是 linter 的生死线,一个每天对你狂叫的工具活不过一周——从 Harper 的 issue 区看,团队对误报的处理优先级明显高于新功能,这个价值观排序是对的。

六、代码实战

6.1 五分钟上手:CLI 检查一个 Markdown 文件

# 安装(也可用 brew install harper / cargo install harper-cli)
cargo binstall harper-cli

# 检查单个文件
harper-cli lint README.md

# 输出示意:
# README.md:14:23  Did you mean to repeat this word? [repeated_words]
#     the the quick brown fox
#         ^^^^^^^
# README.md:27:8   `an` should be used before vowel sounds. [a_an]

把它挂进 CI 也就三行:

# .github/workflows/docs-lint.yml
- name: Lint docs
  run: |
    cargo binstall -y harper-cli
    harper-cli lint docs/**/*.md --count && echo "docs clean"

6.2 Neovim:10 行配置接入 harper-ls

-- Neovim 0.11+ 原生 LSP 配置
vim.lsp.config('harper_ls', {
  settings = {
    ["harper-ls"] = {
      userDictPath = "~/.config/harper/dict.txt", -- 个人词典
      linters = {
        SentenceCapitalization = true,
        SpellCheck = true,
        RepeatedWords = true,
        ToDoHyphen = false,        -- 按名字精细开关规则
      },
      diagnosticSeverity = "hint", -- 别让语法建议盖过编译错误
      isolateEnglish = true,        -- 混合语言文档里只检查英文
    },
  },
})
vim.lsp.enable('harper_ls')

体验上最直观的对比:以前用 ltex-ls(LanguageTool 的 LSP 封装),打开一个大 Markdown 要等两三秒才出诊断,内存稳定 1GB+;换 harper-ls 之后诊断几乎是「键入即出现」,ps 里看常驻内存只有几十 MB。这不是玄学,是 Rust 无 GC 运行时 + 索引化词典 + 增量检查共同作用的结果。

6.3 VS Code / Zed / Helix

VS Code 直接装官方扩展 Harper,零配置开箱即用;Zed 和 Helix 因为原生支持 LSP,加几行配置指向 harper-ls --stdio 即可。所有编辑器共享同一个 LSP 服务器实现,行为完全一致——这就是 LSP 架构的红利:N 个编辑器 × M 种检查能力的适配矩阵,被压缩成 N + M。

6.4 harper.js:在浏览器里跑同一套引擎

harper-core 编译成 WASM 后以 harper.js 发布到 npm,浏览器和 Node 里可以直接用:

import { WorkerLinter } from 'harper.js';

// WorkerLinter 在 Web Worker 里跑 WASM,不阻塞主线程
const linter = new WorkerLinter();

const lints = await linter.lint(
  'This sentence contain a error, and and it need fixed.'
);

for (const lint of lints) {
  console.log(
    lint.message(),                       // 人类可读的问题描述
    lint.span(),                          // 问题在原文中的区间
    lint.suggestions().map(s => s.toString()) // 结构化修复建议
  );
}

这个能力的想象空间在于:任何 Web 编辑器都能以零服务器成本获得语法检查。 评论框、CMS 后台、在线 Markdown 编辑器、Obsidian 插件(官方已提供)——检查逻辑全在用户设备上跑,站方不用为语法检查建一台服务器,用户的文字也不会离开浏览器。对比一下:接 Grammarly SDK 要谈商务、走数据合规;接 harper.js 是 npm install 的事。

6.5 进阶:用 harper-core 写自己的检查工具

因为 harper-core 就是个普通 Rust 库,你完全可以把它嵌进自己的工具链。比如写一个「检查 Git commit message」的钩子:

use harper_core::{Document, linting::{LintGroup, Linter}, FstDictionary};

fn main() {
    let msg = std::fs::read_to_string(".git/COMMIT_EDITMSG").unwrap();

    let dict = FstDictionary::curated();          // 内置词典
    let doc = Document::new_plain_english_curated(&msg);
    let mut linters = LintGroup::new_curated(dict.into(), Default::default());

    let lints = linters.lint(&doc);
    for lint in &lints {
        eprintln!("commit-msg: {}", lint.message);
    }
    if !lints.is_empty() {
        std::process::exit(1);                    // 有问题就拦下这次提交
    }
}

(API 细节随版本演进,以官方 docs.rs 文档为准,但「词典 + Document + LintGroup」的三件套结构是稳定的心智模型。)

自定义规则同样是实现 Linter trait 然后注册进组。团队内部约定——比如禁止在文档里出现 simplyobviously 这类居高临下的词——十几行就能变成一条自动执行的规则。把 code review 里的口头约定固化成 linter 规则,是所有基础设施团队都该学会的杠杆。

七、性能分析:毫秒级响应是怎么来的

Harper 官方给自己的性能目标非常激进:单次检查在毫秒量级完成,慢了就是 bug。拆解一下这个速度的来源:

1. 无运行时开销的语言底座。 Rust 没有 GC 暂停、没有 JIT 预热,二进制启动即满速。对比 LanguageTool:JVM 启动 + 类加载 + 规则初始化,冷启动就是秒级,这在「编辑器每次打开文件都要拉起检查」的场景里是致命的。

2. 数据结构为查询而生。 词典是预构建的索引结构(FST/trie 家族),拼写查询和建议搜索都是带剪枝的树遍历,不是线性扫描。规则匹配面对的是 Token 流而不是原始字符串,避免了重复解析。

3. 增量与并行。 LSP 场景下文档是持续编辑的,Harper 只对变更影响的区域重新分析,而不是每次全文重跑;规则之间相互独立,天然适合并行执行——这两条加起来,让「每次击键都触发检查」的成本被摊薄到无感。

4. 内存纪律。 官方 README 直接把「低内存」写进卖点。几十 MB 的常驻内存意味着你可以毫无心理负担地在每个编辑器实例里都挂一个 harper-ls,而 1GB 级别的 ltex-ls 你只会想开一个、甚至一个都嫌多。

一个值得记住的工程结论:当一个高频工具的延迟低到无感、资源低到无感时,它的使用方式会发生质变——从「写完了跑一遍检查」变成「写的过程中实时纠正」。这跟当年 tsserver 之于 TypeScript、rust-analyzer 之于 Rust 是同一个故事。工具快到一定程度,工作流本身会被重塑。

八、横向对比:Harper vs LanguageTool vs Grammarly vs Vale

维度HarperLanguageTool(自部署)GrammarlyVale
隐私完全本地本地可控全量上云完全本地
内存~几十 MB数百 MB~GB 级云端(本地插件轻)轻量
延迟毫秒级数百 ms 起依赖网络
语言仅英文30+ 语言英文为主不做语法只做风格
检查深度拼写+语法+常见风格更全的语法规则最强(含改写)风格规则(自定义强)
编辑器集成LSP 原生需 ltex-ls 封装私有插件CLI/LSP
扩展方式Rust 写规则XML 规则不可扩展YAML 规则

几个实际选型建议:

  • 写英文文档/注释/commit 的开发者:Harper 基本是当前的最优解,快、省、离线,LSP 直插现有编辑器。
  • 需要多语言(德语、法语等)检查:LanguageTool 仍不可替代,Harper 目前只做英文。
  • 团队文档风格规范(术语表、禁用词、语气):Vale 和 Harper 是互补而非竞争——Vale 管风格合规,Harper 管语法拼写,CI 里可以同时挂。
  • 需要 AI 级别的改写建议:这不是 Harper 的赛道,也大概率永远不会是。规则引擎和 LLM 各有适用面:前者确定、快速、可解释、零成本;后者灵活但慢、贵、且要交出你的数据。

九、Automattic 为什么要养这个项目?

一个容易被忽略的背景:Harper 原本是个人开源项目,2025 年被 Automattic 收编,原作者以全职身份继续开发。Automattic 的算盘不难猜:

WordPress 生态承载着互联网上相当比例的内容生产,「写作体验」是其核心竞争力的一部分。把一个 WASM 就能跑、毫秒级响应、零边际成本的语法检查器嵌进 Gutenberg 编辑器,对比接第三方云端 API 的方案,成本和隐私叙事上都是碾压。开源在这里不是慈善,是把基础设施成本社会化、把生态话语权私有化的经典打法——跟 Meta 养 React、Google 养 Chromium 是同一套逻辑的小号版本。

对用户来说这反而是好事:有大厂发工资的全职维护者,比纯爱发电的个人项目可持续得多。4400+ 次提交、稳定的发版节奏、活跃的 issue 响应,都印证了这一点。

十、局限与踩坑提示

诚实地列一下你上手前该知道的事:

  1. 只有英文。 再强调一遍。中英混排文档里它有 isolateEnglish 模式只挑英文段落检查,但中文部分完全不管。
  2. 规则覆盖不如 LanguageTool 全。 一些长尾语法错误(复杂时态一致性、虚拟语气)它可能抓不到。它的策略是优先覆盖高频错误、压低误报,而不是追求学术级查全率。
  3. 专业术语误报需要喂个人词典。 技术文档里的产品名、缩写词第一次见面会被标为拼写错误,把 userDictPath 配好、遇到就 add to dictionary,一周后就安静了。
  4. Windows 支持成熟度略低于 macOS/Linux,个别编辑器插件在 Windows 上的路径处理偶有 issue,好在修得很快。

十一、总结与展望

Harper 值得关注,不只因为它是个好用的工具,更因为它代表了一种值得被反复讲述的工程路线:

在所有人都往云端和大模型跑的时候,把一个高频、轻量、隐私敏感的任务用系统级语言在本地做到极致——快到无感、小到无感、离线到无感。

它的三个可迁移经验:

  1. 核心引擎零 I/O 化,一份代码通吃 native/WASM/桌面三端;
  2. 用 LSP 做分发,一次实现接入所有编辑器,把 N×M 适配压成 N+M;
  3. 用「标记而非删除」处理混合格式文档,让位置映射贯穿全链路,报错精确到字符。

可以预见的演进方向:规则库随社区贡献持续膨胀(低门槛的规则 DSL 已经证明能滚起雪球)、更多格式适配(issue 区已有 Org-mode、reStructuredText 的呼声)、以及最大的悬念——多语言支持。规则引擎架构本身不排斥其他语言,但每种语言都需要重建词典和规则库,这是社区运营问题而非技术问题。

如果你每天要写英文注释和文档,花五分钟装个 harper-ls,大概率会像当年第一次用 rust-analyzer 一样回不去了。快,本身就是一种功能;而「你的文字永远不离开你的机器」,在 2026 年,已经是一种稀缺的美德。


参考资料:Harper 官方仓库(github.com/Automattic/harper)、Harper 官网文档(writewithharper.com)、tree-sitter 项目文档、Hunspell affix 格式规范。文中代码为示意用途,具体 API 以对应版本官方文档为准。

推荐文章

php客服服务管理系统
2024-11-19 06:48:35 +0800 CST
PHP 8.4 中的新数组函数
2024-11-19 08:33:52 +0800 CST
Go语言中的`Ring`循环链表结构
2024-11-19 00:00:46 +0800 CST
记录一次服务器的优化对比
2024-11-19 09:18:23 +0800 CST
JavaScript 实现访问本地文件夹
2024-11-18 23:12:47 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
MySQL 优化利剑 EXPLAIN
2024-11-19 00:43:21 +0800 CST
设置mysql支持emoji表情
2024-11-17 04:59:45 +0800 CST
PyMySQL - Python中非常有用的库
2024-11-18 14:43:28 +0800 CST
程序员茄子在线接单