[Rust] ureq - reqwest の軽量な代替クレート

# はじめに

[reqwest](https://crates.io/crates/reqwest) はよくできたクレートなんですが, ちょっと気軽にスクレイピングしたいみたいな用途では初回ビルドに時間がかかりすぎるしディスクも食うのがちょっと嫌です, 特に非力な Surface 使っているときには... 別に blocking API で十分ですよね. なので代わりになるようなクレートはないかなと思ったら, そのものズバリなスレッドが先月 (2019-10-15) フォーラムに立っていました.

- [Lightweight alternative for `reqwest`? - Rust Users Forum](https://users.rust-lang.org/t/lightweight-alternative-for-reqwest/33601)

このスレッドで挙がっていたのは [isahc](https://crates.io/crates/isahc), [surf](https://crates.io/crates/surf), [ureq](https://crates.io/crates/ureq), [attohttpc](https://crates.io/crates/attohttpc) で, 前 2 つは async しているため今の目的には適いません. attohttpc も悪くなさそうですが, 本記事では `ureq = "0.11.2"` を試してみます.

# ureq について

[ureq](https://crates.io/crates/ureq) はミニマルな依存性を持つことを目指して開発中の HTTP/HTTPS リクエストクレートです. 依存ツリーが肥大化することを嫌って現状では async 対応していません.

## フィーチャーフラグ

ureq には以下のフィーチャーフラグがあります.

- tls (デフォルト)
- json
- charset


## 依存ツリー

本当に依存性がミニマルか確認しましょう. ureq のみに依存するプロジェクトを作成して `cargo-tree` します.

```shell
$ cargo tree
hello-req v0.1.0 (/home/osanshouo/misc/req)
└── ureq v0.11.2
    ├── base64 v0.10.1
    │   └── byteorder v1.3.2
    ├── chunked_transfer v1.0.0
    ├── cookie v0.12.0
    │   ├── time v0.1.42
    │   │   └── libc v0.2.65
    │   │   [dev-dependencies]
    │   │   └── winapi v0.3.8
    │   └── url v1.7.2
    │       ├── idna v0.1.5
    │       │   ├── matches v0.1.8
    │       │   ├── unicode-bidi v0.3.4
    │       │   │   └── matches v0.1.8 (*)
    │       │   └── unicode-normalization v0.1.9
    │       │       └── smallvec v0.6.13
    │       │           └── maybe-uninit v2.0.0
    │       ├── matches v0.1.8 (*)
    │       └── percent-encoding v1.0.1
    ├── lazy_static v1.4.0
    ├── qstring v0.7.0
    │   └── percent-encoding v2.1.0
    ├── rustls v0.16.0
    │   ├── base64 v0.10.1 (*)
    │   ├── log v0.4.8
    │   │   └── cfg-if v0.1.10
    │   ├── ring v0.16.9
    │   │   ├── lazy_static v1.4.0 (*)
    │   │   ├── libc v0.2.65 (*)
    │   │   ├── spin v0.5.2
    │   │   └── untrusted v0.7.0
    │   │   [build-dependencies]
    │   │   └── cc v1.0.47
    │   │   [dev-dependencies]
    │   │   └── libc v0.2.65 (*)
    │   ├── sct v0.6.0
    │   │   ├── ring v0.16.9 (*)
    │   │   └── untrusted v0.7.0 (*)
    │   └── webpki v0.21.0
    │       ├── ring v0.16.9 (*)
    │       └── untrusted v0.7.0 (*)
    │   [dev-dependencies]
    │   └── log v0.4.8 (*)
    ├── url v2.1.0
    │   ├── idna v0.2.0
    │   │   ├── matches v0.1.8 (*)
    │   │   ├── unicode-bidi v0.3.4 (*)
    │   │   └── unicode-normalization v0.1.9 (*)
    │   ├── matches v0.1.8 (*)
    │   └── percent-encoding v2.1.0 (*)
    ├── webpki v0.21.0 (*)
    └── webpki-roots v0.18.0
        └── webpki v0.21.0 (*)
```

確かに重そうな依存性はありません. HTTPS 通信が不要な場合, デフォルトで有効なフラグ `tls` をオフにすることで rustls への依存性も解消できます. 

とはいえ多少のディスクは必要です. `rustls` なしだと

```shell
$  du --max-depth=1 -h ./target/
155M    ./target/debug
116M    ./target/release
```

で, rustls ありだと 100 MB ほど追加されます. `reqwest` に比べるとずっと軽いのも確かですが. ちなみにビルド時間はノート PC でデバッグビルドに 10.2 秒程度でした (依存クレートのダウンロード時間を除く).

# 使い方

## GET リクエスト

[GET リクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/GET) を投げるのはとても簡単です. [ureq::get](https://docs.rs/ureq/0.11.2/ureq/fn.get.html) 関数に URL を入れて [ureq::Request](https://docs.rs/ureq/0.11.2/ureq/struct.Request.html) を作成し, `.call()` するだけです. 例として [Gaia DR1](https://ja.wikipedia.org/wiki/%E3%82%AC%E3%82%A4%E3%82%A2%E8%A8%88%E7%94%BB) による恒星の位置・年周視差データを取得してみます.

```rust
let url = "http://cdn.gea.esac.esa.int/Gaia/gdr1/gaia_source/csv/GaiaSource_000-000-000.csv.gz";
let resp = ureq::get(url).call();
```

そうすると [ureq::Response](https://docs.rs/ureq/0.11.2/ureq/struct.Response.html) が得られるので, ステータスエラーでないことを確認して中身をバッファーに取得します. テキストデータなら直接 `.into_string()` できますが, いまは圧縮データなので `Vec<u8>` として扱う必要があります. `.into_reader()` によりリーダーを取得し, `std::io::Read` トレイトを通じてバイナリをバッファーに書き込みます.

```rust
use std::io::Read;

if resp.ok() {
    // バッファーの確保
    let mut buf = {
         let len = resp.header("Content-Length").unwrap()
            .parse::<usize>().unwrap();

        vec![ 0; len ]
    };

    // レスポンスの内容をバッファーに書き込む
    let mut resp = resp.into_reader();
    resp.read_exact(&mut buf).unwrap();
    
    /* snip */
} else {
    println!("Status error: {:?}", resp.status_line());
}
```

(なぜか `read_to_end` だとうまくいかなかったので `Content-Length` の長さのバッファーを用意して `read_exact` しています.) 後はファイル出力するなり解凍して解析するなりお好みでどうぞ.

## ヘッダーの指定

HTTP リクエストのヘッダーを設定する必要がある場合. ureq::Request の [set](https://docs.rs/ureq/0.11.2/ureq/struct.Request.html#method.set) メソッドを使います. 例えばこんな感じ.

```rust
let resp = ureq::get(url)
    .set("Accept", "application/gzip")
    .call();
```

## 他のメソッド

ureq は [POST リクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/POST) や [PUT リクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/PUT) といった他の HTTP メソッドにも対応しています. 使い方は GET とほぼ同じで, 主な違いは送信する中身を `.send`, `.send_bytes` または `.send_json` メソッドで設定して送る (従って `.call()` 不要) ことだけです. 詳細は[ドキュメント](https://docs.rs/ureq/0.11.2/ureq/)を参照してください.

さらに [ureq::Agent](https://docs.rs/ureq/0.11.2/ureq/struct.Agent.html) を用いればクッキーなどのセッションの状態をリクエスト間で保持できるようです.