spaCy是自然语言处理领域广泛使用的开源库,它的实体识别功能可以快速从文本中提取出人名、地名、机构名等命名实体。但识别出来的结果如果只是打印在控制台里,项目一关数据就没了,更谈不上后续的分析和复用。这篇文章就用一个完整的实战项目,演示如何把spaCy识别出的实体数据存入SQLite数据库,并实现高效的查询和统计,最终搭起一套可以反复使用的NLP数据管理流程。

一、项目准备:环境搭建与数据库表设计
开始之前需要安装两个核心依赖:spaCy负责实体识别,SQLite则内置于Python标准库中,无需额外安装。如果选用轻量级的封装,也可以用sqlite3模块直接操作。spaCy安装完成后还要下载对应的语言模型,中文场景常用zh_core_web_sm,英文场景常用en_core_web_sm。
表结构设计是这类项目最容易出错的地方。很多人图省事只建一张表,把整句话和实体文本塞进去,结果后期想按实体类型统计时才发现字段粒度不够。这里推荐两张表的方案:一张entities表存实体明细,一张documents表存原始文档,两表通过文档ID关联。这样既保留了上下文信息,又能对实体本身做灵活查询。
import sqlite3
# 建表语句,主键自增,doc_id关联文档表
def init_db(db_path="ner_data.db"):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS documents (
doc_id INTEGER PRIMARY KEY AUTOINCREMENT,
raw_text TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS entities (
entity_id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id INTEGER NOT NULL,
entity_text TEXT NOT NULL,
entity_label TEXT NOT NULL,
start_pos INTEGER,
end_pos INTEGER,
FOREIGN KEY (doc_id) REFERENCES documents(doc_id)
)
""")
conn.commit()
return conn
init_db()
start_pos和end_pos两个字段存的是实体在原文中的字符偏移位置,这是很多人容易忽略的细节。有了这两个字段,后续可以把实体高亮回原文,也能验证数据提取的准确性。如果不存位置信息,一旦需要做可视化或者前后文分析,就只能重新跑一遍识别流程了。
二、把spaCy识别结果批量写入SQLite
表建好之后,下一步是把spaCy的处理结果转换成数据库记录。spaCy识别出的每个实体是一个Span对象,包含text、label_、start_char、end_char等属性,正好与表字段一一对应。写入时建议使用executemany批量插入,比逐条execute快很多,尤其是处理大批量语料时差距明显。
import spacy
nlp = spacy.load("zh_core_web_sm")
def extract_and_save(texts, conn):
cursor = conn.cursor()
for text in texts:
doc = nlp(text)
# 先插入文档,拿到doc_id
cursor.execute(
"INSERT INTO documents (raw_text) VALUES (?)", (text,)
)
doc_id = cursor.lastrowid
# 收集该文档所有实体,批量插入
rows = [
(doc_id, ent.text, ent.label_, ent.start_char, ent.end_char)
for ent in doc.ents
]
if rows:
cursor.executemany(
"INSERT INTO entities "
"(doc_id, entity_text, entity_label, start_pos, end_pos) "
"VALUES (?, ?, ?, ?, ?)",
rows
)
conn.commit()
texts = [
"华为公司在深圳发布了新款手机。",
"张三于2023年10月访问了北京清华大学。"
]
conn = init_db()
extract_and_save(texts, conn)
这段代码有几个值得注意的点。第一,commit放在整个批次结束后统一执行,而不是每插一条就提交一次,频繁提交会显著拖慢写入速度。第二,如果一段文本没有任何实体,rows为空列表,跳过插入即可,不需要特殊处理。第三,spaCy处理长文本时建议先做分句,单条记录对应的文本越短,后续按文档定位数据的体验越好。
实际项目中还可能遇到重复数据问题。同一段文本被两次入库,实体记录也会翻倍。解决办法是在documents表的raw_text字段上建唯一索引,插入前先查重,或者用INSERT OR IGNORE语句,让数据库自动跳过重复文档。
三、高效查询:按类型、文本和组合条件检索实体
数据入库后,价值体现在检索上。最常见的三类查询是:按实体类型统计、按关键词模糊搜索、按文档反查上下文。SQLite的SQL能力完全可以覆盖这些场景。
def query_entities(conn):
cursor = conn.cursor()
# 查询一:统计每种实体类型的数量
cursor.execute("""
SELECT entity_label, COUNT(*) AS cnt
FROM entities
GROUP BY entity_label
ORDER BY cnt DESC
""")
print("实体类型统计:", cursor.fetchall())
# 查询二:模糊搜索包含"大学"的机构实体
cursor.execute("""
SELECT entity_text, entity_label
FROM entities
WHERE entity_label = 'ORG' AND entity_text LIKE '%大学%'
""")
print("机构搜索:", cursor.fetchall())
# 查询三:根据实体反查原文上下文
cursor.execute("""
SELECT d.raw_text, e.entity_text, e.start_pos
FROM entities e
JOIN documents d ON e.doc_id = d.doc_id
WHERE e.entity_text = '北京'
""")
print("上下文查询:", cursor.fetchall())
query_entities(init_db())
上面三个查询覆盖了日常分析的绝大多数需求。JOIN查询把实体和原文串起来,是这类表结构设计的核心收益。如果想进一步提升查询速度,可以在entity_label和entity_text上分别建立索引:
CREATE INDEX idx_label ON entities(entity_label); CREATE INDEX idx_text ON entities(entity_text);
索引不是越多越好,它会拖慢写入速度并占用额外磁盘空间。这个项目里实体表通常是读多写少的场景,加索引收益明显;如果还在持续大批量写入阶段,可以等数据灌完之后再统一建索引。
四、进阶技巧:去重、导出与数据库维护
项目跑起来之后,还有几件事值得做。首先是去重策略,除了文档级别的唯一索引,实体级别也可以按doc_id加entity_text加start_pos建联合唯一约束,防止同一条实体被重复插入。其次,分析结果经常需要导出给其他工具使用,SQLite的查询结果可以直接写成CSV,配合Python的csv模块几行代码就能完成。
import csv
def export_to_csv(conn, out_path="entities.csv"):
cursor = conn.cursor()
cursor.execute("""
SELECT d.raw_text, e.entity_text, e.entity_label,
e.start_pos, e.end_pos
FROM entities e
JOIN documents d ON e.doc_id = d.doc_id
""")
headers = ["原文", "实体", "类型", "起始位置", "结束位置"]
with open(out_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(headers)
writer.writerows(cursor.fetchall())
print("导出完成:", out_path)
export_to_csv(init_db())
编码问题要特别留心。导出CSV如果用utf-8编码,用Excel打开可能出现乱码,改成utf-8-sig会在文件头写入BOM标记,Excel就能正确识别中文了。这是数据处理项目中反复出现的小坑,提前规避能省不少排查时间。
最后是数据库本身的维护。SQLite是单文件数据库,备份只需要复制.db文件,但更稳妥的方式是使用官方提供的备份接口,它能在数据库被占用时也能安全完成快照:
def backup_db(src_path="ner_data.db", dst_path="backup.db"):
src = sqlite3.connect(src_path)
dst = sqlite3.connect(dst_path)
with dst:
src.backup(dst)
src.close()
dst.close()
print("备份完成")
backup_db()
整个项目跑通之后,你就拥有了一条完整的流水线:文本进来,实体识别,数据入库,按需查询,随时导出。这套结构同样适用于关键词抽取、文本分类结果的存储,只需调整几个字段就能复用。SQLite配合spaCy这种轻量级组合,对于个人项目、原型验证和中小规模语料管理来说,是性价比非常高的选择。