Gepuro Task Viewsがあるでしょ、というツッコミが入りそうですが、意外とAPIでできたりしないかな?と思ったのでやってみた時のメモです。
結果とかコードはここに散らばっています(未整理):
GitHub検索APIの種類
GitHubの検索は、今のところ以下の種類があります。
- レポジトリ
- コミット
- コード
- issue
- ユーザ (参考:https://developer.github.com/v3/search/)
この中で使えそうなものを考えてみましょう。
レポジトリ検索
レポジトリ検索ではlanguage:
という記法で、レポジトリを言語で絞り込むことができます。
ということで、language:r
でやれば勝利なのでは?と思うかもしれませんが、GitHubの言語判定は必ずしもあてにならなくて、例えばRcpp関連とかはC++のファイルがほとんどなので引っかかりません。
例えば、以下のようなクエリを投げても、RcppCore/RcppEigenは引っかかりません。
language:r rcppeigen
ユーザ検索
ユーザ検索でも同じく言語を絞り込めますが、上と同じ理由であてになりません。例えば以下のクエリで私は引っかかりません。
language:r yutannihilation
なぜかJavaScriptの人ということになってるんですが、たぶんRmarkdownから生成されるウェブサイト系にJavaScriptがたくさん含まれているからなんだと思います…
コード検索
残されたのはコード検索です。一つ前の記事の知識から、Rのパッケージには最低でもDESCRIPTION
とNAMESPACE
があることがわかっています。
コード検索ではfilename:
という記法でファイル名を検索できます。これに指定すればいいわけですが、問題はこの検索が完全一致ではないのでnamespace.c
とかnamespace.php
みたいなやつが大量に引っかかることです。
コード検索ではまた、拡張子で絞り込むためのextension:
が使えます。ここでもしNAMESPACE
がNAMESPACE.R
とかいう名前だったらextension:r
でかなり絞り込めたんですが、拡張子はないので、地道に違うやつを取り除いていくしかありません。
-
を付けるとNOT検索になるので、とりあえず以下の拡張子のファイルを取り除きます。
extensions <- c( "c", "h", # C "pl", "pm", # Perl "html", "htm", # HTML "cpp", "hpp", # C++ "java", # Java "js", # JavaScript "3", "n", # Roff "ts", # TypeScript "xml", # XML "diff", "patch", # Diff "cmd", # Batchfile "rb", # Ruby "php", # PHP "cs", # C# "txt", # Text "json", # JSON "e", # Efieel "dart", # Dart "dot", # Graphviz "ll", # LLVM "lisp", # Lisp "py", # Python "rst", # reStructuredText "props", # Weka? "plist", # ? "scala", # Scala "cmake", # CMake "m" # Objective-C )
クエリを組み立てればこんな感じになるはずです。
extensions_query <- sprintf("-extension:%s", extensions) %>% paste(collapse = " ") extensions_query #> [1] "-extension:c -extension:h -extension:pl -extension:pm -extension:html -extension:htm ..."
GitHub検索APIの制限
ではこれでクエリを投げてみるか、という感じですが、ちょっと待ってください。まだだめです。GitHub検索APIのドキュメントをもうちょっと読んでみるとこんなことが書かれています。
Think of it the way you think of performing a search on Google. It’s designed to help you find the one result you’re looking for (or maybe the few results you’re looking for). Just like searching on Google, you sometimes want to see a few pages of search results so that you can find the item that best meets your needs. To satisfy that need, the GitHub Search API provides up to 1,000 results for each search.
ちょっと長めに引用しましたが、重要なのは最後の部分です。GitHub検索APIは先頭1000件までしか返してくれないのです。
GitHub上にあるRのパッケージは1000は優に超えるでしょう。このままでは検索できないので、もっとノイズを取り除いたうえで、検索結果を分割して2000件以下(昇順と降順に並び替えれるので先頭1000件と末尾1000件、あわせて2000件にはアクセスできる)になるようにする必要があります。
NAMESPACE
かDESCRIPTION
か
これは、何回か試しただけですが、DESCRIPTION
の方がノイズが多そうな感じでした。ということでNAMESPACE
に賭けてみることにします。
大量にレポジトリを持っているユーザを除外する
ミラー用ユーザ
以下のユーザは、CRAN、Bioconductor、RForgeのミラーです。
- cran(CRANのミラー)
- Bioconductor-mirror(Bioconductorのミラー)
- rforge(RForgeのミラー)
ミラーなのでこのユーザのレポジトリが本体ではありません。除外してしまいましょう。
-user:cran -user:Bioconductor-mirror -userrforge
レポジトリを大量に持っているユーザ
大量に持っているといっても、1000はいかないので、そういうユーザは、
user:user1
で絞り込めばすぐにレポジトリの一覧をとれます。ここでは、そういうのに引っかかってこないレポジトリだけに絞り込みましょう。
ということで、まずはユーザ検索でレポジトリを100個以上持っているユーザを調べます。レポジトリ数の絞り込みにはrepos:
という記法を使います。ここはある程度雑でもいいのでlanguage:r
を使います。
ghパッケージを使ってみます。
res <- gh::gh("/search/users", q = "language:r repos:>100", page = 1L, per_page = 100L)
結果は3つの要素を持つリストになっています。total_count
は検索結果の数、incomplete_results
は時間内に検索が終わったか(これがTRUE
の場合、結果がさらにある可能性がある)、items
は検索結果です。
names(res) #> [1] "total_count" "incomplete_results" "items" res$total_count #> [1] 229 res$incomplete_results #> [1] FALSE
ユーザはこんな感じです。
purrr::map_chr(res$items, "login") #> [1] "hadley" "jeroen" "kbroman" "hrbrmstr" "karthik" #> [6] "sckott" "leeper" "seankross" "olgabot" "cpsievert"
直感を働かせてさらにユーザを除外
検索してると、なんかこいつノイズっぽい、というやつがでてくるので、その辺は直感で除外しましょう。
ユーザ除外クエリを組み立てる。
こんな感じです。
users_query <- c("cran", "Bioconductor-mirror", "rforge", purrr::map_chr(res$items, "login")) %>% sprintf("-user:%s", .) %>% paste(collapse = " ") users_query #> [1] "-user:cran -user:Bioconductor-mirror -user:rforge -user:hadley ..."
キーワードで絞り込み
あと、ちょっとだけキーワードを加えて絞り込みます。
export
NAMESPACE
は空でもいいですが、とはいえexport
していないと関数をなにも使うことができません。ということで、これはたぶんある、はず(あってる?)。
NOT D1tr
なんかよくわからないんですけど、ESSの中に入っている練習用のパッケージ?かなにかみたいです。必要ないので除外。
NAMESPACE
が一番上のディレクトリにあるかでリクエストを分割
Rパッケージのためだけのレポジトリでは、NAMESPACE
は一番上の階層に置かれます。しかし、ものによっては更にディレクトリを掘ってその中に入れることもあります。
ということで、path:/
と-path:/
でリクエストを分けましょう。
packrat
注意点として、ディレクトリをさらに掘っている場合は、packrat
などのようにパッケージのバージョンを固定するために作られたディレクトリがそのままGitに入っていることがあります。これを取り除くために、-path:/
には-path:packrat
も指定しましょう。
キーワードを使い分けてリクエストを分割
あと、NAMESPACE
に入ってそうなキーワードとしては以下のものがあります。
exportPattern
S3method
import
この辺もexportPattern
とNOT exportPattern
とかを組み合わせてリクエストを分割していきましょう。
最終的にできたクエリ
こんな感じになりました。
filename:NAMESPACE fork:false export NOT D1tr path:/ exportPattern import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr path:/ exportPattern NOT import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern S3method {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern NOT S3method import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern NOT S3method NOT import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat exportPattern import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat exportPattern NOT import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern S3method {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern NOT S3method import {extensions_query} {users_query} filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern NOT S3method NOT import {extensions_query} {users_query}
コード検索
だいたいこんな感じのコードです(ちょっと端折って書いたので動くかは不明)。
do_search <- function(page, query, sort = "indexed") { message("requesting page ", page, " ...") res <- gh::gh("/search/code", q = query, sort = sort, page = page, per_page = 100) message(sprintf("total count: %d, incomplete: %s", res$total_count, res$incomplete_results)) repo <- purrr::map_chr(res$items, c("repository", "name")) owner <- purrr::map_chr(res$items, c("repository", "owner", "login")) filename <- purrr::map_chr(res$items, "name") path <- purrr::map_chr(res$items, "path") tibble::tibble(owner, repo, filename, path) } queries_tmpl <- c( "filename:NAMESPACE fork:false export NOT D1tr path:/ exportPattern import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr path:/ exportPattern NOT import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern S3method {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern NOT S3method import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr path:/ NOT exportPattern NOT S3method NOT import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat exportPattern import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat exportPattern NOT import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern S3method {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern NOT S3method import {extensions_query} {users_query}", "filename:NAMESPACE fork:false export NOT D1tr -path:/ -path:packrat NOT exportPattern NOT S3method NOT import {extensions_query} {users_query}" ) queries <- map_chr(queries_tmpl, glue::glue) for (i in seq_along(queries)) { csv_dir <- file.path(RESULT_NAMESPACE_DIR, sprintf("query%d", i)) dir.create(csv_dir, showWarnings = FALSE) page_namespace <- get_next_page(csv_dir) if (page_namespace >= 10) { message(sprintf("%s has already 1000 records. Skip.", csv_dir)) next } for (page in seq(page_namespace, 10)) { result <- do_search(page, query = queries[i]) write_csv(result, path = file.path(csv_dir, sprintf("page%d.csv", page))) # わりとたっぷりsleepしないとabuse rate limitにすぐひっかかる Sys.sleep(60) } }
これで2時間ほどかかります。お茶でも飲んで待ちましょう。
ユーザを指定してコード検索
さっき除外したユーザについても検索します。↑のusers_query
をuser:ユーザ名
に置き換えるだけです。
結果
さて、実際どれくらいの数を捕捉できたんでしょう。この結果をGepuro Task Viewと比べてみましょう。DescriptionMinerをcloneしてきて以下のコードを流せば同じ結果になるはずです。
library(tidyverse) # result以下に散らばっているCSVをすべて列挙 csvs <- list.files("results", pattern = ".*.csv", recursive = TRUE, full.names = TRUE) # 読み込み。たまに列が数値になるときがあったので.default = col_character()を指定 l <- map(csvs, read_csv, col_types = list(.default = col_character())) d <- bind_rows(l) # 変な名前のやつが混じってるのでNAMESPACEだけに絞る d %>% filter(filename == "NAMESPACE") %>% distinct(owner, repo) #> # A tibble: 8,876 x 2 #> owner repo #> <chr> <chr> #> 1 swihart libstableR #> 2 jeffeaton epp #> 3 spedygiorgio markovchain #> 4 RomanHornung survmediation #> 5 Spatial-R EnvExpInd #> 6 CreRecombinase FGEM #> 7 bplloyd CoreHF #> 8 SachaEpskamp bootnet #> 9 harrelfe Hmisc #> 10 simonpedrobolivar coopbreed5 #> # ... with 8,866 more rows
その数、8876。
一方、Gepuro Task Viewsは…
res <- httr::GET("http://rpkg.gepuro.net/download") j <- httr::content(res) # ユーザ名/レポジトリ名 が入っている要素 pkg_names_vec <- map_chr(j$pkg_list, "pkg_name") # / で分割してデータフレームに pkg_names_mat <- stringr::str_split_fixed(pkg_names_vec, "/", 2) colnames(pkg_names_mat) <- c("user", "repo") pkg_names <- as_tibble(pkg_names_mat) # Gepuro Task ViewsはcranもBioconductor-mirrorも入ってるので除外(rforgeはない) x <- stringr::str_subset(pkg_names_vec, "^(?!cran/|^Bioconductor-mirror/)") glimpse(x) #> chr [1:28022] "0xh3x/hellodublinr" "100sunflower100/MethylChiPAnno" "100sunflower100/git" "11010tianyi/latticist" ...
28022。ということで惨敗でした。。
感想
途中書いてませんでしたが、今回投げたクエリはすさまじく長くなるので、incomplete_results
がTRUE
になりっぱなしでした。なのでたぶん、時間内には検索できないやつがあるんだろうなあと。なかなか厳しいですね。