用documentformat.openxml读.docx文本最稳,但paragraph.innertext在生产环境不可靠:它会吞并注释、删除内容等非正文节点,混淆格式,遗漏修订痕迹;正确做法是手动递归遍历paragraph→run→text节点提取text属性值。

直接说结论:用 DocumentFormat.OpenXml 读 .docx 文本最稳,但只遍历 Paragraph.InnerText 会丢内容、混格式、漏修订;真正可用的文本提取必须手动递归遍历 Run → Text 节点。
为什么 Paragraph.InnerText 在生产环境不可靠
它看似一行代码就能拿到段落文字,实际是“表面正确、底层危险”:
- 把
CommentRangeStart、DeletedText、ProofError这类非正文节点全吞进字符串,导致输出里冒出乱码或空格 - 自动把换行符
\r\n替换成空格,段落结构塌陷 - 遇到加粗/斜体/高亮文字时,若
Run下有多个Text(比如带Break的),InnerText只取第一个,后面文字直接消失 - 文档含修订模式(Track Changes)时,删除内容可能残留空白或占位符,而
InnerText不区分状态
用 OpenXML 正确提取正文文本的实操路径
核心是绕过 InnerText,自己走一遍 DOM 树:
- 打开文档:
WordprocessingDocument.Open(path, false),确保path是真实.docx(不是另存为兼容模式的假.docx) - 取主文档体:
mainPart.Document.Body - 对每个
Paragraph,遍历其所有Run子节点 - 对每个
Run,取run.Elements<text>().FirstOrDefault()?.Text</text>—— 注意是Text元素的Text属性值,不是InnerText - 过滤掉
null或全空白的文本,用\r\n分隔段落(别依赖节点自带换行)
示例关键片段:
var textBuilder = new StringBuilder();
foreach (var para in body.Elements<paragraph>())
{
var paraText = string.Empty;
foreach (var run in para.Elements<run>())
{
var textNode = run.Elements<text>().FirstOrDefault();
if (textNode?.Text != null)
paraText += textNode.Text;
}
if (!string.IsNullOrWhiteSpace(paraText))
textBuilder.AppendLine(paraText.Trim());
}</text></run></paragraph>
表格、页眉页脚、标题样式这些“正文之外”的内容怎么补
它们不在 Body 里,不处理就等于没读全:
-
HeaderPart和FooterPart需单独访问:mainPart.HeaderParts/mainPart.FooterParts,再按同样方式提取Paragraph→Run→Text - 表格内容完全独立:遍历
mainPart.Document.Body.Elements<table>(),每行每单元格再进 <code>Paragraph提取——注意一个单元格可能含多段 - 标题(如
Heading1)不是特殊节点,而是段落的ParagraphProperties里带OutlineLevel或引用了命名样式,需额外判断para.ParagraphProperties?.ParagraphStyleId?.Val -
.doc文件(OLE 复合文档)会直接抛FileFormatException,必须先转格式或换库(如FreeSpire.Doc或NPOI) - NuGet 包必须用
DocumentFormat.OpenXml 2.19+,老版本在.NET 6/8下可能加载失败或缺 API - 大文档(>50MB 或 >10k 段落)下,纯内存解析易 OOM;可考虑流式读取
Body的子节点,或分节处理
别踩的坑:.doc vs .docx、NuGet 版本、性能边界
OpenXML 只认 ZIP+XML 结构,和 Word 程序无关,但也因此有硬限制:
真正难的从来不是“能不能读出来”,而是“读出来的文本是否和用户双击打开看到的一致”——这要求你清楚知道哪些节点该跳过、哪些样式该忽略、哪些结构要展开。OpenXML 给你的是 XML 树,不是渲染结果。











