Help us understand the problem. What is going on with this article?

Go初心者が書くarXiv APIを使って論文リストから論文を取ってくるアプリ

はじめに

自分の分野の有名な学会が開かれました.Accepted Paperのページがあります.とりあえずタイトルだけ見て,面白そうなやつ/自分の立場を脅かしようなやつをチェックしなきゃ...
みたいなことになる人もいるかもしれません.(まぁ普段からアンテナを貼っておけばこんなことにはならないのかもしれませんが,僕みたいにSNSが嫌いな人はこうなります)
とりあえず,タイトルで検索してarXivでダウンロードしてくるわけですが,ポチポチするのは面倒なので途中でやめてしまいます.そこでAccepted Paperのリストからほしい論文のタイトルだけコピペ(全部ブラウザから調べるよりは多少まし)して,タイトルリストからダウンロードしてくれるアプリがあったらいいな,ということで作りました.
僕のモチベーションはあくまでGoの勉強です.

何を作ったの?

dpxgoというクロスプラットホーム CLIアプリ (コマンド) を作りました.
リストにある論文のアブストラクトとpdfをarXiv APIをつかってダウンロードします.
arXivにない場合は自分で調べてください.
ちなみにdpxgoDownload Papers from arXiv with GO app.の略です.

特徴

  • Windows, mac, Linuxに対応しています.(Windowsとmacは動作未確認)
  • 実行ファイルで配布しているので,ダウンロードすればすぐに利用できます
  • アブストラクトだけの保存も可能です.
  • 関連研究のサーベイをするときに便利かもしれません.
  • Goroutineによって並列処理しているので,ほしい論文がたくさんあっても,多分ある程度速くダウンロードできます.

似たようなの

好きなのを使ってください.みんな素晴らしいと思います.

どう使うの?

まず,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を利用しています.UrlFootmax_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()を使えばできます.

おわりに

とりあえず,動いてかつ自己満足にひたれるものができました.
一番苦戦したのはタグのパターンマッチを行うための正規表現でした(普段正規表現をまったくつかないため).

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away