【问题标题】:Really long strings in JSON (>1 gig) with Jackson token stream带有杰克逊令牌流的 JSON (>1 gig) 中非常长的字符串
【发布时间】: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


【解决方案1】:

我认为你问错了问题。

JSON 与 XML 或 CSV 或任何其他结构化文本表示一样,具有三个主要作用:使数据结构易于解析,允许通用工具处理多种不同类型的数据,以及促进系统之间的数据交换使用不同的内部模型。

如果您不需要这些特定特征,结构化文本可能是错误的解决方案。专用的二进制表示可能更有效,并且随着数据大小/复杂性的增长,这种差异会变得巨大。

支持结构化文本格式的导入和导出工具。不过,在内部,您可能应该使用专门针对您的特定任务需求而调整的数据模型。

【讨论】:

  • 好的,这是个好主意。但是我应该如何支持将具有很长字符串(例如 JSON)的结构化文本数据导入到我的内部表示中而不可能解析它呢?我的问题是关于解析它。即使在上传期间完成了一次。
  • 在这种情况下,请参阅其他答案:使用不需要一次将整个文本文档存储在内存中的解析器,并将其逐步转换为您的内部表示。在 XML 中,这意味着使用基于事件的解析器(SAX 或类似的 API)。我没有理由深入研究可用于 JSON 的解析器/API。这可能部分取决于您使用的语言。
【解决方案2】:

现在我知道 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)。但是没有办法通过部分获取它或获取它在 JSON 文件中的位置的任何信息(除了几个私有方法: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。因为它将 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 解析中。这是我的代码:

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 语言编写的工具,首先将我的 JSON 转换为 BSON。使用 BSON,我会尝试做一些更好的处理。这个似乎最有名:https://github.com/dwight/bsontools。在我在我的 1 Gb JSON 文件上运行此包中的“fromjson”命令行工具后,它会将其全部加载到内存中(这太可怕了),然后做了 10 分钟。实际上,我没有等到最后,因为 10 分钟对于 1 Gb 文件来说太多了,不是吗? (注意:我的 java 代码工作不到半分钟)。

所以最终答案是:(1) 不,看起来没有标准方法可以实现所讨论的目标,(2) 在这种情况下,使用 FasterXML/Jackson 可能是最好的 Java 解决方案。

【讨论】:

    【解决方案3】:

    也许这是您编写自己的解析器的有效案例?

    使用 PushbackReader() 解析 JSON 应该相对简单。

    【讨论】:

    • 并不是说这是错误的答案。不,它是通用的:自己发明所有你需要的东西。如果可以避免它,这是我想花时间做的最后一件事。因为这意味着您必须修复作者和社区多年来在其他库中修复的所有错误。我不想说这是错误的方式或不可能的。这将是不幸的浪费时间。
    • 只是这里建议的一些解决方案似乎比这样做更复杂,因为 JSON 语法非常简单:json.org 另一种选择可能是采用现有的简单流式 JSON 解析器并对其进行扩展根据需要。
    猜你喜欢
    • 1970-01-01
    • 2019-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-05
    • 1970-01-01
    • 2021-11-01
    • 2012-12-27
    相关资源
    最近更新 更多