做中文文本处理,很多团队第一反应是上Elasticsearch或者MongoDB,但如果你的数据量在百万级以内、部署环境只有一台服务器甚至一台笔记本,这套重型方案就显得杀鸡用牛刀了。SQLite作为全球使用最广的嵌入式数据库,单个文件就是一个完整数据库,配合HanLP提供的中文分词、命名实体识别和关键词提取能力,完全可以搭建一套零运维的文本分析系统。这篇文章就带大家把这个组合真正跑起来。

为什么是SQLite加HanLP而不是别的组合
先说SQLite这一端。它是一个纯C实现的嵌入式关系型数据库,整个数据库就是磁盘上的一个文件,不需要单独的数据库服务进程,也不需要配置账号密码。Python、Java、Go等主流语言都内置或者提供了成熟的驱动。对于文本分析场景,SQLite还有一个杀手锏:FTS5全文检索扩展,它支持倒排索引、自定义分词器、BM25相关性排序,功能上覆盖了搜索引擎最核心的部分。
再看HanLP。它是纯Java实现的中文自然语言处理库,同时通过pyhanlp提供了Python接口。HanLP内置了分词、词性标注、命名实体识别、依存句法分析、关键词提取、文本摘要等常用功能,词典和模型开箱即用,不需要自己训练。相比jieba这类轻量分词工具,HanLP在准确率和功能完整度上明显更强,尤其是对地名、机构名、人名的识别能力,在处理新闻类文本时差距非常明显。
两者组合的典型架构是:HanLP负责把非结构化的中文文本转换成结构化的分词结果和实体信息,SQLite负责存储这些结构化数据并建立索引。整个系统不依赖任何外部服务,把数据库文件拷走就等于完成了迁移,这在做内网部署、离线分析工具或者个人知识库项目时特别实用。
环境搭建与基础分词入库
环境搭建分两步。第一步安装HanLP,Python环境推荐用pyhanlp,它要求JRE环境,因为底层是Java实现。第二步确认Python自带的sqlite3模块可用,这个标准库模块直接就能操作SQLite,无需额外安装。
安装命令如下:
# 安装pyhanlp,需要本机有Java运行环境
pip install pyhanlp
# 验证安装
python -c "from pyhanlp import HanLP; print(HanLP.segment('你好,欢迎使用SQLite与HanLP'))"
接下来做最核心的一件事:把一段文本分词后写入SQLite。设计上建议建两张表,一张存原始文档,一张存分词结果,分词结果表以词为单位建立记录,方便后续按词聚合统计。建表语句如下:
-- 原始文档表
CREATE TABLE IF NOT EXISTS documents (
doc_id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 分词结果表,term存词语,nature存词性
CREATE TABLE IF NOT EXISTS tokens (
token_id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id INTEGER NOT NULL,
term TEXT NOT NULL,
nature TEXT,
FOREIGN KEY (doc_id) REFERENCES documents(doc_id)
);
-- 按词建立索引,加速后续查询
CREATE INDEX IF NOT EXISTS idx_tokens_term ON tokens(term);
分词入库的Python代码可以这样写:
import sqlite3
from pyhanlp import HanLP, JClass
# 去掉停用词、只保留名词和动词的简单过滤
def extract_terms(text):
terms = []
for term in HanLP.segment(text):
nature = str(term.nature)
if nature.startswith('n') or nature.startswith('v'):
terms.append((str(term.word), nature))
return terms
conn = sqlite3.connect('corpus.db')
cur = conn.cursor()
doc_text = '中国科学技术大学的研究团队在量子计算领域取得新突破。'
cur.execute('INSERT INTO documents (title, content) VALUES (?, ?)',
('量子计算新突破', doc_text))
doc_id = cur.lastrowid
cur.executemany('INSERT INTO tokens (doc_id, term, nature) VALUES (?, ?, ?)',
[(doc_id, t, n) for t, n in extract_terms(doc_text)])
conn.commit()
conn.close()
这段代码里有个细节值得注意:词性过滤用startswith('n')可以把名词大类(普通名词、地名、机构名、人名等)全部保留下来,这是控制入库词条数量的第一道闸门。如果不做过滤,一篇文档动辄几百个词条,其中虚词占了大量空间,对检索和统计几乎没有价值。
用FTS5打造高性能中文全文检索
把分词结果拆成一条条记录存进tokens表固然灵活,但查询效率会成为瓶颈,尤其是做AND查询时需要多次回表。更优雅的方案是使用SQLite的FTS5虚拟表,它内部维护倒排索引,查询性能和功能都远超手写方案。
FTS5默认按空白符分词,对中文不友好,解决办法是空间换逻辑:先用HanLP把文本分好词,词与词之间用空格拼接,再写入FTS5表。查询时同样把查询语句分词后用空格拼接。完整示例如下:
import sqlite3
from pyhanlp import HanLP
def tokenize_with_space(text):
# HanLP分词后用空格连接,供FTS5使用
words = [term.word for term in HanLP.segment(text)]
return ' '.join(words)
conn = sqlite3.connect('corpus.db')
cur = conn.cursor()
# 创建FTS5虚拟表,不使用内置分词器
cur.execute("CREATE VIRTUAL TABLE IF NOT EXISTS doc_fts USING fts5(title, content)")
def add_document(title, content):
cur.execute('INSERT INTO doc_fts (title, content) VALUES (?, ?)',
(tokenize_with_space(title), tokenize_with_space(content)))
add_document('量子计算新突破', '中国科学技术大学的研究团队在量子计算领域取得新突破。')
def search(query, limit=10):
q = tokenize_with_space(query)
# 使用bm25排序,越靠前相关性越高
sql = """SELECT title, content, bm25(doc_fts) AS score
FROM doc_fts WHERE doc_fts MATCH ?
ORDER BY score LIMIT ?"""
for row in cur.execute(sql, (q, limit)):
print(row)
search('量子计算 研究团队')
conn.commit()
conn.close()
这个方案有几个优势。第一,bm25函数直接给出相关性得分,省去了自己实现排序算法的工作;第二,FTS5支持NEAR、OR、NOT等丰富的查询语法,可以表达相当复杂的检索意图;第三,倒排索引的查询复杂度和词条数量基本无关,数据量涨到几十万条时检索仍然在毫秒级。
代价也不是没有。FTS5表会把分词结果再存一份,磁盘占用大约是原文的两到三倍。另外每次写入都要先调HanLP分词,写入吞吐量取决于分词速度,如果是批量导入场景,建议先把全部分词结果算好再统一执行executemany,配合with conn:语句批量提交事务,速度能快一个数量级。
实战:新闻文本的实体统计与相似文章查询
把前面的基础能力组合起来,就能做一个实用的小型文本分析系统。这里演示两个典型需求:统计一段时间内出现频次最高的命名实体,以及找出与某篇文章最相似的若干篇文档。
第一个需求直接基于tokens表做聚合。HanLP的命名实体识别在标准分词模型中已包含,机构名的词性是nt,地名是ns,人名是nr。统计SQL和Python代码如下:
import sqlite3
conn = sqlite3.connect('corpus.db')
cur = conn.cursor()
# 统计出现次数最多的10个机构名
rows = cur.execute("""
SELECT term, COUNT(*) AS cnt
FROM tokens
WHERE nature = 'nt'
GROUP BY term
ORDER BY cnt DESC
LIMIT 10
""").fetchall()
for term, cnt in rows:
print(f'{term}: {cnt}次')
conn.close()
第二个需求用关键词提取加FTS5来完成。先用HanLP的extractKeyword从目标文章中提取出若干关键词,把它们拼成MATCH查询去检索相似文档。由于关键词本身就是文章语义的浓缩,这种伪相似度检索在实际效果上出奇地好:
import sqlite3
from pyhanlp import HanLP
def find_similar(content, top_n=5):
# 提取文章核心关键词
keywords = HanLP.extractKeyword(content, 10)
query = ' OR '.join(keywords)
conn = sqlite3.connect('corpus.db')
cur = conn.cursor()
rows = cur.execute("""
SELECT title, bm25(doc_fts) AS score
FROM doc_fts
WHERE doc_fts MATCH ?
ORDER BY score LIMIT ?
""", (query, top_n)).fetchall()
conn.close()
return rows
# usage: find_similar('量子计算研究取得进展……')
这套方案的能力边界也要说清楚。SQLite的写并发只有单写多读,如果系统需要多个进程同时高频写入,就得引入写队列或者改用客户端服务器架构的数据库;HanLP的完整模型内存占用在1GB上下,低配机器上要留意;FTS5不支持分布式,数据量超过千万级文档时再考虑迁移到专门的搜索引擎。但只要场景在单机、千万级以下这个区间,SQLite加HanLP这套组合的开发效率和运行稳定性,绝对对得起它的轻量身材。