Страницы

Поиск по вопросам

среда, 13 марта 2019 г.

Как прочитать огромный JSON файл в Java?

С сайта безопасныедороги.рф необходимо из открытых данных в виде JSON вытащить информацию о геолокациях каждого дтп: em_place_latitude и em_place_longitude. Сам файл весит 1Гб и просто когда подключаешь файл - то выскакивает ошибка
GC overhead limit exceeded
Я работал с библиотекой simple-json в Eclipse
Так как я только начал знакомство с JSON, то вообще не понимаю как вытаскивать оттуда информацию, тем более из огромного файла.
Спасибо!


Ответ

На самом деле всё достаточно просто, если не загружать всё содержимое документа в память. Нужно, в принципе, помнить два основных подхода в разборе документов различных форматов: читается всё с помощью потока (InputStream/Reader или их аналоги) и решается как будет обрабатываться его содержимое: или собиранием всего в память (аналог XML DOM, когда документ, будучи полностью в памяти, позволяет с лёгкостью обходить его дерево), или же работать согласно событийной модели (аналог XML SAX или его pull-аналоги, когда парсер сообщает о том, какие токены встречаются в документе, а пользователь уже сам решает что и как с ними делать). Очевидно, что первый вариант проще в использовании, но требует куда больше ресурсов, в то время как второй вариант сложнее, но и более гибок, зачастую требуя вообще минимум ресурсов.
Gson позволяет работать с JSON-потоками с помощью pull-метода: это когда парсер не генерирует события, а предполагает, что пользователь сам знает, что и в каком порядке нужно "тянуть" с потока токенов, но и требуя чтобы каждый токен был обработан хоть как-то. Например:
private static final String ITEMS_NAME = "items"; // em_place_latitude не описан в схеме и не встречается в документе private static final String LATITUDE_PROPERTY = "latitude"; // em_place_longitude не описан в схеме и не встречается в документе private static final String LONGITUDE_PROPERTY = "longitude";
static void parseCrashCoordinates(final JsonReader jsonReader, final ICoordinatesListener listener) throws IOException { // Считываем { как начало объекта. // Если его не считать или считать неверно, выбросится исключение -- это и есть суть pull-метода. jsonReader.beginObject(); // Смотрим имя следующего свойства объекта и сравниваем его с ожидаемым. final String itemsName = jsonReader.nextName(); if ( !itemsName.equals(ITEMS_NAME) ) { // Не items? Возможно, у нас нет идей как его обработать -- лучше выбросить исключение. throw new MalformedJsonException(ITEMS_NAME + " expected but was " + itemsName); } // Так же теперь вычитываем [ jsonReader.beginArray(); // И читаем каждый элемент массива while ( jsonReader.hasNext() ) { // Судя по схеме, каждый элемент массива - объект jsonReader.beginObject(); double latitude = 0; double longitude = 0; // И так же пробегаемся по всех свойствах этого объекта while ( jsonReader.hasNext() ) { // Теперь просто смотрим, являются ли они нам известными final String property = jsonReader.nextName(); switch ( property ) { // latitude? Запоминаем. case LATITUDE_PROPERTY: latitude = jsonReader.nextDouble(); break; // longitude? Запоминаем. case LONGITUDE_PROPERTY: longitude = jsonReader.nextDouble(); break; // Иначе просто пропускаем любое значение свойства. default: jsonReader.skipValue(); break; } } // Просто делегируем полученные координаты в обработчик listener.onCoordinates(latitude, longitude); // И говорим, что с текущим элементом массива, именно объектом, покончено. jsonReader.endObject(); } // Также закрываем последние ] и } jsonReader.endArray(); jsonReader.endObject(); }
Как выглядит обработчик:
interface ICoordinatesListener {
void onCoordinates(double latitude, double longitude);
}
В принципе, возможны ситуации, когда в документе в координатах указана или широта или долгота, или оба значения отсутствуют (и, в принципе, обработка таких случаев является хорошим тоном), но при обработке 148817911056482-crash.json мне такие случаи не встречались.
Теперь протестируем всё это дело.
public static void main(final String... args) throws IOException { testOutput(); testCollecting(); }
// Этот тест просто выводит содержимое координат в стандартный поток вывода // Заметьте: parseCrashCoordinates() сам не решает _что_ делать с координатами -- это целиком наше дело // Поскольку мы вообще просто передаём данные дальше, нас вообще не волнует размер входных данных // Теоретически, это может быть бесконечный поток данных -- круто ведь? private static void testOutput() throws IOException { readAndParse((lat, lng) -> System.out.println("(" + lat + "; " + lng + ")")); }
// Здесь мы, напротив, собираем координаты в список. // Выдержит ли JVM увеличение списка coordinates? Возможно, но не факт: зависит от размера данных и памяти, доступной JVM. private static void testCollecting() throws IOException { final List coordinates = new ArrayList<>(); readAndParse((lat, lng) -> coordinates.add(new Coordinate(lat, lng))); System.out.println(coordinates.size()); }
private static final class Coordinate {
private final double latitude; private final double longitude;
private Coordinate(final double latitude, final double longitude) { this.latitude = latitude; this.longitude = longitude; }
}
private static void readAndParse(final ICoordinatesListener listener) throws IOException { try ( final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(new FileInputStream(...)))) ) { parseCrashCoordinates(jsonReader, listener); } }
Этим примером мне удалось разобрать 148817911056482-crash.json размером 746 МБ, не особо тратясь на вычислительные ресурсы (громко сказано, тем не менее :)). Хвост вывода таков:
(55.632584; 37.80792) (51.5703; 135.8539) (51.9139; 39.2233) 99497
(3 последние координаты и размер списка, в который были собраны все координаты).

Комментариев нет:

Отправить комментарий