ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

驾考科目一科目四题库SQL与JSON数据方案:从建表到校验全流程

驾考科目一科目四题库SQL与JSON数据方案:从建表到校验全流程 简介这份资源面向准备驾照理论考试的学员及驾考类应用开发者提供科目一与科目四的完整题库数据覆盖小车、客车、货车、摩托车四类车型可满足刷题练习、题库检索与二次开发等需求。压缩包共约2000个文件以1995个webp图片素材为主用于题目配图展示另含2个sql与2个json文件分别对应数据库建表导入和结构化数据读取方便直接接入项目或做格式转换整体约103.05MB。题库内容按车型与科目细分客车科目一2154题、科目四2126题小车科目一1600题、科目四1300题摩托车科目一446题、科目四383题货车科目一2162题、科目四1206题题量充足且分类明确。目前已有1014人学习下载读者可据此快速搭建本地题库、批量导入数据库或开发刷题工具省去逐题整理与配图收集的繁琐工作。1. 驾照考试科目一科目四题库从 SQL 表结构到 JSON 格式一套能直接跑起来的数据方案做过驾考类应用的人都知道科目一和科目四的题库数据是整个产品的命脉。但真正动手时你会发现网上能找到的所谓“题库”要么是残缺的文本片段要么格式混乱到没法直接入库更别提还要区分小车、客车、货车、摩托车这些不同准驾车型的题目差异。我最近刚把一套包含 SQL 表数据和 JSON 格式的题库方案落地到一个模拟考试系统里从建表、导入、图片素材管理到前端消费整条链路走了一遍。这套方案的核心思路是用关系型数据库存结构化题目和选项用 JSON 做接口层的数据交换格式图片素材按题型和车型分目录管理。适合正在做驾考刷题类应用、模拟考试系统或者需要批量处理题库数据的开发者。下面把我踩过的坑和最终跑通的路径完整拆开讲。2. 题库数据建模SQL 表结构怎么设计才不返工2.1 题目主表、选项表、图片表的三层拆分很多人一开始会把题目和选项塞在一张表里用逗号分隔选项内容。这种设计在题目量少的时候看着省事一旦要按选项内容搜索、要做错题统计、要支持多车型复用同一道题立刻就会翻车。我采用的是三层结构题目主表存题干、题型、答案、解析、车型标签选项表存每个选项的内容和序号图片表存图片路径和关联的题目 ID。先看题目主表的核心字段设计CREATE TABLE questions ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 题目唯一ID, question_type TINYINT NOT NULL COMMENT 1-单选题 2-多选题 3-判断题, subject TINYINT NOT NULL COMMENT 1-科目一 4-科目四, vehicle_type VARCHAR(32) NOT NULL DEFAULT car COMMENT car-小车 bus-客车 truck-货车 motorcycle-摩托车, content TEXT NOT NULL COMMENT 题干内容支持HTML片段, correct_answer VARCHAR(8) NOT NULL COMMENT 正确答案单选如A多选如ABD判断如Y/N, explanation TEXT COMMENT 答案解析, difficulty TINYINT DEFAULT 1 COMMENT 难度等级 1-3, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_subject_vehicle (subject, vehicle_type), KEY idx_type (question_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目主表;选项表单独拆出来每个选项一行方便做选项乱序和选项级统计CREATE TABLE question_options ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, question_id INT UNSIGNED NOT NULL COMMENT 关联题目ID, option_key CHAR(1) NOT NULL COMMENT 选项标识 A/B/C/D, option_text VARCHAR(512) NOT NULL COMMENT 选项内容, sort_order TINYINT NOT NULL DEFAULT 0 COMMENT 展示顺序, PRIMARY KEY (id), KEY idx_question (question_id), CONSTRAINT fk_option_question FOREIGN KEY (question_id) REFERENCES questions (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目选项表;图片素材表用来管理题干配图、选项配图以及解析中的示意图CREATE TABLE question_images ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, question_id INT UNSIGNED NOT NULL, image_path VARCHAR(255) NOT NULL COMMENT 相对路径如 /images/car/sign/001.png, image_role TINYINT NOT NULL DEFAULT 1 COMMENT 1-题干图 2-选项图 3-解析图, option_key CHAR(1) DEFAULT NULL COMMENT 如果是选项图标记对应选项, PRIMARY KEY (id), KEY idx_question (question_id), CONSTRAINT fk_image_question FOREIGN KEY (question_id) REFERENCES questions (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目图片关联表;这样拆的好处是同一道题如果同时出现在小车和客车题库里只需要在关联表里加一条车型映射不用复制整道题。图片素材按车型和题型分目录存放数据库里只存相对路径迁移环境时改一个根路径配置就行。2.2 车型与科目维度的关联设计车型和科目的关系不是简单的字段能覆盖的。科目一和科目四的题型分布不同小车、客车、货车、摩托车的题库有大量重叠但又不完全一致。我见过有人用一张表加多个布尔字段来标记车型结果查询时要写一堆 OR 条件索引根本用不上。更合理的做法是加一张题目与车型的关联表CREATE TABLE question_vehicle_map ( question_id INT UNSIGNED NOT NULL, vehicle_type VARCHAR(32) NOT NULL, PRIMARY KEY (question_id, vehicle_type), KEY idx_vehicle (vehicle_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目与车型关联表;导入数据时先判断这道题属于哪些车型然后批量插入关联记录。查询小车科目一题目时直接 JOIN 这张表加 WHERE vehicle_typecar走索引效率很高。科目四的题目通常包含更多情景判断题和动画题图片素材的占比也更高建表时给图片表加一个题型过滤索引会明显提升列表页加载速度。2.3 从 SQL 导出 JSON 的转换脚本数据库建好之后前端和移动端通常不直接连数据库而是通过接口拿 JSON 数据。我写了一个 Python 脚本从 MySQL 里按科目和车型导出嵌套的 JSON 结构每个题目对象包含选项数组和图片数组。import json import pymysql def export_questions(subject, vehicle_type, output_file): conn pymysql.connect(hostlocalhost, userroot, password, databasedriving_exam, charsetutf8mb4) cursor conn.cursor(pymysql.cursors.DictCursor) # 查询题目主表 cursor.execute( SELECT q.id, q.question_type, q.content, q.correct_answer, q.explanation, q.difficulty FROM questions q JOIN question_vehicle_map m ON q.id m.question_id WHERE q.subject %s AND m.vehicle_type %s ORDER BY q.id , (subject, vehicle_type)) questions cursor.fetchall() result [] for q in questions: # 查询选项 cursor.execute(SELECT option_key, option_text FROM question_options WHERE question_id %s ORDER BY sort_order, (q[id],)) options cursor.fetchall() # 查询图片 cursor.execute(SELECT image_path, image_role, option_key FROM question_images WHERE question_id %s, (q[id],)) images cursor.fetchall() result.append({ id: q[id], type: q[question_type], content: q[content], options: [{key: o[option_key], text: o[option_text]} for o in options], answer: q[correct_answer], explanation: q[explanation], difficulty: q[difficulty], images: [{path: img[image_path], role: img[image_role], optionKey: img[option_key]} for img in images] }) with open(output_file, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) cursor.close() conn.close() print(f导出完成{output_file}共 {len(result)} 题) # 导出小车科目一 export_questions(1, car, car_subject1.json) # 导出摩托车科目四 export_questions(4, motorcycle, motorcycle_subject4.json)这个脚本的关键点在于用 DictCursor 让查询结果直接是字典省去手动映射字段的麻烦导出时保留 indent2 方便人工检查ensure_asciiFalse 保证中文正常显示。参数 subject 控制科目vehicle_type 控制车型输出文件名按车型和科目组合命名方便后续按需加载。如果题目量很大可以改成流式写入避免一次性把所有数据加载到内存。3. 图片素材管理小车、客车、货车、摩托车题库的图片怎么存怎么取3.1 按车型和题型分目录的存储结构图片素材是驾考题库最容易被忽视的部分。科目一有大量交通标志、标线、手势图科目四有情景动画截图和复杂路况示意图。小车、客车、货车、摩托车的图片有重叠但尺寸和内容细节不同。我建议的目录结构是这样的/images /car /sign # 交通标志 /marking # 路面标线 /gesture # 交警手势 /scene # 情景题配图 /bus /sign /scene /truck /sign /scene /motorcycle /sign /scene数据库里存的是相对路径比如/images/car/sign/001.png。前端拿到 JSON 后拼接一个 CDN 前缀或者本地资源根路径就能显示。这样做的好处是迁移服务器时只需要改一个配置项不用动数据库里的任何记录不同车型的图片可以独立更新不会互相影响。3.2 图片命名规范与数据库路径映射图片命名我踩过一个坑一开始用中文名结果在某些 Linux 环境下出现乱码前端加载失败。后来统一改成“车型缩写_题型缩写_序号”的格式比如car_sign_001.png、truck_scene_015.png。序号用三位数字不足补零保证文件列表排序时顺序正确。数据库里的 image_path 字段存的是完整相对路径但为了灵活我在应用层加了一个映射函数import os IMAGE_ROOT os.environ.get(IMAGE_ROOT, /static/images) def resolve_image_url(image_path): 将数据库中的相对路径转换为可访问的URL if not image_path: return # 去掉可能存在的开头斜杠避免路径拼接问题 clean_path image_path.lstrip(/) return f{IMAGE_ROOT}/{clean_path}这个函数看起来简单但解决了两个实际问题一是环境变量控制根路径开发环境和生产环境可以用不同的值二是统一处理路径拼接避免出现双斜杠或者缺少斜杠的情况。如果图片走 CDN只需要把 IMAGE_ROOT 改成 CDN 域名即可。3.3 图片缺失时的降级处理题库数据里偶尔会有图片路径存在但文件实际缺失的情况尤其是在批量导入或者迁移之后。如果不做处理前端会显示一个裂图用户体验很差。我的做法是在导出 JSON 时做一次文件存在性检查把缺失的图片标记出来import os def check_image_exists(image_path, base_dir/static/images): 检查图片文件是否真实存在 full_path os.path.join(base_dir, image_path.lstrip(/)) return os.path.isfile(full_path) # 在导出循环中增加检查 valid_images [] for img in images: if check_image_exists(img[image_path]): valid_images.append({...}) else: # 记录日志方便后续补图 print(f警告图片缺失 - 题目ID {q[id]}路径 {img[image_path]})对于确实缺失的图片前端可以显示一个占位图同时后端记录日志。这样既不影响用户答题又能让运维人员知道哪些图片需要补充。我一般会在导出脚本里加一个统计最后输出缺失图片的总数超过阈值就报警。4. 避坑与排查题库数据处理中最容易翻车的五个地方4.1 判断题答案格式不统一导致判分错误现象判断题在数据库里有的存 Y/N有的存 对/错有的存 1/0前端提交答案后判分逻辑匹配不上用户明明选对了却显示错误。原因不同来源的题库数据合并时没有做格式归一化导入脚本直接透传了原始值。解决在导入阶段强制统一。写一个转换函数把所有变体映射到标准值def normalize_judge_answer(raw): mapping { Y: Y, N: N, 对: Y, 错: N, 正确: Y, 错误: N, 1: Y, 0: N, true: Y, false: N } return mapping.get(str(raw).strip().lower(), Y)导入前先跑一遍数据清洗把异常值打印出来人工确认。这个坑我在两个项目里都遇到过血泪经验就是永远不要相信上游数据的格式一致性。4.2 多选题答案顺序不一致导致比对失败现象多选题正确答案存的是 “ABD”用户提交的是 “ADB”字符串直接比对不相等判为错误。原因没有对答案做排序归一化。解决判分前把用户答案和标准答案都拆成字符数组排序后再拼接比对def check_multi_answer(user_answer, correct_answer): user_set set(user_answer.upper().replace(,, ).replace(, )) correct_set set(correct_answer.upper().replace(,, ).replace(, )) return user_set correct_set用集合比对而不是字符串比对既解决了顺序问题也顺带处理了用户可能输入逗号分隔的情况。4.3 图片路径大小写敏感导致 Linux 部署后图片全部 404现象本地 Windows 开发时图片显示正常部署到 Linux 服务器后所有图片都加载不出来。原因Windows 文件系统不区分大小写Linux 区分。数据库里存的是/images/Car/Sign/001.png实际目录是/images/car/sign/001.png。解决导入数据时统一把路径转为小写同时检查实际文件是否存在。批量修正脚本import os def fix_image_path_case(image_path, base_dir): lower_path image_path.lower() full_path os.path.join(base_dir, lower_path.lstrip(/)) if os.path.isfile(full_path): return lower_path # 如果小写路径不存在尝试在目录中查找实际文件名 dir_name os.path.dirname(full_path) file_name os.path.basename(full_path) if os.path.isdir(dir_name): for f in os.listdir(dir_name): if f.lower() file_name: return os.path.join(os.path.dirname(lower_path), f).replace(\\, /) return image_path # 找不到就返回原值记录日志这个坑的教训是开发环境和生产环境的文件系统差异一定要提前考虑最好在 CI 流程里加一步路径检查。4.4 批量导入时事务过大导致锁表超时现象一次性导入几万道题目时MySQL 报锁等待超时导入中断部分数据已写入部分未写入。原因整个导入过程放在一个事务里表被锁住的时间太长。解决分批提交每 500 条提交一次事务。同时关闭自动提交手动控制事务边界def batch_import(questions, batch_size500): conn pymysql.connect(..., autocommitFalse) cursor conn.cursor() try: for i, q in enumerate(questions): cursor.execute(INSERT INTO questions ...) if (i 1) % batch_size 0: conn.commit() print(f已提交 {i 1} 条) conn.commit() # 提交剩余部分 except Exception as e: conn.rollback() print(f导入失败{e}) finally: cursor.close() conn.close()batch_size 设成 500 是在导入速度和锁竞争之间取的平衡值。如果数据库负载较高可以降到 200如果导入的是空表且没有并发写入可以适当加大。4.5 JSON 导出时中文字符转义导致文件体积翻倍现象导出的 JSON 文件里中文全部变成\uXXXX格式文件体积比预期大了一倍多前端解析后显示正常但传输成本高。原因json.dump 默认 ensure_asciiTrue。解决导出时设置 ensure_asciiFalse同时确保文件以 UTF-8 编码写入with open(output_file, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)如果接口返回 JSON 时也遇到同样问题检查框架的序列化配置。大多数现代框架默认已经是 ensure_asciiFalse但老版本或者某些配置下需要手动指定。5. 进阶技巧用 JSON Schema 校验题库数据完整性题库数据在多次导入导出和人工编辑之后很容易出现字段缺失、类型错误、答案与选项不匹配等问题。与其等到线上用户反馈不如在数据入库前做一次结构化校验。我现在的习惯是为题库 JSON 写一份 Schema每次导出后自动跑一遍校验不通过就不允许发布。先定义 Schema 文件question_schema.json{ $schema: http://json-schema.org/draft-07/schema#, type: array, items: { type: object, required: [id, type, content, options, answer], properties: { id: { type: integer }, type: { type: integer, enum: [1, 2, 3] }, content: { type: string, minLength: 1 }, options: { type: array, minItems: 2, items: { type: object, required: [key, text], properties: { key: { type: string, pattern: ^[A-D]$ }, text: { type: string, minLength: 1 } } } }, answer: { type: string, minLength: 1 }, explanation: { type: string }, images: { type: array } } } }然后用 Python 的 jsonschema 库做校验import json from jsonschema import validate, ValidationError def validate_question_file(json_file, schema_file): with open(json_file, r, encodingutf-8) as f: data json.load(f) with open(schema_file, r, encodingutf-8) as f: schema json.load(f) try: validate(instancedata, schemaschema) print(f校验通过{json_file}共 {len(data)} 题) return True except ValidationError as e: print(f校验失败{json_file}) print(f错误路径{ - .join(str(p) for p in e.path)}) print(f错误信息{e.message}) return False这个校验能拦住大部分低级错误选项数量不足、答案字段为空、题目类型超出范围、选项 key 不是 A-D 等。我还会在 Schema 之外加一些业务规则检查比如单选题的答案必须是单个字符、多选题的答案长度必须大于 1、判断题的选项必须恰好是两个且 key 为 A 和 B。这些规则用 JSON Schema 表达起来比较别扭用 Python 函数补充更灵活def check_business_rules(questions): errors [] for q in questions: if q[type] 1 and len(q[answer]) ! 1: errors.append(f题目 {q[id]}单选题答案长度不为1) if q[type] 2 and len(q[answer]) 2: errors.append(f题目 {q[id]}多选题答案少于2个选项) if q[type] 3: if len(q[options]) ! 2: errors.append(f题目 {q[id]}判断题选项数量不为2) if q[answer] not in (Y, N): errors.append(f题目 {q[id]}判断题答案不是Y或N) # 检查答案中的选项key是否都存在于options中 option_keys {o[key] for o in q[options]} for ans_key in q[answer]: if ans_key not in option_keys: errors.append(f题目 {q[id]}答案 {ans_key} 不在选项列表中) return errors把 Schema 校验和业务规则检查串成一个流水线每次导出 JSON 后自动执行。校验不通过就阻断发布流程把错误列表输出到日志。这套机制帮我拦住了好几次因为人工编辑导致的答案错位问题尤其是多选题答案和选项不匹配的情况肉眼检查很难发现但脚本一跑就暴露了。还有一个实用技巧把校验脚本挂到 Git 的 pre-commit 钩子上只要题库 JSON 文件有改动就自动跑一遍。这样能在提交阶段就发现问题而不是等到部署后才暴露。对于团队协作的场景还可以把校验结果输出成 HTML 报告方便非技术人员查看哪些题目需要修正。最后说一个我自己的习惯每次批量导入新题库之前先拿 100 条数据跑一遍完整流程——建表、导入、导出 JSON、校验、前端加载、答题判分。这 100 条里故意混入一些边界数据比如空解析、单选项、超长题干、特殊字符。跑通了再全量导入能省下大量返工时间。题库数据这玩意儿前期多花一小时检查后期少熬三个晚上排查。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进