强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Hunspell 拼写检查完全教程 / 第 06 章:自定义词典开发

第 06 章:自定义词典开发

6.1 为什么需要自定义词典

标准词典无法覆盖所有场景:

场景问题解决方案
项目术语技术词汇不在标准词典中项目级自定义词典
专有名词人名、地名、公司名个人词典
行业术语医学、法律、金融专业词行业词典
缩略语API、HTTP、JSON 等缩略语词典
新造词新出现的网络用语、品牌名动态更新词典
多语言文档混合语言内容多语言合并词典

6.2 个人词典(Personal Dictionary)

6.2.1 文件格式

个人词典是简单的文本文件,每行一个单词:

# ~/.hunspell_personal
# 个人词典 - 第一行可选编码声明
Hunspell
API
APIs
JSON
HTTP
GitHub
Docker
kubernetes
golang

6.2.2 使用个人词典

# 检查时加载个人词典
hunspell -d en_US -p ~/.hunspell_personal document.txt

# 多个个人词典合并
hunspell -d en_US -p ~/.hunspell_personal -p ./.hunspell_project document.txt

6.2.3 在交互模式中管理

# 启动交互模式,指定个人词典
hunspell -c -p ~/.hunspell_personal document.txt

# 遇到新词时的交互命令:
# A — 将当前词加入个人词典(保留大小写)
# I — 将当前词小写形式加入个人词典
# U — 将当前词词根形式加入个人词典

6.2.4 带 affix 标志的个人词典

# 个人词典也可以使用 affix 标志
# 格式与 .dic 文件中的条目相同
API/S               # API → APIs(S 标志 = 复数)
Kubernetes          # 只接受此形式
docker/S            # docker → dockers

6.3 项目词典

6.3.1 典型项目词典结构

myproject/
├── docs/
│   ├── README.md
│   └── ...
├── .hunspell/           # 词典目录
│   ├── project.dic      # 项目词典
│   └── project.aff      # 项目词缀文件(可选)
└── .gitignore           # 可选择是否忽略词典

6.3.2 创建项目词典

# 创建项目词典
mkdir -p .hunspell

# 从项目文件中提取专有名词和技术术语
cat > .hunspell/project.dic << 'EOF'
# 项目术语词典
# 编程语言
golang
TypeScript
JavaScript
Python
Rust

# 框架和工具
Docker
Kubernetes
Redis
PostgreSQL
GraphQL
RESTful
gRPC

# 项目特有术语
BaaS
microservice
webhook
tokenization
autoscaler

# 人名/团队
GitHub
GitLab
EOF

6.3.3 自动提取项目术语

#!/bin/bash
# extract_terms.sh - 从项目文件中提取候选术语
# 找出被 Hunspell 标记为错误但出现频率较高的词

DIR="${1:-.}"
DICT="${2:-en_US}"
FREQ_THRESHOLD=3

echo "=== 项目术语提取 ==="

# 收集所有拼写"错误"的词
find "$DIR" -type f \( -name "*.md" -o -name "*.txt" -o -name "*.rst" \) -print0 | \
xargs -0 hunspell -l -d "$DICT" 2>/dev/null | \
sort | uniq -c | sort -rn | \
while read count word; do
    if [ "$count" -ge "$FREQ_THRESHOLD" ]; then
        echo "$count $word"
    fi
done

输出示例:

=== 项目术语提取 ===
47 kubernetes
35 golang
28 webhook
19 microservice
12 tokenization

6.3.4 词典生成脚本

#!/bin/bash
# gen_project_dict.sh - 自动生成项目词典
# 用法: ./gen_project_dict.sh <目录> [主词典]

DIR="${1:-.}"
MAIN_DICT="${2:-en_US}"
OUTPUT=".hunspell/project.dic"
FREQ_THRESHOLD=2

mkdir -p .hunspell

echo "# 自动生成的项目词典" > "$OUTPUT"
echo "# 生成时间: $(date)" >> "$OUTPUT"
echo "# 频率阈值: $FREQ_THRESHOLD" >> "$OUTPUT"

find "$DIR" -type f \( -name "*.md" -o -name "*.txt" -o -name "*.rst" -o -name "*.html" \) -print0 | \
xargs -0 hunspell -l -d "$MAIN_DICT" 2>/dev/null | \
sort | uniq -c | sort -rn | \
while read count word; do
    if [ "$count" -ge "$FREQ_THRESHOLD" ]; then
        echo "$word" >> "$OUTPUT"
    fi
done

TOTAL=$(grep -cv "^#" "$OUTPUT")
echo "已生成项目词典: $OUTPUT"
echo "包含 $TOTAL 个术语"

6.4 词表添加技巧

6.4.1 分类词表管理

# 按类别组织词典
.hunspell/
├── project.dic        # 主项目词典(合并所有)
├── terms/
│   ├── programming.dic    # 编程术语
│   ├── company.dic        # 公司/产品名
│   ├── people.dic         # 人名
│   └── abbreviations.dic  # 缩略语
└── build.sh           # 合并脚本
# build.sh - 合并分类词典
#!/bin/bash
cat .hunspell/terms/*.dic | grep -v "^#" | sort -u > .hunspell/project.dic
echo "合并完成: $(wc -l < .hunspell/project.dic) 个术语"

6.4.2 编程术语词表示例

# programming.dic
# 编程语言
TypeScript
JavaScript
Python
Rust
Golang
golang
Kotlin
Swift
Ruby
Perl
Lua
Haskell
Scala
Clojure
Erlang
Elixir

# 概念和模式
async
await
callback
closure
middleware
polymorphism
abstraction
encapsulation
inheritance
multithreading
concurrency
idempotent
refactor
deserialization
serialization
normalization
denormalization

# 工具和平台
Docker
Kubernetes
K8s
Redis
PostgreSQL
MySQL
MongoDB
Elasticsearch
Kafka
RabbitMQ
Nginx
Grafana
Prometheus
Jenkins
GitLab
GitHub
Bitbucket
Ansible
Terraform
Vagrant

6.4.3 公司/产品名词表示例

# company.dic
# 公司名
Google
Microsoft
Apple
Amazon
Meta
Netflix
Uber
Airbnb
Slack
Stripe
Cloudflare
Datadog
NewRelic
PagerDuty
Twilio
Auth0
Okta
Algolia
Terraform
Vercel
Netlify
DigitalOcean
Hetzner
OVH
Linode

6.4.4 从 CSV 导入词表

#!/bin/bash
# import_csv_dict.sh - 从 CSV 文件导入词表
# CSV 格式: word,category,notes

INPUT="$1"
OUTPUT="${2:-.hunspell/project.dic}"

echo "# 从 CSV 导入的词表" > "$OUTPUT"
echo "# 来源: $INPUT" >> "$OUTPUT"

# 提取第一列(word),跳过标题行
tail -n +2 "$INPUT" | cut -d',' -f1 | sort -u >> "$OUTPUT"

echo "导入完成: $(grep -cv "^#" "$OUTPUT") 个词条"

6.4.5 从 NPM/PyPI 包名提取

# 从 package.json 提取依赖名
cat package.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
deps = list(data.get('dependencies', {}).keys())
deps += list(data.get('devDependencies', {}).keys())
for d in sorted(set(deps)):
    print(d)
" > .hunspell/npm_deps.dic

# 从 requirements.txt 提取 Python 包名
grep -v '^#' requirements.txt | grep -v '^$' | \
    sed 's/[>=<].*//' | sort -u > .hunspell/pip_deps.dic

6.5 词频与优先级

6.5.1 词频在建议中的作用

虽然 Hunspell 本身不直接支持词频(词典是无权重的),但可以通过以下方式影响建议质量:

# TRY 指令中的字符频率
TRY esianrtolcdugmphbyfvkwz
# 这个顺序影响建议排序,常用字母开头的词优先

# REP 指令中的替换优先级
REP 5
REP ie ei           # recieve → receive(更常见的错误)
REP ei ie           # 这个较少见
REP gh f            # enouf → enough

6.5.2 使用 affix 标志控制展开

# 不同词根使用不同标志,控制生成的词形数量
# 高频词:给予更多标志
run/DGS             # 可生成:runs, ran, running
# 低频词:只给复数标志
abbreviation/SM     # 只生成:abbreviations
# 技术术语:不展开
API                 # 只接受原始形式

6.6 Affix 标志使用技巧

6.6.1 标志设计原则

# 原则 1: 按词性分组标志
# 名词相关
SFX S ...           # 复数 -s/-es
SFX M ...           # 所有格 -'s

# 动词相关
SFX D ...           # 过去式 -ed
SFX G ...           # 现在分词 -ing
SFX T ...           # 第三人称 -s

# 形容词相关
SFX R ...           # 比较级 -er
SFX E ...           # 最高级 -est
SFX L ...           # 副词 -ly
SFX N ...           # 名词化 -ness

# 前缀
PFX U ...           # un-
PFX R ...           # re-
PFX D ...           # dis-

# 原则 2: 保留特殊标志
KEEPCASE K          # 大小写敏感词
NOSUGGEST X         # 不提供建议
NEEDAFFIX N         # 必须有词缀
ONLYINCOMPOUND O    # 仅复合词
FORBIDDENWORD F     # 禁用词

6.6.2 标志组合策略

# 动词标志组合示例
walk/DGS            # walk, walked, walking, walks
run/DGS             # run, ran? running, runs (ran 需手动处理)

# 名词标志组合示例
cat/SM              # cat, cats, cat's
child/SM            # child, children? child's (不规则需手动处理)

# 形容词标志组合示例
happy/RYLN          # happy, happier, happiest, happily, happiness
great/RYL           # great, greater, greatest, greatly

6.6.3 创建完整的项目 affix 文件

# .hunspell/project.aff
SET UTF-8
FLAG long

# 通用复数
SFX S Y 2
SFX S   0   s       [^sxzh]
SFX S   0   es      [sxzh]

# 技术词汇复数 (y → ies)
SFX J Y 1
SFX J   y   ies     [^aeiou]y

# 动词 -ing
SFX G Y 2
SFX G   e   ing     e
SFX G   0   ing     [^e]

# 动词 -ed
SFX D Y 2
SFX D   0   d       e
SFX D   0   ed      [^e]

# 不使用建议
NOSUGGEST X

# 保持大小写
KEEPCASE K

# 建议字符频率
TRY esianrtolcdugmphbyfvkwz
# .hunspell/project.dic
100
Docker/K
Kubernetes/K
API/S
APIs
TypeScript/K
JSON
HTTP
HTTPS
GraphQL/K
gRPC
webhook/S
microservice/JS
tokenization/S
autoscaler/S

6.7 大规模词典管理

6.7.1 词典版本控制

# .gitignore 建议配置
# 排除临时文件
.hunspell/*.tmp
.hunspell/*.bak

# 保留词典文件(团队共享)
# .hunspell/project.dic  ← 不忽略
# .hunspell/project.aff  ← 不忽略

6.7.2 词典合并脚本

#!/bin/bash
# merge_dicts.sh - 合并多个词典
# 用法: ./merge_dicts.sh dict1.dic dict2.dic dict3.dic > merged.dic

{
    echo "# 合并词典"
    echo "# 生成时间: $(date)"
    for f in "$@"; do
        echo "# 来源: $f"
    done
    
    # 合并所有词典,跳过注释和空行,去重排序
    cat "$@" | grep -v "^#" | grep -v "^$" | sort -u
} | tee /dev/stderr | wc -l | xargs -I{} echo "合并后: {} 个词条"

6.7.7 词典差异比较

#!/bin/bash
# diff_dicts.sh - 比较两个词典的差异
DICT_A="$1"
DICT_B="$2"

echo "=== 词典差异比较 ==="
echo "词典 A: $DICT_A ($(grep -cv "^#" "$DICT_A") 个词条)"
echo "词典 B: $DICT_B ($(grep -cv "^#" "$DICT_B") 个词条)"
echo ""

# 只在 A 中
comm -23 <(grep -v "^#" "$DICT_A" | sort) <(grep -v "^#" "$DICT_B" | sort) > /tmp/only_a.txt
echo "只在 A 中: $(wc -l < /tmp/only_a.txt) 个"
head -10 /tmp/only_a.txt | sed 's/^/  /'

# 只在 B 中
comm -13 <(grep -v "^#" "$DICT_A" | sort) <(grep -v "^#" "$DICT_B" | sort) > /tmp/only_b.txt
echo "只在 B 中: $(wc -l < /tmp/only_b.txt) 个"
head -10 /tmp/only_b.txt | sed 's/^/  /'

# 两者共有
comm -12 <(grep -v "^#" "$DICT_A" | sort) <(grep -v "^#" "$DICT_B" | sort) > /tmp/common.txt
echo "共有: $(wc -l < /tmp/common.txt) 个"

6.7.4 词典验证脚本

#!/bin/bash
# validate_dict.sh - 验证自定义词典格式
DICT_FILE="$1"

echo "=== 词典验证: $DICT_FILE ==="

# 检查文件存在
if [ ! -f "$DICT_FILE" ]; then
    echo "错误: 文件不存在"
    exit 1
fi

# 统计行数
TOTAL=$(wc -l < "$DICT_FILE")
COMMENT=$(grep -c "^#" "$DICT_FILE")
EMPTY=$(grep -c "^$" "$DICT_FILE")
VALID=$((TOTAL - COMMENT - EMPTY))

echo "总行数:    $TOTAL"
echo "注释行:    $COMMENT"
echo "空行:      $EMPTY"
echo "有效词条:  $VALID"
echo ""

# 检查是否有重复
DUPES=$(grep -v "^#" "$DICT_FILE" | grep -v "^$" | sort | uniq -d)
if [ -n "$DUPES" ]; then
    echo "警告: 发现重复词条:"
    echo "$DUPES" | head -10 | sed 's/^/  /'
else
    echo "✓ 无重复词条"
fi

# 检查空格开头的行
SPACE_LINES=$(grep -c "^ " "$DICT_FILE")
if [ "$SPACE_LINES" -gt 0 ]; then
    echo "警告: $SPACE_LINES 行以空格开头"
fi

# 检查行尾空格
TRAIL_SPACE=$(grep -c " $" "$DICT_FILE")
if [ "$TRAIL_SPACE" -gt 0 ]; then
    echo "警告: $TRAIL_SPACE 行有尾部空格"
fi

echo ""
echo "=== 验证完成 ==="

6.8 CI/CD 集成词典

6.8.1 GitHub Actions 词典检查

# .github/workflows/spellcheck.yml
name: Spell Check

on: [push, pull_request]

jobs:
  spellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Hunspell
        run: sudo apt-get install -y hunspell hunspell-en-us

      - name: Run spell check
        run: |
          # 使用项目词典
          ERRORS=$(find . -name "*.md" -not -path "./.git/*" \
            -exec cat {} \; | \
            hunspell -d en_US -p .hunspell/project.dic -l | \
            sort -u)
          
          if [ -n "$ERRORS" ]; then
            echo "发现拼写错误:"
            echo "$ERRORS"
            exit 1
          fi
          echo "拼写检查通过 ✓"

6.8.2 pre-commit 钩子

#!/bin/bash
# .git/hooks/pre-commit - 拼写检查钩子

# 获取暂存的 Markdown 文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.md$')

if [ -z "$STAGED_FILES" ]; then
    exit 0
fi

echo "运行拼写检查..."

ERRORS=""
for file in $STAGED_FILES; do
    file_errors=$(hunspell -l -d en_US -p .hunspell/project.dic "$file" 2>/dev/null)
    if [ -n "$file_errors" ]; then
        ERRORS="$ERRORS\n$file:\n$file_errors"
    fi
done

if [ -n "$ERRORS" ]; then
    echo -e "拼写检查失败:$ERRORS"
    echo ""
    echo "提示: 将正确的词添加到 .hunspell/project.dic"
    echo "或使用 git commit --no-verify 跳过检查"
    exit 1
fi

echo "拼写检查通过 ✓"
exit 0

6.9 业务场景案例

6.9.1 技术文档团队

# 技术文档团队的词典管理策略

1. 公司级词典 (.hunspell/company.dic)
   - 公司名称、产品名称、品牌名
   - 公司内部术语
   - 由文档团队负责人维护

2. 产品级词典 (.hunspell/product.dic)
   - 产品特有的技术术语
   - API 名称、端点名称
   - 由各产品团队维护

3. 个人词典 (~/.hunspell_personal)
   - 个人常用词汇
   - 拼写习惯差异(如 color vs colour)
   - 各自维护

4. 加载顺序:
   hunspell -d en_US \
     -p ~/.hunspell_personal \
     -p .hunspell/company.dic \
     -p .hunspell/product.dic \
     document.md

6.9.2 多语言文档

# 多语言文档的词典管理
# 文档中混合中英文技术内容

# 方案 1: 分段检查
# 英文段落用英文词典
grep -P '[a-zA-Z]{3,}' document.md | hunspell -d en_US -l

# 方案 2: 使用排除列表
# 提取所有英文单词,排除已知正确词
cat document.md | \
    grep -oP '\b[a-zA-Z]{3,}\b' | \
    sort -u | \
    hunspell -d en_US -p .hunspell/mixed.dic -l

6.9.3 学术论文

# 学术论文词典

# 学科术语
neural
backpropagation
epoch
tensor
gradient
lstm
transformer
attention
embedding
tokenization
tokenizer

# 引用格式
doi
arxiv
preprint

# 作者常见引用
Vaswani
Bahdanau
Cho
Bengio
Hinton
LeCun

6.10 本章小结

词典类型用途维护者更新频率
系统词典基础语言覆盖发行版维护者随系统更新
个人词典个人习惯词汇用户本人按需
项目词典项目专有术语项目团队随项目迭代
行业词典专业领域词汇行业组织定期

扩展阅读