クイズアプリ開発入門書を書きました。

【Swift5/Xcode】Swiftでスクレイピングする方法〜初心者にもわかりやすく解説〜

サトリク

Swiftでスクレイピングの方法を調べてみたら、結構記事が古すぎてちょっと手こずりました。

なので、この記事では、Swiftでのスクレイピングの仕方をわかりやすく解説していきたいと思います。

確認済動作環境

Item Version
Swift 5.3.1
Xcode 12.2

バージョンの確認方法

 

スクレイピングとは?

簡単に言うと、Webサイト上の情報を取得することです。

注意

「Webサイトのここの値を取得する」と言う感じなので、取得するWebサイトのレイアウト(HTML)が変わってしまうと、値が取得できなくなってしまいます。

 

スクレイピングをやってみる

今回の目的

Googleで「Swift スクレイピング」と検索してみると以下の記事がヒットします。

参考 クソiOSアプリ開発〜Swiftでスクレイピングしてみた〜Qiita

牛丼のサイトから牛丼のサイズと値段をとってくるという面白い記事が見つかりました。

しかし、この記事は、初心者に向けての記事ではないため、途中の解説がされていませんでした。しかも、この記事の通りやってみると、色々とエラーが出てしまうので、今回はこの記事のアップデート的な感じで書いていきたいと思います。

やりたいこと

牛丼のWebサイトの「ここ」の情報を取得して

その情報をテーブルに表示させたい。

Qiitaの牛丼の記事の情報

Qiitaの牛丼の記事では、初心者向けの記事ではないので、Storyboardなどの解説はなく、ただコードを貼っているだけでした。

2つのファイルのコードが記載されています。

import UIKit
//牛丼オブジェクト
class Gyudon: NSObject {
    var size: String = ""
    var price: String = ""
}
import UIKit
//HTTP通信してくれるやつ
import Alamofire
//スクレイピングしてくれるやつ
import Kanna

class GyudonPriceTableViewController: UITableViewController {
    var beefbowl = [Gyudon]()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.getGyudonPrice()
    }
    func getGyudonPrice() {
    //スクレイピング対象のサイトを指定
    Alamofire.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in
            if let html = response.result.value {
                if let doc = try? HTML(html: html, encoding: .utf8) {

                    // 牛丼のサイズをXpathで指定
                    var sizes = [String]()
                    for link in doc.xpath("//th[@class='menu-size']") {
                        sizes.append(link.text ?? "")
                    }

                    //牛丼の値段をXpathで指定
                    var prices = [String]()
                    for link in doc.xpath("//td[@class='menu-price']") {
                        prices.append(link.text ?? "")
                    }

                    //牛丼のサイズ分だけループ
                    for (index, value) in sizes.enumerated() {
                        let gyudon = Gyudon()
                        gyudon.size = value
                        gyudon.price = prices[index]
                        self.beefbowl.append(gyudon)
                    }
                    self.tableView.reloadData()
                }
            }
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.beefbowl.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)

        let gyudon = self.beefbowl[indexPath.row]
        cell.textLabel?.text = gyudon.size
        cell.detailTextLabel?.text = gyudon.price

        return cell
    }
}

この2つのコードを参考にしながらやっていきます。

牛丼のサイズと値段をスクレイピングしてテーブルに表示する方法

プロジェクト作成からやっていきます。

STEP.1
プロジェクト作成

まずは、プロジェクトを新規で作成します。

Create a new Xcode projectApp → InterfaceをStoryboardでその他は適当で「Create 」

STEP.2
まずはライブラリを追加

ここでは、以下の2つのライブラリを使用します。

  • Alamofire
  • Kanna

なので、cocoapodsで2つのライブラリをインストールします。

cocoapodsでライブラリをインストールする方法がわからない方は、こちらの記事を見ながら、上記のように2つのライブラリをインストールしてください。

【CocoaPods】超初心者向け!CocoaPodsのインストールの仕方と使い方を丁寧に解説
STEP.3
Gyudonファイルを作成

command + n

②「Swift File」を選択

Gyudon.swiftと言う名前でそのまま「Create」

そのファイルを以下のコードを追記します。

class Gyudon: NSObject {
    var size: String = ""
    var price: String = ""
}

これで、牛丼のオブジェクトが完成しました。

STEP.4
ViewController.swiftを丸々コピぺ

以下のコードをViewController.swiftに丸々コピペしてください。

import UIKit
//HTTP通信してくれるやつ
import Alamofire
//スクレイピングしてくれるやつ
import Kanna

class GyudonPriceTableViewController: UITableViewController {
    var beefbowl = [Gyudon]()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.getGyudonPrice()
    }
    func getGyudonPrice() {
    //スクレイピング対象のサイトを指定
    Alamofire.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in
            if let html = response.result.value {
                if let doc = try? HTML(html: html, encoding: .utf8) {

                    // 牛丼のサイズをXpathで指定
                    var sizes = [String]()
                    for link in doc.xpath("//th[@class='menu-size']") {
                        sizes.append(link.text ?? "")
                    }

                    //牛丼の値段をXpathで指定
                    var prices = [String]()
                    for link in doc.xpath("//td[@class='menu-price']") {
                        prices.append(link.text ?? "")
                    }

                    //牛丼のサイズ分だけループ
                    for (index, value) in sizes.enumerated() {
                        let gyudon = Gyudon()
                        gyudon.size = value
                        gyudon.price = prices[index]
                        self.beefbowl.append(gyudon)
                    }
                    self.tableView.reloadData()
                }
            }
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.beefbowl.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)

        let gyudon = self.beefbowl[indexPath.row]
        cell.textLabel?.text = gyudon.size
        cell.detailTextLabel?.text = gyudon.price

        return cell
    }
}

ファイルの名前(ViewController.swift)も以下のように変えておきましょう。

GyudonPriceTableViewController.swiftに変更

この状態でビルド(command + b)してみると、エラーが出てしまいますので対応します。

STEP.5
Alamofireのエラー①

このようにエラーが出てしまうと思います。

Module 'Alamofire' has no member named 'request'

このエラーは、Alamofirerequestがないと言っています。

Alamofireのライブラリのアップデートで書き方が変わりました。

以下のように変更すると、エラーが消えます。

before

Alamofire.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in

after

AF.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in

ただ単に先頭のAlamofireAFにしただけです。

STEP.6
Alamofireのエラー②

またもやエラーが出ました。

Value of type 'Result<Any, AFError>' has no member 'value'

これも書き方が変わりました。

以下のように変更すると、綺麗ではありませんが、エラーは消えます。

before

AF.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in
    if let html = response.result.value {
        if let doc = try? HTML(html: html, encoding: .utf8) {
            
            // 牛丼のサイズをXpathで指定
            var sizes = [String]()
            for link in doc.xpath("//th[@class='menu-size']") {
                sizes.append(link.text ?? "")
            }
            
            //牛丼の値段をXpathで指定
            var prices = [String]()
            for link in doc.xpath("//td[@class='menu-price']") {
                prices.append(link.text ?? "")
            }
            
            //牛丼のサイズ分だけループ
            for (index, value) in sizes.enumerated() {
                let gyudon = Gyudon()
                gyudon.size = value
                gyudon.price = prices[index]
                self.beefbowl.append(gyudon)
            }
            self.tableView.reloadData()
        }
    }
}

after

AF.request("https://www.yoshinoya.com/menu/gyudon/gyu-don/").responseString { response in
    switch response.result {
    case let .success(value):
        if let doc = try? HTML(html: value, encoding: .utf8) {
            
            // 牛丼のサイズをXpathで指定
            var sizes = [String]()
            for link in doc.xpath("//th[@class='menu-size']") {
                sizes.append(link.text ?? "")
            }
            
            //牛丼の値段をXpathで指定
            var prices = [String]()
            for link in doc.xpath("//td[@class='menu-price']") {
                prices.append(link.text ?? "")
            }
            
            //牛丼のサイズ分だけループ
            for (index, value) in sizes.enumerated() {
                let gyudon = Gyudon()
                gyudon.size = value
                gyudon.price = prices[index]
                self.beefbowl.append(gyudon)
            }
            self.tableView.reloadData()
        }
    case let .failure(error):
        print(error)
    }
}

これでエラーは消えます。

STEP.7
実行

実行してみましょう。

ビルドは通りますが、画面に何も表示されません。

当たり前ですが、TableViewController.swiftに変更したので、Storyboard側も変えてあげなければいけません。

STEP.8
Storyboardの修正①

まず、ViewControllerではなく、TableViewControllerなので、TableViewControllerを用意します。

STEP.9
Storyboardの修正②

先ほど配置したTableViewControllerを一番最初に表示する画面に変更します。

STEP.10
Storyboardの修正③

この画面のClassにGyudonPriceTableViewController.swiftを設定します。

STEP.11
Storyboardの修正④

セルの「Style」をRight Detailに変更して、「Identifier」にreuseIdentifierを入力します。

STEP.12
実行してみる

実行してみましょう。

このように、牛丼のサイズと、値段が表示されると思います!

完了

無事、現バージョンでも、スクレイピングできました!

 

他のサイトから取得する方法

この記事では牛丼のサイトから、サイズと値段を取得しましたが、当然他のサイトから取得もできます。

★★ここにサイトのURL★★と言うところに、サイトのURLを入力

★★ここにタグ★★と言うところに、取得したい値のHTMLタグを入力

★★ここにクラス★★と言うところに、取得したい値のクラスを入力

すると、できます!

AF.request("★★ここにサイトのURL★★").responseString { response in
    switch response.result {
    case let .success(value):
        if let doc = try? HTML(html: value, encoding: .utf8) {
            
            // 牛丼のサイズをXpathで指定
            var sizes = [String]()
            for link in doc.xpath("//★★ここにタグ★★[@class='★★ここにクラス★★']") {
                sizes.append(link.text ?? "")
            }
            
            //牛丼の値段をXpathで指定
            var prices = [String]()
            for link in doc.xpath("//td[@class='menu-price']") {
                prices.append(link.text ?? "")
            }
            
            //牛丼のサイズ分だけループ
            for (index, value) in sizes.enumerated() {
                let gyudon = Gyudon()
                gyudon.size = value
                gyudon.price = prices[index]
                self.beefbowl.append(gyudon)
            }
            self.tableView.reloadData()
        }
    case let .failure(error):
        print(error)
    }
}

Webサイト上で、command + shift + Cでデベロッパーツールを起動します。

起動できたら、右上の  をクリックします。

そしたら、取得したい場所にマウスカーソルを当てると、タグとクラスが表示されます。

こんな感じで取得できます。

注意

ただ、この牛丼のスクレイピングは、複数の値のサイズと複数の値の値段を取得しているので、1つの値を取りたいという方は、コードを少し変える必要があります。

 

まとめ

うまくできましたでしょうか。

Swiftでのスクレイピングの記事があまりなかったので、Qiitaの記事を参考に書いてみました。

もう少しいい書き方があると思うので、ちょっと勉強していつかこの記事をリライトしようと思います。

サトリク

他にも、Swift/Xcodeの記事を書いているので、ぜひ読んでみてください!