使用7z对xlsx文件进行压缩的方法

9
我正在尝试编写程序修改一个Excel文件(xlsx)。我可以成功地解压、按需修改xml并重新压缩。然而,每次打开Excel时都会出现警告,尽管它确实读取了文件。我相信错误是由于使用的压缩方法造成的。以下是最接近的示例:
解压缩
7z x original.xlsx -o./decomp_xlsx

..做一些事情..

压缩

7z a -tzip new ./decomp_xlsx/*

重命名

mv ./new.zip ./new.xlsx

我得到的错误是:Excel 在 'new.xlsx' 中发现无法读取的内容。是否要恢复此工作簿的内容?如果你信任此工作簿的来源,请点击“是”。
根据 ECMA-376-2 Office Open Formats Part 2(包装约定),支持的压缩算法是 DEFLATE,如 ZIP 规范中所述。包实现者不应使用除 DEFLATE 之外的任何压缩算法。
那么,在 7z 或其他 Linux 兼容程序中需要使用哪些开关才能完成工作而没有出现警告呢?我已经尝试了删除 -tzip 并使用 -m0=COPY,但 Excel 甚至无法从这个版本中恢复。
下面是 zip 程序和 zipinfo 的结果。我猜我找不到其他工具来解决这个问题,除非使用下面提供的工具,因此我将授予该答案,并查看是否可以找到某人将其翻译成 Python 进行测试。我不确定它是否处理 4.5 / 3.0,然后是 b- / tx 或 defS / defF 之间的差异。
$ zipinfo original.xlsx
Archive:  original.xlsx
Zip file size: 228039 bytes, number of entries: 20
-rw----     4.5 fat     1969 b- defS 80-Jan-01 00:00 [Content_Types].xml
-rw----     4.5 fat      588 b- defS 80-Jan-01 00:00 _rels/.rels
-rw----     4.5 fat     1408 b- defS 80-Jan-01 00:00 xl/_rels/workbook.xml.rels
-rw----     4.5 fat      908 b- defS 80-Jan-01 00:00 xl/workbook.xml
-rw----     4.5 fat    35772 b- defS 80-Jan-01 00:00 xl/worksheets/sheet4.xml
-rw----     4.5 fat      322 b- defS 80-Jan-01 00:00 xl/worksheets/_rels/sheet4.xml.rels
-rw----     4.5 fat      322 b- defS 80-Jan-01 00:00 xl/worksheets/_rels/sheet1.xml.rels
-rw----     4.5 fat   230959 b- defS 80-Jan-01 00:00 xl/worksheets/sheet2.xml
-rw----     4.5 fat   263127 b- defS 80-Jan-01 00:00 xl/worksheets/sheet3.xml
-rw----     4.5 fat   295775 b- defS 80-Jan-01 00:00 xl/worksheets/sheet1.xml
-rw----     4.5 fat     1947 b- defS 80-Jan-01 00:00 xl/sharedStrings.xml
-rw----     4.5 fat    22698 b- defS 80-Jan-01 00:00 xl/styles.xml
-rw----     4.5 fat     7079 b- defS 80-Jan-01 00:00 xl/theme/theme1.xml
-rw----     4.5 fat      220 b- defS 80-Jan-01 00:00 xl/printerSettings/printerSettings2.bin
-rw----     4.5 fat   464247 b- defS 80-Jan-01 00:00 xl/externalLinks/externalLink1.xml
-rw----     4.5 fat      338 b- defS 80-Jan-01 00:00 xl/externalLinks/_rels/externalLink1.xml.rels
-rw----     4.5 fat      220 b- defS 80-Jan-01 00:00 xl/printerSettings/printerSettings1.bin
-rw----     4.5 fat      593 b- defS 80-Jan-01 00:00 docProps/core.xml
-rw----     4.5 fat    62899 b- defS 80-Jan-01 00:00 xl/calcChain.xml
-rw----     4.5 fat     1031 b- defS 80-Jan-01 00:00 docProps/app.xml
20 files, 1392422 bytes uncompressed, 223675 bytes compressed:  83.9%

$ zipinfo new.xlsx
Archive:  new.xlsx
Zip file size: 233180 bytes, number of entries: 20
-rw-r--r--  3.0 unx     1031 tx defF 80-Jan-01 00:00 docProps/app.xml
-rw-r--r--  3.0 unx      593 tx defF 80-Jan-01 00:00 docProps/core.xml
-rw-r--r--  3.0 unx    62899 tx defF 80-Jan-01 00:00 xl/calcChain.xml
-rw-r--r--  3.0 unx   464247 tx defF 80-Jan-01 00:00 xl/externalLinks/externalLink1.xml
-rw-r--r--  3.0 unx      338 tx defF 80-Jan-01 00:00 xl/externalLinks/_rels/externalLink1.xml.rels
-rw-r--r--  3.0 unx      220 bx defF 80-Jan-01 00:00 xl/printerSettings/printerSettings1.bin
-rw-r--r--  3.0 unx      220 bx defF 80-Jan-01 00:00 xl/printerSettings/printerSettings2.bin
-rw-r--r--  3.0 unx     1947 tx defF 80-Jan-01 00:00 xl/sharedStrings.xml
-rw-r--r--  3.0 unx    22698 tx defF 80-Jan-01 00:00 xl/styles.xml
-rw-r--r--  3.0 unx     7079 tx defF 80-Jan-01 00:00 xl/theme/theme1.xml
-rw-r--r--  3.0 unx      908 tx defF 80-Jan-01 00:00 xl/workbook.xml
-rw-r--r--  3.0 unx   295775 tx defF 80-Jan-01 00:00 xl/worksheets/sheet1.xml
-rw-r--r--  3.0 unx   230959 tx defF 80-Jan-01 00:00 xl/worksheets/sheet2.xml
-rw-r--r--  3.0 unx   263127 tx defF 80-Jan-01 00:00 xl/worksheets/sheet3.xml
-rw-r--r--  3.0 unx    35772 tx defF 80-Jan-01 00:00 xl/worksheets/sheet4.xml
-rw-r--r--  3.0 unx      322 tx defF 80-Jan-01 00:00 xl/worksheets/_rels/sheet1.xml.rels
-rw-r--r--  3.0 unx      322 tx defF 80-Jan-01 00:00 xl/worksheets/_rels/sheet4.xml.rels
-rw-r--r--  3.0 unx     1408 tx defF 80-Jan-01 00:00 xl/_rels/workbook.xml.rels
-rw-r--r--  3.0 unx     1969 tx defF 80-Jan-01 00:00 [Content_Types].xml
-rw-r--r--  3.0 unx      588 tx defF 80-Jan-01 00:00 _rels/.rels
20 files, 1392422 bytes uncompressed, 229608 bytes compressed:  83.5%

2
运行解包/打包程序,并在其中进行修改。然后使用差异工具比较“zip”内容。它们真的一样吗?是否存在意外更改?有什么遗漏吗? - usr
7z x original.xlsx -o./original_decomp && 7z a -tzip new ./original_decomp/* && mv ./new.zip ./new.xlsx && 7z x new.xlsx -o./new_decomp && diff -r original_decomp new_decomp && diff original.xlsx new.xlsx。这表明解压后的文件夹是相同的,但原始/新的差异显示二进制文件不同。 - jnewt
其他的差异并不重要,只有需要提取的版本才是关键。 - Mark Adler
2个回答

8
由于某种奇怪的原因,微软正在查看本地文件头和中央目录头中的“需要提取的版本”中的操作系统编码。它希望这些值为零,但7z将它们设置为Unix的3。如果您坚持使用7z,则需要修补生成的文件。
以下程序可以完成此操作:
/* needz.c - zero the operating system byte for "version needed to extract" in
    the local and central headers of the zip files given on the command line.
    Placed in the public domain by Mark Adler, 23 Feb 2013. */

#include <stdio.h>
#include <stdlib.h>

static void bail(char *why, char *what)
{
    fprintf(stderr, "needz error: %s%s\n", why, what);
    exit(1);
}

/* Read len bytes from offset as a little-endian integer.  Negative offsets are
    considered to be from the end of the file. */
static unsigned long peek(FILE *stream, off_t offset, int len)
{
    int ret, shift;
    unsigned long val;

    ret = fseeko(stream, offset, offset < 0 ? SEEK_END : SEEK_SET);
    if (ret)
        bail("not a zip file", "");
    val = 0;
    shift = 0;
    while (len--) {
        ret = getc(stream);
        if (ret == EOF)
            bail("not a zip file", "");
        val += (unsigned long)ret << shift;
        shift += 8;
    }
    return val;
}

/* Write len bytes to offset from val as a little-endian integer.  Negative
    offsets are considered to be from the end of the file. */
static void poke(FILE *stream, off_t offset, int len, unsigned long val)
{
    int ret;

    ret = fseeko(stream, offset, offset < 0 ? SEEK_END : SEEK_SET);
    if (ret)
        bail("not a zip file", "");
    while (len--) {
        ret = putc(val, stream);
        if (ret == EOF)
            bail("could not write", "");
        val >>= 8;
    }
}

/* Zero out the OS byte in the extract fields.  This assumes the classic zip
    format (not Zip64), and no zip file comment. */
static void zip_zero_os(char *path)
{
    FILE *zip;
    unsigned entries;
    off_t central, local;

    zip = fopen(path, "r+b");
    if (zip == NULL)
        bail("could not open", path);
    if (peek(zip, -22, 4) != 0x06054b50)
        bail(path, " is not a zip file or has an end comment");
    entries = peek(zip, -12, 2);
    central = peek(zip, -6, 4);
    while (entries--) {
        if (peek(zip, central, 4) != 0x02014b50)
            bail(path, " has a structure error or is Zip64");
        poke(zip, central + 7, 1, 0);
        local = peek(zip, central + 42, 4);
        if (peek(zip, local, 4) != 0x04034b50)
            bail(path, " has a structure error or is Zip64");
        poke(zip, local + 5, 1, 0);
        central += 46 + peek(zip, central + 28, 2) +
                    peek(zip, central + 30, 2) + peek(zip, central + 32, 2);
    }
    if (fclose(zip) == EOF)
        bail("could not close ", path);
}

int main(int argc, char **argv)
{
    while (--argc)
        zip_zero_os(*++argv);
    return 0;
}

嗯,听起来你已经找到了问题Mark,以及一个可能有效的解决方案,尽管这有点超出了我的能力范围。这让我想到了另外两个问题,如果更合适的话,我可以在另一个SO帖子中提出。1. 这是否可以使用预打包的nix工具完成,如果可以,那么是哪一个?2. 是否可以使用Python或直接从Bash中完成(我正在调用一个Python脚本对文件进行内部修改),如果可以,如何实现? - jnewt
1
除了上面提到的工具,没有其他Unix工具可以做到这一点。您肯定可以将上面的程序转换为Python或任何可以读取、写入和寻找文件的语言。我不认为bash是其中之一,但也许有一种我不知道的在bash中进行查找的方法。 - Mark Adler
最终抽出时间编译并尝试了一下,没错,它确实解决了问题,同时仍然使用7z。文件有所不同,但它以最直接的方式回答了这个问题。 - jnewt

4
您可以使用系统的zipunzip来完成。我通常会使用以下类似的方法。
将xlsx文件解压到目录中:
$ unzip -o -d xlsx_dir Workbook1.xlsx
Archive:  Workbook1.xlsx
  inflating: xlsx_dir/[Content_Types].xml  
  inflating: xlsx_dir/_rels/.rels    
  inflating: xlsx_dir/xl/_rels/workbook.xml.rels  
  inflating: xlsx_dir/xl/workbook.xml  
  inflating: xlsx_dir/xl/sharedStrings.xml  
  inflating: xlsx_dir/xl/theme/theme1.xml  
  inflating: xlsx_dir/xl/styles.xml  
  inflating: xlsx_dir/xl/worksheets/sheet1.xml  
 extracting: xlsx_dir/docProps/thumbnail.jpeg  
  inflating: xlsx_dir/docProps/core.xml  
  inflating: xlsx_dir/docProps/app.xml  

然后修改一个或多个XML文件并重新压缩它们:

$ cd xlsx_dir

# Do something with the files like:
$ sed -i '' s/Foo/Bar/ xl/sharedStrings.xml     

$ find . -type f | xargs zip ../newfile.xlsx

在目录内部使用find|zip并不是很美观,但它会生成与原始文件结构匹配的文件结构,而不需要额外的路径剥离。

jmcnamara - 就 Excel 而言,这似乎是有效的。我很好奇为什么它会生成不同的文件(使用 diff),但仍然有效?我的新文件比原始文件小,但是当解压缩新文件并对未压缩文件的目录进行 diff 时,它们是相同的。这是另一种压缩差异吗? - jnewt
可能的差异在于压缩级别。您可以尝试使用“ -4 ”,“ -5 ”等其他“ zip”压缩级别。从记忆中,Excel接受从0到大约8或9的所有压缩级别。而且0表示未压缩的文件,这意味着Excel实际上并不关心文件中使用的压缩。因此,为了与Excel兼容,最好专注于生成相同的目录/文件结构,而不是完全匹配压缩。 - jmcnamara
我希望我能接受两个答案,我会接受这两个答案,因为它们都是正确的。 - jnewt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接