ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java用Apache POI实现Word与TXT互转的完整指南

Java用Apache POI实现Word与TXT互转的完整指南 接手这类文档转换需求我一度以为就是把文件后缀名改一改。真正做下来才发现Word 里的文字根本不是“整齐地摆在那里”TXT 也不是换个名字就能读懂的。如果你正打算在 Java 项目中写 Word 和 TXT 互转或者接了个批量文档清洗的需求这篇文章应该能帮你少踩几个坑。我会围绕 Apache POI 这套常用的开源方案把从零开始接 Word、掌住编码、处理旧版 .doc 文件、写回带格式 Word 的完整过程拆开讲顺便把那些测试环境没问题、一到生产环境就翻车的细节也交代清楚。1. 整体思路与方案选择1.1 先搞清楚 Word 和 TXT 的本质差异很多人拿着一个a.doc就想通过改后缀变成a.txt拿a.docx改名为a.zip后确实能看到一堆 XML但直接把这些 XML 拼起来当文本用得到的全是标签和乱码。这里面的关键点在于TXT 是纯文本文件文字就是文字而 Word 文件是“容器”文字、样式、图片、页眉页脚、修订记录都被打包在一个结构化容器里。.docx本质是一个 zip 压缩包里面有word/document.xml这样的 XML 文件文字内容是嵌在w:p、w:r、w:t这类节点里的。.doc更旧采用的是 OLE 复合文档格式一种二进制结构需要专门的解析器才能读出文本。所以想用 Java 实现互转不能走“字符串读入再写出去”的野路子必须借助解析 Office 文档结构的专用工具。我最早接手一个需求是给某公司做历史合同归档几千份合同全是.doc老格式。如果直接按字节读取出来全是乱码甚至卡死。后面用了专门解析二进制文档的库才把正文干净地提出来。这个项目也让我养成了看到转换需求先确认文件版本的习惯.doc、.docx、.txt这三者的处理策略完全不同一上来就写统一代码注定要返工。1.2 常见实现路线对比做 Word 和 TXT 互转圈子里能选的路线就那么几条各有利弊。我列个表给你看实现路线优点缺点适合场景Apache POIHWPF XWPF社区成熟、API 稳定、支持读写对.doc支持不如.docx完善绝大多数后端批量处理纯 Java 文件流没有额外依赖、速度快只能处理纯文本Word 结构解析不了TXT 到 TXT、简单拼接调用外部命令行工具如第三方文档转换器兼容性最强依赖系统环境、性能损耗大、部署复杂容器环境、格式很复杂的文档在线转换 API使用简单有数据安全风险、不适合离线环境临时个人处理生产项目慎用我后续的示例会以 Apache POI 为主原因很直接它在 Java 生态里对 Word 文件的支持最完整而且不需要额外部署独立服务打成 jar 就能跑运维成本低。选择 Apache POI 不是因为它没有缺点。.doc老格式的富文本处理、复杂样式还原POI 的能力是有边界的。但如果你只是做“把 Word 正文提取成纯文本”和“把纯文本写进 Word 文档”这套组件完全够用上手难度也最低。对于那种还要保留图片、表格、复杂排版的转换需求POI 力不从心得另想办法。我遇到的老合同案例里就需要弄清楚“哪些要素必须保留”这决定了选型。1.3 方案选型背后的考量我之所以强调先读懂需求再选方案是因为 Word 转 TXT 的“转”字有很多层含义。有人只要正文内容有人要保留段落顺序有人要连表格里的格子内容都按顺序导出还有人希望把页眉页脚一起抓出来。只提取正文段落用 XWPFParagraph 遍历即可连表格一起提取需要额外遍历 XWPFTable提取文本并保留层级需要在段落中识别样式、编号提取后交给下游搜索/分析建议输出成带分隔符的结构化文件而不是纯 TXT。这些需求直接影响代码实现。我在下面几节会按“从最简到稍复杂”的层次来写你可以按需取用。2. 核心细节原理解析2.1 .docx 的文本到底藏在哪很多时候你打开一个.docx眼睛看到的是排版整齐的文章但用文本编辑器打开文件看到的是乱码。原因就是上面提到的 zip 压缩结构。.docx的内部结构大致是这样mimetype _rels/.rels word/document.xml word/_rels/document.xml.rels word/styles.xml真正承载正文的word/document.xml是一个 XML 文件。文本流藏在w:body里每一段是w:p段落中的不同样式片段是w:r真正的内容在w:t标签里。举例一句“今天天气不错”可能是这样存的w:p w:r w:t今天天气不错/w:t /w:r /w:p如果中间某个词被加粗就会多出一个w:rPr节点。复杂文档里还会有文本框、表格、批注它们嵌在不同的节点结构中。所以直接从 XML 里做正则匹配理论可行但你会被各种边角情况折磨。Apache POI 的作用就是帮你把这些 XML 节点解析成 Java 对象然后通过 API 取文本省去手工解析的麻烦。2.2 编码问题UTF-8 与 GBK 的恩怨TXT 转 Word 时最常见的问题是中文乱码。这不是 Java 的锅而是编码不统一导致的。文本文件本身没有元信息显示编码Windows 记事本保存的 TXT 默认是 GBK或者说系统 ANSI 编码而 Linux 环境和 Java 代码里普遍默认 UTF-8。你写Files.readAllLines(path)时如果不指定字符集Java 会用平台默认值。在 Windows 上可能还有救在 Linux 服务器上就会读取 GBK 文件变成乱码。正确做法是读文件时显式传入编码比如StandardCharsets.UTF_8或者Charset.forName(GBK)。写 TXT 时同理。你导出的文件如果拿到 Windows 上打开可能因为少了 UTF-8 BOM 头而显示乱码。Java 默认不写 BOM这时需要在输出流最前面手动写入EF BB BF三个字节。很多“为什么我导出的文件客户打开是乱码”的问题就是 BOM 的问题。2.3 区分.doc和.docx的 APIApache POI 对两种 Word 格式提供不同的 API文件格式POI 包核心类.doc二进制poiHWPFDocument、WordExtractor.docxXMLpoi-ooxmlXWPFDocument、XWPFParagraph.docx的 API 设计得比较现代操作对象像标准 Java 集合遍历段落、创建段落都很直观。.doc的老 API 则更像一个“只读提取器”读写支持和.docx差了一截尤其对复杂样式的写入踩坑概率高。如果你要处理大量.doc老文件我建议升级到较新的 POI 版本老版本对某些新版 WPS 生成的.doc文件解析会直接抛异常或者丢内容。后面“常见问题”一节我会专门聊几个真实遇到的报错。3. 实操Word 转 TXT 的完整过程3.1 项目初始化和依赖引入我用 Maven 做依赖管理。如果你用 Gradle 或手动引 jar思路一致。下面是pom.xml里的关键部分properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.apache.poi/groupId artifactIdpoi/artifactId version5.2.5/version /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency /dependencies这里要留意poi和poi-ooxml的版本号一定要对齐。我以前曾把poi用 4.1.2、poi-ooxml用 5.0.0启动时直接报NoSuchMethodError排查了半天才发现是版本不一致导致的。POI 5.x 要求 Java 8 以上建议 Java 11 起跳。3.2 解析 .docx 并输出为 TXT读.docx的核心代码非常短import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class DocxToTxt { public static void convert(String docxPath, String txtPath) throws IOException { // 读取原始 Word 文档 try (InputStream is Files.newInputStream(Paths.get(docxPath)); XWPFDocument document new XWPFDocument(is); BufferedWriter writer Files.newBufferedWriter( Paths.get(txtPath), StandardCharsets.UTF_8)) { for (XWPFParagraph paragraph : document.getParagraphs()) { String text paragraph.getText(); if (text null || text.isBlank()) { // 遇到空段落也输出空行保持原有文档的大致行结构 writer.newLine(); } else { writer.write(text); writer.newLine(); } } } } public static void main(String[] args) throws IOException { convert(input.docx, output.txt); System.out.println(转换完成); } }这里XWPFDocument会负责解析整个 zip 结构和 XMLdocument.getParagraphs()返回正文中的所有段落。注意getText()拿到的只是段落里的文本不含图片内容。如果一个段落只有一张图片getText()会返回空字符串。我还习惯在遍历时打印一下段落总数和总字符数用于快速判断文件是否解析成功long totalChars 0; for (XWPFParagraph paragraph : document.getParagraphs()) { totalChars paragraph.getText().length(); } System.out.println(段落数: document.getParagraphs().size()); System.out.println(总字符数: totalChars);这个“总字符数”是后面做批量校验的重要指标。如果源文件有 5000 字你转出来只剩 300那一定有问题。3.3 解析老式 .doc 文件.doc老格式用WordExtractor更方便import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.extractor.WordExtractor; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class DocToTxt { public static void convert(String docPath, String txtPath) throws IOException { try (InputStream is Files.newInputStream(Paths.get(docPath)); WordExtractor extractor new WordExtractor(is)) { String content extractor.getText(); // 这里可以按业务需求做进一步清洗 Files.writeString(Paths.get(txtPath), content, StandardCharsets.UTF_8); } } public static void main(String[] args) throws IOException { convert(input.doc, output_from_doc.txt); System.out.println(转换完成); } }WordExtractor.getText()会把正文、页眉、页脚、文本框等多个来源的文本拼接在一起中间可能有额外的换行符或分隔符。如果你只想要正文需要在提取后做过滤。一个简单的做法是按行拆分然后丢弃明显属于页眉页脚重复内容的行但这属于“脏活”得结合具体文档来写规则。另一个重要点是.doc文件可能在末尾包含版本历史、批注等元信息getText()有时会把它们带出来。生产环境里我通常会在提取后跑一遍正则清洗和关键词过滤确保导出文件里只有真正要交付的内容。3.4 关于表格、文本框和页眉页脚上面两段代码只处理了段落。如果 Word 里有表格document.getParagraphs()是拿不到表格里文字的。这就涉及一个经常被忽略的问题Word 转 TXT 到底要不要转表格我的建议是如果业务上表格内容必须保留那么不能只遍历段落要另写逻辑遍历document.getTables()。示例代码如下for (XWPFTable table : document.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { StringBuilder cellText new StringBuilder(); for (XWPFParagraph paragraph : cell.getParagraphs()) { cellText.append(paragraph.getText()); } // 用制表符或竖线分隔单元格 writer.write(cellText.toString()); writer.write(\t); } writer.newLine(); } }这样导出的 TXT 依然不是“漂亮”的表格但保留了行列顺序至少比直接把表格丢掉强得多。同理文本框里的文字在不同版本的 POI 里支持程度不一样如果发现漏内容可以试试直接解析 XML 节点但那种方案只适合特别较真的场景日常批量处理先接受“部分丢失”。4. 实操TXT 转 Word 的实现方法4.1 最简单但能用的方案TXT 转 Word 相对简单因为 Word 文档是“可创建”的。你新建一个空白XWPFDocument每读一行 TXT 就新建一个段落。基础代码如下import org.apache.poi.xwpf.usermodel.*; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; public class TxtToDocx { public static void convert(String txtPath, String docxPath) throws IOException { try (BufferedReader reader Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8); XWPFDocument document new XWPFDocument(); OutputStream out Files.newOutputStream(Paths.get(docxPath))) { String line; while ((line reader.readLine()) ! null) { // 每个非空行作为一个段落 XWPFParagraph paragraph document.createParagraph(); XWPFRun run paragraph.createRun(); run.setText(line); } document.write(out); } } public static void main(String[] args) throws IOException { convert(notes.txt, notes.docx); System.out.println(转换完成); } }这个方案的问题是TXT 里的空行会直接丢失因为readLine()读取空行时返回空字符串我上面的代码还会创建空段落但如果你在循环里加了continue跳过空串就应该后续补document.createParagraph()。哪种更好看你对“保留原文行结构”的要求个人建议保留空段落转出来的 Word 会更接近原 TXT 的观感。4.2 让 Word 更“像样”标题、加粗和居中只有文字的 Word 文档在交付时往往会被嫌弃尤其是给业务方看的时候。既然用 POI你可以轻松地为 TXT 中的特定行加格式。比如约定规则以“# ”开头的行变成标题以“## ”开头的变成二级标题其余是正文。public static void convertWithFormat(String txtPath, String docxPath) throws IOException { try (BufferedReader reader Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8); XWPFDocument document new XWPFDocument(); OutputStream out Files.newOutputStream(Paths.get(docxPath))) { String line; while ((line reader.readLine()) ! null) { XWPFParagraph paragraph document.createParagraph(); if (line.startsWith(# )) { paragraph.setAlignment(ParagraphAlignment.CENTER); XWPFRun run paragraph.createRun(); run.setText(line.substring(2)); run.setBold(true); run.setFontSize(22); run.setFontFamily(宋体); } else if (line.startsWith(## )) { XWPFRun run paragraph.createRun(); run.setText(line.substring(3)); run.setBold(true); run.setFontSize(16); } else { XWPFRun run paragraph.createRun(); run.setText(line); run.setFontSize(12); } } document.write(out); } }有几个细节值得注意。setFontFamily(宋体)在 Linux 服务器上并不会真正“嵌入”字体它只是写入字体名称最终打开文档的电脑如果没有宋体会自行替换。这个行为是正常的不用慌。如果业务上要求字体必须是某种可以预先在服务器上安装对应字体但这属于系统层面的事Java 代码改变不了。4.3 把 TXT 转换结果存成 .doc 的注意事项如果你遇到的业务非要.doc后缀我的建议是原则上报错或生成.docx后改名而不是用 HWPF 硬生成.doc。原因很现实POI 对.doc的写入支持远不如.docx。HWPF 能创建文档、写入基本段落但对中文样式、页眉、复杂分节的支持都有各种限制容易踩到内存或未实现功能的坑。我做过一个项目客户明确要求导出.doc因为内部老系统只认这个格式。我的折中方案是先用XWPFDocument生成.docx再用第三方转换服务统一转为.doc。如果不想引入重量级方案也可以把.docx文件名直接改成.doc很多 Office 软件能识别并打开但这本质上是不规范的不推荐用于正式交付。5. 进阶批处理转换与结果校验5.1 批量转换目录下的所有文件实际业务中很少只有一个文件要转更多是一整个目录下的 Word 文件批量变 TXT或者反过来。批量处理最怕的就是单文件异常导致整个任务中断。我推荐的做法是每个文件单独捕获异常并把失败信息记录下来。Path sourceDir Paths.get(/data/docs); Path targetDir Paths.get(/data/txts); Files.createDirectories(targetDir); try (StreamPath paths Files.walk(sourceDir)) { paths.filter(Files::isRegularFile) .filter(p - p.toString().endsWith(.docx) || p.toString().endsWith(.doc)) .forEach(p - { String fileName p.getFileName().toString(); int dotIndex fileName.lastIndexOf(.); String baseName dotIndex 0 ? fileName.substring(0, dotIndex) : fileName; Path target targetDir.resolve(baseName .txt); try { if (p.toString().endsWith(.docx)) { DocxToTxt.convert(p.toString(), target.toString()); } else { DocToTxt.convert(p.toString(), target.toString()); } System.out.println(OK: p); } catch (Exception e) { System.err.println(FAIL: p - e.getMessage()); } }); }这里有两个性能相关的细节。Files.walk遍历大目录时是惰性加载配合try-with-resources能避免文件句柄泄漏。如果想要更快可以改造成并行流或者线程池。但要注意POI 解析文档属于内存密集型操作并行度不宜太高我一般控制在 CPU 核心数以内否则反而因为 GC 频繁导致整体变慢。5.2 转换后的内容质量如何校验转换软件和手工交付不一样自动化批量转换后必须有一套校验机制。我常用的轮子有三层字符数对比统计原文档字符总数可以通过 POI 的paragraph.getText()累加和输出 TXT 的字符数对比允许一定误差但不能差太多内容抽样随机抽几个段落确认含有关键业务词汇行数稳定性转换后的 TXT 行数应在合理区间内如果比段落数还少说明有空段落被跳过。public static long countCharsInTxt(String txtPath) throws IOException { long count 0; try (BufferedReader reader Files.newBufferedReader(Paths.get(txtPath), StandardCharsets.UTF_8)) { String line; while ((line reader.readLine()) ! null) { count line.length(); } } return count; }这种“自动化校验”在别人看来可能多余但真正经历过一次漏转换事故后就会明白它多重要。那次事故是某个.doc文件用旧版 POI 解析时内容提取完全失败但没抛异常返回的是空字符串。如果没有字符数校验这批空文件就静默交付出去了。5.3 大文件转换的内存建议POI 解析.docx时会把文档结构加载进内存一个 10MB 的 Word 文件解析时可能占用 150MB 以上堆内存。如果内存紧张有几个方向使用 SXSSF 的“流式写”思维在这里不适用XWPF 没有真正意义上的流式读尽量使用XWPFDocument(InputStream)不要用XWPFDocument(File)因为传入 File 时 POI 会再做一次内存映射转换完立即关闭资源循环中不要持有多个XWPFDocument实例对超大文档考虑分页加载或使用底层 XML 事件解析但开发成本高一般不做。我遇到过一个 80MB 的.docx里面有大量图片用默认参数解析直接 OOM。最后的处理方式是把文档先拆分成多个章节再转换或者干脆换用底层解析模式。这类极端情况跑一次就能涨不少经验好在日常工作里 5MB 以下的文档占绝大多数。6. 常见问题与排查技巧实录6.1 中文乱码到底是编码还是字体的问题我最早遇到中文乱码第一反应是“编码设置错了”于是反复在new String(bytes, UTF-8)和GBK之间切换。后来才发现乱码分很多种先要分清是读取乱码、写入乱码还是字体缺失导致的显示乱码。读取 TXT 乱码表现为或“锟斤拷”大概率是读取时用的字符集和文件实际编码不一致写入 TXT 后在 Windows 记事本打开乱码大概率是写入时用了 UTF-8 但是没有 BOM 头Windows 默认按 ANSI 解读生成的 Word 在别人电脑上打开中文变成方块或换字体这不是乱码是字体缺失和编码无关。排查技巧用十六进制工具看一眼前几个字节。如果文件开头是EF BB BF是 UTF-8 带 BOM如果以FF FE开头是 UTF-16 LE如果什么都没有可能是 ANSI/GBK需要进一步用文本编辑器探测。6.2 读取 .docx 抛异常或者内容为空POI 读取.docx最常见的异常有两类。一类是java.lang.NoClassDefFoundError比如缺少xmlbeans依赖解决方法是把poi-ooxml完整引入带齐它的传递依赖。另一类是org.apache.xmlbeans.XmlException通常是文件本身损坏或不是标准 Office 文件。还有一种情况文件后缀是.docx但实际是旧版.doc改后缀得到的。POI 使用XWPFDocument读这种文件会失败。改用WordExtractor读又能成功。所以在文件格式判断上不能只看后缀必要时要做“嗅探”也就是读文件头字节判断真实格式。6.3 转换后丢失内容表格和文本框我在 3.4 节提过表格和文本框的提取问题。这里再强调一次如果只用getParagraphs()表格内容一定会丢。遇到“内容少了”的情况第一件事是检查源文档里是否有表格、文本框、公式等非段落对象。想比较完整地导出可以做一个简单的扩展先遍历段落再遍历表格最后遍历图形文本。但这种做法也无法 100% 保证顺序因为文档中的对象嵌套关系很复杂。我的建议是按业务需求取舍明确告诉下游“TXT 只保留正文段落”避免后续扯皮。6.4 文件被占用或权限问题Windows 服务器上做批量转换时经常遇到文件被打开的报错。POI 读取文件时如果检测到文件被独占会抛FileNotFoundException或AccessDeniedException。这时候不要立刻重试先检查是否有 Office 进程占用了目标文件。输出文件路径也不要有奇怪字符。我遇到过目标路径包含中文空格导致写入失败的情况。稳妥的做法是统一用英文路径或者提前用Files.createDirectories创建目录。6.5 性能太慢瓶颈往往在 I/O 而不在 POI有一次批量转换几百个文件跑了一个小时还没结束。我下意识以为是 POI 解析慢后来加了耗时统计才发现绝大多数时间花在读取网络磁盘上。如果你的 PDF/Word 文件放在共享存储或远程目录Files.newInputStream的耗时常常比解析还高。优化方向把远程文件先下载到本地临时目录再交给 POI 解析批量场景下用固定线程池控制并发转换完成后的目标 TXT 不逐行写而是攒成StringBuilder一次性写出减少系统调用。这里要提醒一句一次性写大字符串也可能爆内存所以我一般攒到 1MB 左右就 flush 一次。7. 扩展从工具方法到可复用服务7.1 封装一个支持多格式的转换接口写到这里你已经有了 Word 转 TXT 和 TXT 转 Word 的代码。但如果你直接把这些方法扔给业务方对方大概率会嫌“不够好用”。更好的做法是封装一个统一接口比如FileConverter让调用方只关心源文件和目标路径。public interface FileConverter { void convert(String sourcePath, String targetPath) throws IOException; } public class WordToTxtConverter implements FileConverter { Override public void convert(String sourcePath, String targetPath) throws IOException { if (sourcePath.toLowerCase().endsWith(.docx)) { DocxToTxt.convert(sourcePath, targetPath); } else if (sourcePath.toLowerCase().endsWith(.doc)) { DocToTxt.convert(sourcePath, targetPath); } else { throw new IllegalArgumentException(不支持的文件格式: sourcePath); } } }接口的好处是后续可以替换具体实现比如从 Apache POI 换成别的组件不需要改调用方代码。7.2 配合 Spring Boot 暴露上传下载接口很多人的实际需求是做成一个 HTTP 接口前端上传 Word后端返回 TXT。这个场景下代码和文件流处理几乎是一样的不同点是要从MultipartFile读取输入流并把结果写入HttpServletResponse的输出流。PostMapping(/convert/word-to-txt) public ResponseEntitybyte[] convertWordToTxt(RequestParam(file) MultipartFile file) throws IOException { XWPFDocument document new XWPFDocument(file.getInputStream()); StringWriter writer new StringWriter(); for (XWPFParagraph paragraph : document.getParagraphs()) { writer.write(paragraph.getText()); writer.write(\n); } byte[] result writer.toString().getBytes(StandardCharsets.UTF_8); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filenameconverted.txt) .contentType(MediaType.TEXT_PLAIN) .body(result); }这个接口看起来简单但有两个隐患。一是上传文件过大时MultipartFile会把整个文件加载进内存容易把服务压垮。二是StringWriter对超大文档同样吃内存。生产环境里要结合项目实际情况做限制或者换成流式处理。7.3 定期任务与日志埋点最后分享一个我自己的习惯任何文件转换任务都要打结构化日志。不是System.out.println而是记录文件大小、解析耗时、段落数、字符数。这些数据平时不起眼你要排查“为什么凌晨批量任务挂了”时就成了救命稻草。long start System.currentTimeMillis(); // 执行转换 long cost System.currentTimeMillis() - start; System.out.printf(file%s, size%d, paragraphs%d, chars%d, cost%dms%n, sourcePath, fileSize, paragraphCount, charCount, cost);日志记录多了你会慢慢摸出规律比如“超过 3MB 的.docx转换耗时基本在 5 秒以上”“带大量图片的文档容易内存飙升”。这些经验值用在实际容量规划上非常有用。我自己的体会是Word 和 TXT 转换本身不算一个高深的技术点真正考验人的地方在于格式判断、编码处理、异常兜底和批量场景的工程化。很多人写完核心转换代码就以为完事了等到生产环境跑起来才发现各种“不按套路出牌”的文件。如果你能把上面这些边界情况都处理好这套转换功能就能稳定撑起大部分业务需求。最后再分享一个小技巧凡是转换类的工具代码我建议一定加一个“转换前后对比”的测试用例用一个你自己准备的、包含表格、图片、中文、长段落的样例文档每次改完版本跑一遍比什么都稳。
RELATED READING

延伸阅读

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