从ZIP字符串中提取文件


Extract a file from a ZIP string

我有一个包含一个XML文件的zip文件的BASE64字符串。

关于如何在不处理磁盘上的文件的情况下获得XML文件的内容,有什么想法吗?

我非常希望将整个过程保留在内存中,因为XML只有1-5k。

编写压缩文件,提取XML文件,然后加载并删除所有内容,这是很烦人的。

我有一个类似的问题,我最终手工完成。
https://www.pkware.com/documents/casestudies/APPNOTE.TXT

提取一个文件(只是第一个文件),没有错误/crc检查,假设使用了deflate

// zip in a string
$data = file_get_contents('test.zip');
// magic
$head = unpack("Vsig/vver/vflag/vmeth/vmodt/vmodd/Vcrc/Vcsize/Vsize/vnamelen/vexlen", substr($data,0,30));
$filename = substr($data,30,$head['namelen']);
$raw = gzinflate(substr($data,30+$head['namelen']+$head['exlen'],$head['csize']));
// first file uncompressed and ready to use
file_put_contents($filename,$raw);

经过几个小时的研究,我认为没有临时文件是不可能处理zip文件的:

  1. php://memory的第一次尝试将不起作用,因为它是一个不能被file_get_contents()ZipArchive::open()等函数读取的流。在评论中有一个指向php-bug跟踪器的链接,因为缺乏这个问题的文档。
  2. 有一个流支持ZipArchive::getStream(),但正如在手册中所述,它只支持读取打开的文件操作。所以你不能用它动态地构建存档。
  3. zip://包装器也是只读的:使用fopen()包装器创建ZIP文件
  4. 我也做了一些尝试与其他php包装/协议,如

     file_get_contents("zip://data://text/plain;base64,{$base64_string}#test.txt")
     $zip->open("php://filter/read=convert.base64-decode/resource={$base64_string}")
     $zip->open("php://filter/read=/resource=php://memory")
    

    但对我来说它们根本不起作用,即使手册中有这样的例子。所以你必须吞下药丸并创建一个临时文件。


原始答:

这只是临时存储的方式。我希望您自己管理zip处理和xml解析。

使用php php://memory (doc)包装器。请注意,这只对小文件有用,因为它存储在内存中——很明显。否则请使用php://temp

<?php
// the decoded content of your zip file
$text = 'base64 _decoded_ zip content';
// this will empty the memory and appen your zip content
$written = file_put_contents('php://memory', $text);
// bytes written to memory
var_dump($written);
// new instance of the ZipArchive
$zip = new ZipArchive;
// success of the archive reading
var_dump(true === $zip->open('php://memory'));

toster-cx做得对,你应该奖励他点,这是一个示例,其中zip来自soap响应作为字节数组(二进制),内容是XML文件:

$objResponse = $objClient->__soapCall("sendBill",array(parameters));
$fileData=unzipByteArray($objResponse->applicationResponse);
header("Content-type: text/xml");
echo $fileData;
function unzipByteArray($data){
  /*this firts is a directory*/
  $head = unpack("Vsig/vver/vflag/vmeth/vmodt/vmodd/Vcrc/Vcsize/Vsize/vnamelen/vexlen", substr($data,0,30));
  $filename = substr($data,30,$head['namelen']);
  $if=30+$head['namelen']+$head['exlen']+$head['csize'];
 /*this second is the actua file*/
  $head = unpack("Vsig/vver/vflag/vmeth/vmodt/vmodd/Vcrc/Vcsize/Vsize/vnamelen/vexlen", substr($data,$if,30));
  $raw = gzinflate(substr($data,$if+$head['namelen']+$head['exlen']+30,$head['csize']));
  /*you can create a loop and continue decompressing more files if the were*/
  return $raw;
}

如果您知道.zip中的文件名,只需执行以下操作:

<?php
$xml = file_get_contents('zip://./your-zip.zip#your-file.xml');

如果您有一个普通字符串,只需这样做:

<?php
$xml = file_get_contents('compress.zlib://data://text/plain;base64,'.$base64_encoded_string);

[edit]文档在那里:http://www.php.net/manual/en/wrappers.php

从注释中:如果你没有base64编码的字符串,你需要在使用data://包装器之前urlencode()它。

<?php
$xml = file_get_contents('compress.zlib://data://text/plain,'.urlencode($text));

[edit 2]即使你已经找到了一个解决方案的文件,有一个解决方案(测试)我没有看到在你的答案:

<?php
$zip = new ZipArchive;
$zip->open('data::text/plain,'.urlencode($base64_decoded_string));
$zip2 = new ZipArchive;
$zip2->open('data::text/plain;base64,'.urlencode($base64_string));

如果您在Linux上运行并且拥有系统管理权限。您可以使用tmpfs挂载一个小的ramdisk,然后标准的file_get/put和ZipArchive函数就可以工作了,只是它不写入磁盘,而是写入内存。要使它永久地准备好,fstab类似于:

/media/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=2M 0 0

设置你的大小和位置,使其适合你。使用php挂载一个ramdisk并在使用它之后删除它(如果它甚至有特权的话)可能比直接写入磁盘效率更低,除非您有大量的文件要一次性处理。虽然这不是一个纯粹的php解决方案,也不是可移植的。你仍然需要在使用后删除"文件",或者让操作系统清理旧文件。它们通常不会在重新启动或重新挂载ramdisk后持续存在。

如果你想从zip和XML中读取文件的内容你应该看看这个我用它来计数docx(这是一个zip)中的单词

if (!function_exists('docx_word_count')) {
    function docx_word_count($filename)
    {
        $zip = new ZipArchive();
        if ($zip->open($filename) === true) {
            if (($index = $zip->locateName('docProps/app.xml')) !== false) {
                $data = $zip->getFromIndex($index);
                $zip->close();
                $xml = new SimpleXMLElement($data);
                return $xml->Words;
            }
            $zip->close();
        }
        return 0;
    }
}

这个想法来自 toster-cx 对于处理格式错误的zip文件也非常有用!

我有一个头文件中缺少数据,所以我必须使用他的方法提取中央目录文件头:

$CDFHoffset = strpos( $zipFile, "'x50'x4b'x01'x02" );                                                       
$CDFH = unpack( "Vsig/vverby/vverex/vflag/vmeth/vmodt/vmodd/Vcrc/Vcsize/Vsize/vnamelen/vexlen", substr( $zipFile, $CDFHoffset, 46 ) );

感谢@toster-cx的主要思想-我已经升级了它,并解决了零$head['csize']的问题。

它可以在某些情况下设置,并且有一个卡住:具有此长度的另一个头位于长度未知的压缩内容块之后。幸运的是,还有另一组名为中央目录的标头,我们可以在其中提取所有丢失的数据并重新应用@toster-cx的方法。

我的版本还具有多个文件提取功能,将它们放入一个数组中,并使用keys =文件名。

https://stackoverflow.com/a/76642785/22194816

这是链接-请欣赏和分发解决方案))为了更好地理解,里面也有@see,这导致了zip规范。