[2021/05/22更新]FlutterでWebスクレイピング(QiitaのマイページからLGTMの数を取得)


※2021/4/13追記
現在、QiitaのURLが変更されており、そのままだと動かなくなってしまっています。時間見つけて更新したいです。

※2021/5/22追記
URLの変更に対応しました。

なぜFlutterでスクレイピングしようと思ったのか

きっかけはQiitaのアプリを作成中に、QiitaのユーザAPIからは、LGTM数が取得出来ないことに気づいたことでした。

しかし以前LGTM数のランキングのようなものを見たことある気がしたので、調べるとWebスクレイピングをしてランキングを作成されているようだったので、アプリから自分のLGTM数を取得しようと思うとスクレイピングするしかないと考えました。
※Webスクレイピングは、マナーや規約に気をつけて実施する必要があります。

実現までに考えたこと

Webスクレイピングと言えば、Pythonなどを中心にライブラリが多く、あまりスクレイピングの経験も少なかったりしたので、今回はまずPythonで実際に情報を取得するようにしました。

いきなりFlutterから実践しても良いと思いますが、私のように自信がなかったり、スクレイピング自体が久しぶりの人はスクリプト言語で試してみると良いと思います。その方が情報も豊富なので。

STEP1: Pythonのスクリプトで自分のマイページからLGTM数を取得
STEP2: Flutterで実現する方法を考える
STEP3: サンプルアプリ実装

という順番で実践しました。

STEP1: Pythonのスクリプトで自分のマイページからLGTM数を取得

今回は情報が豊富なPythonのBeaufulSoupを使いました。以下の記事などを参考にしました。

Python Webスクレイピング テクニック集「取得できない値は無い」JavaScript対応@追記あり6/12

動作環境: Python 3.9.1

qiita_lgtm.py

# coding: UTF-8
import requests
from bs4 import BeautifulSoup

# 自分のアカウントのQiitaのマイページのURL
url = "https://qiita.com/toda-axiaworks"
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0"}
response = requests.get(url=url, headers=headers)
html = response.content
soup = BeautifulSoup(html, "html.parser")

# セレクタは、上記の記事を参考に記述しました
selector = "span.css-mf9wc5"

print(soup.select_one(selector).text)
python qiita_lgtm.py

を実行すると、109と正しい結果が返ってきました。(2020/02/14時点)

ここでセレクタのパラメータなどが正しいことを、しっかり確認しました。

STEP2: Flutterで実現する方法を考える

なるべくライブラリの力を借りたかったので、ライブラリを探しました。

今回は、universal_htmlというライブラリを使用しました。主な理由はSTEP1で作成したPythonのコードと、ほとんど同じコードで情報が取得出来ることが分かったことが、大きなポイントでした。
(当初は別のライブラリで実装を試みたのですが、サンプルも少なく、私があまり使いこなせなかったこともあり、universal_htmlを採用しました。)

STEP3: サンプルアプリ実装

STEP2で挙げたuniversal_htmlを使うことで、ほぼSTEP1のPythonコードを再現するだけでFlutterアプリ上で取得したかった情報が表示出来ました。

pubspec.yaml
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  universal_html: ^1.2.4 // 今回インストールするライブラリ
(以下略)

pubspec.yamlに追加するライブラリ情報を記載して、

pub get

コマンドを実行します。メインのコードは以下のようになりました。

main.dart
import 'package:flutter/material.dart';
import 'package:universal_html/driver.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Qiita LGTM',
      theme: ThemeData(
        primarySwatch: Colors.lightGreen,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyLgtmNumberPage(),
    );
  }
}

class MyLgtmNumberPage extends StatefulWidget {
  @override
  _MyLgtmNumberPageState createState() => _MyLgtmNumberPageState();
}

class _MyLgtmNumberPageState extends State<MyLgtmNumberPage> {
  // 最終的にはBaseURLにAPIから取得したユーザーIDを渡せば良さそう
  static const url = 'https://qiita.com/toda-axiaworks';

  String _lgtmNumber = '';

  @override
  void initState() {
    super.initState();

    _getQiitaLgtmNumber();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Qiita LGTM Sample',
          style: TextStyle(
            color: Colors.white,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      body: Center(
        child: _lgtmNumber.isNotEmpty
            ? _RoundLgtm(
                label: _lgtmNumber,
              )
            : CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(
          Icons.refresh,
          color: Colors.white,
        ),
        onPressed: _getQiitaLgtmNumber,
      ),
    );
  }

  _getQiitaLgtmNumber() async {
    final driver = HtmlDriver();
    await driver.setDocumentFromUri(Uri.parse(url));

    setState(() {
      _lgtmNumber = driver.document
          .querySelector("span.css-mf9wc5")
          .text;
    });
  }
}

// LGTM数を表示する円型のWidget
class _RoundLgtm extends StatelessWidget {
  const _RoundLgtm({
    this.label,
  });

  final String label;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      height: 200,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.lightGreen,
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            label,
            style: TextStyle(
              color: Colors.white,
              fontSize: 40,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            'LGTM',
            style: TextStyle(
              color: Colors.white,
              fontSize: 16,
            ),
          ),
        ],
      ),
    );
  }
}

まとめ

ひとまずLGTMの数字がアプリ上で表示できていることを確認出来ました。今回は特にSTEP1で最初にPythonで動作をしっかり確認して、それをFlutter側で同じように実装を行えたことで、とてもやりやすくなりました。

サンプルなので固定のURLになっていますが、https://qiita.com/toda-axiaworks というURLなので、https://qiita.com/ のベースURLにAPIから取得したユーザIDを渡せば、他のユーザーの情報も表示出来そうです。

念のため試したところ、他のユーザーのLGTMを表示することも出来ました。