LoginSignup
2
4

More than 5 years have passed since last update.

[翻訳]curl vignette: 「curl パッケージ:libcurlへのモダンなRインターフェイス」

Posted at

この文書は,Jeroen Ooms 他によるRパッケージ curl (version 2.1) のビネット "The curl package: a modern R interface to libcurl" の日本語訳です.

License: MIT


curl パッケージは,Cライブラリ libcurl へのRバインディングを提供します.このパッケージは,メモリへのデータ読込やディスクへのダウンロード,Rのコネクションインターフェイスを用いたストリーミング処理をサポートしています.よりユーザにやさしいHTTPクライアントが必要であれば,curlをもとにしてHTTP用のツールや処理が追加された httr パッケージを見てみてください.

リクエストのインターフェイス

curl パッケージはURLからデータを読み込むためのいくつかのインターフェイスを実装しています.

  • curl_fetch_memory() はレスポンスをメモリに保存します
  • curl_download()curl_fetch_disk() はレスポンスをディスクに書き込みます
  • curl()curl_fetch_stream() はレスポンスのデータをストリーミング処理します
  • curl_fetch_multi() はレスポンスをコールバック関数によって処理します(高度な機能)

どのインターフェイスも同じHTTPリクエストを行いますが,レスポンスのデータをどう処理するかだけが異なります.

メモリに読み込む場合

curl_fetch_memory() 関数はブロッキングインターフェイスであり,リクエストが完了してサーバのレスポンス内容すべて(データ,ヘッダ,ステータス,タイミング)を含むリストが返ってくるまで待機します.

req <- curl_fetch_memory("https://httpbin.org/get")
str(req)
List of 6
 $ url        : chr "https://httpbin.org/get"
 $ status_code: int 200
 $ headers    : raw [1:220] 48 54 54 50 ...
 $ modified   : POSIXct[1:1], format: NA
 $ times      : Named num [1:6] 0 0 0.265 1.513 1.778 ...
  ..- attr(*, "names")= chr [1:6] "redirect" "namelookup" "connect" "pretransfer" ...
 $ content    : raw [1:232] 7b 0a 20 20 ...
parse_headers(req$headers)
[1] "HTTP/1.1 200 OK"                       
[2] "Server: nginx"                         
[3] "Date: Sat, 15 Oct 2016 13:21:29 GMT"   
[4] "Content-Type: application/json"        
[5] "Content-Length: 232"                   
[6] "Connection: keep-alive"                
[7] "Access-Control-Allow-Origin: *"        
[8] "Access-Control-Allow-Credentials: true"
cat(rawToChar(req$content))
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "r/curl/jeroen"
  }, 
  "origin": "106.161.101.127", 
  "url": "https://httpbin.org/get"
}

curl_fetch_memory() インターフェイスは,APIクライアントを構築するための最も簡単で強力なインターフェイスです.ですがメモリ内ですべての処理を行うので,非常に大きなファイルをダウンロードするのには向いていません.もしデータが100GBあると予想しているのであれば,きっとどれか他のインターフェイスが必要になるでしょう.

ディスクにダウンロードする場合

ふたつめの方法は curl_download() です.これは base R の download.file() 関数を簡単に代替できるように設計されています.この関数はレスポンスを直接ディスクに書き込むので,(大きな)ファイルをダウンロードするのに便利です.

tmp <- tempfile()
curl_download("https://httpbin.org/get", tmp)
cat(readLines(tmp), sep = "\n")
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "r/curl/jeroen"
  }, 
  "origin": "106.161.101.127", 
  "url": "https://httpbin.org/get"
}

データをストリーミングする場合

最も柔軟なインターフェイスは curl() 関数です.これは base R のurl() を簡単に代替できるように設計されています.この関数はいわゆるコネクションオブジェクトを作成し,レスポンスを漸進的に(非同期に)読み込むことを可能にします.

con <- curl("https://httpbin.org/get")
open(con)

# 3行取得
out <- readLines(con, n = 3)
cat(out, sep = "\n")
{
  "args": {}, 
  "headers": {
# さらに3行取得
out <- readLines(con, n = 3)
cat(out, sep = "\n")
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
# 残りの行を取得
out <- readLines(con)
close(con)
cat(out, sep = "\n")
    "User-Agent": "r/curl/jeroen"
  }, 
  "origin": "106.161.101.127", 
  "url": "https://httpbin.org/get"
}

この例では開いたコネクションに対して readLines() を使い,一度に n 行ずつ読み込む方法を示しています.同様に,ストリームから一度にデータを n バイトずつ読み込んでパースするには readBin() を使います.

ノンブロッキングリクエスト

curl 2.0 から,curl パッケージは同時に多数のリクエストを実行するノンブロッキングインターフェイスを提供しています.curl_fetch_multi()はリクエストをプールに追加したらすぐに返ってきて,実際にはリクエストを実行しません.

pool <- new_pool()
cb <- function(req){cat("done:", req$url, ": HTTP:", req$status, "\n")}
curl_fetch_multi('https://www.google.com', done = cb, pool = pool)
curl_fetch_multi('https://cloud.r-project.org', done = cb, pool = pool)
curl_fetch_multi('https://httpbin.org/blabla', done = cb, pool = pool)

multi_run() を呼ぶと,登録されたすべてのリクエストが並行的に実行されます.リクエストが完了するとコールバック関数が呼ばれます.

# これで実際にリクエストが実行される
out <- multi_run(pool = pool)
done: https://cloud.r-project.org/ : HTTP: 200 
done: https://www.google.co.jp/?gfe_rd=cr&ei=XC0CWKXAL4SL8QfUgJ6ICA : HTTP: 200 
done: https://httpbin.org/blabla : HTTP: 404 
print(out)
$success
[1] 3

$error
[1] 0

$pending
[1] 0

このシステムによって多数の並行的なノンブロッキングリクエストを実行することができますが,かなり複雑ですし,ハンドラ関数を注意深く設計する必要があります.

例外処理

HTTPリクエストには2種類のエラーがあり得ます.

  1. 接続失敗:ネットワークがダウンしている,ホストが見つからない,SSL証明書が不正,等

  2. HTTPステータスが非成功:401(アクセス拒否),404(リソースが見つからない),503(サーバの問題),等

ひとつめの種類のエラー(接続失敗)は,各インターフェイスにおいて必ずRのエラーを発生させます.しかし,リクエストは成功したもののサーバが非成功のHTTPステータスコードを返したという場合には, curl()curl_download() だけがエラーを発生させます.これについてもう少し詳しく見てみましょう.

自動的にエラーを発生させる場合

curl() 関数と curl_download() 関数が最も安全に使えるものです.というのもこれらの関数は,リクエストは完了したがサーバが非成功の(400やそれより大きい)HTTPステータスを返した場合にエラーを発生させるからです.これは base R の url() 関数と download.file() 関数のふるまいを真似たものです.これによって,以下のようなコードを安全に書くことができます.

# これでOK
curl_download('https://cran.r-project.org/CRAN_mirrors.csv', 'mirrors.csv')
mirros <- read.csv('mirrors.csv')
unlink('mirrors.csv')

もしHTTPリクエストが失敗した場合,Rは実行を停止します.

# おっと!URLを書き間違えた!
curl_download('https://cran.r-project.org/CRAN_mirrorZ.csv', 'mirrors.csv')
Error in curl_download("https://cran.r-project.org/CRAN_mirrorZ.csv", : HTTP error 404.
con <- curl('https://cran.r-project.org/CRAN_mirrorZ.csv')
open(con)
Error in open.connection(con): HTTP error 404.

手動で確認する場合

curl_fetch_*() 系関数のいずれかを使う場合,これらの関数は,リクエストは完了したが200以外のステータスコードが返ってきた場合にエラーを発生させない,ということを理解しておくのが重要です.curl_fetch_memory()curl_fetch_disk() を使うときには,自分でそのようなアプリケーションのロジックを実装して,レスポンスが成功かどうか確認する必要があります.

req <- curl_fetch_memory('https://cran.r-project.org/CRAN_mirrors.csv')
print(req$status_code)
[1] 200

ディスクにダウンロードする場合も同様です.ステータスをチェックしないと,エラーページをダウンロードしてしまっているかもしれません!

# おっと書き間違えた!
req <- curl_fetch_disk('https://cran.r-project.org/CRAN_mirrorZ.csv', 'mirrors.csv')
print(req$status_code)
[1] 404
# 思ってたCSVファイルと違うぞ!
head(readLines('mirrors.csv'))
[1] "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"                               
[2] "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\""               
[3] "  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"                 
[4] "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">"
[5] "<head>"                                                                   
[6] "<title>Object not found!</title>"                                         
unlink('mirrors.csv')

もし本当に curl_fetch_*() 系関数が自動的にエラーを発生させるようにしたいのであれば,リクエストハンドルの FAILONERROR オプションを TRUE に設定するべきです.

h <- new_handle(failonerror = TRUE)
curl_fetch_memory('https://cran.r-project.org/CRAN_mirrorZ.csv', handle = h)
Error in curl_fetch_memory("https://cran.r-project.org/CRAN_mirrorZ.csv", : HTTP response code said error

リクエストのカスタマイズ

デフォルトの設定では, libcurl は HTTP URL へのリクエストを発行するのに HTTP GET を使います.カスタマイズしたリクエストを送るには,ダウンロードインターフェイスに渡すための curl ハンドルオブジェクトの作成と設定をはじめに行う必要があります.

ハンドルの設定

new_handle()を使うと新しいハンドルが作成されます.ハンドルオブジェクトを作成したら, libcurl のオプションと HTTP リクエストのヘッダを設定することができます.

h <- new_handle()
handle_setopt(h, copypostfields = "moo=moomooo");
handle_setheaders(h,
  "Content-Type" = "text/moo",
  "Cache-Control" = "no-cache",
  "User-Agent" = "A cow"
)

自分が使っているバージョンの libcurl でサポートされているオプションのリストを見るには,curl_options() 関数を使ってください.各オプションが何をするものなのかは libcurl のドキュメントで解説されています.オプション名の大文字と小文字は区別されません.

設定が終わったら,ハンドルはどのダウンロードインターフェイスでリクエストを実行するのにも使えます.

req <- curl_fetch_memory("http://httpbin.org/post", handle = h)
cat(rawToChar(req$content))
{
  "args": {}, 
  "data": "moo=moomooo", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "no-cache", 
    "Content-Length": "11", 
    "Content-Type": "text/moo", 
    "Host": "httpbin.org", 
    "User-Agent": "A cow"
  }, 
  "json": null, 
  "origin": "106.161.101.127", 
  "url": "http://httpbin.org/post"
}

代わりに curl() を使ってコネクションインターフェイスでデータを読むこともできます.

con <- curl("http://httpbin.org/post", handle = h)
cat(readLines(con), sep = "\n")
{
  "args": {}, 
  "data": "moo=moomooo", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "no-cache", 
    "Content-Length": "11", 
    "Content-Type": "text/moo", 
    "Host": "httpbin.org", 
    "User-Agent": "A cow"
  }, 
  "json": null, 
  "origin": "106.161.101.127", 
  "url": "http://httpbin.org/post"
}

また, curl_download() を使ってレスポンスをディスクに書き込むこともできます.

tmp <- tempfile()
curl_download("http://httpbin.org/post", destfile = tmp, handle = h)
cat(readLines(tmp), sep = "\n")
{
  "args": {}, 
  "data": "moo=moomooo", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "no-cache", 
    "Content-Length": "11", 
    "Content-Type": "text/moo", 
    "Host": "httpbin.org", 
    "User-Agent": "A cow"
  }, 
  "json": null, 
  "origin": "106.161.101.127", 
  "url": "http://httpbin.org/post"
}

あるいは,同じリクエストをプールして実行することもできます.

curl_fetch_multi("http://httpbin.org/post", handle = h, done = function(res){
  cat("Request complete! Response content:\n")
  cat(rawToChar(res$content))
})

# リクエストの実行
out <- multi_run()
Request complete! Response content:
{
  "args": {}, 
  "data": "moo=moomooo", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "no-cache", 
    "Content-Length": "11", 
    "Content-Type": "text/moo", 
    "Host": "httpbin.org", 
    "User-Agent": "A cow"
  }, 
  "json": null, 
  "origin": "106.161.101.127", 
  "url": "http://httpbin.org/post"
}

クッキーの読込

curl ハンドルはサーバが設定したクッキーを自動的に記録します.handle_cookies()を使って,現在ハンドルに記録されているクッキーのリストをいつでも見ることができます.

# 新しいハンドル
h <- new_handle()

# サーバにクッキーを設定してくれるよう依頼
req <- curl_fetch_memory("http://httpbin.org/cookies/set?foo=123&bar=ftw", handle = h)
req <- curl_fetch_memory("http://httpbin.org/cookies/set?baz=moooo", handle = h)
handle_cookies(h)
       domain  flag path secure expiration name value
1 httpbin.org FALSE    /  FALSE       <NA>  foo   123
2 httpbin.org FALSE    /  FALSE       <NA>  bar   ftw
3 httpbin.org FALSE    /  FALSE       <NA>  baz moooo
# クッキーの設定を解除
req <- curl_fetch_memory("http://httpbin.org/cookies/delete?foo", handle = h)
handle_cookies(h)
       domain  flag path secure          expiration name value
1 httpbin.org FALSE    /  FALSE 2016-10-15 22:21:50  foo  <NA>
2 httpbin.org FALSE    /  FALSE                <NA>  bar   ftw
3 httpbin.org FALSE    /  FALSE                <NA>  baz moooo

handle_cookies() 関数はnetscapeのcookieファイル形式で指定されている7項目を列に持つデータフレームを返します.

ハンドルの再利用

既にみたように, curl ではひとつのハンドルを複数のリクエストに対して使いまわすことが可能です.ですが,そうするのがいつでもよい考えというわけでははありません.新たなハンドルオブジェクトを作成して設定するためのパフォーマンス上のオーバーヘッドは,ふつうは無視できる程度です.複数のリクエストを単一あるいは複数のサーバに対して発行する最も安全な方法は,それぞれのリクエストで別々のハンドルを使うことです

req1 <- curl_fetch_memory("https://httpbin.org/get", handle = new_handle())
req2 <- curl_fetch_memory("http://www.r-project.org", handle = new_handle())

複数のリクエストに対してハンドルを再利用したくなる理由は2つあります.ひとつめの理由は,ハンドルがサーバによって設定されるクッキーを自動的に記録してくれることです.これはホストがセッションクッキーの使用を要求してくる場合に有用かもしません.

もうひとつの理由は, HTTP Keep-Alive の利用です. curl は,各ハンドル内の開いているHTTP接続のプールを自動的に維持してくれます.ひとつのハンドルを使って同一のサーバに多数のリクエストを発行する場合, curl は可能なら既存の接続を使います.これによって接続のオーバヘッドが少し削減されます.ただし,よいネットワークであればそれほど大きな効果はないでしょう.

h <- new_handle()
system.time(curl_fetch_memory("https://api.github.com/users/ropensci", handle = h))
   user  system elapsed 
   0.03    0.00    1.19 
system.time(curl_fetch_memory("https://api.github.com/users/rstudio", handle = h))
   user  system elapsed 
   0.00    0.00    0.33 

ハンドルの再利用に反対する論拠は, curl が各リクエストの後でハンドルを片付けないということです.明示的にリセットや上書きを行わない限り,オプションや内部で持っている項目のすべてが将来のリクエストでも残ってしまいます.

handle_reset(h)

handle_reset() 関数は curl のすべてのオプションとリクエストヘッダをデフォルトの値にリセットします.クッキーは削除されませんし,接続は keep-alive なままです.したがって,後続のリクエストでハンドルを再利用したい場合には,リクエストの実行後に handle_reset() を呼ぶのがよいやり方です.とはいえ可能であれば新しいハンドルを作成するほうが,古いものを使いまわすより常に安全です.

フォームの送信

handle_setform() 関数は multipart/form-data の HTTP POST リクエスト(つまりフォームの送信)を行うために使います.値には文字列,rawベクトル(バイナリデータの場合),ファイルのいずれかが指定できます.

# マルチパートの送信
h <- new_handle()
handle_setform(h,
  foo = "blabla",
  bar = charToRaw("boeboe"),
  description = form_file(system.file("DESCRIPTION")),
  logo = form_file(file.path(Sys.getenv("R_DOC_DIR"), "html/logo.jpg"), "image/jpeg")
)
req <- curl_fetch_memory("http://httpbin.org/post", handle = h)

form_file() 関数はフォーム送信でファイルをアップロードするために使います.この関数には2つの引数があります.ファイルパスと,オプションとして指定できる content-type の値です. content-type が何も指定されなかった場合, curl は content-type をファイルの拡張子から推測します.

パイプの使用

handle_xxx() 系関数はどれもハンドルオブジェクトを返すので,人気のパイプ演算子を使って関数呼び出しを連鎖させることができます.

library(magrittr)

new_handle() %>% 
  handle_setopt(copypostfields = "moo=moomooo") %>% 
  handle_setheaders("Content-Type" = "text/moo", "Cache-Control" = "no-cache", "User-Agent" = "A cow") %>%
  curl_fetch_memory(url = "http://httpbin.org/post") %$% content %>% rawToChar %>% cat
{
  "args": {}, 
  "data": "moo=moomooo", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "no-cache", 
    "Content-Length": "11", 
    "Content-Type": "text/moo", 
    "Host": "httpbin.org", 
    "User-Agent": "A cow"
  }, 
  "json": null, 
  "origin": "106.161.101.127", 
  "url": "http://httpbin.org/post"
}

訳注:翻訳の際,コードは以下の環境で実行した.

devtools::session_info()
Session info --------------------------------------------------------------
 setting  value                       
 version  R version 3.3.1 (2016-06-21)
 system   x86_64, mingw32             
 ui       RTerm                       
 language (EN)                        
 collate  Japanese_Japan.932          
 tz       Asia/Tokyo                  
 date     2016-10-15                  
Packages ------------------------------------------------------------------
 package       * version  date       source                            
 assertthat      0.1      2013-12-06 CRAN (R 3.3.1)                    
 curl          * 2.1      2016-09-22 CRAN (R 3.3.1)                    
 devtools        1.12.0   2016-06-24 CRAN (R 3.3.1)                    
 digest          0.6.10   2016-08-02 CRAN (R 3.3.1)                    
 evaluate        0.9      2016-04-29 CRAN (R 3.3.1)                    
 formatR         1.4      2016-05-09 CRAN (R 3.3.1)                    
 htmltools       0.3.5    2016-03-21 CRAN (R 3.3.1)                    
 knitr           1.14     2016-08-13 CRAN (R 3.3.1)                    
 magrittr      * 1.5      2014-11-22 CRAN (R 3.3.1)                    
 memoise         1.0.0    2016-01-29 CRAN (R 3.3.1)                    
 Rcpp            0.12.6   2016-07-19 CRAN (R 3.3.1)                    
 RevoUtils       10.0.1   2016-08-24 local                             
 RevoUtilsMath * 8.0.3    2016-04-13 local                             
 rmarkdown       1.0.9016 2016-10-13 Github (rstudio/rmarkdown@2158b9d)
 stringi         1.1.1    2016-05-27 CRAN (R 3.3.0)                    
 stringr         1.1.0    2016-08-19 CRAN (R 3.3.1)                    
 tibble          1.2      2016-08-26 CRAN (R 3.3.1)                    
 withr           1.0.2    2016-06-20 CRAN (R 3.3.1)                    
 yaml            2.1.13   2014-06-12 CRAN (R 3.3.1)                    
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4