用Python拆解看图猜成语小程序:题库生成与前端实现精讲

发布时间:2026/9/11 22:26:30
用Python拆解看图猜成语小程序:题库生成与前端实现精讲 简介一个基于Python与微信小程序搭建的看图猜成语游戏项目面向Python初学者和小程序开发者完整还原了从后端接口到前端交互的开发链路。资源共40个文件压缩包仅559KB其中6个py脚本实现Flask后端逻辑4个wxml与5个js构成小程序页面交互7个wxss负责视觉样式6个json承担数据配置另有sql数据库脚本、png示例图片以及doc/docx使用与配置说明目录分为Idiom、Flask、weapp-idiom等模块方便按需学习。已有116人学习下载。项目以图片展示、用户输入与结果判断为主线覆盖变量、条件分支、函数封装、文件读写和异常捕获等Python核心知识点可进一步掌握接口编写、小程序事件绑定、数据库设计以及成语与图片的关联存储随包文档还给出配置、启动与排错要点适合课程设计或入门练手。1. 用 Python 拆解看图猜成语小程序从题库设计到成品交付的思路如果把“看图猜成语小程序”当作一个纯前端问题你很快会撞上一堵墙图片从哪来、题目怎么组织、交互逻辑和题库怎么解耦、后续怎么加题而不动代码。用 Python 写这个项目不是为了在手机里跑 Python而是把 Python 当作内容生产、题包生成和逻辑验证的“后方工厂”前端小程序只负责渲染和接收用户输入。这个分工才是这类项目最常见的可靠做法。如果你刚接触小程序开发这个项目是极好的练手对象——它不涉及支付、登录、复杂组件核心只有“出图、猜词、判对错、跳下一题”。对五年以上工程师来说这个标题真正值得关注的是两个容易被忽视的点一是 Python 生成题目数据包时的编码与资源管理二是小程序端拿到中文答案后的比对策略这两处恰恰是大多数开源版“看图猜成语”做得最粗糙的地方。下文会先立住技术选型和数据设计再给出可跑的代码、可调的参数和会踩的坑。2. 把技术选型说透为什么用 Python 做小程序的后端与题库生成2.1 小程序端不能跑 Python那 Python 在这个项目里到底干什么微信小程序的前端运行环境是 JavaScriptWebView 与逻辑层分离它既不支持 Python 代码直接执行也无法直接 import 一个.py文件。因此“基于 Python 所写”的看图猜成语小程序通常不是指小程序本身用 Python 开发而是指整个项目的完整链路中有 Python 参与而且参与得很深。最常见的分工有两类题库与资源生成Python 脚本从原始图片、文字描述等素材出发生成小程序可用的 JSON 题库、图片资源配置表、甚至裁剪压缩后的图片。后端服务小程序通过wx.request请求 Python 编写的后端接口Flask / FastAPI动态拉取题目。数据不在前端写死便于更新题库。实际开发中我个人更推荐“题库生成 静态资源托管”的做法。原因很简单看图猜成语这类小程序的核心玩法对实时性要求不高题目数量通常在几十到几百之间完全可以把题库打成 JSON 文件随小程序包发布省去服务器成本和接口维护。只有当你的题目量上千、需要每日更新或做用户答题记录时再引入 Python 后端。2.2 题目 JSON 结构设计编码、图片 URL 与答案的存储方式无论选哪种模式Python 脚本输出的题目数据结构都要提前定好。它决定了前端编写的复杂度也决定了你将来加题时是否需要改动小程序代码。一个合理的最小结构如下{ version: 20240601, total: 3, questions: [ { id: 1, image: /images/questions/001.png, answer: 鸡飞蛋打, hint: 形容两头落空一无所得, difficulty: 1, options: [鸡飞蛋打, 鸡犬不宁, 鸡鸣狗盗, 鸡毛蒜皮] }, { id: 2, image: https://cdn.example.com/images/002.png, answer: 画蛇添足, hint: 比喻做了多余的事, difficulty: 2, options: [画蛇添足, 画龙点睛, 守株待兔, 掩耳盗铃] } ] }image字段既支持本地路径也支持网络 URL本地路径用于小程序包内图片网络 URL 用于远程资源。answer是标准答案options是四个候选选项注意options数组里必须包含answer否则用户永远选不对。difficulty字段用于后续扩展难度筛选虽然没有它也能跑但加上后不用改结构前端就能做“简单/困难”模式。有一个细节值得专门强调JSON 文件必须使用 UTF-8 编码。Python 的json.dump默认会保证这一点但如果你的图片资源或题目文本来自 Windows 环境要格外小心编码转换问题否则小程序端解析时会出现中文乱码表现就是“页面能打开题目文字全是问号”。2.3 Flask 与 FastAPI 的选择小程序后端接口用哪个框架更稳如果题目量不大但你就是想引入 Python 后端那么框架选择会直接影响开发效率和部署复杂度。目前社区里针对小程序后端用得最多的两个 Python 框架是 Flask 和 FastAPI它们各有明确的适用边界。对比维度FlaskFastAPI上手难度极低源码精简较低但需要理解类型注解性能同步框架适合低并发异步原生适合高并发 IO 场景接口文档需手动集成 Swagger自动生成 OpenAPI 文档数据校验手动写或另加 marshmallowPydantic声明即校验适合场景几十到几百题量、内部使用面向大量用户、需要接口文档对这个项目而言只要你的预期用户量不是“上线即爆”的量级Flask 是更稳妥的选择。原因在于它的代码更少、依赖更少、部署时不容易出现异步环境导致的怪问题。FastAPI 虽然性能好、文档自动生成很诱人但它引入了 Pydantic 和 Uvicorn对初学者排查环境问题的成本更高。如果你决定用 Flask 编写小程序的后端接口下面是最小可用的代码框架from flask import Flask, jsonify, request import json import random app Flask(__name__) def load_questions(): with open(questions.json, r, encodingutf-8) as f: data json.load(f) return data[questions] app.route(/api/question, methods[GET]) def get_question(): questions load_questions() difficulty request.args.get(difficulty, typeint) if difficulty: pool [q for q in questions if q[difficulty] difficulty] else: pool questions question random.choice(pool) return jsonify(question) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这段代码的逻辑不复杂但有三个点需要说明load_questions每次请求都重新读文件开发时方便但上线后频繁磁盘 IO 会拖慢响应改进方案是全局加载一次只在文件变动时重新读取。difficulty参数通过request.args.get获取并转为 int如果用户传了非数字值typeint会使其变为None不会抛异常——这是 Flask 的隐藏容错机制值得记住。random.choice是直接从池子里随机选一题没有做“已答题目排除”用户可能连续抽到同一题。要避免这个体验问题需要为每个用户维护一个已答列表这又引出了会话状态管理复杂度会上升。3. 用 Python 脚本批量生成题库与图片素材从零到一的最小可跑方案3.1 图片名规范化为什么不要用中文文件名看图猜成语的核心素材是图片。很多人最初会直接把图片命名为“鸡飞蛋打.png”放在目录里Python 脚本读取时确实没问题但一旦交给小程序前端使用就会踩坑微信小程序开发者工具对中文路径的支持尚可但在部分 Android 真机或某些 CDN 环境下中文文件名会出现 URL 编码不一致的情况表现为“图片加载不出来”。排查到最后问题往往不在代码而在文件名。常见的做法是用图片对应的题目 ID 作为文件名答案信息只存进 JSON。Python 脚本可以自动完成从“中文名”到“ID 名”的映射和重命名这样既保留了人工整理的便利又避免了前端资源引用的隐患。下面这段脚本演示了如何批量处理一个目录下的图片文件import os import re source_dir ./raw_images target_dir ./processed_images os.makedirs(target_dir, exist_okTrue) for filename in os.listdir(source_dir): if not filename.endswith(.png): continue # 去掉扩展名取中文名部分 name os.path.splitext(filename)[0] # 假设原有的中文名就是答案例如 鸡飞蛋打.png # 用一个字典为每道题分配固定 ID answer_to_id { 鸡飞蛋打: 1, 画蛇添足: 2, 守株待兔: 3, } if name in answer_to_id: new_name f{answer_to_id[name]:03d}.png src_path os.path.join(source_dir, filename) dst_path os.path.join(target_dir, new_name) os.rename(src_path, dst_path) print(f{filename} - {new_name})这段代码有几个值得注意的要点answer_to_id字典是硬编码的实际项目中建议从一个 CSV 或 Excel 题库表读取脚本负责映射不负责维护数据。:03d将 ID 格式化为三位数保证文件名长度一致方便前端按模式匹配。如果图片本身是 JPG 或 WebP 格式判断条件需要同步扩展更稳妥的做法是不以扩展名过滤而是用PIL.Image.open验证文件是否为有效图片。3.2 图片压缩策略小程序包体限制与加载速度的平衡微信小程序主包大小限制是 2MB超过后将无法上传。假设一道题的图片平均 100KB20 张图就接近极限这还不包括代码和其它资源。因此图片压缩不是可选项而是上线前的必经步骤。Python 配合 Pillow 库可以批量完成这项工作以下代码实现了“等比缩放 质量压缩”from PIL import Image import os def compress_image(src_path, dst_path, max_width600, quality70): img Image.open(src_path) if img.width max_width: ratio max_width / img.width new_height int(img.height * ratio) img img.resize((max_width, new_height), Image.LANCZOS) img.save(dst_path, JPEG, qualityquality, optimizeTrue) source_dir ./processed_images target_dir ./compressed_images os.makedirs(target_dir, exist_okTrue) for filename in os.listdir(source_dir): if filename.endswith(.png): src os.path.join(source_dir, filename) dst os.path.join(target_dir, os.path.splitext(filename)[0] .jpg) compress_image(src, dst) original_size os.path.getsize(src) new_size os.path.getsize(dst) print(f{filename}: {original_size // 1024}KB - {new_size // 1024}KB)参数的选取直接关系到用户在小程序里看到的图片清晰度max_width600适合绝大多数手机屏幕的显示密度。微信小程序的image组件默认宽度是 300px在 2 倍屏下实际需要 600px 宽的资源设置太大会浪费包体设置太小会模糊。quality70是 JPEG 压缩的甜点值低于 60 时肉眼可见的压缩痕迹会增多高于 80 时体积下降不明显。转换成 JPEG 而不是保留 PNG是因为成语图片通常是照片、插画或复杂场景JPEG 的压缩率远高于 PNG如果图片包含文字截图或透明背景才需要保持 PNG 格式。3.3 选项干扰项生成让 Python 自动构造“像样”的错误答案看图猜成语的选项设计直接影响游戏质量。如果四个选项毫无相似度用户不用看图片都能猜到答案。常见的错误选项设计思路有三种同字干扰、近义干扰、结构干扰。用 Python 自动生成这些干扰项比纯靠手工录入效率高得多但需要一份候选成语库作为原料。import random idioms [鸡飞蛋打, 鸡犬不宁, 鸡鸣狗盗, 鸡毛蒜皮, 画蛇添足, 画龙点睛, 守株待兔, 掩耳盗铃, 狐假虎威, 亡羊补牢, 自相矛盾, 刻舟求剑] def generate_options(correct_answer, pool, num_options4): options [correct_answer] candidates [w for w in pool if w ! correct_answer] # 优先选与正确答案有相同字的成语作为干扰项 same_char [w for w in candidates if set(w) set(correct_answer)] rest [w for w in candidates if w not in same_char] random.shuffle(same_char) random.shuffle(rest) options.extend(same_char[:num_options - 1]) if len(options) num_options: # 干扰项不够时再从剩余池补齐 options.extend(rest[:num_options - len(options)]) random.shuffle(options) return options for answer in idioms: opts generate_options(answer, idioms) print(f{answer} - {opts})输出的选项列表每次运行都不同因为random.shuffle会打乱顺序。这里的重点是same_char的筛选逻辑用set(w) set(correct_answer)求交集存在共同汉字就认为它是“同字干扰”。对于“鸡飞蛋打”鸡犬不宁和鸡毛蒜皮会被优先选为干扰项用户不能只看单个字就排除所有错误选项。需要说明的是这个干扰项生成策略有一个已知盲区如果候选池里的成语和正确答案之间没有共享汉字但意思相近就不会被选中。例如“画蛇添足”和“多此一举”是近义关系但两者没有共同汉字。要覆盖这种情况需要额外引入一个近义词映射表Python 脚本负责把近义词也纳入候选集。投入产出比是否划算取决于你的题目总量——题少时手工整理反而更快。4. 微信小程序端的实现页面结构、数据绑定与核心交互4.1 目录结构与静态题库的引用方式小程序端的代码结构与普通微信小程序无异但针对“看图猜成语”这个特定场景目录组织有一个推荐实践miniprogram/ ├── pages/ │ └── game/ │ ├── game.wxml │ ├── game.wxss │ ├── game.js │ └── game.json ├── images/ │ └── questions/ │ ├── 001.jpg │ ├── 002.jpg │ └── 003.jpg ├── data/ │ └── questions.json └── app.json关键点是data/questions.json这个目录划分。很多入门教程会把题库直接写进game.js的data对象里这样做在题目只有三五道时没有问题但题量上来后game.js会变得极度臃肿而且setData和刷新逻辑缠绕在一起。把题库独立成 JSON 文件然后通过require引入是更干净的分离方式// game.js const questionBank require(../../data/questions.json); Page({ data: { currentIndex: 0, currentQuestion: null, selectedOption: , showResult: false, isCorrect: false }, onLoad() { this.initGame(); }, initGame() { const first questionBank.questions[0]; this.setData({ currentQuestion: first, currentIndex: 0 }); } });这里有一个微信小程序的隐藏知识require一个 JSON 文件时路径是相对于当前 JS 文件的且不需要写扩展名。另外由于小程序打包机制的限制JSON 文件里不能包含注释所以你在 Python 生成题库时不要把带有//注释的 JSON 直接放进小程序目录要先做一次压缩或去注释处理。4.2 猜题页面的 WXML 结构与选项点击判定页面模板是整个小程序最直观的部分。图片展示、选项列表、反馈区域三层结构清晰分离便于后期调整样式view classcontainer image classquestion-image src{{currentQuestion.image}} modeaspectFit/image view classoptions-grid view wx:for{{currentQuestion.options}} wx:keyindex classoption-item {{selectedOption item ? selected : }} bindtaponOptionTap >onOptionTap(e) { const selected e.currentTarget.dataset.option; const correct this.data.currentQuestion.answer; // 统一去掉首尾空格后比较 const normalizedSelected selected.replace(/\s/g, ).trim(); const normalizedCorrect correct.replace(/\s/g, ).trim(); const isCorrect normalizedSelected normalizedCorrect; this.setData({ selectedOption: selected, showResult: true, isCorrect: isCorrect }); }处理逻辑不复杂但replace(/\s/g, )这一步很多人会忽略。它把字符串里所有空白字符包括全角空格、制表符都删除再比较虽然代价是“对答案内部的空格完全不敏感”但对于成语这种固定四字结构而言这个取舍是值得的——你不会希望用户因为不小心打了个空格被判错。4.4 修改导航栏标题动态设置当前关卡或难度标识看图猜成语通常会分关卡或难度很多人会把“第 X 关”“难度简单”这类信息放在导航栏标题上。微信小程序的原生导航栏支持动态修改标题只改game.json里的静态配置是不够的需要在 JS 中调用 API 或者使用页面配置{ navigationBarTitleText: 看图猜成语, navigationBarBackgroundColor: #4A90D9, navigationBarTextStyle: white }这是在game.json中设置的静态标题。如果每关的标题不同需要在 JS 里调用wx.setNavigationBarTitlechangeLevel(levelName) { wx.setNavigationBarTitle({ title: 看图猜成语 - ${levelName} }); }如果题目按难度分文件存储例如easy.json、hard.json在切换题库时同步调用这个 API 即可。注意wx.setNavigationBarTitle的调用时机要在setData之后或之前都没有问题它和页面数据不在同一条渲染链路上但建议在页面onLoad或onShow里做一次设置避免用户从别的页面返回时标题还停留在上一关的状态。5. 题库更新与版本迭代Python 脚本在前、小程序发版在后的协作流程5.1 题库去重与完整性校验脚本当题库规模增长到几十甚至上百题后人为录入错误的概率显著上升。常见的错误包括选项里没有正确答案、答案重复、图片文件缺失、JSON 格式错误。与其上线后由用户发现不如在 Python 侧做一轮自动化校验。下面的脚本可以帮你提前抓住大部分问题import json def validate_questions(file_path, image_dirNone): with open(file_path, r, encodingutf-8) as f: data json.load(f) questions data.get(questions, []) errors [] seen_answers set() for idx, q in enumerate(questions): qid q.get(id, idx 1) if not q.get(answer): errors.append(f第 {qid} 题缺少答案) if q.get(answer) in seen_answers: errors.append(f第 {qid} 题答案重复: {q.get(answer)}) seen_answers.add(q.get(answer)) options q.get(options, []) if len(options) ! 4: errors.append(f第 {qid} 题选项数量为 {len(options)}应为 4) if q.get(answer) not in options: errors.append(f第 {qid} 题选项中不包含正确答案) if image_dir: import os img_path os.path.join(image_dir, os.path.basename(q.get(image, ))) if not os.path.exists(img_path): errors.append(f第 {qid} 题图片不存在: {q.get(image)}) if errors: print(校验未通过发现以下问题) for err in errors: print(f - {err}) else: print(f校验通过共 {len(questions)} 题无异常。) return len(errors) 0 if __name__ __main__: validate_questions(questions.json, image_dir./compressed_images)这个脚本最适合放在“更新题库后、提交小程序代码前”这个时间点执行。它会即时反馈出题时的低级错误比如忘记把正确答案塞进选项、或图片文件名对不上。值得说明的是这个脚本没有校验“语义正确性”——它无法判断一张图片是否真的表达了对应成语。这类主观校验仍需要人工看一遍。实践中更高效的做法是让脚本生成一份“题目概览表”for idx, q in enumerate(questions): print(f{idx 1:03d}. {q.get(answer)} - {q.get(image)})然后人工快速扫一遍列表确认图片与答案的对应关系没有错位。这比一张张打开图片校验快得多尤其是在图片本身已按 ID 重命名的前提下。5.2 版本号管理与用户端缓存刷新策略小程序发版的经典痛点是用户手机上运行的是旧版本你已经更新了题库但他们看不到新题。微信小程序的更新机制是冷启动时会向微信服务器检查版本发现新版本后会异步下载但用户本次启动仍然运行旧代码等下次冷启动才切换新版本。针对这个问题在看图猜成语这类内容型小程序中一个有效的做法是给题库 JSON 加上version字段并在前端启动时向后端或静态服务器请求一次版本信息checkVersionUpdate() { wx.request({ url: https://api.example.com/question/version, success: (res) { const remoteVersion res.data.version; const localVersion questionBank.version; if (remoteVersion ! localVersion) { wx.showModal({ title: 发现新题库, content: 是否立即更新, success: (r) { if (r.confirm) { // 跳转到更新页面或重新拉取题库 this.downloadNewQuestions(remoteVersion); } } }); } } }); }这个方案需要 API 存在如果你的架构里没有后端替代方案是把版本号写进小程序app.json的config自定义字段每次加题后手动修改。由于微信的版本机制你改了app.json并发布新版本用户侧更新后自然会拿到新题库——这个方案更省事但用户感知到新题的时间会更滞后。两种方式的取舍很简单题目更新后希望“立刻生效”就要上远程版本检查能接受“等用户自然升级小程序”就只改本地题库。大多数项目从后者起步做大了再迁移到前者。5.3 微信开发者工具里的踩坑记录地铁路径与资源加载最后整理几条在微信开发者工具里经常遇到、且看图猜成语项目里特别容易被触发的坑它们都不是代码逻辑错误但排查起来非常浪费生命。第一项目目录直接打开与导入的差异。如果直接用微信开发者工具打开整个项目文件夹包含 Python 脚本、原始素材、压缩脚本开发者工具可能会尝试编译所有文件导致报错或警告。正确做法是工具里选择“导入项目”目录指向miniprogram这一层。Python 脚本放在项目外层不进小程序编译链路。your-project/ ├── scripts/ # Python 脚本不进小程序 │ ├── generate_questions.py │ └── compress_images.py ├── raw_images/ # 原始图片素材不进小程序 └── miniprogram/ # 微信开发者工具导入这个目录 ├── app.js ├── app.json └── pages/这种目录分离还有一个附带好处Python 脚本生成的questions.json和压缩后的图片会直接输出到miniprogram/data和miniprogram/images下后续重新生成时不会把乱七八糟的中间文件带进小程序包。第二清缓存调试的正确姿势。当你改了图片或题库但小程序里没变化多半是缓存问题。微信开发者工具顶部菜单的“工具 - 清除缓存 - 清除全部缓存”然后再编译。如果还不行检查project.config.json里是否有setting: { urlCheck: false }当你的后端接口是 HTTP 而非 HTTPS 时这个配置缺失会导致请求被拦截页面拿到不到数据表现形式和缓存一模一样。第三真机预览与开发者工具的差异。开发者工具里图片能显示真机上图片空白十有八九是图片路径问题。开发者工具对路径解析更宽容真机则严格按照app.json注册的页面路径和资源路径查找。调试时优先在真机上验证图片加载不要只看模拟器效果就发版。6. 把 Python 脚本沉淀成自动化工作流一键生成题库、压缩图片并自检发展到这个阶段你应该把之前写的零散脚本整合为一个可重复执行的流水线。这样无论是新增 10 题还是更换全部题库都只需要一条命令而不是手动跑三个脚本再人工检查。这里给出一个用 Python 标准库argparse写的一键脚本框架用最少的代码组合前面的功能import argparse import subprocess import sys from pathlib import Path def main(): parser argparse.ArgumentParser(description看图猜成语题库生成流水线) parser.add_argument(--source-dir, default./raw_images, help原始图片目录) parser.add_argument(--output-dir, default./miniprogram, help小程序项目根目录) parser.add_argument(--no-compress, actionstore_true, help跳过图片压缩步骤) parser.add_argument(--no-validate, actionstore_true, help跳过题库校验步骤) args parser.parse_args() project_root Path(args.output_dir) # 步骤一重命名并映射图片 if not args.no_compress: print( 步骤 1/3压缩图片) subprocess.run([sys.executable, compress_images.py, --src, args.source_dir, --dst, str(project_root / images / questions)], checkTrue) # 步骤二生成题库 JSON print( 步骤 2/3生成题库 JSON) subprocess.run([sys.executable, generate_questions.py, --src, args.source_dir, --dst, str(project_root / data / questions.json)], checkTrue) # 步骤三完整性校验 if not args.no_validate: print( 步骤 3/3校验题库) from validate_questions import validate_questions ok validate_questions( str(project_root / data / questions.json), image_dirstr(project_root / images / questions) ) if not ok: sys.exit(1) print( 全部完成可以打开微信开发者工具预览了) if __name__ __main__: main()这个脚本本质上是在编排三个独立模块而不是把所有逻辑塞进一个文件。每个子脚本仍然可以单独运行便于调试和复用。--no-validate这类开关适合在快速迭代时跳过校验但提交代码前必须完整跑一遍带校验的流程。使用方式是python build_pipeline.py --source-dir ./new_content --output-dir ./miniprogram在new_content里放好新题目的原始图片和一份题目描述 CSV脚本执行完小程序目录下的questions.json和图片就已经更新完毕开发者工具里直接重新编译就能看到效果。这套自动化流程的最大收益不是省掉那几分钟执行时间而是让“加新题”变成一个不会因为人为遗漏而出错的标准动作每一次生成结果都是可预期的。等到团队协作时你甚至可以让非技术人员只准备素材、跑一条命令不需要碰任何代码。本文还有配套的精品资源点击获取