最近想用 js 搞搞 docx 文档的处理,其实 docx 文档也是属于一个压缩包,我们解压看看里面的结构吧

使用 Node JS 处理 docx 文档-天真的小窝

解压 docx 文档后我们大概能得到一个这样的结构包含这些目录和组件:

[Content_Types].xml:这个文件描述的是整个文档内容的类型,把各个xml文件组合成一个整体。

docProps 文件夹:这个文件夹中的xml记录了docx文档的主要属性信息,Core.xml:描述文件的创建时间,标题,主题和作者等给予open xml约定文档格式的通用文件属性,App.xml:描述文档的其他属性,文档类型,版本,只读信息,共享,安全属性等特定的文件属性

rels 文件夹:这个文件夹存放了所有指定的rels文件

rels 文件:这些文件描述了文档结构中的起始关系,也可以叫做关系部件

word 文件夹:这个文件夹基本就包含了一些样式和文档内容了,我们这里也只操作这里面的 document.xml 内容文件

我这里只是简单描述了一下,如果想了解更多可以上维基百科看看相关文章:https://en.wikipedia.org/wiki/Office_Open_XML

我们看看怎么用 JavaScript 解压我们的 Docx 吧,解压文件夹使用 unzip2 模块

var fs = require('fs');
const unzip = require('unzip2')
fs.createReadStream(__dirname + '/test.docx')
  .pipe(unzip.Extract({ path: __dirname + '/test' }));

解压成功之后我们就可以解析 xml 了,这里用 xml2js 模块,我这里写了一篇博客讲过怎么用 xml2js 的有兴趣的可以去看看 https://bin.zmide.com/?p=494

var xml2js = require("xml2js");
var parseString = xml2js.parseString;

var fs = require("fs");
var xml = fs.readFileSync(__dirname + '/test/document.xml', "utf-8");

parseString(xml, function(err, result) {
    const xmlDocsRootNode = result["w:document"]["w:body"][0]["w:p"];
    // ... 处理文档实现处 ...
});

这里我贴一下我的处理实现吧(因为文档内容不同可能获取的 key 不同,这里仅供参考)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpsCustomData="http://www.wps.cn/officeDocument/2013/wpsCustomData" mc:Ignorable="w14 w15 wp14"><w:body><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:ind w:firstLine="640"/><w:jc w:val="center"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/><w:rPr><w:rFonts w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:pPr><w:r><w:rPr><w:rFonts w:hint="eastAsia" w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>xxxx</w:t></w:r><w:r><w:rPr><w:rFonts w:hint="eastAsia" w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>这是一个deom</w:t></w:r><w:commentRangeStart w:id="0"/><w:r><w:rPr><w:rFonts w:hint="eastAsia" w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>demo</w:t></w:r></w:p><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:ind w:firstLine="640"/><w:jc w:val="center"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/><w:rPr><w:rFonts w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:pPr><w:r><w:rPr><w:rFonts w:hint="eastAsia" w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>[文档:xxxx</w:t></w:r><w:r><w:rPr><w:rFonts w:hint="eastAsia" w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>] V1.0</w:t></w:r><w:commentRangeEnd w:id="0"/><w:r><w:rPr><w:rStyle w:val="7"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:commentReference w:id="0"/></w:r></w:p><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:ind w:firstLine="640"/><w:jc w:val="center"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/><w:rPr><w:rFonts w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr></w:pPr><w:r><w:rPr><w:rFonts w:ascii="华文中宋" w:hAnsi="华文中宋" w:eastAsia="华文中宋"/><w:sz w:val="32"/><w:szCs w:val="32"/></w:rPr><w:t>this is APP</w:t></w:r></w:p><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/><w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr></w:pPr></w:p><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/><w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr></w:pPr><w:r><w:rPr><w:rFonts w:hint="eastAsia"/></w:rPr><w:t>import React, { Component, useContext, useCallback, useEffect, useMemo } from 'react';</w:t></w:r></w:p><w:p><w:pPr><w:keepNext w:val="0"/><w:keepLines w:val="0"/><w:pageBreakBefore w:val="0"/><w:widowControl w:val="0"/><w:kinsoku/><w:wordWrap/><w:overflowPunct/><w:topLinePunct w:val="0"/><w:autoSpaceDE/><w:autoSpaceDN/><w:bidi w:val="0"/><w:adjustRightInd/><w:snapToGrid/><w:spacing w:line="240" w:lineRule="atLeast"/><w:textAlignment w:val="auto"/><w:outlineLvl w:val="9"/></w:pPr><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/></w:p><w:sectPr><w:headerReference r:id="rId5" w:type="default"/><w:pgSz w:w="11900" w:h="16840"/><w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0"/><w:cols w:space="425" w:num="1"/><w:docGrid w:type="lines" w:linePitch="312" w:charSpace="0"/></w:sectPr></w:body></w:document>

// -------------------------------------
// 上面是我的 document.xml 文件内容,下面是解析处理代码,这里是向文档的最后一行插入一行 Hello world
// -------------------------------------

const xmlDocsRootNode = result["w:document"]["w:body"][0]["w:p"];
    
const xmlDocsNode = result["w:document"]["w:body"][0]["w:p"][4];
let newXmlDocsNode = ObjCopy(xmlDocsNode);
newXmlDocsNode["w:r"][0]["w:t"][0] = "Hello world";
let xmlButton = xmlDocsRootNode[xmlDocsRootNode.length - 1];
xmlDocsRootNode[xmlDocsRootNode.length - 1] = newXmlDocsNode;
xmlDocsRootNode[xmlDocsRootNode.length] = xmlButton;

通过 xml2js 解析后会返回一个 JSON 对象这边直接处理相关对象就好了

这里我再加一个辅助函数,JavaScript 深度克隆对象函数,因为我处理的时候要插入文本内容,就需要深度克隆一个对象(避免改变值的时候把原来的内容也改变了)

function ObjCopy(target) {
  let copyed_objs = [];
  // 此数组解决了循环引用和相同引用的问题,它存放已经递归到的目标对象
  function _deepCopy(target) {
    if (typeof target !== "object" || !target) {
      return target;
    }
    for (let i = 0; i < copyed_objs.length; i++) {
      if (copyed_objs[i].target === target) {
        return copyed_objs[i].copyTarget;
      }
    }
    let obj = {};
    if (Array.isArray(target)) {
      obj = [];
      // 处理target是数组的情况
    }
    copyed_objs.push({ target: target, copyTarget: obj });
    Object.keys(target).forEach(key => {
      if (obj[key]) {
        return;
      }
      obj[key] = _deepCopy(target[key]);
    });
    return obj;
  }
  return _deepCopy(target);
}

处理过后还是 JSON 对象,我们怎么把它转回 xml 并且保存呢?

var fs = require("fs");
var xml2js = require("xml2js");

var builder = new xml2js.Builder({
  renderOpts: { pretty: false, indent: " ", newline: "" }
});
var xmlTest = builder.buildObject({ ...result });
fs.writeFile(__dirname + '/test/document.xml', xmlTest, function(err) {
  if (err) {
    return console.error(err);
  }
  // console.log("数据写入成功!");
});

好了,现在我们的 Docx 文档基本处理完成了,就差最后一步打包了

我压缩文件这里用的是 archiver 模块

var fs = require('fs');
var archiver = require('archiver');

var output = fs.createWriteStream(__dirname + '/example.docx');
var archive = archiver('zip', {
  zlib: { level: 9 } // Sets the compression level.
});


output.on('close', function() {
  console.log(archive.pointer() + ' total bytes');
  console.log('archiver has been finalized and the output file descriptor has closed.');
});

output.on('end', function() {
  console.log('Data has been drained');
});


archive.on('warning', function(err) {
  if (err.code === 'ENOENT') {
    // log warning
  } else {
    // throw error
    throw err;
  }
});

archive.on('error', function(err) {
  throw err;
});

archive.pipe(output);

archive.directory(__dirname + '/test/', false);

archive.finalize();

好的,这样我们就吧处理好的文档输出到 example.docx 里了,用 Docx 软件打开看看吧