[iOS] Swift + Kannaでtableをスクレイピング

# 概要

アメダスの観測結果のページは、前日／当日に分かれています。
この2つのHTMLそれぞれの「気温」の列をスクレイピングして1つのArrayに仕立てます。

【札幌のアメダス】
[前日の観測データのページ](http://www.jma.go.jp/jp/amedas_h/yesterday-14163.html)
<img width="504" alt="スクリーンショット 2017-02-23 22.26.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/0bbbdacc-ba2b-e2a3-e7e6-cd40237bac30.png">

[当日の観測データのページ](http://www.jma.go.jp/jp/amedas_h/today-14163.html)
<img width="500" alt="スクリーンショット 2017-02-23 22.27.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/35247b55-b262-4a99-2c86-6266bd5f8650.png">

そして、こんな感じで、UITextViewに表示してみました。
<img width="508" alt="スクリーンショット 2017-02-23 22.49.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/122917/8a0b4f05-6932-28bf-d84f-34dee1f600d8.png">

# ポイント

- スクレイピングにはライブラリ"Kanna"を利用
- xPath記法でtableの特定列をスクレイピング
- 2ページを別のQueueでスクレイピングして別のArrayに一時格納、全Queueが終了後にArrayを統合

# Kannaについて

Kannaのセットアップと使い方については、以下の記事を参考にさせていただきました。

[Swift製HTMLパーサ「Kanna」](http://qiita.com/_tid_/items/c228b1931cd9b23d52d3)

# 環境

| Items | Version |
|:--    |:--      |
| Xcode | 8.2     |
| Swift | 3.0.2   |

# コード

```swift:ViewController.swift
import UIKit
import Kanna

/// Arrayの要素
class element {
    var day = ""
    var hour = ""
    var temperature: Double = 0
}

class ViewController: UIViewController {

    // MARK: - Variable
    
    /// 最終データのArray
    var finalData: [element] = []
    /// 昨日のデータのArray
    var yesterdayData: [element] = []
    /// 今日のデータのArray
    var todayData: [element] = []
    
    // MARK: - IBOutlet
    
    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var textView: UITextView!
    
    // MARK: - UIViewController LifeCycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: - IBAction
    
    @IBAction func buttonTapped(_ sender: Any) {
        self.execute()
    }
    
    // MARK: - Method
    
    /// メイン処理
    func execute() {
        self.activityIndicator.startAnimating()
        
        // DispatchGroupとQueueを生成
        let group = DispatchGroup()
        let queue1 = DispatchQueue(label: "hoge.fuga.queue1")
        let queue2 = DispatchQueue(label: "hoge.fuga.queue2")
        
        // キューとグループを紐付ける
        queue1.async(group: group) {
            // 札幌のアメダス（昨日）
            guard let url = URL(string: "http://www.jma.go.jp/jp/amedas_h/yesterday-14163.html") else {
                fatalError("Error: URL")
            }
            let data = self.getHtml(url: url)
            self.yesterdayData = self.parseHtml(day: "yesterday", data: data)
        }
        queue2.async(group: group) {
            // 札幌のアメダス（今日）
            guard let url = URL(string: "http://www.jma.go.jp/jp/amedas_h/today-14163.html") else {
                fatalError("Error: URL")
            }
            let data = self.getHtml(url: url)
            self.todayData = self.parseHtml(day: "today", data: data)
        }
        
        // タスクが全て完了したらメインスレッド上で処理を実行する
        group.notify(queue: DispatchQueue.main) {
            // 今日のArrayと昨日のArrayを結合
            self.finalData =  self.yesterdayData + self.todayData
            // 編集してUITextViewに表示
            var s = ""
            for e in self.finalData {
                s += e.day + "-" + e.hour + "h: " + String(e.temperature) + "\n"
            }
            self.textView.text = s
            self.activityIndicator.stopAnimating()
        }
        
    }
    
    /// HTML取得
    /// - parameter url: URL
    /// - returns Data
    func getHtml(url: URL) -> Data {
        do {
            return try Data(contentsOf: url)
        } catch {
            fatalError("fail to download")
        }
    }
    
    /// スクレイピング
    /// - parameter day: "today"/"yesterday"
    /// - parameter data: Data
    /// - returns [element]
    func parseHtml(day: String, data: Data) -> [element] {
        // KannaでHTMLDocumentを生成
        guard let doc = HTML(html: data, encoding: String.Encoding.utf8) else {
            fatalError("Error: HTML")
        }
        var retData: [element] = []
        // HTMLの<table>の時刻の列を基準にLoopし、該当行の気温の列をKannaでスクレイピング
        for i in (1...24) {
            let node = doc.xpath("//*[@id='tbl_list']//tr[td[1][text()='\(i)']]/td[2]")
            if let nodeFirst = node.first, let content = nodeFirst.content, let value = Double(content) {
                // 値が入っている場合のみ取得
                let e = element()
                e.day = day
                e.hour = String(i)
                e.temperature = value
                retData.append(e)
            }
        }
        return retData
    }
    
}
```

SwiftではArrayの結合が"+"でできるんですね。
直感的でイイ！!

# 注意事項

気象庁のサイトに以下のような注意事項が掲載されています。

[気象庁ホームページについて＞利用上の注意について](http://www.jma.go.jp/jma/kishou/info/coment.html#exclusion)

>当ホームページは、通常のブラウザで閲覧することを前提に各種情報を掲載しております。自動巡回ソフト等による、定期的、自動的な気象データの収集等は、サーバーに負荷がかかる等の理由から、原則としてご遠慮いただいております。ご理解お願いします。

本記事の内容を活用する際は、スクレイピング対象サイトの利用規約を確認の上、自己責任でお願いします。