bd7635986a
Vue 前端 + FastAPI 后端,含部署脚本与词典数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""从线上 ECDICT 词库下载并导入 MySQL。
|
||
|
||
数据源: https://github.com/skywind3000/ECDICT (开源英汉词典,约 340 万词条)
|
||
|
||
用法(在 backend 目录下):
|
||
python -m scripts.import_dictionary_online
|
||
python -m scripts.import_dictionary_online --preset full
|
||
python -m scripts.import_dictionary_online --preset standard --skip-download
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import sqlite3
|
||
import sys
|
||
import zipfile
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
from urllib.request import urlretrieve
|
||
|
||
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||
CACHE_DIR = BACKEND_DIR / "data" / "cache"
|
||
ECDICT_ZIP_URL = (
|
||
"https://github.com/skywind3000/ECDICT/releases/download/1.0.28/ecdict-sqlite-28.zip"
|
||
)
|
||
ECDICT_ZIP_PATH = CACHE_DIR / "ecdict-sqlite-28.zip"
|
||
ECDICT_DB_PATH = CACHE_DIR / "stardict.db"
|
||
|
||
sys.path.insert(0, str(BACKEND_DIR))
|
||
|
||
from database import SessionLocal # noqa: E402
|
||
from scripts.dictionary_importer import upsert_entries # noqa: E402
|
||
|
||
PRESET_SQL = {
|
||
# 常用词:Collins / 牛津核心词,或词频排名前 5 万
|
||
"standard": """
|
||
translation IS NOT NULL AND translation != ''
|
||
AND (
|
||
collins >= 1 OR oxford = 1
|
||
OR (frq IS NOT NULL AND frq <= 50000)
|
||
)
|
||
""",
|
||
# 核心词:Collins 星级或牛津 3000
|
||
"core": """
|
||
translation IS NOT NULL AND translation != ''
|
||
AND (collins >= 1 OR oxford = 1)
|
||
""",
|
||
# 全量(约 338 万,耗时较长)
|
||
"full": """
|
||
translation IS NOT NULL AND translation != ''
|
||
""",
|
||
}
|
||
|
||
|
||
def download(url: str, dest: Path) -> None:
|
||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||
print(f"正在下载 ECDICT SQLite(约 207MB)…")
|
||
print(f" {url}")
|
||
|
||
def progress(block_num: int, block_size: int, total_size: int) -> None:
|
||
if total_size <= 0:
|
||
return
|
||
done = block_num * block_size
|
||
pct = min(100, done * 100 // total_size)
|
||
mb = done / 1024 / 1024
|
||
total_mb = total_size / 1024 / 1024
|
||
print(f"\r 进度: {pct:3d}% ({mb:.1f}/{total_mb:.1f} MB)", end="", flush=True)
|
||
|
||
urlretrieve(url, dest, reporthook=progress)
|
||
print()
|
||
|
||
|
||
def ensure_ecdict_db(skip_download: bool) -> Path:
|
||
if ECDICT_DB_PATH.exists():
|
||
return ECDICT_DB_PATH
|
||
|
||
if not ECDICT_ZIP_PATH.exists():
|
||
if skip_download:
|
||
print(f"错误: 未找到本地词库,请先下载或去掉 --skip-download", file=sys.stderr)
|
||
sys.exit(1)
|
||
download(ECDICT_ZIP_URL, ECDICT_ZIP_PATH)
|
||
|
||
print(f"正在解压 {ECDICT_ZIP_PATH.name} …")
|
||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||
with zipfile.ZipFile(ECDICT_ZIP_PATH, "r") as zf:
|
||
zf.extract("stardict.db", CACHE_DIR)
|
||
print(f"词库就绪: {ECDICT_DB_PATH}")
|
||
return ECDICT_DB_PATH
|
||
|
||
|
||
def count_entries(db_path: Path, where: str) -> int:
|
||
conn = sqlite3.connect(db_path)
|
||
try:
|
||
cur = conn.execute(f"SELECT COUNT(*) FROM stardict WHERE {where}")
|
||
return int(cur.fetchone()[0])
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def iter_ecdict_rows(db_path: Path, where: str, limit: Optional[int]):
|
||
conn = sqlite3.connect(db_path)
|
||
conn.row_factory = sqlite3.Row
|
||
try:
|
||
sql = f"""
|
||
SELECT word, phonetic, translation
|
||
FROM stardict
|
||
WHERE {where}
|
||
ORDER BY COALESCE(frq, 9999999), word
|
||
"""
|
||
if limit:
|
||
sql += f" LIMIT {int(limit)}"
|
||
cur = conn.execute(sql)
|
||
while True:
|
||
rows = cur.fetchmany(5000)
|
||
if not rows:
|
||
break
|
||
for row in rows:
|
||
yield {
|
||
"word": row["word"],
|
||
"phonetic": row["phonetic"],
|
||
"translation": row["translation"],
|
||
}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def import_from_ecdict(
|
||
db_path: Path,
|
||
preset: str,
|
||
limit: Optional[int],
|
||
batch_size: int,
|
||
) -> int:
|
||
where = PRESET_SQL[preset]
|
||
total_expected = count_entries(db_path, where)
|
||
if limit:
|
||
total_expected = min(total_expected, limit)
|
||
print(f"预设: {preset},预计导入 {total_expected:,} 条")
|
||
|
||
session = SessionLocal()
|
||
imported = 0
|
||
buffer: list[dict] = []
|
||
try:
|
||
for row in iter_ecdict_rows(db_path, where, limit):
|
||
buffer.append(row)
|
||
if len(buffer) >= batch_size:
|
||
imported += upsert_entries(session, buffer, source="ecdict", batch_size=batch_size)
|
||
buffer.clear()
|
||
print(f"\r 已导入 {imported:,} / {total_expected:,}", end="", flush=True)
|
||
if buffer:
|
||
imported += upsert_entries(session, buffer, source="ecdict", batch_size=batch_size)
|
||
print()
|
||
except Exception:
|
||
session.rollback()
|
||
raise
|
||
finally:
|
||
session.close()
|
||
return imported
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="从线上 ECDICT 下载并导入离线翻译词典")
|
||
parser.add_argument(
|
||
"--preset",
|
||
choices=list(PRESET_SQL.keys()),
|
||
default="standard",
|
||
help="standard=常用约 83 万 | core=核心约 1.4 万 | full=全量约 338 万(默认 standard)",
|
||
)
|
||
parser.add_argument("--limit", type=int, default=None, help="最多导入条数(调试用)")
|
||
parser.add_argument("--batch-size", type=int, default=2000, help="MySQL 批量写入大小")
|
||
parser.add_argument("--skip-download", action="store_true", help="跳过下载,使用本地 cache")
|
||
args = parser.parse_args()
|
||
|
||
db_path = ensure_ecdict_db(args.skip_download)
|
||
imported = import_from_ecdict(db_path, args.preset, args.limit, args.batch_size)
|
||
print(f"导入完成: {imported:,} 条(source=ecdict, preset={args.preset})")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|