
简介一份面向小学生及其家长的文言文启蒙资料由教育精品资料整理内含《陈元方候袁公》《画蛇添足》《父善游》《人有亡斧者》等经典短篇。这些故事短小精悍语言浅近分别展现少年机智应答、做事勿多此一举、反对经验主义以及警惕主观臆断等道理可帮助孩子在阅读中积累文言词汇、熟悉常见句式同时潜移默化地建立诚信、节制与思辨品格。资源仅包含一个docx文档压缩包约57KB文件精炼便携便于按需打印或投屏教学适合晨读、预习、课堂拓展或家庭亲子共读。目前已有63人学习下载内容与实用性获得学习者认可。无论是家长进行传统文化启蒙还是教师布置文言小作业都能从中获取可直接使用的注释译文、赏析点拨和道理归纳是一份短小却充实的文言文学习小册子。1. 一份 docx 收藏夹为什么值得动手处理家长群里经常能见到这种资源包文件名写着“优秀资料2021-2022年收藏小学生文言文小故事.docx”打开后是一篇篇小古文标题、原文、注释、译文挤在一个 Word 文件里。用 Word 看没有问题真要让孩子做复习卡片、按遗忘曲线抽背就发现这格式处处别扭。docx 的真实结构是 zip 压缩包正文内容塞在 XML 标签里直接复制文本出来会得到大量空段和乱序内容手工整理又费时间。我会以“从 docx 到结构化数据”为主线演示一套可复现的本地处理流程先读段落和表格再用正则切分故事字段最后导出 Markdown 与 SQLite。这套流程适合想自己给孩子做工具的工程师也适合需要批量整理 Word 课件的老师。2. 先读文件用 python-docx 把小学生文言文小故事的段落和表格拆出来解析 docx 的第一原则是“忘记 Word 的视觉层”。docx 本质是一个 zip 包里面 word/document.xml 按顺序记录段落、表格、图片引用。python-docx 把它重新暴露成doc.paragraphs和doc.tables。对于这种文言文小故事合集第一步不是做自然语言处理而是把这两个集合完整摸清。我用来探路的最小脚本是这样from docx import Document doc Document(优秀资料2021-2022年收藏小学生文言文小故事.docx) for i, para in enumerate(doc.paragraphs[:20]): text para.text.strip() if text: print(i, para.style.name, text[:30]) print(tables:, len(doc.tables))这段代码只做两件事打印前 20 个非空段落的索引、样式名和文本前 30 字打印文档里表格总数。doc.paragraphs只包含文档正文的顶级段落表格里的段落不会出现在这里所以不要急着判断内容缺失。先看前 30 字是判断这份文档是不是每个故事占一个标题加若干自然段如果这里全是“书名号开头”的短句基本可以认定是标题行。2.1 先判断文档是段落流还是表格流2010 年之后的 Word 模板经常用表格排版尤其在“优秀资料”这类下载文档里常见做法是每个故事一个一行两列的表格左侧原文右侧译文。这种表格流的内容如果用paragraphs去读会漏掉一大半。所以要用一个可以同时打印段落和表格的探针from docx import Document def dump_structure(path): doc Document(path) print(fparagraphs: {len(doc.paragraphs)}) print(ftables: {len(doc.tables)}) for t_idx, table in enumerate(doc.tables[:3]): print(f--- table {t_idx} ---) for row in table.rows: cells [cell.text.strip().replace(\n, ) for cell in row.cells] print(cells) dump_structure(优秀资料2021-2022年收藏小学生文言文小故事.docx)这个函数在表里打印的是合并单元格展开后的结果一行最多会有两列分别是原文和译文。如果观察到表格流后面切分时就要多一个“从单元格拼正文”的步骤如果表格很少多数故事就是普通段落那按段落流走就好。判断这个分支能避免花半天时间调正则最后发现漏了表格里的课文。2.2 用文本特征而不是样式名识别故事标题Word 从网页复制过来的内容样式名通常全是“正文”或“列出段落”偶尔有“标题 1”“标题 2”但编号和居中对齐经常乱掉。真正可靠的识别方法是组合文本特征。对于小学生文言文小故事标题行有这样几个特点字数一般在 5 到 15 个字之间末尾没有句读内容往往是“守株待兔”“刻舟求剑”这类成语并且后面隔一两行才开始正文。我常用的识别规则如下特征判定优先级长度小于等于 20、无句末标点可能是标题高以“注释”“译文”等词开头字段标记高以“选自”或“《”开头出处中长度大于 50 且含“曰”“乎”“也”正文低对应的识别函数import re def looks_like_title(line): line line.strip() if not line: return False if len(line) 20: return False if re.search(r[。]$, line): return False if re.search(r^(注释|译文|出处|寓意|道理|提示), line): return False return True这里几个参数值得解释len(line) 20是经验值小学文言文标题很少超过 20 个汉字但“铁杵成针”“王戎不取道旁李”都在 10 字上下末尾标点排除掉正文以“注释”“译文”开头的行是区块标记不能误判成标题。如果你的文档里标题带序号“一、二、三”可以把正则放宽为匹配^[一二三四五六七八九十]、的也放进标题候选。这个函数只是给后面的切分脚本一个“候选标题”的信号宁可比黄金标准多给候选也不能给漏掉。2.3 统一空段与首行缩进带来的问题Word 文档里每个故事之间经常夹两三个空段段首还有首行缩进。读取时para.text不会自动去掉缩进但段首是两个全角空格的情况很常见。我在读文本时统一做text para.text.strip()它会把全角空格也去掉。另外有些资料会把拼音注解做成行内域或字体颜色直接读段落文本会混入拼音需要后续用正则把jí这类完整注释删除或者先保留原始文本等切分后再清洗。这一步没有标准答案但提前把“空段过滤”和“全角空格归一化”写成公共函数后面处理所有故事会轻松很多。表格里文本的换行也不能忽视cell.text默认会把单元格里的多个段落用\n连接所以我在dump_structure里用replace(\n, )展示避免控制台输出被换行切断。真正写结构化脚本时反而要保留\n因为表格单元格内部换行往往是原文和译文的段落边界。3. 正则切分把文言文小故事切成标题、出处、注释、译文读完段落流之后文档大概长这样守株待兔 宋人有耕者。田中有株。兔走触株折颈而死。因释其耒而守株冀复得兔。兔不可复得而身为宋国笑。 【注释】株树桩。 走跑。 释放下。 【译文】从前宋国有个种田的人田里有一截树桩一只兔子跑过来撞上树桩折断脖子死了。这个人便放下农具守在树桩边希望再捡到兔子。兔子不可能再得到他自己却被宋国人笑话。 【寓意】不主动努力而心存侥幸希望得到意外收获只能一事无成。每个故事的段落数量不固定但结构基本是“标题、正文、注释、译文”。切分时我会先定一个“字段标记”集合再用正则匹配段首。3.1 先定一个“分隔词表”常用分隔词和它们对应的字段名段首标记字段名说明注释annotation字词解释通常分条译文translation白话翻译可能多段出处source选自哪本书常以《》开头寓意 / 道理moral中心思想非必选小古文 / 文言文genre扩展字段一般用不到选择用“段首标记”而不是“行内出现”很重要。有些文档把“译文”和译文正文放在同一个段落里有些分两段极少数放在同一段但标记后没有换行。正则可以同时覆盖这两种^(注释|译文|出处|寓意|道理)[:]?\s*这样“译文……”和“译文”单独一行都能识别。3.2 用带命名的正则把标记从段首剥掉我写切分脚本时会让正则输出一个命名组marker然后用字典映射字段名。下面是一个可复用的切分骨架import re import json field_map { 注释: annotation, 译文: translation, 出处: source, 寓意: moral, 道理: moral, } marker_re re.compile(r^(【?)(注释|译文|出处|寓意|道理)(】?)[:]?\s*) def split_doc(paragraphs): stories [] story None current_field None for para in paragraphs: text para.text.strip() if not text: continue m marker_re.match(text) if m: field field_map[m.group(2)] if field source or field moral: if story is not None: story[field] text[m.end():].strip() else: current_field field content text[m.end():].strip() if content and story is not None: story[field].append(content) continue if story is None: story new_story(text) current_field content elif current_field: story[current_field].append(text) if story is not None: stories.append(story) return stories def new_story(title): return { title: title, source: , content: [], annotation: [], translation: [], moral: , }逻辑说明遇到标题当前没有 story就用new_story创建并把标题填进title。遇到标记段更新current_field。标记后的内容如果和标记在同一段则直接追加如果标记独立成段则之后的普通段落自动归属到current_field。source和moral一般只有一行所以我直接存成字符串而content、annotation、translation保留为列表等导出时再拼接。m.group(2)是正则里的第二个捕获组也就是“注释”“译文”这些汉字text[m.end():]是从匹配结束的位置截到段落末尾。正则开头的【?表示左方括号可有可无】?同理这样能兼容“【注释】”和“注释”两种排版。如果文档里出现“注释株树桩”这个代码会把“株树桩”追加到 annotation 列表。3.3 没有标记段的文档怎么兜底并不是所有资料都规规矩矩带“译文”。有些文档只给原文末尾用括号标一两句解释。这时需要一套兜底规则段落长度小于 40 且含“曰”或“也”结尾的视为正文。以“选自”“出自”开头的视为 source。括号内以“注”“同”开头的段落视为 annotation。兜底规则放在split_doc的else分支里优先级低于标记匹配。我不建议用机器学习模型处理这种小批量文档规则跑一次就能覆盖八成情况剩下的手工改 JSON 更划算。3.4 输出 JSON结构化数据长什么样切分完成后把列表写进一个文件def save_json(stories, outputstories.json): with open(output, w, encodingutf-8) as f: json.dump(stories, f, ensure_asciiFalse, indent2)ensure_asciiFalse一定要写否则中文会全部变成\u转义序列手工检查时没法看。indent2是为了让 Git diff 更友好提交到仓库后能看到哪个故事内容变了。最终 JSON 的大致形态{ title: 守株待兔, source: 《韩非子·五蠹》, content: [宋人有耕者。……而身为宋国笑。], annotation: [株树桩。, 走跑。], translation: [从前宋国有个种田的人……], moral: 不主动努力而心存侥幸…… }这个格式已经可以被前端页面、手机 App、打印脚本共同消费。到这一步docx 里那段缠绕在一起的文本才算是真正拆开了。4. 生成复习材料把文言文小故事变成双栏对照和抽查清单结构化数据不是为了放着好看下一步要变成能“用”的东西。对小学生文言文小故事最常见的两个使用场景是打印双栏对照学习页和每天抽背。两者都只需要几十行代码。4.1 用 Python 生成 Markdown 学习页我习惯先生成 Markdown再用 Pandoc 转 PDF 或 Word这样原始数据不污染。一个故事转一节的函数def to_markdown(story): lines [f### {story[title]}, ] if story[source]: lines.append(f 出处{story[source]}) lines.append() lines.append(story[content].strip()) lines.append() if story[annotation]: lines.append(**注释**) lines.extend(f- {item} for item in story[annotation]) lines.append() if story[translation]: lines.append(**译文**) lines.extend(f- {item} for item in story[translation]) lines.append() return \n.join(lines)这个函数把annotation和translation渲染成列表比挤成一个段落更容易让孩子逐条对照。source放在引用块里打印时字号会比普通文字小一点正好符合出处信息的位置。content.strip()会去掉首尾空行避免输出出现大块空白。把所有故事拼起来后写入study.mdwith open(study.md, w, encodingutf-8) as f: f.write(\n\n.join(to_markdown(s) for s in stories))用 Pandoc 转 PDF 时加-V CJKmainfontNoto Serif CJK SC中文才能正常显示。这个参数和内容无关但每次都会踩我直接写进 Makefile。4.2 把间隔重复的排期写进 SQLite学习页是静态的复习计划需要动态排期。我在本地用 SQLite 建两张表CREATE TABLE IF NOT EXISTS story ( id TEXT PRIMARY KEY, title TEXT NOT NULL, source TEXT DEFAULT , content TEXT NOT NULL, annotation TEXT DEFAULT , translation TEXT DEFAULT , moral TEXT DEFAULT ); CREATE TABLE IF NOT EXISTS review_plan ( story_id TEXT NOT NULL, review_date TEXT NOT NULL, box INTEGER DEFAULT 0, PRIMARY KEY (story_id, review_date), FOREIGN KEY (story_id) REFERENCES story(id) );box是记忆盒子编号0 表示新学之后每次复习成功就加 1。表结构不复杂但PRIMARY KEY (story_id, review_date)很重要可以防止同一天对同一个故事重复插入排期。如果不用这个约束脚本多次运行后会出现大量重复行抽查清单就会把人看晕。4.3 按今天日期拉出今天要背的故事排期用“首次学习日期 间隔天数”生成。我按小学生的实际情况取[0, 1, 3, 7, 15]这组间隔第 0 天表示当天学习后面分别是隔一天、隔三天、隔一周、隔半个月复习。import sqlite3 from datetime import date, timedelta def build_plan(conn, story_ids, startNone, intervals(0, 1, 3, 7, 15)): start start or date.today() cursor conn.cursor() for story_id in story_ids: for delta in intervals: review_date start timedelta(daysdelta) cursor.execute( INSERT OR IGNORE INTO review_plan(story_id, review_date, box) VALUES (?, ?, ?), (story_id, review_date.isoformat(), delta), ) conn.commit()这里的box我直接用间隔天数而不是递进编号让你一眼看出该天的复习属于哪个周期。INSERT OR IGNORE是配合主键设计的容错脚本重复跑不会报错只会跳过已存在的排期。抽背时执行def today_list(conn, target_dayNone): target (target_day or date.today()).isoformat() return conn.execute( SELECT s.title, s.content, s.translation FROM story s JOIN review_plan r ON s.id r.story_id WHERE r.review_date ? , (target,), ).fetchall()这段 SQL 用JOIN把故事内容挂在排期上输出就是当天的完整复习列表。平时我可以直接跑python review.py today.md晚上打印一份孩子边看原文边想译文。把target_day暴露成命令行参数就能提前预览未来几天的动作这个习惯在排错时特别有用。5. 批量处理 2021-2022 年收藏的文言文小故事路径遍历、批处理参数与三个易踩的坑真实收藏的文件夹里往往不止一个 docx可能还有“文言文小故事上”“文言文小故事下”等文件。批量遍历时用pathlib是最省事的办法from pathlib import Path def process_folder(folder): folder Path(folder) for path in sorted(folder.glob(*.docx)): if path.name.startswith(~$): continue print(f处理 {path.name}) # 调用前面的 split_doc 和 save_json这里glob(*.docx)只匹配当前目录不递归子目录如果资料按年份分文件夹就改成folder.glob(**/*.docx)。跳过~$开头的文件是必须的Word 打开文档时会生成这类临时文件python-docx 打开它大概率直接抛PackageNotFoundError。5.1 三个“看不出来”的坑第一个坑是页眉页脚内容会混进段落流。python-docx 的doc.paragraphs只包含页面正文页眉页脚在另一个集合里但如果有人把批注、脚注复制到正文中就能看到一串页码和日期文本。处理前先打印一遍[p.style.name for p in doc.paragraphs]把包含Header、Footer的段落过滤掉。第二个坑是文本框。很多中国版 Word 模板使用文本框放标题doc.paragraphs读不到。需要在 XML 层遍历w:txbxContentfrom docx import Document ns {http://schemas.openxmlformats.org/wordprocessingml/2006/main} doc Document(sample.docx) textbox_text [] for el in doc.element.body.iter(): if el.tag ns txbxContent: for t in el.iter(ns t): textbox_text.append(t.text or )sorted(folder.glob(*.docx))会把文件名按 Unicode 编码排序中文名称基本符合拼音直觉比系统自带的随机顺序稳定。拿到textbox_text后手动和正常段落合并再送进切分逻辑标题就不会凭空丢失。第三个坑是繁体字和生僻字注音。有些古文资料用繁体显示没问题但在 SQLite 里排序和搜索可能乱掉。我一般用 OpenCC 统一转简体生僻字注音保留在原文里不强行剥离。别在切分阶段尝试做注音那会破坏正文结构放到生成复习页时再做也不迟。5.2 批处理参数怎么设计为了让脚本能复用我会把输入输出都做成参数python build_stories.py \ --input 优秀资料2021-2022年收藏小学生文言文小故事.docx \ --output stories.json \ --start 2022-09-01--start传给build_plan用来控制第一轮复习从哪天开始。这样就不用为了换开学日期去改代码也方便给不同孩子设置不同的复习起点。脚本末尾增加main函数读取这些参数中间所有函数都不直接读命令行测试时可以直接在 Python 里调用。最后一个实用技巧把--date参数暴露给review.py这样能随时指定某一天生成清单。我调好间隔后总是先跑一次python review.py --date 2022-09-01验证输出看那一天的课文数量是否符合预期。这个参数不解决算法问题但能让你在出问题的时候一眼看出是排期逻辑错了还是数据本身少了故事。本文还有配套的精品资源点击获取