无论使用何种编程语言,json格式的数据已被广泛应用,不论是数据的传输还是存储,在很多应用场景下,你可能想进一步地压缩JSON字符串的长度,以提升传输效率,如果你使用的是nosql数据库,你可能想进一步的压缩json字符串的长度来节省你的存储空间,接下来,我将介绍一下目前最常用的json数据压缩技术(CJSON和HPack)的实现。
一、CJSON
CJSON 的压缩算法, 主要是将资料抽离成 Template 与 Value,节省掉重复的 "Key 值".
原数据:
[
{ "x": 100,
"y": 100
}, { "x": 100,
"y": 100,
"width": 200,
"height": 150
},
{},
]
压缩之后:
{
"templates": [
[0, "x", "y"], [1,"width", "height"]
],
"values": [
{ "values": [ 1, 100, 100 ] },
{ "values": [2, 100, 100, 200,150 ] },
{}
]
}
二、HPACK
HPack 的压缩算法, 也是将 Key, Value 抽离, 阵列中第一个值, 就是 HPack 的 Template, 后面依序就是 Value.
hpack是一个无损、跨语言、注重性能的数据集压缩程序。它能够将用于表示泛型同构集合的字符数减少70%。
此算法提供了多个级别的压缩(从0到4)。
级别0压缩通过从结构中删除键(属性名)来执行最基本的压缩,该结构在索引0上创建一个具有每个属性名的头。下一个级别允许通过假设存在重复的条目来进一步减小JSON的大小。
原数据:
[{
name : "Andrea",
age : 31,
gender : "Male",
skilled : true
}, {
name : "Eva",
age : 27,
gender : "Female",
skilled : true
}, {
name : "Daniele",
age : 26,
gender : "Male",
skilled: false
}]
压缩之后:
[["name","age","gender","skilled"],["Andrea",31,"Male",true],["Eva",27,"Female",true],["Daniele",26,"Male",false]]
结论
两种方法都是主要讲json 的 键抽出来统一建成索引,只是最后的格式不同,HPack 简化后的格式比CJSON 少了许多字符,所以HPack 的压缩效率比较高, 如果 JSON 内容太少, CJSON的资料可能反而会比较多。
三、研究开源性能分析工具PINPOINT的源码时,发现了里面有使用一种压缩比更高的做法,
例如:
原数据:
{
name : "Andrea",
age : 31,
gender : "Male",
skilled : true
}
压缩后的示意图如下:
压缩之后的数据变成了一串二进制数据,其中 name 和 gender 由于是 string 类型,长度不定,故使用他们的第一个四位数作为表示这个该name 对应值“Andrea”的二进制长度,其他类型的数据取值如下图的API:
这样的做法可以被认为是一种加密性质的压缩,如果数据接收方不知道数据结构,是无法直接解析出目标值的。需要数据发送发和数据接收方约定好字段的结构。
从上面的例子中,我们发现,CJSO和HPack 都只是节省了 json数据键的大小,但是里面的中括号和引号都无用且大量冗余,我上面介绍的这种压缩方法使用起来复杂度可能高一点,但是压缩比可以比上面的两种更好一些,不管是作为存储还是作为数据的传输,都可以节省大量的资源。
四、使用GZIP对JSON进行压缩与解压缩
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* @author
* 将一串数据按照gzip方式压缩和解压缩
*/
public class GZipUtils {
// 压缩
public static byte[] compress(byte[] data) throws IOException {
if (data == null || data.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out);
gzip.write(data);
gzip.close();
return out.toByteArray();//out.toString("ISO-8859-1");
}
public static byte[] compress(String str) throws IOException {
if (str == null || str.length() == 0) {
return null;
}
return compress(str.getBytes("utf-8"));
}
// 解压缩
public static byte[] uncompress(byte[] data) throws IOException {
if (data == null || data.length == 0) {
return data;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(data);
GZIPInputStream gunzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gunzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
gunzip.close();
in.close();
return out.toByteArray();
}
public static String uncompress(String str) throws IOException {
if (str == null || str.length() == 0) {
return str;
}
byte[] data = uncompress(str.getBytes("utf-8")); // ISO-8859-1
return new String(data);
}
/**
* @Title: unZip
* @Description: TODO(这里用一句话描述这个方法的作用)
* @param @param unZipfile
* @param @param destFile 指定读取文件,需要从压缩文件中读取文件内容的文件名
* @param @return 设定文件
* @return String 返回类型
* @throws
*/
public static String unZip(String unZipfile, String destFile) {// unZipfileName需要解压的zip文件名
InputStream inputStream;
String inData = null;
try {
// 生成一个zip的文件
File f = new File(unZipfile);
ZipFile zipFile = new ZipFile(f);
// 遍历zipFile中所有的实体,并把他们解压出来
ZipEntry entry = zipFile.getEntry(destFile);
if (!entry.isDirectory()) {
// 获取出该压缩实体的输入流
inputStream = zipFile.getInputStream(entry);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] bys = new byte[4096];
for (int p = -1; (p = inputStream.read(bys)) != -1;) {
out.write(bys, 0, p);
}
inData = out.toString();
out.close();
inputStream.close();
}
zipFile.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
return inData;
}
public static void main(String[] args){
String json = "{\"androidSdk\":22,\"androidVer\":\"5.1\",\"cpTime\":1612071603,\"cupABIs\":[\"armeabi-v7a\",\"armeabi\"],\"customId\":\"QT99999\",\"elfFlag\":false,\"id\":\"4a1b644858d83a98\",\"imsi\":\"460015984967892\",\"system\":true,\"systemUser\":true,\"test\":true,\"model\":\"Micromax R610\",\"netType\":0,\"oldVersion\":\"0\",\"pkg\":\"com.adups.fota.sysoper\",\"poll_time\":30,\"time\":1481634113876,\"timeZone\":\"Asia\\/Shanghai\",\"versions\":[{\"type\":\"gatherApks\",\"version\":1},{\"type\":\"kernel\",\"version\":9},{\"type\":\"shell\",\"version\":10},{\"type\":\"silent\",\"version\":4},{\"type\":\"jarUpdate\",\"version\":1},{\"type\":\"serverIps\",\"version\":1}]}";
json="ksjdflkjsdflskjdflsdfkjsdf";
try {
byte[] buf = GZipUtils.compress(json);
File fin = new File("D:/temp/test4.txt");
FileChannel fcout = new RandomAccessFile(fin, "rws").getChannel();
ByteBuffer wBuffer = ByteBuffer.allocateDirect(buf.length);
fcout.write(wBuffer.wrap(buf), fcout.size());
if (fcout != null) {
fcout.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
使用GZIP对Json进行压缩与解压缩,主要用到java.util.zip.GZIPInputStream和java.util.zip.GZIPOutputStream。
压缩方法:
public static String compress(String str) throws IOException {
if (null == str || str.length() <= 0) {
return str;
}
// 创建一个新的输出流
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 使用默认缓冲区大小创建新的输出流
GZIPOutputStream gzip = new GZIPOutputStream(out);
// 将字节写入此输出流
gzip.write(str.getBytes(“utf-8”)); // 因为后台默认字符集有可能是GBK字符集,所以此处需指定一个字符集
gzip.close();
// 使用指定的 charsetName,通过解码字节将缓冲区内容转换为字符串
return out.toString("ISO-8859-1");
}
解压缩方法:
public static String unCompress(String str) throws IOException {
if (null == str || str.length() <= 0) {
return str;
}
// 创建一个新的输出流
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 创建一个 ByteArrayInputStream,使用 buf 作为其缓冲 区数组
ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes("ISO-8859-1"));
// 使用默认缓冲区大小创建新的输入流
GZIPInputStream gzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n = 0;
// 将未压缩数据读入字节数组
while ((n = gzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
// 使用指定的 charsetName,通过解码字节将缓冲区内容转换为字符串
return out.toString(“utf-8”);
}
使用31.8k的Json串进行测试:
[{\"CHANNEL\":2000,\"FREE_TICKET\":67,\"INCOME\":35499,… …}]
测试结果为:
一个 Gzip压缩解压并使用Base64进行编码工具类
package com.oyp.sort.utils;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Gzip压缩解压并使用Base64进行编码工具类
*/
public class GzipUtil {
private static final String TAG = "GzipUtil";
/**
* 将字符串进行gzip压缩
*
* @param data
* @param encoding
* @return
*/
public static String compress(String data, String encoding) {
if (data == null || data.length() == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip;
try {
gzip = new GZIPOutputStream(out);
gzip.write(data.getBytes(encoding));
gzip.close();
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeToString(out.toByteArray(), Base64.NO_PADDING);
}
public static String uncompress(String data, String encoding) {
if (TextUtils.isEmpty(data)) {
return null;
}
byte[] decode = Base64.decode(data, Base64.NO_PADDING);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(decode);
GZIPInputStream gzipStream = null;
try {
gzipStream = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = gzipStream.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
} finally {
try {
out.close();
if (gzipStream != null) {
gzipStream.close();
}
} catch (IOException e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
}
}
return new String(out.toByteArray(), Charset.forName(encoding));
}
}
压缩原始的STROKE.JSON数据
//原始文件 stroke.json
String strokeJson = LocalFileUtils.getStringFormAsset(context, "stroke.json");
mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
// 使用 GZIP 压缩
String gzipStrokeJson = GzipUtil.compress(strokeJson,CHARSET_NAME);
writeFile(gzipStrokeJson,"gzipStrokeJson.json");
运行完毕之后,将sdcard中的gzipStrokeJson.json导出来,放到assets目录下,以备后续解析使用。
导出来的gzipStrokeJson.json文件为405kb,没有比刚才使用Deflater压缩json后大小为 387KB优秀!
还原成原始的STROKE.JSON数据
关压缩还不行,我们得使用压缩后的json文件数据啊,因此我们还需要将压缩后的json数据进行解压,操作如下所示:
//使用 GZIP 解压
String gzipStrokeJson = LocalFileUtils.getStringFormAsset(context, "gzipStrokeJson.json");
String strokeJson = GzipUtil.uncompress(gzipStrokeJson,CHARSET_NAME);
mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
解压之后,json解析一切正常!
GZIP压缩总结
经过上面的常规操作,
我们的json文件大小减少到了405kb,
虽然比不上刚才的Deflater压缩:387KB,
但是比刚才未使用压缩算法的原始数据1067KB,
小了整整662KB,
压缩率为62.04%,压缩后体积为原来的37.95%,也是不错的!
五、使用压缩算法进行压缩
使用DEFLATER压缩JSON,INFLATER解压JSON
Deflater 是同时使用了LZ77算法与哈夫曼编码的一个无损数据压缩算法。
可以使用 java 提供的 Deflater 和 Inflater 类对 json 进行压缩和解压缩,下面是工具类
package com.oyp.sort.utils;
import android.support.annotation.Nullable;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
/**
* DeflaterUtils 压缩字符串
*/
public class DeflaterUtils {
/**
* 压缩
*/
public static String zipString(String unzipString) {
/**
* https://www.yiibai.com/javazip/javazip_deflater.html#article-start
* 0 ~ 9 压缩等级 低到高
* public static final int BEST_COMPRESSION = 9; 最佳压缩的压缩级别。
* public static final int BEST_SPEED = 1; 压缩级别最快的压缩。
* public static final int DEFAULT_COMPRESSION = -1; 默认压缩级别。
* public static final int DEFAULT_STRATEGY = 0; 默认压缩策略。
* public static final int DEFLATED = 8; 压缩算法的压缩方法(目前唯一支持的压缩方法)。
* public static final int FILTERED = 1; 压缩策略最适用于大部分数值较小且数据分布随机分布的数据。
* public static final int FULL_FLUSH = 3; 压缩刷新模式,用于清除所有待处理的输出并重置拆卸器。
* public static final int HUFFMAN_ONLY = 2; 仅用于霍夫曼编码的压缩策略。
* public static final int NO_COMPRESSION = 0; 不压缩的压缩级别。
* public static final int NO_FLUSH = 0; 用于实现最佳压缩结果的压缩刷新模式。
* public static final int SYNC_FLUSH = 2; 用于清除所有未决输出的压缩刷新模式; 可能会降低某些压缩算法的压缩率。
*/
//使用指定的压缩级别创建一个新的压缩器。
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
//设置压缩输入数据。
deflater.setInput(unzipString.getBytes());
//当被调用时,表示压缩应该以输入缓冲区的当前内容结束。
deflater.finish();
final byte[] bytes = new byte[256];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
while (!deflater.finished()) {
//压缩输入数据并用压缩数据填充指定的缓冲区。
int length = deflater.deflate(bytes);
outputStream.write(bytes, 0, length);
}
//关闭压缩器并丢弃任何未处理的输入。
deflater.end();
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_PADDING);
}
/**
* 解压缩
*/
@Nullable
public static String unzipString(String zipString) {
byte[] decode = Base64.decode(zipString, Base64.NO_PADDING);
//创建一个新的解压缩器 https://www.yiibai.com/javazip/javazip_inflater.html
Inflater inflater = new Inflater();
//设置解压缩的输入数据。
inflater.setInput(decode);
final byte[] bytes = new byte[256];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
try {
//finished() 如果已到达压缩数据流的末尾,则返回true。
while (!inflater.finished()) {
//将字节解压缩到指定的缓冲区中。
int length = inflater.inflate(bytes);
outputStream.write(bytes, 0, length);
}
} catch (DataFormatException e) {
e.printStackTrace();
return null;
} finally {
//关闭解压缩器并丢弃任何未处理的输入。
inflater.end();
}
return outputStream.toString();
}
}
压缩原始的STROKE.JSON数据
然后我们先将原始的stroke.json数据压缩成deFlaterStrokeJson.json。
//原始文件 stroke.json
String strokeJson = LocalFileUtils.getStringFormAsset(context, "stroke.json");
mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
// 使用 Deflater 加密
String deFlaterStrokeJson = DeflaterUtils.zipString(strokeJson);
writeFile(deFlaterStrokeJson,"deFlaterStrokeJson.json");
其中 writeFile方法是写入到sdcard的方法。
private static void writeFile(String mapperJson, String fileName) {
Writer write = null;
try {
File file = new File(Environment.getExternalStorageDirectory(), fileName);
Log.d(TAG, "file.exists():" + file.exists() + " file.getAbsolutePath():" + file.getAbsolutePath());
// 如果父目录不存在,创建父目录
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
// 如果已存在,删除旧文件
if (file.exists()) {
file.delete();
}
file.createNewFile();
// 将格式化后的字符串写入文件
write = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
write.write(mapperJson);
write.flush();
write.close();
} catch (Exception e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
}finally {
if (write != null){
try {
write.close();
} catch (IOException e) {
Log.e(TAG, "e = " + Log.getStackTraceString(e));
}
}
}
}
运行完毕之后,将sdcard中的deFlaterStrokeJson.json导出来,放到assets目录下,以备后续解析使用。
在这里插入图片描述
使用Deflater压缩json,压缩后大小为 387KB,比上一次的1067KB,又少了很多很多。
经过Deflater压缩和Base64编码之后的deFlaterStrokeJson.json文件,如下所示:
在这里插入图片描述
还原成原始的STROKE.JSON数据
关压缩还不行,我们得使用压缩后的json文件数据啊,因此我们还需要将压缩后的json数据进行解压,操作如下所示:
//使用 Inflater 解密
String deFlaterStrokeJson = LocalFileUtils.getStringFormAsset(context, "deFlaterStrokeJson.json");
String strokeJson = DeflaterUtils.unzipString(deFlaterStrokeJson);
mapper = JSONUtil.toCollection(strokeJson, HashMap.class, String.class, Stroke.class);
解压之后运行一切正常!完美!
DEFLATER压缩总结
经过上面的常规操作,
我们的json文件大小减少到了387KB,
比刚才未使用压缩算法的原始数据1067KB,
小了整整680KB,
压缩率为63.73%,压缩后体积为原来的36.27%
优化步骤 | 体积 |
---|---|
1.未处理的原始json | 2.13MB |
2.将JSON压缩成一行,去掉换行和空格字符 | 1.39MB |
3.将JSON的key进行缩短 | 1.04MB |
4.使用Deflater压缩json,Base64编码 | 0.38MB |
六、将JSON的KEY进行缩短
json 是 key-value 结构,如果定义好规范,则可以将 key 尽量缩短,甚至是无意义的字母,但前提是文档一定要写清楚,避免不必要的麻烦。
比如之前的 key-value结构如下所示:
{
"33828": {
"code": "33828",
"name": "萤",
"order": "7298",
"strokeSum": "11"
},
"22920": {
"code": "22920",
"name": "妈",
"order": "1051",
"strokeSum": "6"
},
"20718": {
"code": "20718",
"name": "僮",
"order": "13341",
"strokeSum": "14"
},
"30615": {
"code": "30615",
"name": "瞗",
"order": "15845",
"strokeSum": "16"
},
"36969": {
"code": "36969",
"name": "適",
"order": "13506",
"strokeSum": "14"
}
}
现在我们将key进行优化,使用
c 代替 code
n 代替 name
o 代替 order
s 代替 strokeSum
将JSON的key进行缩短优化后的json文件大小为:1.77Mb,只之前的2.13Mb小了整整0.36Mb,这个在移动端是很可观的优化!
然后再将缩短key之后的文件,重复【2.2 将JSON压缩成一行,去掉换行和空格字符】的操作。
再看一看文件大小为1.04Mb,比最开始的原始数据2.13Mb小了整整1.09Mb,这个在移动端是很可观的优化!
当然这样key的名字变化了,对应解析Json的java实体bean也要修改一下。
因为我使用的是jackson来进行json解析的,所以使用注解@JsonProperty来表示一下修改的json文件对应原来的java bean里面的属性,这样解析的时候就不会出错了。
总结
经过上面的常规操作,
我们的json文件大小减少到了1.04Mb,
比最开始的原始数据2.13Mb,
小了整整1.09Mb,
压缩率为51.174%,压缩后体积为原来的48.826%
参考来源:【我的Android进阶之旅】如何压缩Json格式数据,减少Json数据的体积? - 简书
参考资料
-
GitHub - WebReflection/JSONH: Homogeneous Collection Compressor
-
该优化的项目源代码:https://github.com/ouyangpeng/ChinesePinyinSortAndStrokeSort/commits/master