尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

汉语词典数据库HTTP服务化:从表结构设计到接口实现

汉语词典数据库HTTP服务化:从表结构设计到接口实现 简介这是一份面向中文自然语言处理开发者、前端工程师及汉字数据爱好者的汉语词典数据库资源以JavaScript与JSON格式组织可用于汉字检索、拼音转换、字形分析等场景。资源包共33个文件以28个JSON数据文件和3个JavaScript脚本为主另含说明文档与配置文件压缩包约33.36MB。核心数据收录21104个汉字及标点、数学符号等完整数据细分为词语、带声调与无声调拼音、笔画数、偏旁、来源页面URL及详情HTML与文本等字段同时提供分片数据便于版本比对精简版则剔除详情、URL与HTML另有仅保留汉字的词表及单韵母拼音对照表。脚本文件可用于数据拆分与精简处理目录结构清晰方便按需取用。目前已有510人学习下载适合需要构建汉字查询、拼音标注或字形分析功能的开发者参考使用个人学习适用禁止商用。1. 汉语词典数据库为什么要做成 HTTP 服务手里有一份 chinese-dictionary 的词典数据几万到几十万条词条字段包括词形、拼音、释义、例句、部首、笔画。放在本地用脚本查没问题可一旦要给手机端、桌面端、内部工具同时用麻烦就来了每台机器都要拷一份数据更新一次全量同步一次拼音检索和模糊匹配的逻辑还得在每个端各写一遍。把这份汉语词典数据库包成一个 HTTP 服务本质上是把「数据 查询逻辑」收敛到一个进程里其他端只发请求拿 JSON。这件事适合谁手上有词典数据、想给多个客户端提供统一查询接口的后端或全栈工程师想练手 HTTP 服务设计、又不想拿业务系统开刀的开发者以及需要在内网做离线词典服务、不希望依赖外部接口的团队。它解决的核心问题是数据分发和查询逻辑复用顺带把 HTTP 连接复用、状态码语义、Content-Type 这些平时容易忽略的细节逼着你认真对待。下面按「数据怎么进 → 接口怎么出 → 坑在哪」推一遍。2. 把词典数据装进数据库表结构与导入脚本2.1 词条表怎么设计才够查汉语词典的数据有几个特点一个词可能有多条释义拼音带声调还要支持按拼音首字母、按部首、按笔画数检索。最省事的做法是一张主表加一张释义表而不是把所有释义塞进一个字段。主表存词条级别的属性释义表存一对多的义项。这样查「这个词有几个意思」和「按拼音查词」互不干扰索引也好建。-- 词条主表一个词一行 CREATE TABLE entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL, -- 词形如「银行」 pinyin TEXT NOT NULL, -- 带声调拼音如「yín háng」 pinyin_flat TEXT NOT NULL, -- 无声调拼音用于模糊检索 radical TEXT, -- 部首 strokes INTEGER, -- 总笔画数 UNIQUE(word, pinyin) -- 同形异音词算两条 ); -- 释义表一个义项一行 CREATE TABLE senses ( id INTEGER PRIMARY KEY AUTOINCREMENT, entry_id INTEGER NOT NULL, pos TEXT, -- 词性如「名」「动」 definition TEXT NOT NULL, -- 释义正文 example TEXT, -- 例句 FOREIGN KEY(entry_id) REFERENCES entries(id) ); CREATE INDEX idx_word ON entries(word); CREATE INDEX idx_pinyin_flat ON entries(pinyin_flat); CREATE INDEX idx_radical ON entries(radical, strokes); CREATE INDEX idx_sense_entry ON senses(entry_id);逻辑说明pinyin_flat是冗余字段专门为「用户只敲 yinhang 也能查到」这种场景准备。如果只存带声调的拼音检索时就得在 SQL 里做字符串处理索引直接失效。UNIQUE(word, pinyin)而不是UNIQUE(word)是因为「行」这种字有 xíng 和 háng 两个读音属于两条独立词条。参数说明strokes用整数存方便做范围查询比如「查 8 到 10 画的字」。radical存部首本身而不是部首编号省掉一次关联查询代价是占一点空间词典规模下完全可接受。2.2 导入脚本从原始文本到入库原始词典数据常见格式是每行一条、字段用制表符或特定分隔符隔开。导入时最容易翻车的是编码和空字段。下面是一个可复用的导入脚本骨架。import sqlite3 import re def flatten_pinyin(pinyin: str) - str: 去掉声调符号只留字母用于模糊检索 # 常见带声调元音到无调形式的映射 table str.maketrans( āáǎàēéěèīíǐìōóǒòūúǔùǖǘǚǜ, aaaaeeeeiiiioooouuuuvvvv ) return pinyin.translate(table).replace( , ).lower() def import_dict(src_path: str, db_path: str): conn sqlite3.connect(db_path) cur conn.cursor() cur.execute(PRAGMA foreign_keys ON) with open(src_path, encodingutf-8) as f: for lineno, line in enumerate(f, 1): line line.rstrip(\n) if not line or line.startswith(#): continue parts line.split(\t) if len(parts) 3: print(f第 {lineno} 行字段不足跳过: {line[:30]}) continue word, pinyin, definition parts[0], parts[1], parts[2] example parts[3] if len(parts) 3 else None radical parts[4] if len(parts) 4 else None strokes int(parts[5]) if len(parts) 5 and parts[5].isdigit() else None cur.execute( INSERT OR IGNORE INTO entries(word, pinyin, pinyin_flat, radical, strokes) VALUES (?, ?, ?, ?, ?), (word, pinyin, flatten_pinyin(pinyin), radical, strokes) ) cur.execute(SELECT id FROM entries WHERE word? AND pinyin?, (word, pinyin)) entry_id cur.fetchone()[0] cur.execute( INSERT INTO senses(entry_id, definition, example) VALUES (?, ?, ?), (entry_id, definition, example) ) conn.commit() conn.close() if __name__ __main__: import_dict(dict_raw.txt, chinese_dict.db)逻辑说明INSERT OR IGNORE配合唯一约束保证重复词条不会报错中断适合边导边修数据。每插一条主表就回查一次entry_id虽然多一次查询但避免了依赖lastrowid在批量场景下的歧义。参数说明flatten_pinyin里的映射表覆盖了普通话常用带调元音如果数据里有生僻注音符号需要自行补全。PRAGMA foreign_keys ON必须显式打开SQLite 默认不强制外键不开的话释义表可能挂到不存在的词条上。提示导入前先用file dict_raw.txt确认编码是 UTF-8。GBK 编码的词典文件直接读会抛 UnicodeDecodeError先转码再导。3. 用 HTTP 把查询接口暴露出去路由、参数与响应3.1 最小可用的查询服务用 Python 标准库或 Flask 都行这里用 Flask 写一个最小版本重点看接口设计而不是框架选型。from flask import Flask, request, jsonify import sqlite3 app Flask(__name__) DB chinese_dict.db def get_conn(): conn sqlite3.connect(DB) conn.row_factory sqlite3.Row return conn app.route(/api/word/word, methods[GET]) def query_word(word): conn get_conn() cur conn.cursor() cur.execute(SELECT id, word, pinyin, radical, strokes FROM entries WHERE word?, (word,)) rows cur.fetchall() if not rows: conn.close() return jsonify({error: not found, word: word}), 404 result [] for row in rows: cur.execute( SELECT pos, definition, example FROM senses WHERE entry_id?, (row[id],) ) senses [dict(s) for s in cur.fetchall()] result.append({ word: row[word], pinyin: row[pinyin], radical: row[radical], strokes: row[strokes], senses: senses }) conn.close() return jsonify({data: result}) app.route(/api/search, methods[GET]) def search(): q request.args.get(q, ).strip() if not q: return jsonify({error: missing query parameter q}), 400 conn get_conn() cur conn.cursor() # 同时按词形前缀和无声调拼音前缀匹配 cur.execute( SELECT word, pinyin FROM entries WHERE word LIKE ? OR pinyin_flat LIKE ? LIMIT 20, (q %, q.lower() %) ) items [{word: r[word], pinyin: r[pinyin]} for r in cur.fetchall()] conn.close() return jsonify({query: q, count: len(items), items: items}) if __name__ __main__: app.run(host127.0.0.1, port5000)逻辑说明/api/word/word走精确匹配返回完整释义/api/search?q走前缀匹配只返回词形和拼音用于输入联想。两个接口分开是因为精确查询和联想查询对响应体大小、延迟的要求完全不同混在一起会让前端难做缓存。参数说明LIMIT 20是联想接口的硬上限防止用户输入单个字母时返回全表。q.lower()只对拼音做小写化词形保持原样因为中文没有大小写问题。3.2 状态码和 Content-Type 别乱来HTTP 接口的语义靠状态码和头字段撑起来这块偷懒后面全是坑。场景状态码Content-Type说明查到词条200application/json正常返回词条不存在404application/json错误体也要是 JSON缺少 q 参数400application/json客户端请求有问题数据库文件丢失500application/json服务端自身故障请求方法不对405application/json比如对查询接口发 POSTContent-Type必须是application/json不能是text/html。有些老客户端拿到text/html会直接当网页渲染JSON 字符串原样显示出来用户看到一堆花括号。错误响应也返回 JSON前端才能用同一套解析逻辑处理成功和失败。注意404 和 400 的区别经常被写反。词条不存在是资源不存在用 404参数缺失是请求本身不合法用 400。写反了会让调用方的重试逻辑失效——400 通常不该重试404 在某些场景下可以。3.3 连接复用别让每次查询都重新握手HTTP 连接复用keep-alive在词典服务这种「高频小请求」场景下收益很明显。默认情况下 HTTP/1.1 是开启 keep-alive 的但有几个地方会把它关掉。服务端侧Flask 自带的开发服务器对 keep-alive 支持一般生产环境建议用 gunicorn 加 sync worker并设置合理的--keep-alive参数。客户端侧如果用 Python 的requests一定要用Session而不是每次requests.get。import requests # 错误做法每次请求新建连接 # for w in words: # requests.get(fhttp://127.0.0.1:5000/api/word/{w}) # 正确做法复用 Session底层复用 TCP 连接 session requests.Session() session.headers.update({Accept: application/json}) for w in [银行, 行走, 行人]: resp session.get(fhttp://127.0.0.1:5000/api/word/{w}, timeout3) if resp.status_code 200: data resp.json()[data] print(w, -, len(data), 条读音) else: print(w, 查询失败:, resp.status_code) session.close()逻辑说明Session内部维护连接池对同一 host 的连续请求会复用已建立的 TCP 连接省掉三次握手和 TLS 握手如果上了 HTTPS。批量查词时这个差异在局域网里可能只有几十毫秒但请求量上千后差距会放大到秒级。参数说明timeout3是必须的不设超时的话某个请求卡住会拖垮整个循环。Accept: application/json是礼貌性声明服务端可以据此做内容协商虽然本例没实现。4. 汉语词典 HTTP 服务的避坑与排查4.1 拼音检索查不到无声调字段没建索引现象用户输入yinhang搜不到「银行」但输入yín háng能搜到。原因检索走的是pinyin字段而不是pinyin_flat带声调的拼音和用户输入对不上。或者pinyin_flat字段建了但没建索引查询退化成全表扫描数据量大时超时。解决确认查询语句用的是pinyin_flat并检查idx_pinyin_flat索引存在。导入时flatten_pinyin的映射表要覆盖数据里所有带调字符漏一个就会导致该词条永远搜不到。4.2 请求头过长导致 400现象客户端发查询请求服务端返回HTTP Error 400. A request header field is too long。原因有些客户端把查询词塞进了自定义请求头或者 Cookie 累积过多。HTTP 头字段有长度限制超过就被服务器拒绝。解决查询词一律走 URL 参数或请求体不要放请求头。如果确实需要传长文本改用 POST 加 JSON body。服务端侧可以适当调大 header 大小限制但治本还是改客户端行为。4.3 跨域请求被浏览器拦截现象前端页面用fetch调词典接口控制台报Access to XMLHttpRequest at http://127.0.0.1:8000/... from origin ... has been blocked by CORS policy。原因浏览器同源策略前端页面和词典服务不同端口或不同 host属于跨域。解决服务端加 CORS 响应头。Flask 可以用flask-cors扩展或者手动在响应里加Access-Control-Allow-Origin。生产环境不要图省事写*明确列出允许的来源。4.4 数据库被并发写坏现象多个导入脚本同时跑或者导入时还有查询请求报database is locked。原因SQLite 默认的锁粒度是数据库级写操作会阻塞其他读写。解决导入用单独进程导入期间停掉查询服务或者导入时用PRAGMA journal_mode WALWAL 模式下读写可以并发。但 WAL 不是万能药高频写入场景还是建议换 PostgreSQL。4.5 返回体太大拖慢联想现象输入单个字母a联想接口返回几千条前端卡死。原因LIKE a%在拼音字段上匹配范围太广LIMIT设得太大或没设。解决LIMIT控制在 20 以内并且对单字符查询做特殊处理——要么要求至少输入两个字符要么按词频排序只返回高频词。词典数据里可以加一个frequency字段联想时ORDER BY frequency DESC。5. 让词典服务更耐用的几个进阶技巧5.1 用缓存挡住重复查询词典查询有个特点热门词条被反复查冷门词条几乎没人碰。在服务层加一层内存缓存命中率会很高。from functools import lru_cache lru_cache(maxsize2048) def cached_query_word(word: str): 缓存精确查询结果返回可序列化的 dict conn get_conn() cur conn.cursor() cur.execute(SELECT id, word, pinyin, radical, strokes FROM entries WHERE word?, (word,)) rows cur.fetchall() if not rows: conn.close() return None result [] for row in rows: cur.execute(SELECT pos, definition, example FROM senses WHERE entry_id?, (row[id],)) result.append({ word: row[word], pinyin: row[pinyin], senses: [dict(s) for s in cur.fetchall()] }) conn.close() return result逻辑说明lru_cache按参数缓存函数返回值maxsize2048表示最多缓存 2048 个不同词条。词典服务里热门词条集中这个容量通常够用。参数说明缓存的是不可变结果所以函数返回None或list都行但不能返回数据库连接对象。如果词典数据会热更新需要在更新后调用cached_query_word.cache_clear()。5.2 用 curl 和抓包验证接口行为写完接口别只用浏览器点用curl看原始响应能发现很多前端帮你隐藏的问题。# 看状态码和响应头 curl -i http://127.0.0.1:5000/api/word/%E9%93%B6%E8%A1%8C # 只看状态码 curl -o /dev/null -s -w %{http_code}\n http://127.0.0.1:5000/api/word/不存在的词 # 测联想接口 curl -s http://127.0.0.1:5000/api/search?qyin | python -m json.tool逻辑说明-i把响应头一起打出来确认Content-Type和Content-Length。-o /dev/null -s -w只输出状态码适合写进自动化测试脚本。python -m json.tool把返回的 JSON 格式化方便肉眼检查字段。参数说明URL 里的中文要 percent-encodecurl不会自动编码路径部分。用--data-urlencode配合-G可以自动编码查询参数。5.3 一个我踩过的坑最早做这个服务时我把所有释义拼成一个长字符串存在主表里查询确实快但后来要按词性筛选、要统计每个词有几个义项全得在应用层做字符串切割改一次数据格式就崩一次。后来拆成两张表虽然多了一次关联查询但数据模型清晰了加字段、加索引都不用动已有逻辑。另一个习惯是接口上线前一定用curl把 200、400、404、405 四种状态码各测一遍确认错误响应也是合法 JSON。前端同事最烦的就是成功时拿到 JSON、失败时拿到一坨 HTML 错误页解析逻辑得写两套。词典服务本身不复杂难的是数据模型和接口语义一开始就定对。先把表结构拆清楚再把状态码和 Content-Type 守规矩最后加缓存和连接复用这套东西放到别的数据服务上也一样能用。希望帮到你。本文还有配套的精品资源点击获取
返回列表