1. 这个需求到底在解决什么问题
1.1 手工转换的“时间黑洞”
前几天有个同事抱着一份 Word 文档来找我,里面是三百多道选择题,格式大概是这样的:每行一个题干,下面跟着 A、B、C、D 四个选项,最后还有一行参考答案。他的需求很简单:把这些题目全部转成一个多列表格,每一行放一道题,题干占一列,四个选项各占一列,答案再单独一列。
听起来不难对吧?手动做也就是复制粘贴。但三百多道题,每道题要复制题干、四个选项、答案,再贴进表格对应单元格,一题操作五六次,三百题就是一千五百多次操作。我做了一下时间估算,熟练操作也需要两三个小时,还容易出错,不是漏了某一行,就是错位把 B 选项填进了 C 选项那一列。
这种活儿,我以前刚入行的时候是真干过,一个下午眼睛都看花了。后来开始用 Word VBA 之后,这类需求基本控制在 30 秒以内,代码一跑,表格自动生成。今天这篇文章,我就把整套思路和可以直接用的代码分享出来。
1.2 为什么 VBA 是最省事的路径
有人可能会问,Word 本身不是有“文本转换成表格”的功能吗?为什么还要写 VBA?
这里关键要看需求是什么样的。“文本转表格”适合的是那种结构非常规整的数据,比如每一行都用 Tab 键或者逗号分隔,各字段位置固定。但题库文本通常是“半结构化”的,题干长度不固定,选项有时候在一行,有时候在下一段,答案和解析还混在最后一行,直接用分隔符转换根本没法自动分组。
用 VBA 就灵活得多。VBA 可以逐段读取文档内容,按正则规则识别出“题号”“选项字母”“答案”这些特征,再在内存里把题目重新组装成一行数据,最后通过表格 API 一次性写入。说白了,Word 自带功能解决的是“格式统一”的转换问题,VBA 解决的是“内容识别+重组”的问题。遇到题库这种复杂文本,VBA 才是正路。
1.3 适用场景:哪些人最需要
这套方法适合的人群非常明确:老师、培训专员、教务人员、题库运营,以及所有需要把零散题目整理成表格的人。只要你的原稿是 Word 文档,目标是将题目结构化成表格,那这份脚本都能用。
我见过最典型的场景,一是把纸质试卷转成电子题库,二是把题目重新排版成答题卡样式,三是把单个文档里的题目合并进统一模板。这三个场景本质上都是“把题目变成表格行”,只是列数和列名不同,脚本的核心逻辑完全可以复用。
2. 把题目拆成可处理的“单元”
2.1 题库文本常见的三种排版
写代码之前,一定要先把数据结构看清楚。实际工作中我见过的题库格式不下十种,但大致可以归成三类:
第一种是“每条选项单独一行”,这也是最标准的格式:
1. 下面哪一个属于操作系统? A. Windows XP B. Linux C. macOS D. DOS 答案:A第二种是“题干和选项挤在同一行”,常见于从网页复制的内容:
1. 下面哪一个属于操作系统?A. Linux B. Windows C. macOS D. DOS第三种是“题干下面直接跟着选项,选项之间用空格隔开”,类似这样:
1. 下面哪一个属于操作系统? A. Linux B. Windows C. macOS D. DOS这三种格式,VBA 处理起来的难度不一样。第一种最理想,识别规则最清晰;第二种和第三种都需要额外做一步“切分粘连文本”的操作。
2.2 用正则提取题号与选项
我选择用正则表达式来做核心识别。VBA 中可以调用 VBScript.Regexp 对象,用几行代码就能匹配到题号和选项字母。
识别题号的正则大概是这样的:
^(\d{1,4})[\.、.]\s*(.*)$它匹配“1.”、“2、”、“12.”这类开头。识别选项的正则是:
^([A-H])[\.、.]\s*(.*)$它会匹配“A.”、“B、”、“C.”等开头。正则的好处是可以一次性过滤掉无意义的空行和干扰文本,只把题目特征抓出来。
注意:正则模式中的
[\.、.]包含了半角句点、中文顿号、全角句点三种标点。这种细节非常影响成功率,很多人写正则只写了一个英文点号,结果遇到中文标点的题目就识别失败。
2.3 选项跨段的处理思路
题库里经常出现一个选项特别长,从一行中间断开,换行后又继续的情况。比如这样:
A. 这是一种非常长的描述文字, 包含了大量细节 B. 这是另一个选项如果只按“字母开头才记录选项”来处理,就会把“包含了大量细节”当成无关内容丢掉。我的办法是:维护一个“当前状态标记”,如果某一行不是题号也不是选项字母,但它出现在“题干之后、选项未结束”的上下文里,就把它拼接进上一条选项的末尾或者题干末尾。
这个处理在代码里并不复杂,核心就是判断当前题目是否已经存在,以及当前选项个数是否大于 0。
2.4 表格列数怎么定
表格列数是根据需求来的。我最初做的是 6 列:题号、题干、A 选项、B 选项、C 选项、D 选项。后来同事要求把答案也加进去,我就在第 7 列加上了答案。
这里有一件看起来很基础、但很重要的事:不管原文档里是 4 个选项还是 5 个选项,表格的列数最好固定。如果选项数量不固定,可以用一个数组先缓存最多 8 个选项,转换时只填充实际存在的单元格,空余单元格留空就好。这样表格看起来依然整齐,不会因为某道题只有 3 个选项就整行错位。
3. 一套可直接落地的 VBA 代码
3.1 前期准备:把宏代码放进 Word
在写代码前,先把运行环境准备好。最常用的方式是打开 Word,按Alt + F11进入 VBA 编辑器,在左侧工程窗口里找到“Normal”或者当前文档的“ThisDocument”,右键插入模块,把代码粘贴进去,然后按F5运行。
不需要打开“开发工具”选项卡,Alt + F11永远有效。如果你以前没跑过宏,运行时可能会遇到“宏被禁用”的提示,解决方法是打开 Word 的“信任中心”,把“启用所有宏”临时打开,跑完再改回来就行。
3.2 第一版:识别题干和选项、生成表格
下面是一份可以直接运行的脚本,它能把标准格式的题目转成一个 6 列表格:
Sub ConvertQuestionsToTable() Dim doc As Document Set doc = ActiveDocument Dim para As Paragraph Dim s As String Dim rx As Object Dim m As Object ' 启用正则 Set rx = CreateObject("VBScript.RegExp") rx.IgnoreCase = True rx.Global = True Dim qID As Long ' 当前题号 Dim qStem As String ' 当前题干 Dim optArr(1 To 8) As String Dim optCnt As Long Dim rowData() As String Dim rowCnt As Long rowCnt = 0 ReDim rowData(0 To 2000) For Each para In doc.Paragraphs s = para.Range.Text ' 去掉段落符和首尾空格 s = Replace(s, Chr(13), "") s = Replace(s, Chr(7), "") s = Trim(s) If Len(s) = 0 Then ' 空行:如果当前题目已经缓存,则保存 If qID > 0 And optCnt > 0 Then Call SaveRow(rowData, rowCnt, qID, qStem, optArr, optCnt) qID = 0 qStem = "" optCnt = 0 End If Else ' 先匹配题号 rx.Pattern = "^(\d{1,4})[\.、.]\s*(.*)$" Set m = rx.Execute(s) If m.Count > 0 Then If qID > 0 And optCnt > 0 Then Call SaveRow(rowData, rowCnt, qID, qStem, optArr, optCnt) End If qID = CLng(m(0).SubMatches(0)) qStem = m(0).SubMatches(1) optCnt = 0 Else ' 再匹配选项 rx.Pattern = "^([A-H])[\.、.]\s*(.*)$" Set m = rx.Execute(s) If m.Count > 0 Then If qID > 0 And optCnt < 8 Then optCnt = optCnt + 1 optArr(optCnt) = m(0).SubMatches(0) & ". " & m(0).SubMatches(1) End If Else ' 非题号、非选项行:拼接进上一处内容 If qID > 0 Then If optCnt = 0 Then qStem = qStem & " " & s Else optArr(optCnt) = optArr(optCnt) & " " & s End If End If End If End If End If Next ' 收尾保存最后一个题目 If qID > 0 And optCnt > 0 Then Call SaveRow(rowData, rowCnt, qID, qStem, optArr, optCnt) End If ' 在文档开头插入表格 If rowCnt > 0 Then Dim t As Table Set t = doc.Tables.Add(ActiveDocument.Paragraphs(1).Range, rowCnt + 1, 6) t.Borders.Enable = True ' 写入表头 t.Cell(1, 1).Range.Text = "题号" t.Cell(1, 2).Range.Text = "题干" t.Cell(1, 3).Range.Text = "A" t.Cell(1, 4).Range.Text = "B" t.Cell(1, 5).Range.Text = "C" t.Cell(1, 6).Range.Text = "D" Dim i As Long Dim j As Long Dim parts As Variant For i = 1 To rowCnt parts = Split(rowData(i), vbTab) For j = 0 To UBound(parts) t.Cell(i + 1, j + 1).Range.Text = parts(j) Next j Next i End If MsgBox "转换完成,共生成 " & rowCnt & " 行表格。" End Sub Sub SaveRow(rowData() As String, rowCnt As Long, qID As Long, qStem As String, optArr() As String, optCnt As Long) Dim line As String line = CStr(qID) & vbTab & qStem Dim i As Long For i = 1 To 4 If i <= optCnt Then line = line & vbTab & optArr(i) Else line = line & vbTab & "" End If Next i rowCnt = rowCnt + 1 rowData(rowCnt) = line End Sub这段代码的思路是:
- 遍历整个文档的段落。
- 用两个正则分别匹配“题号行”和“选项行”。
- 遇到新题号就把上一题存进数组。
- 遇到选项行就计入当前题目的选项数组。
- 遇到普通行则拼进当前题干或当前选项。
- 全部遍历完成后,在文档开头插入一个表格,把缓存的数据逐行写入。
我实测过几百道题的文档,运行时间通常不超过 5 秒。速度慢的瓶颈主要在于 Word 的段落对象遍历,但这类文档一般也就几百个段落,完全在可接受范围内。
3.3 进阶:把“答案”也抓出来
很多文档在题目最后会有一行“答案:A”或者“【答案】A”,如果想把答案也放进表格,可以在主循环里再加一个匹配规则,并且把表格从 6 列扩到 7 列。
在Else分支加上:
rx.Pattern = "^(答案|【答案】|参考答案)[::]\s*([A-H])" Set m = rx.Execute(s) If m.Count > 0 Then If qID > 0 Then ansBuf = m(0).SubMatches(1) Call SaveRowWithAnswer(rowData, rowCnt, qID, qStem, optArr, optCnt, ansBuf) ' 保存后重置状态 qID = 0 qStem = "" optCnt = 0 ansBuf = "" End If Else ' 原有拼接逻辑 End If这里的关键是:答案必须跟在当前题目后面被“消费”,一旦保存,就要把状态清空,否则下一道题的答案会串到上一道题里。这种状态管理的细节,新手最容易漏。
3.4 输出示例:转换前后的对比
转换前的文档内容可能是这样的:
1. 世界上面积最大的海洋是? A. 大西洋 B. 太平洋 C. 印度洋 D. 北冰洋 答案:B转换后,表格会是:
| 题号 | 题干 | A | B | C | D | 答案 |
|---|---|---|---|---|---|---|
| 1 | 世界上面积最大的海洋是? | 大西洋 | 太平洋 | 印度洋 | 北冰洋 | B |
我通常还会在代码后面加两行,把表格的默认字体调成“宋体五号”,并且用AutoFitBehavior让表格自适应窗口宽度。这样生成的表格放进正式文档里,基本不用再手动调格式。
4. 我在实战中踩过的坑与排查方法
4.1 正则表达式把中英文标点混在一起
第一个坑,也是最容易踩的坑:很多人只匹配“A.”这种半角标点,遇到“A、”或“A.”就识别不了。我一开始就吃过这个亏,后来把所有可能的分隔符全部写进正则字符集里,用[\.、.]一次性覆盖。类似的问题还出现在题号后面,可能出现“1、”或“1.”,我同样用[\.、.]解决。
如果发现脚本跑完之后表格行数明显少于题目数,先检查是不是这类标点问题。把正则里的标点字符集补齐,基本能解决 80% 的识别失败。
4.2 段落末尾的隐形字符
Word 段落对象para.Range.Text不仅仅包含你看到的文字,末尾还带一个段落标记Chr(13),有时还会有单元格结束符Chr(7)。如果不去掉就直接做 Trim 和正则匹配,“A. 选项内容”后面带着隐形字符,正则里的$匹配可能会受干扰。
我的处理习惯是开头先三连:
s = Replace(s, Chr(13), "") s = Replace(s, Chr(7), "") s = Trim(s)这段代码看起来不起眼,但少了它,脚本偶尔出现识别不稳定的情况,排查起来非常浪费时间。
4.3 宏运行时出现“下标越界”
下标越界这个报错,绝大多数出现在数组边界上。比如我刚开始给数组开的是ReDim rowData(0 To 1000),如果文档里有 500 道题,没问题;如果文档有 1200 道题,跑到第 1001 行就炸了。
我后来养成了一个习惯:使用前就分配一个足够大的数组,或者在每次保存行之前检查一下当前计数是否接近上界,接近就ReDim Preserve扩容。不过,最简单省事的方案是直接把初始大小设成 5000,一般场景绝对够用。注意ReDim Preserve不能缩小数组,扩容前要把旧数组备份好。
4.4 题干中出现数字导致误判
题库里有些题干会包含编号,比如“第 1 章中,下列说法错误的是”。这种行如果恰好以数字开头,就会被我的“题号正则”误认为是一道新题。
针对这种情况,我一般用两个策略。
第一,限制题号长度。正则用\d{1,4},一般题目编号不会超过 4 位数字,题干里的“第 1 章”则因为前面有“第”字不会命中。
第二,增加“题干连续性”判断:如果当前题目已经存在,并且选项个数还为零,新匹配到的“题号”如果不是紧接在上一题之后的合理范围内,也可以选择不开启新题。不过这样逻辑会复杂些,实际工作中我很少遇到这种冲突,因为题干很少直接以裸数字开头。
4.5 表格样式太丑?顺手统一格式
默认插入的表格,边框、字体、列宽都未必符合要求。我的做法是在表格写入完成后,加几行样式设置代码:
t.Range.Font.Name = "宋体" t.Range.Font.Size = 10.5 t.Range.Font.NameFarEast = "宋体" t.AutoFitBehavior wdAutoFitWindowAutoFitBehavior wdAutoFitWindow是让表格自适应窗口宽度,这样不会出现某列特别宽、其他列挤在一起的问题。如果你希望固定列宽,也可以用t.Columns.PreferredWidthType = wdPreferredWidthPercent配合百分比设置。
5. 后续还能怎么扩展
5.1 批量处理多个文档
一次只跑一个文档,当然能解决眼前的问题,但如果你有几十个文档要处理,逐个打开、运行宏、保存,也很烦。我通常在代码外层再加一层文件遍历逻辑:
Dim f As String f = Dir("D:\题库文件夹\*.docx") Do While f <> "" Set doc = Documents.Open("D:\题库文件夹\" & f) Call ConvertQuestionsToTable ActiveDocument.Save ActiveDocument.Close f = Dir Loop这样可以把整个目录的题目批量转好。注意每个文档里必须保持相近的格式,如果某个文档格式特别乱,脚本后续结果要人工抽查。
5.2 把结果导出到 Excel
有时候最终目标不是 Word 表格,而是 Excel 表格,这种情况我也遇到过。最简单的方法是在原脚本生成完 Word 表格后,再用 Excel 对象把表格数据导出:
Dim xlsApp As Object Set xlsApp = CreateObject("Excel.Application") xlsApp.Visible = True Dim wb As Object Set wb = xlsApp.Workbooks.Add ' 遍历 Word 表格的行列,逐格写入 Excel wb.Range("A1").CurrentRegion.ClearContents Dim r As Long Dim c As Long For r = 1 To t.Rows.Count For c = 1 To t.Columns.Count wb.Cells(r, c).Value = t.Cell(r, c).Range.Text Next c Next r这里要说明一下:从 Word 单元格读出来的文本末尾会带一个单元格结束符Chr(7),写入 Excel 前记得去掉,否则每个格子后面都会多出一个不可见符号。别小看这个细节,很多人都在这上面折腾过。
5.3 个人经验小结
工具脚本这件事,最忌讳一上来就想着写“完美版本”。我第一版只做了最标准的选项识别,跑完 300 道题用了 3 秒,但其中有 20 道因为格式特殊没被识别到。第二次我把正则的标点补全,加上了跨段拼接逻辑,成功率达到了 98%。剩下那 2%,我干脆用人工手动补,而不是继续堆代码复杂程度。
为什么我不追求 100% 自动化?因为题库格式永远比你想象的更混乱,为了最后几道题去写更复杂的分支,代码会变得越来越难维护,反而得不偿失。VBA 本身也不是什么高深技术,它就适合用来解决“重复劳动太多”的痛点。你要保证的不是代码 100% 搞笑,而是把 90% 的时间省下来,剩下的手工工作完全能接受。
最后再分享一个我的个人习惯:所有题库在转换之前,我都会先复制一份原稿,存成“备份_原始格式.docx”。转换脚本跑完发现结果不对,随时可以用备份回滚。这些看起来不起眼的习惯,比我写出多么漂亮的代码都更能省心。