【SwiftSoup】ADVENTARから取得したHTMLをパースしてアプリに表示する

[ADVENTAR](https://adventar.org/)から25日分の記事情報を取得して、アプリに表示したいというケースがありました。

とここで悲報ですが、ADVENTARには公式で公開されているAPIがありません。。なのでスクレイピングしました。普段HTMLをあまり見ていないのと、スクレイピング自体が初めてだったこともあってなかなか大変でした。 :sweat: 

せっかく頑張ったので記録を残しておこうと思います。
当然のことですが、HTMLの構造が変わったら使えなくなる可能性が高いので、その点ご了承ください。 :pray: 

# 環境

- Xcode14.2
- SwiftSoup2.4.3

# 完成したもの

以下のデータを取得します。タイトルが設定されていなかった場合は、コメントをタイトルとして使用します。

- 日付
- 投稿者名
- 投稿者のプロフィール画像
- 記事タイトル
- コメント
- 記事URL

<img width="300" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/608e3c88-f46f-b66b-dbc4-ec6cabac0e05.png">


# 実装

前述したようにADVENTARにはAPIがないので、[SwiftSoup](https://github.com/scinfu/SwiftSoup)というライブラリを使ってHTMLをパースすることにしました。流れとしては以下です。

1. ADVENTARの特定のURLからHTMLを取得する（今回は「https://adventar.org/calendars/7577 」を使いました）
1. SwiftSoupを使って頑張ってパースする

２つ目の頑張ってパースするところについて少し説明します。

まず取得したString文字列を、SwiftSoupが提供しているDocument型に変換します。
以下はコードの一部を取り出していますが、tryがついていることからもわかるように例外が投げられることがありますので、実際に書くときはdo catchで囲う必要があります。

```swift
let doc: Document = try SwiftSoup.parse(htmlString)
```

この後は、HTMLの要素を指定して、欲しいデータを取得していくことになります。
がその前に、パースしたいHTMLの構造をよく見て、どの要素に欲しいデータが入っているかを特定します。

まず今回はADVENTARのカレンダー表示の下にあるリスト？からデータを取得することにしました。
構造としては、`EntryList`というクラスの`div`タグの中に、`item`クラスの`li`タグがずらっと並んでいることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/40506a8a-fc36-566f-9758-fec72a2d6387.png)

これを取得するためには、これもSwiftSoupが提供している[`Elements.select(_ selector: String)`メソッド](https://github.com/scinfu/SwiftSoup#use-selector-syntax-to-find-elements)を使います。
これは引数にStringでセレクターを指定することで、該当のデータを取得します。

```swift
let entries = try doc.select("ul.EntryList").select("li.item")
```

`entries`はこれもSwiftSoupが提供する`Elements`型を返します。

ここからは、日付や投稿者名など一つ一つの投稿情報を取得していきます。

![スクリーンショット 2022-12-25 16.50.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/0dbf1b44-0c38-f13d-758d-bb979016ca4b.png)


基本的には先ほど使った、`Elements.select(_ selector: String)`メソッドと要素のテキストを返す[`Element.text()`メソッド](https://github.com/scinfu/SwiftSoup#extract-attributes-text-and-html-from-elements)、属性を指定する`Node.attr(_ String key)`メソッドを使いました。

`entryIndex`は、`entries`の要素番号が入ります。（今回は0~24）

```swift
// 日付
let date = try entries[safe: entryIndex]?.select("div.date").text()
// 投稿者名
let name = try entries[safe: entryIndex]?.select("div.user").text()
// 記事URL
let url = try entries[safe: entryIndex]?.select("div.article").select("div.link").first()?.text()
// 投稿者プロフィール画像URL
let iconImage = try entries[safe: entryIndex]?.select("div.user").select("img").attr("src")
// コメント
let comment = try entries[safe: entryIndex]?.select("div.comment").text()
```

タイトルだけ、`div`タグを特定しやすいクラスが設定されておらず、取得の仕方を少し工夫しました。
構成としては`div`タグの`left`クラスに２つの`div`タグがあり、その２つ目が取得したいタイトルです。

```swift
// 1. divタグのleftを取得する
let leftHTMLElements = try entries[safe: entryIndex]?.select("div.left")
// 2. Stringに変換する
let leftHTMLString = try leftHTMLElements?.html() ?? ""
// 3. StringをDocument型に変換し、divタグを指定して、2つ目の要素を取得する -> それがタイトル
try SwiftSoup.parse(leftHTMLString).select("div")[safe: 1]?.text()

```

もっといいやり方があるかもしれないのですが、なかなかできなかった結果、こうなりました・・・（誰か教えて。。）

# おわり

最初にも書いたように、HTMLの構造が変わったら役に立たないコードなのですが、
スクレイピングって大変だ・・・ということを実感できたのでいいかなと思いました。 :sweat: 

SwiftSoupは使いやすかったです。

ADVENTARさん、公開APIお待ちしております。