【问题标题】:How to cut up large JSON file into chunks and sort using GSON如何将大型 JSON 文件分割成块并使用 GSON 进行排序
【发布时间】:2018-03-14 09:14:53
【问题描述】:

我有一个名为 something.json 的巨大 JSON 文件。该文件为 20 MB。我正在与 GSON 一起阅读此内容。它正在标准的 Android Nexus 5X 上读取。

Json 示例:

[
    {"country":"UA","name":"Hurzuf","_id":707860,"coord":{"lon":34.283333,"lat":44.549999}},
    {"country":"UA","name":"Il’ichëvka","_id":707716,"coord":{"lon":34.383331,"lat":44.666668}},
    {"country":"BG","name":"Rastnik","_id":727762,"coord":{"lon":25.283331,"lat":41.400002}}
...
]

代码:

@Override
protected ArrayList<City> doInBackground(File... files) {
    ArrayList<City> cities = new ArrayList<>();
    try {
        InputStream is = new FileInputStream(files[0]);
        JsonReader reader = new JsonReader(new InputStreamReader(is, "UTF-8"));
        reader.beginArray();
        while (reader.hasNext()) {
            City city = new Gson().fromJson(reader, City.class);
            cities.add(city);
        }
        reader.endArray();
        reader.close();
    } catch (Exception e) {
        mResult.onFinish(cities, e.getMessage());
    }

    Collections.sort(cities, (o1, o2) -> o1.getName().compareTo(o2.getName()));
    mResult.onFinish(cities, CityService.SUCCESS);
    return cities;
}

使用的库:

com.google.code.gson:gson:2.8.0

它需要从 Android API 16 到最新版本。

我需要将其读入 mCities,并按城市名称的字母顺序对其进行排序。现在这需要 3 分钟,并且必须在大约 10 秒内完成。我的方法是将 json 文件分成 10 个较小的块,将它们读入、连接和排序。

所以我的问题是:如何将文件分成更小的块,这是解决这个问题的正确方法吗?

文件链接:http://www.jimclermonts.nl/docs/cities.json

【问题讨论】:

  • 如果城市相同,你想如何选择哪一行在前?
  • @ArchitKhosla 不是必需的。
  • 您是否同时需要所有这些数据?数据将用于什么用途?也许你可以做一些服务器端分页/过滤
  • @TimCastelijns 你又来了 ;)。可惜没有分页。
  • 只是一个想法:是否可以在外部任务/作业中对文件进行排序?这样您的程序所做的就是在需要时读取文件。

标签: java json sorting parsing gson


【解决方案1】:

我本身基本上从不进行 Android 编码,但我有一些笔记和可能的想法供您参考,因为这是 纯 Java。 您的读者在阅读每个元素时做了非常过多的工作。 首先,不需要每次需要时都创建Gson

  • 它是不可变且线程安全的。
  • 创建成本相对较高。
  • 实例化 Gson 实例也会在堆上花费 更多 时间来执行然后进行垃圾收集。

接下来,Gson 中的仅反序列化和 JSON 流读取之间存在区别:第一个可能在底层使用重型类型适配器组合,而后者可以简单地逐个令牌解析 JSON 文档。 话虽如此,您在读取 ​​JSON 流时可以获得更好的性能:众所周知,您的 JSON 文件具有非常严格的结构,因此可以更简单地实现高级解析器。

假设一个简单的测试套件针对您的问题具有不同的实现:

数据对象

City.java

final class City {

    @SerializedName("_id")
    final int id;

    @SerializedName("country")
    final String country;

    @SerializedName("name")
    final String name;

    @SerializedName("coord")
    final Coordinates coordinates;

    private City(final int id, final String country, final String name, final Coordinates coordinates) {
        this.id = id;
        this.country = country;
        this.name = name;
        this.coordinates = coordinates;
    }

    static City of(final int id, final String country, final String name, final Coordinates coordinates) {
        return new City(id, country, name, coordinates);
    }

    @Override
    public boolean equals(final Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        final City that = (City) o;
        return id == that.id;
    }

    @Override
    public int hashCode() {
        return id;
    }

    @SuppressWarnings("ConstantConditions")
    public static int compareByName(final City city1, final City city2) {
        return city1.name.compareTo(city2.name);
    }

}

坐标.java

final class Coordinates {

    @SerializedName("lat")
    final double latitude;

    @SerializedName("lon")
    final double longitude;

    private Coordinates(final double latitude, final double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    static Coordinates of(final double latitude, final double longitude) {
        return new Coordinates(latitude, longitude);
    }

    @Override
    public boolean equals(final Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        final Coordinates that = (Coordinates) o;
        return Double.compare(that.latitude, latitude) == 0
                && Double.compare(that.longitude, longitude) == 0;
    }

    @Override
    public int hashCode() {
        final long latitudeBits = Double.doubleToLongBits(latitude);
        final long longitudeBits = Double.doubleToLongBits(longitude);
        final int latitudeHash = (int) (latitudeBits ^ latitudeBits >>> 32);
        final int longitudeHash = (int) (longitudeBits ^ longitudeBits >>> 32);
        return 31 * latitudeHash + longitudeHash;
    }

}

测试基础架构

ITest.java

interface ITest {

    @Nonnull
    default String getName() {
        return getClass().getSimpleName();
    }

    @Nonnull
    Collection<City> test(@Nonnull JsonReader jsonReader)
            throws IOException;

}

主要

    public static void main(final String... args)
            throws IOException {
        final Iterable<ITest> tests = ImmutableList.of(
                FirstTest.get(),
                ReadAsWholeListTest.get(),
                ReadAsWholeTreeSetTest.get(),
                ReadJsonStreamIntoListTest.get(),
                ReadJsonStreamIntoTreeSetTest.get(),
                ReadJsonStreamIntoListChunksTest.get()
        );
        for ( int i = 0; i < 3; i++ ) {
            for ( final ITest test : tests ) {
                try ( final ZipInputStream zipInputStream = new ZipInputStream(Resources.getPackageResourceInputStream(Q49273660.class, "cities.json.zip")) ) {
                    for ( ZipEntry zipEntry = zipInputStream.getNextEntry(); zipEntry != null; zipEntry = zipInputStream.getNextEntry() ) {
                        if ( zipEntry.getName().equals("cities.json") ) {
                            final JsonReader jsonReader = new JsonReader(new InputStreamReader(zipInputStream)); // do not close
                            System.out.printf("%1$35s : ", test.getName());
                            final Stopwatch stopwatch = Stopwatch.createStarted();
                            final Collection<City> cities = test.test(jsonReader);
                            System.out.printf("in %d ms with %d elements\n", stopwatch.elapsed(TimeUnit.MILLISECONDS), cities.size());
                            assertSorted(cities, City::compareByName);
                        }
                    }
                }
            }
            System.out.println("--------------------");
        }
    }

    private static <E> void assertSorted(final Iterable<? extends E> iterable, final Comparator<? super E> comparator) {
        final Iterator<? extends E> iterator = iterable.iterator();
        if ( !iterator.hasNext() ) {
            return;
        }
        E a = iterator.next();
        if ( !iterator.hasNext() ) {
            return;
        }
        do {
            final E b = iterator.next();
            if ( comparator.compare(a, b) > 0 ) {
                throw new AssertionError(a + " " + b);
            }
            a = b;
        } while ( iterator.hasNext() );
    }

测试

FirstTest.java

这是最慢的。 它只是根据测试调整您的问题。

final class FirstTest
        implements ITest {

    private static final ITest instance = new FirstTest();

    private FirstTest() {
    }

    static ITest get() {
        return instance;
    }

    @Nonnull
    @Override
    public List<City> test(@Nonnull final JsonReader jsonReader)
            throws IOException {
        jsonReader.beginArray();
        final List<City> cities = new ArrayList<>();
        while ( jsonReader.hasNext() ) {
            final City city = new Gson().fromJson(jsonReader, City.class);
            cities.add(city);
        }
        jsonReader.endArray();
        cities.sort(City::compareByName);
        return cities;
    }

}

ReadAsWholeListTest.java

这很可能是您实现它的方式。 它不是赢家,但它是最简单的,它使用默认排序。

final class ReadAsWholeListTest
        implements ITest {

    private static final ITest instance = new ReadAsWholeListTest();

    private ReadAsWholeListTest() {
    }

    static ITest get() {
        return instance;
    }

    private static final Gson gson = new Gson();

    private static final Type citiesListType = new TypeToken<List<City>>() {
    }.getType();

    @Nonnull
    @Override
    public List<City> test(@Nonnull final JsonReader jsonReader) {
        final List<City> cities = gson.fromJson(jsonReader, citiesListType);
        cities.sort(City::compareByName);
        return cities;
    }

}

ReadAsWholeTreeSetTest.java

如果您不限于列表,另一个想法是使用已经排序的集合,例如TreeSet。 由于我不知道是否有办法在 Gson 中指定新的 TreeSet 比较器机制,因此它必须使用自定义类型适配器工厂(但如果 City 已经在名称上具有可比性,则不需要这样做,但是它是不灵活)。

final class ReadAsWholeTreeSetTest
        implements ITest {

    private static final ITest instance = new ReadAsWholeTreeSetTest();

    private ReadAsWholeTreeSetTest() {
    }

    static ITest get() {
        return instance;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private static final TypeToken<TreeSet<?>> rawTreeSetType = (TypeToken) TypeToken.get(TreeSet.class);

    private static final Map<Type, Comparator<?>> comparatorsRegistry = ImmutableMap.of(
            City.class, (Comparator<City>) City::compareByName
    );

    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(new TypeAdapterFactory() {
                @Override
                public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
                    if ( !TreeSet.class.isAssignableFrom(typeToken.getRawType()) ) {
                        return null;
                    }
                    final Type elementType = ((ParameterizedType) typeToken.getType()).getActualTypeArguments()[0];
                    @SuppressWarnings({ "rawtypes", "unchecked" })
                    final Comparator<Object> comparator = (Comparator) comparatorsRegistry.get(elementType);
                    if ( comparator == null ) {
                        return null;
                    }
                    final TypeAdapter<TreeSet<?>> originalTreeSetTypeAdapter = gson.getDelegateAdapter(this, rawTreeSetType);
                    final TypeAdapter<?> originalElementTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(elementType));
                    final TypeAdapter<TreeSet<Object>> treeSetTypeAdapter = new TypeAdapter<TreeSet<Object>>() {
                        @Override
                        public void write(final JsonWriter jsonWriter, final TreeSet<Object> treeSet)
                                throws IOException {
                            originalTreeSetTypeAdapter.write(jsonWriter, treeSet);
                        }

                        @Override
                        public TreeSet<Object> read(final JsonReader jsonReader)
                                throws IOException {
                            jsonReader.beginArray();
                            final TreeSet<Object> elements = new TreeSet<>(comparator);
                            while ( jsonReader.hasNext() ) {
                                final Object element = originalElementTypeAdapter.read(jsonReader);
                                elements.add(element);
                            }
                            return elements;
                        }
                    }.nullSafe();
                    @SuppressWarnings({ "rawtypes", "unchecked" })
                    final TypeAdapter<T> castTreeSetTypeAdapter = (TypeAdapter<T>) treeSetTypeAdapter;
                    return castTreeSetTypeAdapter;
                }
            })
            .create();

    private static final Type citiesSetType = new TypeToken<TreeSet<City>>() {
    }.getType();

    @Nonnull
    @Override
    public Set<City> test(@Nonnull final JsonReader jsonReader) {
        return gson.fromJson(jsonReader, citiesSetType);
    }

}

JSON 流阅读器测试

以下课程是一个特殊的阅读器测试,它使用读取城市 JSON 的简化策略。

AbstractJsonStreamTest.java

可能是最简单的(就JSON结构分析而言),它要求JSON文档非常严格。

abstract class AbstractJsonStreamTest
        implements ITest {

    protected static void read(final JsonReader jsonReader, final Consumer<? super City> cityConsumer)
            throws IOException {
        jsonReader.beginArray();
        while ( jsonReader.hasNext() ) {
            jsonReader.beginObject();
            require(jsonReader, "country");
            final String country = jsonReader.nextString();
            require(jsonReader, "name");
            final String name = jsonReader.nextString();
            require(jsonReader, "_id");
            final int id = jsonReader.nextInt();
            require(jsonReader, "coord");
            jsonReader.beginObject();
            require(jsonReader, "lon");
            final double longitude = jsonReader.nextDouble();
            require(jsonReader, "lat");
            final double latitude = jsonReader.nextDouble();
            jsonReader.endObject();
            jsonReader.endObject();
            final City city = City.of(id, country, name, Coordinates.of(latitude, longitude));
            cityConsumer.accept(city);
        }
        jsonReader.endArray();
    }

    private static void require(final JsonReader jsonReader, final String expectedName)
            throws IOException {
        final String actualName = jsonReader.nextName();
        if ( !actualName.equals(expectedName) ) {
            throw new JsonParseException("Expected " + expectedName + " but was " + actualName);
        }
    }

}

ReadJsonStreamIntoListTest.java

这个很像ReadAsWholeListTest,但它使用了一种简化的反序列化机制。

final class ReadJsonStreamIntoListTest
        extends AbstractJsonStreamTest {

    private static final ITest instance = new ReadJsonStreamIntoListTest();

    private ReadJsonStreamIntoListTest() {
    }

    static ITest get() {
        return instance;
    }

    @Nonnull
    @Override
    public Collection<City> test(@Nonnull final JsonReader jsonReader)
            throws IOException {
        final List<City> cities = new ArrayList<>();
        read(jsonReader, cities::add);
        cities.sort(City::compareByName);
        return cities;
    }

}

ReadJsonStreamIntoTreeSetTest.java

与上一个一样,这也是一个更昂贵的实现 (ReadAsWholeTreeSetTest) 的另一种实现,但它不需要自定义类型适配器。

final class ReadJsonStreamIntoTreeSetTest
        extends AbstractJsonStreamTest {

    private static final ITest instance = new ReadJsonStreamIntoTreeSetTest();

    private ReadJsonStreamIntoTreeSetTest() {
    }

    static ITest get() {
        return instance;
    }

    @Nonnull
    @Override
    public Collection<City> test(@Nonnull final JsonReader jsonReader)
            throws IOException {
        final Collection<City> cities = new TreeSet<>(City::compareByName);
        read(jsonReader, cities::add);
        return cities;
    }

}

ReadJsonStreamIntoListChunksTest.java

以下测试基于您最初的想法,但它不会并行排序块(我不确定,但您可以尝试一下)。 我仍然认为前两个更简单,可能更易于维护并提供更多性能提升。

final class ReadJsonStreamIntoListChunksTest
        extends AbstractJsonStreamTest {

    private static final ITest instance = new ReadJsonStreamIntoListChunksTest();

    private ReadJsonStreamIntoListChunksTest() {
    }

    static ITest get() {
        return instance;
    }

    @Nonnull
    @Override
    public List<City> test(@Nonnull final JsonReader jsonReader)
            throws IOException {
        final Collection<List<City>> cityChunks = new ArrayList<>();
        final AtomicReference<List<City>> cityChunkRef = new AtomicReference<>(new ArrayList<>());
        read(jsonReader, city -> {
            final List<City> cityChunk = cityChunkRef.get();
            cityChunk.add(city);
            if ( cityChunk.size() >= 10000 ) {
                cityChunks.add(cityChunk);
                cityChunkRef.set(new ArrayList<>());
            }
        });
        if ( !cityChunkRef.get().isEmpty() ) {
            cityChunks.add(cityChunkRef.get());
        }
        for ( final List<City> cities : cityChunks ) {
            Collections.sort(cities, City::compareByName);
        }
        return merge(cityChunks, City::compareByName);
    }

    /**
     * <p>Adapted from:</p>
     * <ul>
     * <li>Original question: https://stackoverflow.com/questions/1774256/java-code-review-merge-sorted-lists-into-a-single-sorted-list</li>
     * <li>Accepted answer: https://stackoverflow.com/questions/1774256/java-code-review-merge-sorted-lists-into-a-single-sorted-list/1775748#1775748</li>
     * </ul>
     */
    @SuppressWarnings("MethodCallInLoopCondition")
    private static <E> List<E> merge(final Iterable<? extends List<E>> lists, final Comparator<? super E> comparator) {
        int totalSize = 0;
        for ( final List<E> l : lists ) {
            totalSize += l.size();
        }
        final List<E> result = new ArrayList<>(totalSize);
        while ( result.size() < totalSize ) { // while we still have something to add
            List<E> lowest = null;
            for ( final List<E> l : lists ) {
                if ( !l.isEmpty() ) {
                    if ( lowest == null || comparator.compare(l.get(0), lowest.get(0)) <= 0 ) {
                        lowest = l;
                    }
                }
            }
            assert lowest != null;
            result.add(lowest.get(0));
            lowest.remove(0);
        }
        return result;
    }

}

测试结果

对于我的桌面 JRE,我可以获得以下测试结果:

                          FirstTest : in 5797 ms with 209557 elements
                ReadAsWholeListTest : in 796 ms with 209557 elements
             ReadAsWholeTreeSetTest : in 733 ms with 162006 elements
         ReadJsonStreamIntoListTest : in 461 ms with 209557 elements
      ReadJsonStreamIntoTreeSetTest : in 452 ms with 162006 elements
   ReadJsonStreamIntoListChunksTest : in 607 ms with 209557 elements
--------------------
                          FirstTest : in 3396 ms with 209557 elements
                ReadAsWholeListTest : in 493 ms with 209557 elements
             ReadAsWholeTreeSetTest : in 520 ms with 162006 elements
         ReadJsonStreamIntoListTest : in 385 ms with 209557 elements
      ReadJsonStreamIntoTreeSetTest : in 377 ms with 162006 elements
   ReadJsonStreamIntoListChunksTest : in 540 ms with 209557 elements
--------------------
                          FirstTest : in 3448 ms with 209557 elements
                ReadAsWholeListTest : in 429 ms with 209557 elements
             ReadAsWholeTreeSetTest : in 421 ms with 162006 elements
         ReadJsonStreamIntoListTest : in 400 ms with 209557 elements
      ReadJsonStreamIntoTreeSetTest : in 385 ms with 162006 elements
   ReadJsonStreamIntoListChunksTest : in 480 ms with 209557 elements
--------------------

如您所见,创建过多的Gson 实例绝对是一个错误的想法。 更优化的测试获得更好的性能。 但是,将一个大列表拆分为排序块(非并行)以便稍后合并并不会在我的环境中带来太多性能提升。

为了简单起见,可能是最好的选择,我会选择ReadJsonStreamInto_Collection_Test,具体取决于所需的集合。 我不太确定它在真正的 Android 环境中工作得有多好,但你可以简单地做一些 JSON 反序列化,比 Gson 使用它的内部做得更好。

顺便说一句:

  • 我不太确定,但您注意到 162006 个独特的城市了吗?您的 JSON 文件可能有一些重复项(至少如果它的 _id 是身份)。
  • 如果您只是在工作站上提前生成cities.json的排序版本,然后再在Android设备上使用它会怎样?此外,如果我的上述假设正确,您可能希望过滤掉重复项。

【讨论】:

  • 亲爱的 Lyubomyr,首先:哇,感谢您抽出宝贵时间创建如此详尽的解决方案。 - 是的,有更多城市名称相同但 id 不同(例如阿姆斯特丹)。但我们不应该过滤掉它。世界上有3个城市叫阿姆斯特丹。 - 其次,这是一项工作的编程任务。任何理智的人都会在后端进行解析和排序,这就是我成为 Android 开发人员的原因哈哈 :)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 2015-03-09
  • 1970-01-01
  • 1970-01-01
  • 2015-08-27
  • 2020-09-08
  • 2021-06-23
相关资源
最近更新 更多