🪒

DartでWebスクレイピング

2021/04/18に公開1

DartでWebサイトからデータを取得したくなって、やり方を調べて実装してみたので書き残す。

例として、Zennのトップページに表示されているトレンドのタイトルの一覧を取得してみる。

この記事のコードは簡易的なコマンドラインツールとして書いてみたけど、動作自体はそのままFlutterにも組み込めるので、そこはお好みでアレンジを。

自分が動かした環境は以下の通り。だけどFlutterでもWebでもMacでも動くはず。

  • Dart 2.12.2
  • Windows 10

準備とコード

適当なフォルダを作成する。例えば安直にscrapingとか。

フォルダ内に、必要なファイルを作成する。最低限必要なのはpubspec.yamlscraping.dart(メインのコード。名前は何でも良い)の2つ。

pubspec.yaml
name: "scraping"

environment:
  sdk: '>=2.12.0 <3.0.0'

dependencies:
  http: ^0.13.1
  html: ^0.15.0

メインのコードはこんな感じ。

scraping.dart
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が必要かもしれないので、その辺りは適当に。

コードの解説

基本的な部分は上記のコードにコメントとして書いているけど、補足等を以下に記す。

使用パッケージ

httphtmlを使用している。

このhtmlは、コアライブラリであるdart:htmlとは異なるので注意。自分は違う方のドキュメントを読んでしまっていて少し悩んだ。

処理の流れ

URLの文字列を元にしてUriオブジェクトを作成し、httpget関数でファイルを取得する。

一応ステータスコードをチェックしてから、htmlparse関数でHTMLをパースし、Documentを得る。

DocumentquerySelectorAllメソッドを使って要素を絞り込み、それから諸々の処理をして、最後に文字列のリストをprintしている。

querySelectorAll

JavaScriptをはじめとして他の言語でもよくあるやつだけど、CSSセレクタを使用して、要素を絞り込むもの。

上記のコードの場合は、ページ内のh2要素をすべて抽出している。今回は抽出したいものが全部h2要素だけで、余分なものもなかったので、とても簡単。

このメソッドでは他にもIDやclassを元にして絞り込める。例えば引数を今回の'h2'から'#tech-trend h2'にすると、Techのトレンドだけに絞り込める(Ideasを除外できる)。

Documentクラスには他にもgetElementByIdgetElementsByClassNamequerySelectorといったよくあるメソッドがあるので、ドキュメントを参照

querySelectorAllの後の諸々のコード

別に本質とは何の関係もなく、単に文字列のリストを得るためのコード。

querySelectorAllの戻り値の型はList<Element>
mapを使って、List<Element>の中身のElementそれぞれのtextプロパティによりIterable<String>を得る。
最後にtoListList<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