やりたいこと
- 【大目標】 小説家になろうの小説ダウンロードAndroidアプリの作成。
- 【中目標】なろう小説APIを使った小説検索機能の実装。
- 【この記事のゴール】なろう小説APIで取得したYamlデータのパース方法の模索。
SnakeYAMLについて
- InputstreamやStringからYAMLデータを読み込み、適当な型にパースしてくれる便利ライブラリ。
- YAMLデータにおいてアイテムが複数階層で定義される場合、戻り値は入れ子構造となる。
- YAMLデータがおいてアイテムが並列に定義される場合、戻り値は
List
またはArray
となる。 -
load
時に型を指定しない場合、YAML文章のKey
とValue
それぞれに型推測を行いLinkedHashMap<Object,Object>
を返す。 -
load
時に型を指定した場合、指定した型にキャストして返してくれる。- 指定する型にはSetterとGetterの両方が定義されたデータを定義する必要がある。
型指定なしの場合
LinkedHashMap<String,Object> doc = yaml.load(inputStream);
YAML文章 | 生成されるJavaオブジェクト | 備考 |
---|---|---|
novel_type: 1 | LinkedHashMap<String, Integer> | |
keyword: ほのぼの 学園 | LinkedHashMap<String, String> | 空白や改行が含まれる場合もStringは1つ |
updated_at: 2018-10-30 05:53:44 | LinkedHashMap<String, Date> | |
gensaku: null | LinkedHashMap<String, Object> | valueはnull |
型指定ありの場合
Item doc = yaml.loadAs(inputStream, Item.class);
Item.java
public class Item {//Lombokを利用
@Setter @Getter private Integer novel_type;
@Setter @Getter private String keyword;
@Setter @Getter private Date updated_at;
@Setter @Getter private String gensaku;
}
- 入力元のYAML構文に定義済み項目が存在しない場合、プリミティブ型の場合は例外(
YamlException
)、クラスの場合はnull
となる。
パースしたいYAML構文(なろう小説API出力)
---
-
allcount 2
-
title AAA
writer aaa
...(その他多くのパラメータ)
-
title BBB
writer bbb
...(その他多くのパラメータ)
- 1アイテム目に取得した小説数(allcount)を定義される。
- 2アイテム以降が、実際の小説サマリデータとなる。(アイテム数はallcountに依存)
- 入力元のYAML構文はできるだけ加工したくない。
パース後のデータ構造
こんなデータクラスに格納したい.java
public class NovelSummary { // allcount(小説数)は含まない
@Setter @Getter private String title;
@Setter @Getter private String writer;
…その他たくさん
}
パースするのに困ること
- トップレベルのアイテムが複数のため、本Yaml構文は
Array
またはList
型でのみパースできる。 - ジェネリクスでリストの型指定はできず、戻り値は
LinkedHashMap
となる。 - そもそも、1アイテム目(小説数)と2アイテム目以降(小説データ)が並列関係で定義されているため、一括でデータ化できない。
できません.java
List<NovelSummary> docs = yaml.load(target);
他の人はどうしてるの?
- StackOverrun - 入力元のYAML文を改造(ルートにアイテムを追加し、当該アイテムのコンストラクタでパース)
- baeldung - 同上
悩んだ結果、最終的な実装
public static ArrayList<NovelSummary> parseYaml(String input) {
ArrayList<NovelSummary> results = new ArrayList<>();
Yaml yaml = new Yaml();
List list = yaml.load(target); //戻り値は List<LinkedHashMap<String,?>>
ListIterator iterator = list.listIterator(1); //最初の1アイテム目は小説数のため読み飛ばす
while (iterator.hasNext()) {
String docYaml = yaml.dump(iterator.next()); //LinkedHashMap から YAML構文を生成
NovelSummary sr = yaml.loadAs(docYaml, NovelSummary.class); //YAML構文を独自データ型にキャスト
results.add(sr);
}
return results;
}
- 型指定なしでリスト形式にYAMLパースを実施し、リスト内の各要素に対して再度YAMLパースすることで独自データ型にキャストしてみる。
- SnakeYAMLにキャストさせずに、自分でHashMapから独自データ型を生成するほうがベーシックなような
まとめ
- インテリジェンスに解析してくれるため、YAMLはとても便利。
- ただし、データ構造が単純な場合に限る。
- データ構造が複雑(ネストとか、並列階層のアイテムの構造が違うとか)の場合は、つらかとです。