はじめに
自分の分野の有名な学会が開かれました.Accepted Paperのページがあります.とりあえずタイトルだけ見て,面白そうなやつ/自分の立場を脅かしようなやつをチェックしなきゃ...
みたいなことになる人もいるかもしれません.(まぁ普段からアンテナを貼っておけばこんなことにはならないのかもしれませんが,僕みたいにSNSが嫌いな人はこうなります)
とりあえず,タイトルで検索してarXivでダウンロードしてくるわけですが,ポチポチするのは面倒なので途中でやめてしまいます.そこでAccepted Paperのリストからほしい論文のタイトルだけコピペ(全部ブラウザから調べるよりは多少まし)して,タイトルリストからダウンロードしてくれるアプリがあったらいいな,ということで作りました.
僕のモチベーションはあくまでGoの勉強です.
何を作ったの?
dpxgo
というクロスプラットホーム CLIアプリ (コマンド) を作りました.
リストにある論文のアブストラクトとpdfをarXiv APIをつかってダウンロードします.
arXivにない場合は自分で調べてください.
ちなみにdpxgo
はDownload Papers from arXiv with GO app.の略です.
特徴
- Windows, mac, Linuxに対応しています.(Windowsとmacは動作未確認)
- 実行ファイルで配布しているので,ダウンロードすればすぐに利用できます.
- アブストラクトだけの保存も可能です.
- 関連研究のサーベイをするときに便利かもしれません.
- Goroutineによって並列処理しているので,ほしい論文がたくさんあっても,多分ある程度速くダウンロードできます.
似たようなの
- とりあえず
arXiv downloader
とかで調べたらいっぱい出てきます.
- arXiv APIを使いこなすためのライブラリ紹介
- 自動でArxivから論文を取得して、日本語に直してLineに投稿すれば、受動的に最新情報が収集できるかも!?
- arXiv APIのPythonでの取得
好きなのを使ってください.みんな素晴らしいと思います.
どう使うの?
まず,gitlabからダウンロードしてください.
使用例
-
dpxgo paperlist
: paperlistにある論文(アブストラクトとpdf)をダウンロード -
dpxgo -d out/ paperlist
: 保存先ディレクトリの指定 -
dpxgo -w 20 paperlist
: 並列数(Goroutine)の指定 -
dpxgo -a paperlist
: アブストラクトだけ取ってくる.
paperlistの書き方は以下の通りです.Plain textならファイル名は何でもいいです(多分).
TITLE of PAPER1
TITLE of PAPER2
TITLE of PAPER3
TITLE of PAPER4
タイトルごとに改行で区切ってください.空の行があっても読み飛ばします.
オプション
-
-d
: 保存先の指定 (default ./) -
-w
: Goroutineの並列数 (default 10) -
-a
: アブストラクトのみ保存 (default false)
Goコード
全文は,gitlabを参照してください.特に大事そうな部分を説明します.
メイン関数
func main() {
// parse command line variables
parseArg()
// download papers with ?Goroutine?
ch := make(chan string)
wg := sync.WaitGroup{}
// make workers
for i:=0; i<NumWorkers; i++{
wg.Add(1)
go downloadPapers(&wg, ch)
}
paperTitles := parsePaperList(ListPath)
for _, fileName := range paperTitles {
ch <- fileName
}
close(ch)
wg.Wait()
}
並列化の方法は,ここを参考にしています.
コマンドライン引数のパース
func parseArg() {
targetDir := flag.String("d", "./", "directory path to save")
numWorkers := flag.Int("w", 10, "number of workers")
onlyAbst := flag.Bool("a", false, "only save abstract")
flag.Parse()
TargetDir = *targetDir
NumWorkers = *numWorkers
OnlyAbst = *onlyAbst
// is TargetDir exist?
if _, err := os.Stat(TargetDir); os.IsNotExist(err) {
panic(TargetDir+" is not exist")
}
endStr := string(TargetDir[len(TargetDir)-1])
if endStr != "/" {
TargetDir = TargetDir+"/"
}
args := flag.Args()
if flag.NArg() == 0 {
fmt.Println("dpxgo [options] list")
return
} else {
// it does not support multiple paper lists
ListPath = args[0]
}
}
Goではflag
によってコマンドライン引数をパースするみたい(これ以外にもやり方はある)ですが,この処理をfunc main()
でやるとなんか嫌なので,関数にしてまとめて,コマンドライン引数で扱いたい変数は全てグローバル変数にしました.
すこし,苦戦したのがflag.Parse()
の位置です.flag.Type
関数のすぐあとで行うべきです.つまり,グローバル変数に代入した後でもエラーにはならないのですが,コマンドライン引数がすべてdefaultのものになります.
arXiv APIの使い方
UrlHead = "http://export.arxiv.org/api/query?search_query="
UrlFoot = "&start=0&max_results=1" // toriaezu 1 ken dake
strQuery := url.QueryEscape(paperTitle)
resp, _ := http.Get(UrlHead+strQuery+UrlFoot)
defer resp.Body.Close()
html, _ := ioutil.ReadAll(resp.Body)
downloadPapers (*wg, ch)
の中のこの4行の部分でarXiv APIを利用しています.UrlFoot
のmax_results
を変更するとたくさんの論文がヒットして,キーワードで検索したいときは便利だと思います.でも今回の目的はタイトルリストからの論文取得なので,結果件数は1件で十分です.
ここでhtml
は[]byte
型の変数です.ここから必要な情報をパースしていきます.ちなみにstring
型に変換すると普通のhtml形式の文字列です.
HTMLのタグの中身の取得
func isTagExist(html []byte, tagStr string) (bool) {
regStr := `<`+tagStr+`>[\s\S]*</`+tagStr+`>`
regTag := regexp.MustCompile(regStr)
return regTag.Match(html)
}
func fetchTag(html []byte, tagStr string) ([]byte) {
// <tag>[\s\S]</tag>
regStr := `<`+tagStr+`>[\s\S]*</`+tagStr+`>`
regTag := regexp.MustCompile(regStr)
content := regTag.FindSubmatch(html)[0]
content = bytes.Replace(content, []byte("<"+tagStr+">"), []byte(""), -1)
content = bytes.Replace(content, []byte("</"+tagStr+">"), []byte(""), -1)
return content
}
// short version
func isTagExist(html []byte, tagStr string) (bool) {
return regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).Match(html)
}
func fetchTag(html []byte, tagStr string) ([]byte) {
content := regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).FindSubmatch(html)[0]
return regexp.MustCompile(`<`+tagStr+`>|</`+tagStr+`>`).ReplaceAll(content, []byte(``))
}
この2つの関数を実装しました.Goのライブラリにgoqueryというものがあり,これが便利らしいのですが,この存在に気づいたのは書いている途中だったため,見なかったことにしました.
isTagExist
ではhtml形式の[]byte
型変数がtagStr
タグを含むかどうかをチェックします.arXiv APIでは,検索がヒットしたとき,<entry>
タグがつくので,この関数で,arXivに論文が投稿されているかどうかをチェックします.
fetchTag
ではtagStr
タグの中身を取ってきます(タグは含まない).論文のタイトルやアブストラクトは先に取得したhtml
変数に含まれている(もちろん検索がヒットした場合に限る)ので,この関数で取ってきます.
ただし,このfetchTag
関数ではタグが入れ子になっている場合は,おそらく思った通りに動作しません.しかしながら,arXiv APIのレスポンスにはそのような構造はありませんのでこれで十分です.
タイトルの一致判定
func matchTitle(titlePred string, titleTrue string) (bool) {
sp := strings.Split(titlePred, " ")
st := strings.Split(titleTrue, " ")
matchNum := 0.0
for _, tp := range sp {
for _, tt := range st {
if tp == tt {
matchNum += 1.0
break
}
}
}
if threshold:=0.7; matchNum/float64(len(st))>threshold {
return true
} else {
return false
}
}
すこし使って気づいたのですが,arXiv APIの検索は,ある程度タイトルが一致している論文を提示するため,結構違う論文がダウンロードされることがあったので,タイトルの一致判定を行う関数を追加しました.これは,以下の式でタイトルの類似性を測っています(類似性が大きいほど似ている).
\text{similarity} = \frac{|\text{{検索されたタイトルの単語集合}}\cap\text{{指定したタイトルの単語集合}}|}{|\text{{指定したタイトルの単語集合}}|}
まぁ,Recallというか,コサイン距離っていうやつですかね.
pdfファイルの保存
// save pdf (error ハンドリングは省略)
urlPdf := fetchTag(fetchTag(html, `entry`), `id`)
urlPdf = bytes.Replace(urlPdf, []byte("abs"), []byte("pdf"), 1)
urlPdf = bytes.Replace(urlPdf, []byte("http"), []byte("https"), 1)
resp, err = http.Get(string(urlPdf)+".pdf")
defer resp.Body.Close()
pdf, _ := ioutil.ReadAll(resp.Body)
err = ioutil.WriteFile(TargetDir+string(title)+".pdf", pdf, 0666)
pdfの保存はhtml
の取得と同じ要領でhttp.Get()
を使えばできます.
おわりに
とりあえず,動いてかつ自己満足にひたれるものができました.
一番苦戦したのはタグのパターンマッチを行うための正規表現でした(普段正規表現をまったくつかないため).