Python从Word批量提取校徽并构建FTS5检索与感知哈希去重流水线
发布时间:2026/9/18 1:21:00来源:尧图网络
简介面向高校师生、招生宣传与校园文化爱好者整理的《中国大学最全最完整的标志图校徽》文档系统收录全国各类高校的标志与校徽图像帮助读者快速识别院校、了解标识背后的历史沿革与学科特色。内容按院校类型与地域分布编排涵盖综合、理工、农林、医药、师范、财经、艺术体育及民族院校等并涉及北大“红楼”、人大“实事求是”、北航飞机元素、协和医学院医学符号、农林院校自然元素等设计要点可作为校史梳理、视觉设计与校际对比的素材。资源为单一doc文件文件总数1个压缩包约21.96MB以图片与文字说明为主体量适中、便于离线查阅与二次整理。目前已有1517人浏览学习。对于需要集中比对各校校徽、提炼设计语言或准备校园文化展示的读者这份合集省去逐一检索官网的时间具备较高的参考与收藏价值。1. 一份 .doc 里的上千枚校徽为什么要先拆成资产库需求方甩过来一个文件中国大学最全最完整的标志图校徽最全.doc。打开一看几百上千枚校徽挤在 Word 里有的图下面跟着校名有的校名在图上边有的什么都不写还有一小撮是 WMF 矢量图或 OLE 嵌入对象。对方的要求通常是做个能搜的小程序、能按规格下载、能出一份带编号的目录清单。这时候最好先把预期压下来真正花时间的不是写检索接口而是把这份文档还原成「图片 校名 多规格文件」的三元组。做过一次就知道直接从 Word 里肉眼复制粘贴一千枚图能磨掉两天而且还漏、还重、还叫错名。把提取、规范化、索引做成一条可重跑的流水线后面新增学校只需要把新图丢进 inbox 目录。接下来的路径是从 .doc 解包出原始图片并绑定校名把图统一成透明底多规格资产用 SQLite 建可检索索引并暴露接口最后用感知哈希去重并把结果回导成一份新的 .docx 目录。适合后端、数据工程和做校园类产品的同学只要能跑 Python 就能跟着落地。注意校徽属于学校标识涉及商标与著作权素材用于商用前需要取得相应授权流水线只解决技术问题。2. 用 python-docx 从 .doc 中批量提取校徽原图与校名对应关系2.1 老版 .doc 先转 docxsoffice 无头转换的必调参数二进制 .doc 无法直接按 zip 解析先用 LibreOffice 无头模式转成 docx。命令本身很短坑在参数和并发上# 单文件转换filter 名写全避免某些版本默认转成 odt soffice --headless \ --convert-to docx:MS Word 2007 XML \ --outdir ./converted \ ./raw/中国大学最全最完整的标志图校徽最全.doc # 批量并发时每个进程必须给独立 profile否则第二个进程直接退出 for f in ./raw/*.doc; do soffice --headless \ -env:UserInstallationfile:///tmp/lo_$RANDOM \ --convert-to docx:MS Word 2007 XML \ --outdir ./converted $f done参数含义--headless不弹界面适合服务器--convert-to后面用扩展名:FilterName的形式显式指定过滤器只写docx在部分版本上会走到不确定的默认过滤器--outdir必须存在否则静默失败-env:UserInstallationfile:///tmp/lo_xxx是并发关键LibreOffice 用用户 profile 加锁同 profile 并发会互相踢掉。转换完成后先做一次体检对比转换前后的媒体文件数量和体积。老 .doc 里的 WMF 有可能在转换过程中被光栅化成低分辨率 PNG如果发现word/media/里的图片明显变小就别用转换结果改走 2.3 的原始解包路径。2.2 按段落顺序绑定图片与校名rId 到 image part 的映射docx 里正文图片通过r:embed关系 ID 指向word/media/下的实体文件段落顺序就是校名和图片的天然对应关系。python-docx 没有现成的「取图并取邻近文字」接口自己遍历 XML 更稳# extract_logos.py import os, json from docx import Document from docx.oxml.ns import qn NS {a: http://schemas.openxmlformats.org/drawingml/2006/main} def iter_images_with_context(docx_path, ctx_window3): doc Document(docx_path) ctx [] # 最近若干段非空文字作为校名候选 for idx, para in enumerate(doc.paragraphs): text para.text.strip() if text: ctx.append(text) ctx ctx[-ctx_window:] # 只留最近 3 段防止串到上一所学校 for blip in para._p.findall(.//a:blip, NS): rid blip.get(qn(r:embed)) if not rid: continue part doc.part.related_parts.get(rid) if part is None: continue yield { para_index: idx, rid: rid, name_hint: ctx[-1] if ctx else , ext: os.path.splitext(str(part.partname))[1].lower(), blob: part.blob, } if __name__ __main__: rows list(iter_images_with_context(./converted/校徽.docm.docx)) with open(raw_manifest.jsonl, w, encodingutf-8) as fp: for i, r in enumerate(rows): out f./raw_media/{i:05d}{r[ext]} os.makedirs(./raw_media, exist_okTrue) with open(out, wb) as im: im.write(r[blob]) fp.write(json.dumps({k: v for k, v in r.items() if k ! blob}, ensure_asciiFalse) \n) print(extracted, len(rows))逻辑说明ctx是一个滑动窗口只保留最近 3 段文字因为 Word 里常见的排版是「校名 — 图片 — 空白段 — 下一校名」窗口开太大会把上一所学校的名字带进来。related_parts是 rId 到 part 的映射同一个图片被引用多次时 part 相同用rid去重可以避免重复落盘。para._p.findall(.//a:blip, NS)用命名空间前缀写 XPath比触摸inline_shapes更可靠因为浮动图片wp:anchor不会出现在inline_shapes里。一个容易忽略的位置表格里的图片。很多「最全版」文档用三列表格排版图片全在单元格里。补一段for table in doc.tables: for cell in ...: cell.paragraphs的递归遍历复用上面同一套 blip 查找逻辑即可。2.3 WMF/EMF 与 OLE 嵌入提取失败时的三条兜底路径老文档里的校徽有三种形态处理路径完全不同素材形态在 docx 中的表现推荐处理路径主要风险位图 JPG/PNGword/media/image*.png直接落盘走规范化白底、分辨率参差矢量 WMF/EMFword/media/image*.wmfmagick 或 inkscape 转 300dpi PNG线宽丢失、字体缺失OLE 嵌入对象word/embeddings/oleObject*.binLibreOffice 转 PDF 后按页裁切定位不准、需人工兜底先把包结构列出来确认命中了哪一种# docx 本质是 zip先看清单再决定策略 unzip -l ./converted/校徽.docx | grep -E media|embeddings # WMF/EMF 转高分辨率位图密度给到 300 足够印刷级 magick -density 300 -background none ./raw_media/00012.wmf ./png/00012.png # 没有 ImageMagick 时用 Inkscape导出尺寸用 DPI 控制 inkscape ./raw_media/00012.wmf --export-typepng --export-dpi300 --export-filename./png/00012.png # OLE 兜底整体转 PDF再用 pdftocairo 按页导出矢量 soffice --headless --convert-to pdf --outdir ./pdf ./converted/校徽.docx pdftocairo -svg -f 12 -l 12 ./pdf/校徽.pdf ./svg/00012.svg参数上-density 300是矢量转位图的渲染密度直接决定输出像素-background none保留透明通道少了它圆形校徽会被填成白底。pdftocairo -f/-l是起止页码页码要和 manifest 里的para_index对齐这个对齐关系需要人工抽查十几条确认不要假设完全一致。提示三种路径处理完的图片统一进raw_media/并在 manifest 里标记source_kind后面规范化时只对这一层做操作原始文件永远不动方便回滚。3. 校徽图片规范化透明底、统一尺寸与多规格输出3.1 去白底与自动裁边alpha 阈值怎么定校徽多是圆形或盾形图案配白底前端展示时白底会露出方形边框所以必须去底加透明。朴素做法是「接近白色的像素 alpha 置 0」直接这么干会在深色线条边缘留下一圈硬白边因为抗锯齿像素是灰白渐变的。正确的做法是按白度做线性衰减# normalize.py import numpy as np from PIL import Image def normalize(src, dst, size1024, white250, soft12, pad_ratio0.06): im Image.open(src).convert(RGBA) arr np.array(im).astype(np.float32) rgb, alpha arr[..., :3], arr[..., 3] # 用三通道最小值衡量“白度”只有 R/G/B 同时接近 255 才算背景 whiteness rgb.min(axis2) # soft 区间内线性过渡避免边缘出现硬白圈 factor np.clip((white - whiteness) / soft, 0.0, 1.0) alpha alpha * factor out Image.fromarray(np.dstack([rgb, alpha]).astype(uint8), RGBA) bbox out.getbbox() # 基于 alpha0 求有效内容边界 if bbox: out out.crop(bbox) # 等比缩放到最长边不超过 size再补一圈留白 w, h out.size scale size / max(w, h) if scale 1: out out.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS) pad int(size * pad_ratio) canvas Image.new(RGBA, (size 2 * pad, size 2 * pad), (0, 0, 0, 0)) canvas.paste(out, ((canvas.width - out.width) // 2, (canvas.height - out.height) // 2)) canvas.save(dst, optimizeTrue) return dst normalize(./raw_media/00012.png, ./logos/00012/logo1024.png)参数怎么调white250是判定为纯背景的阈值调低会开始吃掉浅灰线条调高则白色残留soft12是过渡带宽校徽线条细就调大到 1620色块粗就调到 68pad_ratio0.06给四周留 6% 空白前端圆形头像裁剪时才不会切到图案边缘。getbbox()依赖 alpha 通道所以裁边必须放在去底之后。3.2 多规格输出与目录结构约定一次规范化同时产出所有需要的规格避免前端临时压缩文件名尺寸格式用途logo1024.png1024padPNG详情页、印刷备用logo512.png512padPNG列表大图logo256.webp256padWebP移动端列表体积约为 PNG 的 1/4logo128.webp128padWebP排行榜、标签logo.svg原始矢量SVG存在矢量源时保留无限缩放logo.ico多尺寸ICO桌面端与收藏夹图标目录按 slug 分文件夹一个学校一个目录内部文件名固定这样接口只要返回 slug前端自己拼路径就行。给 slug 定规则小写、用连字符、不带中文例如peking-university重名的学校加城市后缀区分。所有规格都从 1024 那一份缩放生成保证视觉一致不要各自从原图缩否则圆角粗细会有肉眼可见的差异。3.3 命名与元数据slug、拼音、英文名三者对齐校徽库能不能用八成取决于元数据表准不准。一份catalog.json建议至少包含这些字段slug、name_zh全称、name_short常用简称、name_en、pinyin、pinyin_initials、aliases数组放「北大」「PKU」这类别称、source_kind、file_1024、file_256、file_svg、phash、updated_at。拼音字段不能省用户搜「beida」或「bd」时靠它命中拼音用 pypinyin 生成from pypinyin import lazy_pinyin, Style def pinyin_fields(name): return { pinyin: .join(lazy_pinyin(name)), pinyin_initials: .join(lazy_pinyin(name, styleStyle.FIRST_LETTER)), } print(pinyin_fields(北京大学)) # {pinyin: beijingdaxue, pinyin_initials: bjdx}name_short和aliases只能半自动生成做法是把原始文档里的文字全部抽出来跑一遍频次统计出现次数高且长度在 26 字之间的词组作为候选然后人工过一遍。这一步别省很多校徽库搜不到不是因为接口烂是因为别名表里没有用户真实会输入的那个词。4. 校徽检索服务拼音、别名与模糊匹配的索引设计4.1 SQLite FTS5 建索引表结构与建表语句数据量在万级以内SQLite 完全够用别一上来就上搜索引擎。校名是短文本中文分词器反而会把「北京大学」切成不确定的词用 FTS5 的 trigram 分词更合适它按三字符滑动窗口建索引天然支持任意子串匹配-- logos.sql CREATE TABLE IF NOT EXISTS school_logo ( slug TEXT PRIMARY KEY, name_zh TEXT NOT NULL, name_short TEXT, name_en TEXT, aliases TEXT, -- JSON 数组检索时拼成一行存全文 pinyin TEXT NOT NULL, pinyin_initials TEXT, file_1024 TEXT, file_256 TEXT, file_svg TEXT, phash TEXT, updated_at TEXT ); CREATE INDEX IF NOT EXISTS idx_phash ON school_logo(phash); -- trigram 需要 SQLite 3.34用 content 做外部内容表省一份存储 CREATE VIRTUAL TABLE IF NOT EXISTS school_logo_fts USING fts5( name_zh, name_short, name_en, aliases, pinyin, pinyin_initials, contentschool_logo, content_rowidrowid, tokenizetrigram ); -- 首次及每次重建后同步一次 INSERT INTO school_logo_fts(school_logo_fts) VALUES(rebuild);contentschool_logo表示 FTS 表不复制数据只存倒排索引查询时回原表取行content_rowidrowid是关联键必须写。trigram有一个副作用查询串短于 3 个字符时匹配不到所以「北大」这种两字简称要么靠name_short精确等值查要么走 LIKE 兜底接口里两条路都留着。4.2 FastAPI 暴露按校名查校徽的接口接口只做一件事接收用户输入的中文、拼音或简称返回可用的图片路径。关键点是把查询串也做拼音转换再分别命中中文、全拼、首字母三条路径# api.py import sqlite3 from fastapi import FastAPI, Query from pypinyin import lazy_pinyin, Style DB logos.db app FastAPI(titleCampusLogo API) def to_pinyin(q: str): return (.join(lazy_pinyin(q)), .join(lazy_pinyin(q, styleStyle.FIRST_LETTER))) app.get(/api/logo/search) def search(q: str Query(..., min_length1, max_length32), limit: int Query(20, ge1, le50)): q q.strip() full, initials to_pinyin(q) conn sqlite3.connect(DB) conn.row_factory sqlite3.Row sql SELECT slug, name_zh, name_short, aliases, file_256, file_1024 FROM school_logo WHERE name_zh LIKE :kw OR name_short :raw OR pinyin LIKE :py OR pinyin_initials :ini ORDER BY length(name_zh) ASC, name_zh ASC LIMIT :limit rows conn.execute(sql, { kw: f%{q}%, raw: q, py: f%{full}%, ini: initials, limit: limit, }).fetchall() return {query: q, count: len(rows), items: [dict(r) for r in rows]}min_length和max_length不是装饰q会拼进 LIKE长度失控会拖慢全表扫描ORDER BY length(name_zh) ASC让「北京大学」排在「北京大学医学部」前面符合用户的直觉预期pinyin_initials用等值而不是 LIKE因为首字母是顶点查询用 LIKE 会命中一堆无关项。调用验证curl -s http://127.0.0.1:8000/api/logo/search?qbjdx | python -m json.tool curl -s http://127.0.0.1:8000/api/logo/search?q%E5%8C%97%E5%A4%A7limit54.3 模糊匹配阈值与常见误召回精确路径覆盖不到拼错的输入需要一层模糊匹配用 rapidfuzz 做候选打分但阈值必须卡住否则「中大」会同时召回好几所学校用户输入期望结果命中策略误召回风险北京大學北京大学繁简归一 精确低bj daxue北京大学去空格后拼音匹配中短串易撞中大中山大学 / 中南大学别名表 属地加权高必须给多个结果huazhong华中科技大学等拼音前缀 人工排序高同名高校多打分链路写成三步先用fuzz.ratio对name_zh name_short aliases逐个算分取score_cutoff70然后对分数在 7085 之间的结果用partial_ratio复算一遍避免「华科」这种子串被整体相似度压低最后按「别名精确命中 简称命中 全称包含 拼音包含」重排序。繁简归一用 opencc 或一张小型字符映射表即可校名里的繁体字量很小。提示模糊匹配只用于展示层的候选推荐写库和导出时永远用 slug 做主键别把用户输入直接当文件名否则路径穿越和非法字符问题会在后面集中爆发。5. 去重、质量校验与回导 Word 目录的进阶做法5.1 感知哈希去重与人工复核清单「最全版」文档最大的隐性问题是同一所学校出现两三次图案还略有差别——一次是横版、一次是圆形、一次带中文、一次只带英文。用感知哈希粗筛最快import imagehash from PIL import Image def phash_of(path, size16): # hash_size 越大越敏感16 对应 256 位比默认 8 更适合区分校徽 return imagehash.phash(Image.open(path).convert(L), hash_sizesize) def hamming(a, b): return bin(int(str(a), 16) ^ int(str(b), 16)).count(1) h1, h2 phash_of(./logos/a/logo256.webp), phash_of(./logos/b/logo256.webp) print(hamming(h1, h2)) # 6 视为疑似同图进入复核清单阈值给 6 是个经验值调小会漏、调大会把「圆形 麦穗」这类构图相似的完全不同的校徽也拉进来。所以 phash 只做粗筛复核按三档走哈希距离 ≤ 6 且长宽比差 5% 标为「高度疑似」哈希距离 ≤ 10 标为「待复核」其余直接放过。把结果导出成一张 CSV字段给slug_a、slug_b、distance、ratio_diff、verdict人工只需填最后一列。校验还有两项必做一是用 Pillow 打开每个输出文件确认没有半截损坏图Image.open(p).load()二是统计透明像素占比占比超过 95% 的图基本是去底参数调过头把图案吃掉了。回导一份可交付的目录文档用 python-docx 生成固定列宽的表格图片塞进单元格的 run 里from docx import Document from docx.shared import Cm doc Document() table doc.add_table(rows0, cols4) table.style Table Grid for row in rows: # rows 来自 catalog.json cells table.add_row().cells cells[0].text row[name_zh] cells[1].text row[name_short] or cells[2].text row[pinyin_initials] cells[3].paragraphs[0].add_run().add_picture(row[file_256], widthCm(1.6)) doc.save(./dist/中国高校校徽目录.docx)add_picture只能挂在 run 上宽高必须显式给否则 Word 会按原始像素渲染1024 的图能把表格撑爆固定 1.6 厘米宽配合Table Grid样式导出后行列对齐基本不用再调。整条流水线建议做成make extract make normalize make index make export四步中间产物分目录落盘任何一步失败都能从上一层重跑不用把一千枚图重新抠一遍。本文还有配套的精品资源点击获取
网站建设高端定制企业官网