DartでWebスクレイピング
DartでWebサイトからデータを取得したくなって、やり方を調べて実装してみたので書き残す。
例として、Zennのトップページに表示されているトレンドのタイトルの一覧を取得してみる。
この記事のコードは簡易的なコマンドラインツールとして書いてみたけど、動作自体はそのままFlutterにも組み込めるので、そこはお好みでアレンジを。
自分が動かした環境は以下の通り。だけどFlutterでもWebでもMacでも動くはず。
- Dart 2.12.2
- Windows 10
準備とコード
適当なフォルダを作成する。例えば安直にscraping
とか。
フォルダ内に、必要なファイルを作成する。最低限必要なのはpubspec.yaml
とscraping.dart
(メインのコード。名前は何でも良い)の2つ。
name: "scraping"
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
http: ^0.13.1
html: ^0.15.0
メインのコードはこんな感じ。
import 'package:http/http.dart' as http;
import 'package:html/parser.dart' show parse;
// main関数。非同期処理(await)を使用するのでasync。
void main() async {
// 取得先のURLを元にして、Uriオブジェクトを生成する。
final url = 'https://zenn.dev/';
final target = Uri.parse(url);
// 取得する。
final response = await http.get(target);
// 下の行のコメントを外すことで、返されたHTMLを出力できる。
// print(response.body);
// ステータスコードをチェックする。「200 OK」以外のときはその旨を表示して終了する。
if (response.statusCode != 200) {
print('ERROR: ${response.statusCode}');
return;
}
// 取得したHTMLのボディをパースする。
final document = parse(response.body);
// 要素を絞り込んで、結果を文字列のリストで得る。
final result = document.querySelectorAll('h2').map((v) => v.text).toList();
// 結果を出力する。
print(result);
}
そしてコマンドラインから以下のコマンドを打てば実行できる。
dart scraping.dart
結果として、Zennのトップページのトレンドのタイトル一覧が表示されるはず。完了。
VSCodeを使っていなかったり、VSCodeでもDartの機能拡張を使っていなかったりすると、pub get
が必要かもしれないので、その辺りは適当に。
コードの解説
基本的な部分は上記のコードにコメントとして書いているけど、補足等を以下に記す。
使用パッケージ
このhtml
は、コアライブラリであるdart:html
とは異なるので注意。自分は違う方のドキュメントを読んでしまっていて少し悩んだ。
処理の流れ
URLの文字列を元にしてUri
オブジェクトを作成し、http
のget
関数でファイルを取得する。
一応ステータスコードをチェックしてから、html
のparse
関数でHTMLをパースし、Document
を得る。
Document
のquerySelectorAll
メソッドを使って要素を絞り込み、それから諸々の処理をして、最後に文字列のリストをprint
している。
querySelectorAll
JavaScriptをはじめとして他の言語でもよくあるやつだけど、CSSセレクタを使用して、要素を絞り込むもの。
上記のコードの場合は、ページ内のh2
要素をすべて抽出している。今回は抽出したいものが全部h2
要素だけで、余分なものもなかったので、とても簡単。
このメソッドでは他にもIDやclassを元にして絞り込める。例えば引数を今回の'h2'
から'#tech-trend h2'
にすると、Techのトレンドだけに絞り込める(Ideasを除外できる)。
Document
クラスには他にもgetElementById
、getElementsByClassName
、querySelector
といったよくあるメソッドがあるので、ドキュメントを参照。
querySelectorAll
の後の諸々のコード
別に本質とは何の関係もなく、単に文字列のリストを得るためのコード。
querySelectorAll
の戻り値の型はList<Element>
。
map
を使って、List<Element>
の中身のElement
それぞれのtext
プロパティによりIterable<String>
を得る。
最後にtoList
でList<String>
へ変換している。
Dartではイテレータを使っているケースをあまり見かけない気がするけど、where
でさらに条件を追加して要素を絞り込んだりできるので便利。
(Rustと違ってDartではイテレータにパフォーマンス的なデメリットがあるのかも知れないけど、調べていない)
その他
cookieやUserAgent等を指定したいとき
get
の引数としてヘッダを渡せる。ヘッダはMap<String, String>
で、例えばcookieを以下のように指定できる。
final cookie = {'Cookie': 'flag=1;'};
final response = await http.get(target, headers: cookie);
返ってくるHTMLの確認
Webサイトによっては、アクセス元のユーザエージェントを見て、それぞれに合わせたHTMLを返してくることがある。したがってWebブラウザで見たときとDartのコードから要求したときに構造が異なっている場合があるので、どんなデータが返ってきているかは実際にチェックした方が良い。
そんなときは上記コード中の// print(response.body);
のコメントアウトを外せば、返ってきたHTMLをそのまま表示できる。
Web検索結果の取得
検索エンジンでの検索結果が欲しい、となるとGoogleに検索クエリを投げたくなるところだけど、Googleの検索結果のHTMLは解析するにあたってかなり微妙なのでお勧めしない。特に検索結果のリンク先のURLが欲しいような場合だと、細かい処理が必要になってとても大変。
そもそも上記のコードでGoogleに投げると、エンコードの問題か何かで、検索結果を上手くパースできなかったりする。とりあえず自分はいろいろ試してみたけど諦めたので、気になる人はやってみるがよいさ。
他のパッケージによるスクレイピング
スクレイピング時にフォームを送信するような凝ったことをやるときにはuniversal_html
を使うのが良いのかも知れないけど、調べていない。
スクレイピングのマナー
Webスクレイピングは、節度を持って行う必要がある。
これまであちこちで度々議論されているので自分が特段語ることはないけど、警察という無能集団に逮捕されることもあるので、気を付けた方が良い。
こんな馬鹿げたことがまかり通っている日本のIT後進国っぷりはすごい。
余談
DartはFlutterの文脈でしかほとんど話題に上がらないけど、コマンドラインツールを書くのにもかなり適していると思っているので、もっと流行って欲しいという気持ちで記事を書いてみた。
現代のコマンドラインツール作成の王者であるRustは、処理速度や安全性が魅力ではあるものの、メモリ周りを意識しながら書くのが大変なので、手軽に書いて結果を得るならDartが良い感じ。
Dartと似たような選択肢としてはGoもあるけど、個人的にはDartの方を気に入っているので推したいところ。
Discussion
めちゃくちゃわかりやすかったです。ありがとうございます!