【发布时间】:2015-12-08 15:22:45
【问题描述】:
我正在尝试编写一些代码处理 JSON 文档,其中存储在文件中的字符串值极长(超过 10 亿个字符)。我不想将整个字符串保存在内存中(因为我可以在流中处理它们)。但我在杰克逊解析器中找不到这样的选项。到目前为止,我所做的是使用 Jackson 令牌偏移量(第一轮读取文件)和随机访问文件来处理流中的字符串(第二轮读取文件)的测试:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.MappingJsonFactory;
public class LongStringJsonTest {
public static void main(String[] args) throws Exception {
File tempJson = new File("temp.json");
PrintWriter pw = new PrintWriter(tempJson);
pw.print("{\"k1\": {\"k11\": \"");
for (int i = 0; i < 1e8; i++)
pw.print("abcdefghij");
pw.print("\"}, \"k2\": \"klmnopqrst\", " +
"\"k3\": [\"uvwxyz\", \"0123\"]}");
pw.close();
searchForStrings(tempJson);
}
private static void searchForStrings(File tempJson) throws Exception {
JsonFactory f = new MappingJsonFactory();
JsonParser jp = f.createParser(tempJson);
Map<Long, Long> stringStartToNext = new HashMap<Long, Long>();
long lastStringStart = -1;
boolean wasFieldBeforeString = false;
while (true) {
JsonToken token = jp.nextToken();
if (token == null)
break;
if (lastStringStart >= 0) {
stringStartToNext.put(lastStringStart, (wasFieldBeforeString ? -1 : 1) *
jp.getTokenLocation().getByteOffset());
lastStringStart = -1;
wasFieldBeforeString = false;
}
if (token == JsonToken.FIELD_NAME) {
wasFieldBeforeString = true;
} else if (token == JsonToken.VALUE_STRING) {
lastStringStart = jp.getTokenLocation().getByteOffset();
} else {
wasFieldBeforeString = false;
}
}
jp.close();
jp = f.createParser(tempJson);
RandomAccessFile raf = new RandomAccessFile(tempJson, "r");
while (true) {
JsonToken token = jp.nextToken();
if (token == null)
break;
if (token == JsonToken.VALUE_STRING) {
long start = jp.getTokenLocation().getByteOffset();
long end = stringStartToNext.get(start);
// You are able to process stream without keeping all bytes in memory.
// Here you see strings including quotes around them.
final long[] length = new long[] {0};
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream os = new OutputStream() {
@Override
public void write(int b) throws IOException {
throw new IOException("Method is not supported");
}
@Override
public void write(byte[] b, int off, int len)
throws IOException {
if (baos.size() < 20) {
baos.write(b, off, Math.min(len, 20));
baos.write((int)'.');
baos.write((int)'.');
baos.write((int)'.');
}
if (len > 0)
length[0] += len;
}
};
processString(raf, start, end, os);
String text = new String(baos.toByteArray(), Charset.forName("utf-8"));
System.out.println("String: " + text + ", length=" + length[0]);
}
}
jp.close();
raf.close();
}
private static void processString(RandomAccessFile raf, long start, long end,
OutputStream os) throws Exception {
boolean wasFieldBeforeString = end < 0;
int quoteNum = wasFieldBeforeString ? 3 : 1;
end = Math.abs(end);
byte[] buffer = new byte[10000];
raf.seek(start);
boolean afterBackSlash = false;
int strLen = (int)(end - start);
for (int chunk = 0; strLen > 0; chunk++) {
int ret = raf.read(buffer, 0, Math.min(buffer.length, strLen));
if (ret < 0)
break;
if (ret > 0) {
int offset = 0;
if (chunk == 0) {
// Assumption that key string doesn't contain double quotes
// and it's shorter than buffer size (for simplicity)
for (int n = 0; n < quoteNum; n++) {
while (true) {
if (buffer[offset] == '\"' && !afterBackSlash) {
break;
} else if (buffer[offset] == '\\') {
afterBackSlash = !afterBackSlash;
} else {
afterBackSlash = false;
}
offset++;
}
offset++;
}
offset--;
ret -= offset;
}
// Searching for ending quote
int endQuotePos = offset + (chunk == 0 ? 1 : 0); // Skip open quote
while (endQuotePos < offset + ret) {
if (buffer[endQuotePos] == '\"' && !afterBackSlash) {
break;
} else if (buffer[endQuotePos] == '\\') {
afterBackSlash = !afterBackSlash;
} else {
afterBackSlash = false;
}
endQuotePos++;
}
if (endQuotePos < offset + ret) {
os.write(buffer, offset, endQuotePos + 1 - offset);
break;
}
os.write(buffer, offset, ret);
strLen -= ret;
}
}
}
}
这种方法根本不支持 unicode。我很好奇有什么方法可以做得更好(或者甚至在其他一些库的帮助下)?
【问题讨论】:
-
我会转向基于事件的解析器。看看这个问题stackoverflow.com/questions/444380/…。使用“流”或“事件”解析器将允许您在任何给定时间保存较小尺寸的 JSON 数据。我现在没有时间写一个遮阳篷,否则我会;)
-
如果您有 10 亿个字符的单个值,您应该认真重新考虑 JSON 是否是正确的选择。什么单个文本值这么大,不包括二进制数据的十六进制或 base64 编码?
-
它将成为基因组服务的一部分,允许存储与基因组和/或计算生物学对象相关的用户数据。 JSON 格式可以反映各种类型的对象(递归地带有子列表、子映射等)。它似乎是序列化文档的标准。为什么我们不能考虑将它也用于生物文档(我的意思是一般除了特定的算法问题)?另一方面,您可能是对的,它可能需要处理长字符串并将它们与具有外部引用的主文档分开保存。
-
@rsutormin JSON 绝对不是“序列化文档的标准”......它最初是 JS 的一个子集。如果我是你,我会认真考虑使用其他东西。其他的。
-
@rsutormin 很难准确地说出数据存储的更好选择是什么,但我同意 Navin 关于 的观点……其他方面。还有什么,我会高度考虑任何 SQL 数据库,如果您的数据结构不适合那种格式(关系数据库),那么我会考虑使用 MongoDB 之类的东西。然而,考虑到 SQL 格式的普遍性,它肯定是我第一个考虑数据存储的地方,更不用说强大数据库带来的所有其他好处(索引、外键、强数据类型)
标签: java json jackson out-of-memory token