java代码使用http请求解压zip包并解析xml_Javascript 是如何解析 Excel 文件的?

最近要做一个导入导出 Excel 的功能,上一次做这个功能的时候,还是用的 Java Apache POI,这是一个用 Java 编写的免费开源的跨平台的 Java API,能够对 Microsoft Office 格式文件进行读和写。整体感受就是,做简单场景的 Excel 导入导出还可以,但是如果需要加一些复杂交互功能的话,前后端交互就比较繁琐了,可能需要来来回回好几次请求(比如字段映射,预览,隐藏行列等场景)。

去网上查了下,发下了一个叫做 sheetjs 的前端库,号称是一个用于多种电子表格格式的解析器和编写器,github 上的使用文档很全。大概使用了下,功能确实强大,感慨作者 NB 的同时,顺带看看这么神奇的功能是如何实现的。

本文为大家讲解 Excel 文档的解析方法,所涉及到的代码都来自 sheetjs 源码。

Excel 解析原理

我新建了一个名称为 “user.xlsx” 的 Excel 文件,在 Sheet1 上建立了如下表格,然后保存。

|    A       B          C         D       E
__|_______________________________________________
1 |  Name     Age     Birthday    Hobby           
__|_______________________________________________
2 |  Tom      29      1901/7/7    Music   Football
__|_______________________________________________
  |

xlsx 文件是个二进制文件,使用文本解析的方式只能得到一大堆乱码,那 JS 是如何解析这个文件呢?微软的官方网站有一篇文章 Word, Excel, and PowerPoint Standards Support 中说明了 Word、 Excel 和 PowerPoint 遵循 Open Document Format (ODF) 和 Open XML 协议,并给出了协议的主要约定。

Excel(.xlsx)文件格式遵循的是 Office Open XML 规范,这个规范最初是 Office 2007 产品开发的技术规范,现已成为国际文档格式标准[ISO / IEC-29500-3]。

简而言之,Excel(.xlsx)文件是遵循某种规范的 XML 文件集合。

是不是感觉那里有点不对劲,xml 是个文本协议,而 xlsx 文件明明是个二进制文件,怎么都挨不到一块去。

那是因为,Excel(.xlsx)文件是 XML 文件集合经过 zip 打包后的结果。

Excel(.xlsx)文件可以通过压缩软件进行解压缩。以 user.xlsx 为例,解压缩后得到如下文件夹。

|-- user
    |-- _rels
        |-- .rels
    |-- docProps
        |-- app.xml
        |-- core.xml
    |-- xl
        |-- _rels
            |-- workbook.xml.rels
        |-- printerSettings
            |-- printerSettings1.bin
            |-- printerSettings2.bin
            |-- printerSettings3.bin
        |-- theme
            |-- theme1.xml
        |-- worksheets
            |-- sheet1.xml
            |-- sheet2.xml
            |-- sheet3.xml
        |-- sharedStrings.xml
        |-- styles.xml
        |-- workbook.xml
    |-- [Content_Types].xml

可以看出,Excel(.xlsx)文件经过解压后得到同名文件夹,文件夹中按照一定的规则放置了一些 XML 文档。XML 是一种标记语言,很类似 HTML。此时我们解析 xlsx 文件变成了按照 Open XML 协议的规定解析 XML 文件,难度大大降低。

我们回过头来看看 sheetjs 读取 Excel(.xlsx) 文件逻辑,上传一个 Excel 文件后,sheetjs 会调用 readSync 方法读取这个二进制流。通常,压缩文件包括文件头信息 + 文件压缩数据,其中,文件头信息可以用来确定这个文件的类型,ZIP 文件的文件头是 0X04034B50,只要满足这个头,那文件就是 ZIP 压缩文件。

function readSync(data, opts) {
  ...
  switch((n = firstbyte(d, o))[0]) {
    case 0x50: return (n[1] === 0x4B && n[2] < 0x09 && n[3] < 0x09) ? read_zip(d, o) : read_prn(data, d, o, str);
    ...
  }
  ...
}

function read_zip(data, opts) {
  var zip, d = data;
    var o = opts||{};
    if(!o.type) o.type = (has_buf && Buffer.isBuffer(data)) ? "buffer" : "base64";
    zip = zip_read(d, o);
    return parse_zip(zip, o);
}

所以,sheetjs 将这个 Excel 文件 zip 解压后,得到一个同名文件夹和若干 XML 文件,后续它会解析这些 XML 文件,这就是 JS 解析 Excel 文件的原理。

Excel 解析流程

Excel 文件解析流程实际上是 JavaScript 语言实现 Office Open XML 规范的过程。目前这个规范已经是国际标准了,详见 ISO / IEC-29500-3。文档的解析过程比较繁琐,流程大致如下(由于我的 Excel 没有使用函数,宏命令等高级功能,流程有可能不全)。

总体而言, Excel 解析遵循由大到小,由总到分的规则。

1. 解析 [Content_Types].xml 文件

[Content_Types].xml 文件是一个总体资源文件列表,该文件中描述了这个 excel 文件中的 xml 资源的路径,以及遵守的规范。我们截取一部分源码如下:

...
<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
...

可以看出,在 Override 节点下,PartName 属性表示某个文件的路径,ContentType 表示该文件的类型,实质是该文件遵守的规范,比如 application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml 表示该文件用于描述 Excel Sheet,这个 XML 文件必须按照特定的规则来描述 sheet,否则就会解析错误。

解析 [Content_Types].xml 文件后得如下对象。该对象展示了当前 Excel 文件中不同作用的 XML 文件的路径。后续会根据这个文件中的路径信息,依次处理不同规则的 XML 文件。

{
  TODO: [],
  calcchain: "",
  charts: [],
  comments: [],
  coreprops: ["/docProps/core.xml"],
  custprops: [],
  defaults: {
    bin: "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings", 
    rels: "application/vnd.openxmlformats-package.relationships+xml", 
    xml: "application/xml"
  },
  dialogs: [],
  drawings: [],
  extprops: ["/docProps/app.xml"],
  links: [],
  macros: [],
  rels: Array(0),
  sheets: (3) ["/xl/worksheets/sheet2.xml", "/xl/worksheets/sheet3.xml", "/xl/worksheets/sheet1.xml"],
  sst: "/xl/sharedStrings.xml",
  strs: ["/xl/sharedStrings.xml"],
  style: "/xl/styles.xml",
  styles: ["/xl/styles.xml"],
  themes: ["/xl/theme/theme1.xml"],
  vba: [],
  workbooks: ["/xl/workbook.xml"],
  xmlns: "http://schemas.openxmlformats.org/package/2006/content-types"
}

2. 解析 style, theme 以及 sharedStrings

theme 文件中包含了这个 Excel 中主题的默认配置,比如默认的字体,默认的字号,默认行间距等等。

style 记录当前工作簿的设置。

解析后同样会生成 theme 和 style 对象,就不细看了。

这里只关注下 sharedStrings,这个文件表示的是 sheet 中用到的一些字符串。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="7" uniqueCount="7">
    <si><t>age</t></si>
    <si><t>name</t></si>
    <si><t>Tom</t></si>
    <si><t>birthday</t></si>
    <si><t>bobby</t></si>
    <si><t>music</t></si>
    <si><t>football</t></si>
</sst>

这里最终会转换为字符串数组。后续处理 Sheet 的 XML 文件的时候会用到。

3. 解析 workbook.xml 文件

workbook.xml 位于 "/xl/workbook.xml",该文件用于描述 Excel 工作簿的相关属性,比如当前工作簿大小(workbookView),包含几个工作表(sheets),每个工作表(sheets)的外键(用于关联到 sheet 的具体属性)是多少等等。我们可以

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <fileVersion appName="xl" lastEdited="4" lowestEdited="4" rupBuild="4505"/>
  <workbookPr filterPrivacy="1" defaultThemeVersion="124226"/>
  <bookViews>
    <workbookView xWindow="0" yWindow="90" windowWidth="19200" windowHeight="11640"/>
  </bookViews>

  <sheets>
    <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
    <sheet name="Sheet2" sheetId="2" r:id="rId2"/>
    <sheet name="Sheet3" sheetId="3" r:id="rId3"/>
  </sheets>

  <calcPr calcId="124519"/>
</workbook>

解析 workbook.xml 后,可以得到一个 workbook 对象,如下所示,有个可以看出,该对象有挺多的默认属性的,我们绘制的 Excel 比较简单,很多没有用到。

{
  AppVersion: {
    appName: "xl", 
    lastEdited: "4", 
    ...
  },
  CalcPr: {
    calcCompleted: "true",
    calcId: "124519"
    ...
  },
  Names: [],
  Sheets: [
    {name: "Sheet1", sheetId: "1", sheetid: "1", id: "rId1", Hidden: 0},
    {name: "Sheet2", sheetId: "2", sheetid: "2", id: "rId2", Hidden: 0},
    {name: "Sheet3", sheetId: "3", sheetid: "3", id: "rId3", Hidden: 0}
  ],
  WBProps: {
    CodeName: "",
    allowRefreshQuery: false,
    ...
  },
  WBView: [
    {
      windowHeight: "11640",
      windowWidth: "19200",
      ...
    }
  ],
  xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
}

4. 解析 Excel 配置

Excel 配置 包括 coreprops 和 custprops 两部分。本例中并没有用到 custprops,只有 coreprops,大概看下结构。其实就是一些配置,很简单。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <dc:creator></dc:creator>
    <cp:lastModifiedBy></cp:lastModifiedBy>
    <dcterms:created xsi:type="dcterms:W3CDTF">2006-09-13T11:21:51Z</dcterms:created>
    <dcterms:modified xsi:type="dcterms:W3CDTF">2020-08-06T03:03:51Z</dcterms:modified>
</cp:coreProperties>

解析后得到配置对象如下。

{
  AppVersion: "12.0000",
  Application: "Microsoft Excel",
  Author: "",
  CreatedDate: Wed Sep 13 2006 19:21:51 GMT+0800 (中国标准时间),
  DocSecurity: "0",
  HyperlinksChanged: false,
  LastAuthor: "",
  LinksUpToDate: false,
  ModifiedDate: Thu Aug 06 2020 11:03:51 GMT+0800 (中国标准时间),
  ScaleCrop: false,
  SharedDoc: false,
}

5. 解析工作表(Sheet)

解析 workbook 的时候,获得了 工作簿(workbook)下的工作表(sheet) 的简单信息,并且给了一个外键 ID,这里就解析 workbook.xml.rels 文件,用于获得这个外键 ID 对应的 Sheet 的具体路径。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
...

开始解析 Sheet,这是所有解析中最关键的部分。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <dimension ref="A1:E2"/>
  <sheetViews>
    <sheetView tabSelected="1" workbookViewId="0"><selection activeCell="B2" sqref="B2"/></sheetView>
  </sheetViews>

  <sheetFormatPr defaultRowHeight="13.5"/>

  <sheetData>
    <row r="1" spans="1:5">
        <c r="A1" s="1" t="s"><v>1</v></c>
        <c r="B1" s="1" t="s"><v>0</v></c>
        <c r="C1" s="1" t="s"><v>3</v></c>
        <c r="D1" s="3" t="s"><v>4</v></c>
        <c r="E1" s="3"/>
    </row>

    <row r="2" spans="1:5">
        <c r="A2" s="4" t="s"><v>2</v></c>
        <c r="B2" s="4"><v>29</v></c>
        <c r="C2" s="2"><v>554</v></c>
        <c r="D2" s="5" t="s"><v>5</v></c>
        <c r="E2" s="5" t="s"><v>6</v></c>
    </row>
  </sheetData>

</worksheet>

解析后获得如下 sheet 对象。

{
  !ref: "A1:E2",
  !merges: [
    {
      e: {c: 4, r: 0},
      s: {c: 3, r: 0}
    }
  ],
  A1: {t: "s", v: "name", h: "name", w: "name"},
  B1: {t: "s", v: "age", h: "age", w: "age"},
  B2: {t: "n", v: 29, w: "29"},
  C2: {t: "n", v: 554, w: "7/7/01"},
  ...
}

每一个 Sheet Object 表示一张表格,只要不是 ! 开头的都表示普通 cell,否则,表示一些特殊含义,具体如下:

!ref:表示所有单元格的范围,例如从 A1 到 E2 则记录为 A1:E2。

!merges:存放一些单元格合并信息,是一个数组,每个数组由包含 s 和 e 构成的对象组成,s 表示开始,e 表示结束,r 表示行,c 表示列。

每一个单元格是一个对象(Cell Object),主要有 t、v、r、h、w 等字段:

Key  |  Description               
_____|________________________________________________________
v    |  原始值
w    |  格式化文本
r    |  富文本内容
h    |  HTML内容
c    |  与单元格关联的注释
z    |  与单元格关联的数字格式字符串
l    |  单元格的超链接对象 (.Target 是地址, .Tooltip 是提示消息)
s    |  单元格的样式/主题

t:表示内容类型,下面列出一些 t 的取值

Type |  Description               
_____|________________________________________________________
  b  |  Boolean 
  e  |  Error,遵循特等的错误码 
  n  |  Number 
  d  |  Date 
  s  |  共享的字符串 
  z  |  空白

到此为止,Sheet 就解析好了。后续还有一些 comment 的解析,我们都略过。

sheetjs 很牛的一点就在于它完全根据协议内容对 Excel 进行了建模,但是我们可以看出,这个建立的模型依然不是那么的友好,存在一些 t,v,h 等参数,我们很难一眼看出这个数据到底是个怎样的结构。所以,sheetjs 又封装了一些函数帮助我们处理这些数据。比如 XLSX.utils.sheet_to_json() 方法,该方法会将一个 sheet 变为 json 格式。

sheet 格式转换

对于某个 sheet 转换后的数据,sheetjs 提供了一些工具方法帮我们将其转换为别的格式,列出这些方法:

  1. sheet_to_json:把工作表转换为JS对象数组。
  2. sheet_to_csv:生成分隔符隔开值的输出。
  3. sheet_to_txt:生成UTF16格式化的文本。
  4. sheet_to_html:生成HTML输出。
  5. sheet_to_formulae:生成公式列表。

我们以 sheet_to_json 为例,默认情况下,后首先分析 !ref 字段,确认范围,然后遍历这个范围,将第一行作为 key,第二行作为 value。当然,也可以通过该函数的入参改变这种默认行为。具体的参数配置查看SheetJS官网。

我们这个例子最终会生成如下 JSON 对象,可以看出,提供的工具类没有帮助我们处理单元格合并的问题,需要用户自己处理。

[
  {
    age: 29
    birthday: 554
    bobby: "music"
    name: "Tom"
    __EMPTY: "football"
  }
]

解析 XML 文件的方式

补充一下 JS 解析 XML 文件的方式,以下代码是用于解析 [Content_Types].xml 文件的。

可以看出,JS 使用正则表达式来解析 XML 文件。这个正则表达式会将 XML 文件中的每个 <> 或者 之间的内容找到,然后按照 key=value 的模式将 <> 之间的内容切出来,然后再用 switch case 对每个切出来的元素做处理。

果然是正则学得好,解析没烦恼

var tagregex=/<[/?]?[a-zA-Z0-9:_-]+(?:s+[^"s?>/]+s*=s*(?:"[^"]*"|'[^']*'|[^'">s=]+))*s?[/?]?>/g;

function parse_ct(data) {
    var ct = new_ct();
    if(!data || !data.match) return ct;
    var ctext = {};
    (data.match(tagregex)||[]).forEach(function(x) {
        var y = parsexmltag(x);
        switch(y[0].replace(nsregex,"<")) {
            case '<?xml': break;
            case '<Types': ct.xmlns = y['xmlns' + (y[0].match(/<(w+):/)||["",""])[1] ]; break;
            case '<Default': ctext[y.Extension] = y.ContentType; break;
            case '<Override':
                if(ct[ct2type[y.ContentType]] !== undefined) ct[ct2type[y.ContentType]].push(y.PartName);
                break;
        }
    });
    ...
}

总结

  1. Excel(.xlsx)文件的本质是 XML 文件集合经过 zip 压缩后的产物。XML 文件遵循 Open XML 协议。
  2. JS 解析 Excel 的步骤是,首先将文件解压缩,然后根据 Open XML 协议的内容逐步解析 XML 文件,生成一个 JS 对象。
  3. SheetJS 库封装了整个解析过程,代码兼容性非常好,推荐使用。

解析 Excel 原理很简答,难度在于实现协议的过程以及繁琐的数据转换。实际开发过程中,我们可以通过对 SheetJS 解析后的数据做二次加工,得到我们想要的数据。

如果您觉得有所收获,麻烦点个赞吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值