サトリク
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つのコードを参考にしながらやっていきます。
牛丼のサイズと値段をスクレイピングしてテーブルに表示する方法
プロジェクト作成からやっていきます。
まずは、プロジェクトを新規で作成します。
Create a new Xcode project
→ App
→ InterfaceをStoryboardでその他は適当で「Create 」
ここでは、以下の2つのライブラリを使用します。
- Alamofire
- Kanna
なので、cocoapodsで2つのライブラリをインストールします。
cocoapodsでライブラリをインストールする方法がわからない方は、こちらの記事を見ながら、上記のように2つのライブラリをインストールしてください。
【CocoaPods】超初心者向け!CocoaPodsのインストールの仕方と使い方を丁寧に解説①command + n
②「Swift File」を選択
③Gyudon.swift
と言う名前でそのまま「Create」
そのファイルを以下のコードを追記します。
class Gyudon: NSObject { var size: String = "" var price: String = "" }
これで、牛丼のオブジェクトが完成しました。
以下のコードを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)してみると、エラーが出てしまいますので対応します。
このようにエラーが出てしまうと思います。
Module 'Alamofire' has no member named 'request'
このエラーは、Alamofire
にrequest
がないと言っています。
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
ただ単に先頭のAlamofire
をAF
にしただけです。
またもやエラーが出ました。
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) } }
これでエラーは消えます。
実行してみましょう。
ビルドは通りますが、画面に何も表示されません。
当たり前ですが、TableViewController.swift
に変更したので、Storyboard側も変えてあげなければいけません。
まず、ViewController
ではなく、TableViewController
なので、TableViewController
を用意します。
先ほど配置したTableViewController
を一番最初に表示する画面に変更します。
この画面のClassにGyudonPriceTableViewController.swift
を設定します。
セルの「Style」をRight Detail
に変更して、「Identifier」にreuseIdentifier
を入力します。
実行してみましょう。
このように、牛丼のサイズと、値段が表示されると思います!
無事、現バージョンでも、スクレイピングできました!
他のサイトから取得する方法
この記事では牛丼のサイトから、サイズと値段を取得しましたが、当然他のサイトから取得もできます。
★★ここにサイトの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の記事を書いているので、ぜひ読んでみてください!