随机读文件是很简单的,在 Java 中使用 java.io.RandomAccessFile
就可以了:可以通过 seek(pos)
方法设置读位置,也可以通过 getFilePointer()
来获取读位置,还可以通过 skipBytes(n)
来实现单向的相对移动。
但是考虑一个场景,目标文件都非常巨大,而且都是文本文件,那么对这个文件进行压缩可以获得很高的压缩比(对于常见的日志文件,gzip 格式可以获得 7% ~ 10% 的压缩率)。但是这样就无法直接随机读写原始文件的指定位置的数据了,因为无法根据在原始数据的偏移量换算出对应在压缩数据中的偏移量。
为什么无法换算?
gzip 使用的 zlib 对数据进行压缩,zlib 使用的是 DEFLATE 算法,压缩过的数据被称为 Deflate Stream,它压缩的时候按照下面的过程来进行:
- 首先使用引用来替换重复出现的序列。如果一个序列在 32K 的范围内重复出现了若干次,那么从第二次开始,压缩过的数据中将会使用 23 个 bit 来引用第一次出现的位置(15bit 偏移量 + 8bit 长度),
- 对前面一段流中(称为块)出现的数据进行哈夫曼编码,使用更少的 bit 来表示更常出现的数据,从而降低块的整体大小
所以来说,对于不同的原始数据,替换重复序列后的结果和哈夫曼编码的结果会迥异,因此没有一个通用的方法能够仅仅根据原始数据中的位置来换算出压缩后的位置。
更重要的是,Deflate Stream 是比特流,原始数据中某个位置在压缩以后可能对应到非字节边界上,这是无论如何也无法通过直接 seek 来定位的。
既然无法直接换算和定位,那么有没有别的办法来实现直接根据原始数据的偏移量来直接读取数据呢?
大力出奇迹
大力出奇迹的代表是 zlib 官方提供的 gzseek()
/ gzrewind()
方法:
ZEXTERN z_off_t ZEXPORT gzseek OF((gzFile file, z_off_t offset, int whence));
Sets the starting position for the nextgzread
orgzwrite
on the given compressed file. The offset represents a number of bytes in the uncompressed data stream. The whence parameter is defined as inlseek
(2); the valueSEEK_END
is not supported.
但它是怎么实现的呢?简单来说,就是从文件头部开始一边解压缩一边吭哧吭哧计数,直至到达指定的位置为止。
功能是实现了,不过也挺简单粗暴的。关键是性能堪忧啊,假如是要读最后几个字节的数据,它得把整个文件给你全部解压缩一遍。严格意义上来说,这就不叫随机读。
人家倒也实诚,文档中也说了:
If the file is opened for reading, this function is emulated but can be extremely slow.