2009年3月16日星期一

用Json来存储格式化数据

这篇文章本来是写给实验室同学们的,针对的是做文本处理实验中数据的存储问题。转帖在blog上留存,呵呵。

看看这个场景:你处理了一些文档,得到格式化的结果,包括正文、标题、时间、类别标签和作者。现在你想把这些结果写到文件里面,以便后面程序读取,或是仅仅作为归档。你会选择什么格式和方法来输出这个数据文件呢?下面有几个常见选择:

  • A: 写一个文本文件,第一行是标题,第二行是时间,第三行是类别标签,第四行是作者,最后一行是正文。读取的时候写一个(不算短的)程序解析这个文件。
  • B: 利用java的DataOutputStream,按顺序把正文、标题、时间、类别标签、正文和作者输出。读取的时候按照同样的顺序读出。
  • C: 创建一个类代表一个文档,如class Doc,其中的公共成员变量代表上述的内容,然后用Java的序列化机制写到文件中。读取的时候用反序列化读出。
  • D: 我很聪明,我发明了一种格式来存取。虽然复杂,但是(可)能节约空间。

这些方法都可行,但不一定是最好的方案,其中有一些常见的陷阱。

第一个陷阱是escaping。以A为例,如果正文中含有回车字符(这很常见),那么正文就可能占很多行,如何决定 正文什么时候结束呢?你可以事先去掉所有的回车字符,那日后如果想要按行分析该怎么办呢?如果你聪明地(谜之音:小聪明!)把回车都替换成一个其他字符 串,比如"_huiche_",那当原文中出现这个字符串时怎么办?别认为这不可能,在大规模数据处理中这几乎是一定的。如果你按照通行的办法,把所有的 回车都变成"\n",那么你就得处理"\"这个字符,复杂的规则会让你写很多的代码。A和D都可能被escaping问题困扰。

第二个陷阱是二进制兼容性。以B为例,看起来这个方法简洁漂亮,可如果另一个同学用C++,他想读你的数据,等待他 的就不是一个简洁漂亮的任务了。他需要仔细了解Java输出的格式,如何处理Java诡异的writeUTF输出。如果你恰好用的是方法C,那这位同学就 掉进了一个黑洞,反复研究Java的序列化协议,被折磨的苦不堪言。如果这位同学用的是不同字节序的电脑(如老Mac,或是Sun工作站)更是一场噩梦。 B、C和D方法都可能被二进制兼容性问题困扰。

第三个陷阱是可读性。假设你的数据用A方法保存,在磁盘上睡了一学期,然后另外一个同学(谜之音:或是你自己!)要 重新用这些数据,你还能记起每行代表什么么?更糟糕的是当时的程序可能已经不见了。A方法还好,能从数据的内容大致猜测一下,如果你用的是B、C、D方 法,该怎么去猜呢?如果你已经毕业了,其他同学猜起来的难度不亚于破解密码。A、B、C和D四种方法都受到可读性问题的困扰。

用JSON表示数据

这篇文章介绍了如何利用一种标准格式,JSON,来将格式化的数据存储到文件中,或是在程序之间传递。JSON可以由任何语言读取,可读性强,没有字节序问题,就是原作者人间蒸发,也可以从数据文件中了解到每个域都保存了什么内容。

还是继续前面的例子,在Java中,我们先定义一个类代表这种数据:

class Doc {
public String content;
public String title;
public String classLabel;
public long timestamp;
public String author;
}

对于一个Doc类的对象,我们可以用Google提供的Gson库把它变成一个标准的JSON字符串:

Doc d = someDocument;
Gson g = new Gson();
String jsonStr = g.toJson(d);

非常简单对吧,jsonStr的内容就像这样

{"content":"内容\n另外一行内容","title":"文档标题","classLabel":"体育","timestamp":99817827282,"author":"某人"}

我们通过肉眼观察就能发现这个字符串代表哪些信息,就是有一天完全没人知道数据是怎么生成的,还是能够解析。

用下面的方法可以把一个JSON字符串解析出来:

Doc d = gson.fromJson(jsonStr, Doc.class);

这样d的内容就是jsonStr中代表的内容了。

将JSON格式的数据保存到文件

如果要保存到文件,我建议将数据写到压缩的文本文件中,每行一个JSON字符串。假设所有的数据都放在List dataset中,数据类名是Data,那么把数据保存到文件的代码可以写为:

FileOutputStream fos = new FileOutputStream("data.gz");
GZIPOutputStream gos = new GZIPOutputStream(fos);
OutputStreamWriter osw = new OutputStreamWriter(gos, "UTF-8");
BufferedWriter output = new BufferedWriter(osw);

Gson gson = new Gson();
for (Data d : dataset) {
output.write(gson.toJson(d));
output.write('\n');
}
output.close();

读取数据到同样的一个List中,代码可以写为:

FileInputStream fis = new FileInputStream("data.gz");
GZIPInputStream gis = new GZIPInputStream(fis);
InputStreamReader isr = new InputStreamReader(gis);
BufferedReader input = new BufferedReader(isr);

Gson gson = new Gson();
List dataset = new LinkedList();
String line;
while ((line = input.readLine()) != null) {
dataset.add(gson.fromJson(line, Data.class));
}
input.close();

你可能注意到读写数据代码中壮观的一排new,是的,这很丑很麻烦。不过这么程式化的代码可以简单包一包,例如上面两段代码可以简化为

GzipTextFileWriter output = new GzipTextFileWriter("data.gz", "UTF-8");
Gson gson = new Gson();
for (Data d : dataset) {
output.writeLine(gson.toJson(d);
}
output.close();

GzipTextFileReader input = new GzipTextFileReader("data.gz", "UTF-8");
Gson gson = new Gson();
List dataset = new LinkedList();
String line;
while ((line = input.readLine()) != null) {
dataset.add(gson.fromJson(line, Data.class));
}
input.close();

只要写两个简单的wrapper class,GzipTextFileReader和GzipTextFileWriter就行了。

存储效率,空间和时间

聪明的你一定发现了,用Gson库把数据保存为JSON格式,其中多出了元信息,如"classLabel"。这些元信息在每条数据里面都有,这不是非常浪费么?另外,我们用字符串表示数字和布尔类型,这不也是浪费空间么?

如果我们不用GzipXXX,老老实实地写到文本文件里面,确实是浪费空间。但由于元信息高度重复,写入压缩文件后,JSON格式占用的空间并不比直接写二进制数据多出多少。下面我们用一个小实验来证明。

我们写10000条数据,每条数据由一个字符串,一个整数,一个浮点数和一个布尔值组成,具体的取值随机,字符串长度相似,约十多个字符。我们用三种方法来保存数据,第一种是直接用二进制的DataOutputStream来保存每条数据,没有任何冗余;第二种是利用Json格式,但不保存如"classLabel"之类的元信息;第三种是用上面介绍的Gson库,同时保存元信息和数据。三种方法我们都输出压缩和不压缩两种数据文件。最终文件的尺寸在下表中列出。


不压缩压缩
二进制396KB260KB
Json无元信息648KB264KB
Gson1024KB280KB

由表中数据可以看出,1)Gson输出虽然比前两种方法尺寸大,但是在压缩后,多出的部分非常有限 2)数据压缩和不压缩有很大差别,压缩的Gson仍比不压缩的二进制数据小很多。

听起来可能有点反常,但是输出到压缩文件比直接输出到文本文件要快,而不是慢。这是因为现代CPU的速度远快于磁 盘,写文件的时候主要的时间花在磁盘的机械动作上。如果事先对数据进行压缩,会减少实际写入的字节数,从而节约写数据的时间。而压缩数据对于现代CPU来 说易如反掌,多数情况下消耗的资源都很少。

和Hadoop联用

在Hadoop的map-reduce中,除了极端要求性能或明显可以节约时间的简单情况,我们都推荐用Gson+JSON来表示数据。 Hadoop的key和value如果是Text格式,都会自动进行压缩,所以大家也无需担心压缩问题。Gson本身的序列化和反序列化代码经过仔细优 化,性能很好。

结论

(谜之音:怎么结论都出来了,这是论文么???!!)

使用JSON格式和Gson库来表示数据并存储在压缩文件,这种方法更方便,更快,更节约空间。如果有朝一日忘记了数据保存的格式,看看文件内容就知道了,避免冏况发生。

参考资源

JSON格式标准说明: http://www.json.org

Gson库: http://code.google.com/p/google-gson/

后记:Google的protocol buffer格式也是一个不错的选择,不过它需要先用一种特定的语言描述数据格式,然后从格式文件创建出具体的类代码,数据的序列化和反序列化都是用生成的代码实现的。如果原数据格式描述和代码丢了,基本上也就废了。

2009年3月15日星期日

2009年3月14日星期六

Google Docs最近很不稳定啊

前几天Google Docs打不开,我以为是临时现象,结果今天spreadsheets又打不开了。不知道Google Docs那帮人在想些啥,再这样搞下去,就该流失用户了...