JSON(> 1 gig)中非常长的字符串,带有 Jackson 令牌流
Really long strings in JSON (>1 gig) with Jackson token stream
我正在尝试编写一些代码处理 JSON 文件,文件中存储了非常长的字符串值(超过 10 亿个字符)。我不想将整个字符串保留在内存中(因为我可以在流中处理它们)。但是我在 Jackson 解析器中找不到这样的选项。到目前为止,我所做的是使用 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。我很好奇有什么方法可以做得更好(或者甚至在其他一些库的帮助下)?
我认为你问错了问题。
JSON,与 XML 或 CSV 或任何其他结构化文本表示一样,具有三个主要作用:使数据结构可被人类解析,允许通用工具处理许多不同类型的数据数据,并促进可能使用不同内部模型的系统之间的数据交换。
如果您不需要那些特定的特征,结构化文本可能是错误的解决方案。专用的二进制表示可能更有效,并且随着数据的 size/complexity 增长,这种差异会变得巨大。
支持结构化文本格式导入和导出您的工具。但是,在内部,您可能应该使用专门针对特定任务的需要进行调整的数据模型。
现在我知道 JSON 格式不是具有很长字符串值的文档的最佳解决方案。但是以防万一有人遇到类似的问题(例如,当已经给出了这样的 JSON 文件并且需要将其 运行 转换为更好的格式时)。这意味着文档应该至少以某种方式被解析一次。所以这是我的调查:
1) FasterXML/Jackson 令牌流不允许使用标准方式处理长字符串(按部分加载)。我发现处理它们的唯一方法是按照我的问题做一些事情+手动处理unicode。
2) Google/Gson 具有 JsonReader,还允许用户将 JSON 处理为令牌流。有 nextString 方法 (https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java#L816). But there is no way to get it by parts or get any info where is the position of it in JSON file (except couple private methods: https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java#L1317-L1323).
3) fangyidong/Json-simple 使用 SAX 风格的推送接口。但是对于字符串,那里只有一种方法:https://github.com/fangyidong/json-simple/blob/master/src/main/java/org/json/simple/parser/ContentHandler.java#L108
4) 我唯一的希望是 beckchr/StAXON。因为它t运行将JSON转化为XML,然后使用XMLStreamReader。有一种方法允许按部分读取字符串: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/javax/xml/stream/XMLStreamReader.java#XMLStreamReader.getTextCharacters%28int%2Cchar%5B%5D%2Cint%2Cint%29 。但不幸的是,OutOfMemoryError 发生在 JSON 解析期间 运行sformation。这是我的代码:
private static void useStaxon(File tempJson) throws Exception {
XMLInputFactory factory = new JsonXMLInputFactory();
XMLStreamReader reader = factory.createXMLStreamReader(new FileReader(tempJson));
while (true) {
if (reader.getEventType() == XMLStreamConstants.END_DOCUMENT)
break;
if (reader.isCharacters()) {
long len = reader.getTextLength();
String text;
if (len > 20) {
char[] buffer = new char[20];
reader.getTextCharacters(0, buffer, 0, buffer.length);
text = new String(buffer) + "...";
} else {
text = reader.getText();
}
System.out.println("String: " + text + " (length=" + len + ")");
}
reader.next();
}
reader.close();
}
错误堆栈跟踪是:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at de.odysseus.staxon.json.stream.impl.Yylex.zzRefill(Yylex.java:346)
at de.odysseus.staxon.json.stream.impl.Yylex.yylex(Yylex.java:600)
at de.odysseus.staxon.json.stream.impl.Yylex.nextSymbol(Yylex.java:271)
at de.odysseus.staxon.json.stream.impl.JsonStreamSourceImpl.next(JsonStreamSourceImpl.java:120)
at de.odysseus.staxon.json.stream.impl.JsonStreamSourceImpl.peek(JsonStreamSourceImpl.java:250)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:150)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:153)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:183)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:153)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:183)
at de.odysseus.staxon.base.AbstractXMLStreamReader.initialize(AbstractXMLStreamReader.java:216)
at de.odysseus.staxon.json.JsonXMLStreamReader.initialize(JsonXMLStreamReader.java:87)
at de.odysseus.staxon.json.JsonXMLStreamReader.<init>(JsonXMLStreamReader.java:78)
at de.odysseus.staxon.json.JsonXMLInputFactory.createXMLStreamReader(JsonXMLInputFactory.java:150)
at de.odysseus.staxon.json.JsonXMLInputFactory.createXMLStreamReader(JsonXMLInputFactory.java:45)
at test20150911.LongStringJsonTest.useStaxon(LongStringJsonTest.java:40)
at test20150911.LongStringJsonTest.main(LongStringJsonTest.java:35)
5) 最后的希望是一些用 C 编写的工具 t运行sforming 我的 JSON 首先进入 BSON。对于 BSON,我会尝试做一些更好的处理。这个似乎是最著名的: https://github.com/dwight/bsontools 。在我从我的 1 Gb JSON 文件上的这个包中 运行 "fromjson" 命令行工具后,它会将它全部加载到内存中(这太可怕了),然后做了 10 分钟的事情。实际上我没有等到最后,因为 10 分钟对于 1 Gb 的文件来说太多了,不是吗? (注意:我的 java 有问题的代码工作不到半分钟)。
所以最终的答案是:(1) 不,看起来没有标准的方法来实现所讨论的目标,并且 (2) 使用 FasterXML/Jackson 可能是最好的 Java 解决方案在这种情况下可以完成。
也许这是您编写自己的解析器的有效案例?
JSON 使用 PushbackReader() 进行解析应该相对简单。
我正在尝试编写一些代码处理 JSON 文件,文件中存储了非常长的字符串值(超过 10 亿个字符)。我不想将整个字符串保留在内存中(因为我可以在流中处理它们)。但是我在 Jackson 解析器中找不到这样的选项。到目前为止,我所做的是使用 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。我很好奇有什么方法可以做得更好(或者甚至在其他一些库的帮助下)?
我认为你问错了问题。
JSON,与 XML 或 CSV 或任何其他结构化文本表示一样,具有三个主要作用:使数据结构可被人类解析,允许通用工具处理许多不同类型的数据数据,并促进可能使用不同内部模型的系统之间的数据交换。
如果您不需要那些特定的特征,结构化文本可能是错误的解决方案。专用的二进制表示可能更有效,并且随着数据的 size/complexity 增长,这种差异会变得巨大。
支持结构化文本格式导入和导出您的工具。但是,在内部,您可能应该使用专门针对特定任务的需要进行调整的数据模型。
现在我知道 JSON 格式不是具有很长字符串值的文档的最佳解决方案。但是以防万一有人遇到类似的问题(例如,当已经给出了这样的 JSON 文件并且需要将其 运行 转换为更好的格式时)。这意味着文档应该至少以某种方式被解析一次。所以这是我的调查:
1) FasterXML/Jackson 令牌流不允许使用标准方式处理长字符串(按部分加载)。我发现处理它们的唯一方法是按照我的问题做一些事情+手动处理unicode。
2) Google/Gson 具有 JsonReader,还允许用户将 JSON 处理为令牌流。有 nextString 方法 (https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java#L816). But there is no way to get it by parts or get any info where is the position of it in JSON file (except couple private methods: https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java#L1317-L1323).
3) fangyidong/Json-simple 使用 SAX 风格的推送接口。但是对于字符串,那里只有一种方法:https://github.com/fangyidong/json-simple/blob/master/src/main/java/org/json/simple/parser/ContentHandler.java#L108
4) 我唯一的希望是 beckchr/StAXON。因为它t运行将JSON转化为XML,然后使用XMLStreamReader。有一种方法允许按部分读取字符串: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/javax/xml/stream/XMLStreamReader.java#XMLStreamReader.getTextCharacters%28int%2Cchar%5B%5D%2Cint%2Cint%29 。但不幸的是,OutOfMemoryError 发生在 JSON 解析期间 运行sformation。这是我的代码:
private static void useStaxon(File tempJson) throws Exception {
XMLInputFactory factory = new JsonXMLInputFactory();
XMLStreamReader reader = factory.createXMLStreamReader(new FileReader(tempJson));
while (true) {
if (reader.getEventType() == XMLStreamConstants.END_DOCUMENT)
break;
if (reader.isCharacters()) {
long len = reader.getTextLength();
String text;
if (len > 20) {
char[] buffer = new char[20];
reader.getTextCharacters(0, buffer, 0, buffer.length);
text = new String(buffer) + "...";
} else {
text = reader.getText();
}
System.out.println("String: " + text + " (length=" + len + ")");
}
reader.next();
}
reader.close();
}
错误堆栈跟踪是:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at de.odysseus.staxon.json.stream.impl.Yylex.zzRefill(Yylex.java:346)
at de.odysseus.staxon.json.stream.impl.Yylex.yylex(Yylex.java:600)
at de.odysseus.staxon.json.stream.impl.Yylex.nextSymbol(Yylex.java:271)
at de.odysseus.staxon.json.stream.impl.JsonStreamSourceImpl.next(JsonStreamSourceImpl.java:120)
at de.odysseus.staxon.json.stream.impl.JsonStreamSourceImpl.peek(JsonStreamSourceImpl.java:250)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:150)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:153)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:183)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:153)
at de.odysseus.staxon.json.JsonXMLStreamReader.consume(JsonXMLStreamReader.java:183)
at de.odysseus.staxon.base.AbstractXMLStreamReader.initialize(AbstractXMLStreamReader.java:216)
at de.odysseus.staxon.json.JsonXMLStreamReader.initialize(JsonXMLStreamReader.java:87)
at de.odysseus.staxon.json.JsonXMLStreamReader.<init>(JsonXMLStreamReader.java:78)
at de.odysseus.staxon.json.JsonXMLInputFactory.createXMLStreamReader(JsonXMLInputFactory.java:150)
at de.odysseus.staxon.json.JsonXMLInputFactory.createXMLStreamReader(JsonXMLInputFactory.java:45)
at test20150911.LongStringJsonTest.useStaxon(LongStringJsonTest.java:40)
at test20150911.LongStringJsonTest.main(LongStringJsonTest.java:35)
5) 最后的希望是一些用 C 编写的工具 t运行sforming 我的 JSON 首先进入 BSON。对于 BSON,我会尝试做一些更好的处理。这个似乎是最著名的: https://github.com/dwight/bsontools 。在我从我的 1 Gb JSON 文件上的这个包中 运行 "fromjson" 命令行工具后,它会将它全部加载到内存中(这太可怕了),然后做了 10 分钟的事情。实际上我没有等到最后,因为 10 分钟对于 1 Gb 的文件来说太多了,不是吗? (注意:我的 java 有问题的代码工作不到半分钟)。
所以最终的答案是:(1) 不,看起来没有标准的方法来实现所讨论的目标,并且 (2) 使用 FasterXML/Jackson 可能是最好的 Java 解决方案在这种情况下可以完成。
也许这是您编写自己的解析器的有效案例?
JSON 使用 PushbackReader() 进行解析应该相对简单。