TL;DR
-
- Docker を使って Rust, InfluxDB, Grafana の環境を構築する
-
- FREETEL のマイページのログインセッションを取得する
- InfluxDB にデータを投入して、Grafana で見る
動機
もうギガ数に怯える生活は嫌だ!
せめて、ギガ数が可視化されていれば利用を控えるかもしれない。
Docker の準備
influxDB + Grafanaに入門する | Qiita を参考に Rust, InfluxDB, Grafana の環境を整えます。
$ docker -v
Docker version 17.07.0-ce-rc2, build 36ce605
$ docker-compose -v
docker-compose version 1.15.0, build e12f3b9
Rust のソースを置いたり、 InfluxDB や Grafana のデータディレクトリのために ./mount 配下をコンテナにマウントさせます。
version: "2"
services:
rust:
build: .
volumes:
- "./mount/rust:/opt/rust"
influxdb:
image: influxdb
ports:
- "8083:8083"
- "8086:8086"
volumes:
- "./mount/influxdb:/var/lib/influxdb"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
volumes:
- "./mount/grafana:/var/lib/grafana"
コンテナ内の Rust のソースは /opt/rust に置くことにします。
FROM rust:1.19.0
RUN apt-get update && apt-get install -y pkg-config libssl-dev
RUN mkdir -p /opt/rust
WORKDIR /opt/rust
CMD ["sh", "-c", "tail -f /dev/null"]
$ docker-compose build
$ docker-compose start
Starting influxdb ... done
Starting rust ... done
Starting grafana ... done
$ docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------------------------------------
foo_grafana_1 /run.sh Up 0.0.0.0:3000->3000/tcp
foo_influxdb_1 /entrypoint.sh influxd Up 0.0.0.0:8083->8083/tcp, 0.0.0.0:8086->8086/tcp
foo_rust_1 sh -c tail -f /dev/null Up
Rust のソースを書く
./mount/rust/src/main.rs, ./mount/rust/Cargo.toml のファイルを作成します。
Cargo.toml ファイルは、 nodeで言うところの package.json と似たようなものです。
fn main() {
println!("Hello World!");
}
[package]
name = "freetel_usage"
version = "0.0.1"
authors = [ "tady <a.dat.jp@gmail.com>" ]
docker-compose exec rust cargo run
これで、 rust コンテナの中で cargo が実行され、Rustの実行まで行われます。
Rust で HTTP リクエストを行う
今回は、 nodeでもお世話になったことがある HTTP リクエストライブラリと同名の reqwest を利用します。
これは hyper をクライアント機能のみにしたラッパーです。
非同期処理などを気にせず使えるようにしたもので、今回はこれで十分です。
サンプルのコードは以下のようになります。
// GET
let client = reqwest::Client::new().unwrap();
let mut resp = client.get("http://example.com/").unwrap()
.header(...) // hedderの付与
.send().unwrap();
let mut content = String::new(); // レスポンスの入れ物
resp.read_to_string(&mut content).unwrap();
// POST
let client = reqwest::Client::builder().unwrap()
.redirect(...) // カスタムリダイレクトポリシーの設定
.build().unwrap();
let params = [
("key", "value")
];
// HTTP Post リクエスト実行
let resp = client.post(LOGIN_FORM_URL).unwrap()
.header(...) // hedderの付与
.form(¶ms).unwrap() // formデータの付与
.send().unwrap();
resp.status() // スレータスの取得
resp.headers() // レスポンスヘッダの取得
今回は、 reqwestのサンプルコードの機能に加え、以下の機能を利用します:
-
- カスタムリダイレクトポリシーの設定
ログインの Post リクエストのレスポンスヘッダの Set-Cookie を取得するため
リクエストにヘッダ(Cookie)を付与
ログイン後ページにアクセスするため
カスタムリダイレクトポリシーの設定
reqwest のデフォルトでは、リダイレクトをフォローするようになっています。
https://docs.rs/crate/reqwest/0.7.2/source/src/redirect.rs
今回は、ログイン Post リクエストのレスポンスに含まれる Set-Cookie ヘッダが欲しいため、自動的にフォローしないようにします。
そのために、 HTTP クライアントのビルダーに redirest() というメソッドがあるため、これを利用します。
// カスタムリダイレクトポリシー
// ログインリクエスト後に別のページに遷移するのを防ぐため `stop()` する
let custom = RedirectPolicy::custom(|attempt| {
// attempt.url() //=> リダイレクトしようとしている次のURL
// attempt.previous() //=> リダイレクトしてきた過去のURLの配列
attempt.stop()
});
let client = reqwest::Client::builder().unwrap()
.redirect(custom)
.build().unwrap();
attempt には、「リダイレクトしようとしている次のURL」「リダイレクトしてきた過去のURLの配列」などの情報が含まれています。
デフォルトのリダイレクトポリシーでは、「リダイレクトループの検知」「10回以上のリダイレクトの抑制」などの機能が含まれています。
今回のコードのようにカスタムリダイレクトポリシーを作る際には、同様の検知・抑制の仕組みも自前で実装が必要なことが多いので注意しましょう。
Set-Cookieヘッダの取得
レスポンスヘッダーは resp.headers().get で取得できますが、ヘッダーの型を渡すことで、目的のヘッダを一発で取得することが出来ます。
if let Some(set_cookies) = resp.headers().get::<header::SetCookie>() {
let mut set_cookie_value = String::new();
for set_cookie in &set_cookies.0 {
let c = Cookie::parse(set_cookie.clone()).expect("Failed to parse cookie.");
let (name, value) = c.name_value();
if name == SESSION_COOKIE_NAME {
set_cookie_value = value.to_string();
}
}
if set_cookie_value != "" {
return set_cookie_value;
}
panic!("Set-Cookie '{}' does not exist!, Set-Cookies: {:?}", SESSION_COOKIE_NAME, set_cookies);
} else {
panic!("Set-Cookie '{}' does not exist!", SESSION_COOKIE_NAME);
}
なぜか、FREETELのログインページは、 Set-Cookie を2つ返してくるので、有効だと思われる2つめの値を利用しています。
環境変数の利用
FREETEL のマイページログインには、メールアドレス、パスワード、電話番号の3つが必要です。
これらは環境変数経由で渡すようにしましょう。
$ docker-compose exec rust env FREETEL_EMAIL=<メールアドレス> FREETEL_PASSWORD=<パスワード> FREETEL_TEL=<ハイフン無し電話番号> cargo run
環境変数の取得は以下のようなコードになります:
let email = env::var("FREETEL_EMAIL").expect("env 'FREETEL_EMAIL' not found");message...
HTML のパース
HTML のパースには、 select というパーサと、正規表現クレートの regexを利用しています。
use select::document::Document;
use select::predicate::{Predicate, Attr, Class};
use regex::Regex;
let re_usage = Regex::new(r"([\d\.]+)GB").unwrap();
let mut current_usage: f32 = 0.0;
let mut usage_limit: f32 = 0.0;
let document = Document::from(html);
for node in document.find(Class("sim-usage").descendant((Attr("style", "font-size: x-large;")))).take(1) {
let text = node.text();
let caps = re_usage.captures(&text).unwrap();
current_usage = caps.get(1).unwrap().as_str().parse::<f32>().unwrap();
}
for node in document.find(Class("sim-usage").descendant((Attr("style", "font-size: smaller;")))).take(1) {
let text = node.text();
let caps = re_usage.captures(&text).unwrap();
usage_limit = caps.get(1).unwrap().as_str().parse::<f32>().unwrap();
}
(current_usage, usage_limit)
このあたりのコードは Rubyと比べても難しくはないですね。
InfluxDB へのデータ投入
InfluxDB は curl で言うと以下のようなリクエストを受け付けます。
$ curl -i -XPOST 'http://INFLUXDB_URL' --data 'freetel_usage value=0.64 1503063534888000000'
InfluxDB 用のクレートもあるようですが、今回は reqwest をせっかく導入しているので、これで済ませます。
let timespec = time::get_time();
let current_time_nano = [timespec.sec.to_string(), format!("{:09}", timespec.nsec.to_string())].join("");
let data = [
format!("freetel_usage value={} {}", current_usage, current_time_nano),
format!("freetel_limit value={} {}", usage_limit, current_time_nano)
].join("\n");
let client = reqwest::Client::new().unwrap();
let resp = client.post(INFLUXDB_URL).unwrap()
.body(data.clone())
.send().unwrap();
println!("resp: {:?}", resp);
if !resp.status().is_success() {
panic!("influxdb request failed! {}, {:?}, {:?}", INFLUXDB_URL, resp.status(), data);
}
単純に Post するだけですね。
コード
今回のコードは tadyjp/rust-freetel-usage | Github に置いてあります。
Grafana
このコードを書いていいるうちに、10GBの上限に達してしまった愚かな図はこちらです:
オチ
今の生活のネットインフラは Softbank:FREETEL = 9:1 なので、本当は マイソフトバンクのギガ数を取得したかった。
しかし、マイソフトバンクのログインの仕組みは Capy認証のため、スクレイピングのみで達成するのは困難な上に、Yahoo!ログイン経由でも機械からのアクセスだと文字認証が入るため断念。
次にキャリアを変える時は、ギガ数をプログラムから簡単に扱えるところにします。