Blog

Scraping でも kotlin を使いたい

groovy で scraping するという記事をたまたま見かけた。 ここで、JVM言語でのscraping という観点でいうと、groovy というものは優れているわけだが、、実は kotlin でも scripting は可能。 kotlin 本体の scripting サポートは 2022年10月現在、まだまだ開発途中でとても実用に耐えるものではない。。

https://kotlinlang.org/docs/custom-script-deps-tutorial.html

のだが、実は kscript というコマンドがあって、これを使えば先取りして kotlin scripting の世界を実用することが可能だ。

https://github.com/kscripting/kscript

mac の homebrew でインストールする場合、以下のようにすればかんたんに使う事ができる。

brew install holgerbrandl/tap/kscript

idea で開く

kscript --idea scraping.kts とすると、scraping.kts を開いた状態の IDEA が起動する。この状態からであればすぐにスクリプトを実行可能。便利〜 さきに touch scraping.kts などとしてファイルを作っておく必要があることに注意。

kscript --idea scraping.kts した時点で @file:DependsOn にかかれている依存については build.gradle.kts の中に記載されるため、補完がきく。そうでない場合は IDEA を開き直すか build.gradle.kts を手で編集しないと補完がきかないので注意。

上記ブログ記事のスクリプトを kotlin で実装してみる

kscript scraping.kts とすればすぐに実行もできる。

scraping.kts の中身は以下のように書く。groovy の grab のように依存もかけるので最高便利。

@file:DependsOn("org.jodd:jodd-http:6.2.1")
@file:DependsOn("org.jodd:jodd-lagarto:6.0.6")

import jodd.http.HttpRequest
import jodd.jerry.Jerry

data class Comment(
    val id: String,
    val user: String,
    val time: String,
    val comment: String,
    val parent: String?,
)

var url = args[0]

val commentList = mutableListOf<Comment>()

while (true) {
    println("Scraping $url")

    // Send the HTTP request to our URL
    val response = HttpRequest.get(url).send()

    // Parse the HTML document into a Jerry DOM object
    val doc = Jerry.of(response.bodyText())

    // Find all comments <tr>s in the main comment table
    val comments = doc.find("table.comment-tree tr.comtr")

    // Iterate over each comment and extract its data
    comments.forEach { element ->

        val id = element.attr("id")
        val user = element.find("a.hnuser").text()
        val time = element.find("span.age").attr("title")
        val comment = element.find("div.comment").text()
        val parent = element.find("a:contains(parent)").attr("href")

        // Append the data to comment_list
        commentList.add(Comment(id, user, time, comment, parent))
    }

    // If there is a next link, set the URL and continue the while, otherwise exit
    val next = doc.find("""a[rel="next"]""").attr("href")
    if (next != null) url = "https://news.ycombinator.com/$next"
    else break
}

println(commentList)

配布用パッケージの作成

kscript --package scraping.kts

とすると、java コマンドさえあれば実行できるバイナリを生成可能。このバイナリは mac でも linux でも動く。windows なら WSL の上でなら動くはず。

その実態は↓のような prefix を uber jar の先頭にくっつけるという感じ。

#!/usr/bin/env bash
exec java  -jar $0 "$@"

docopt の利用

docopt というライブラリを利用すると python の docopt のような感じでコマンドライン引数パーサーも定義可能。便利〜

#!/usr/bin/env kscript
@file:DependsOn("com.offbytwo:docopt:0.6.0.20150202", "log4j:log4j:1.2.14")

import org.docopt.Docopt
import java.util.*

val usage = """
Use this cool tool to do cool stuff
Usage: cooltool.kts [options] <igenome> <fastq_files>...

Options:
 --gtf <gtfFile>     Custom gtf file instead of igenome bundled copy
 --pc-only           Use protein coding genes only for mapping and quantification
"""

val doArgs = Docopt(usage).parse(args.toList())

println("Hello from Kotlin!")
println("Parsed script arguments are: \n" + doArgs)

Ruby の ARGF 的な利用

kscript-support-api を使うと、ARGF みたいなこともかんたんにできる。

kscript --text オプションを使うと以下のスクリプトが prepend されるので、lines という変数を使えばかんたんにテキスト処理できる。

@file:DependsOn("com.github.holgerbrandl:kscript-support-api:1.2.5")
        
import kscript.text.*
val lines = resolveArgFile(args)

外部コマンドの実行

kutils を使うとかんたんに外部コマンドを実行できる。便利。

@file:DependsOn("com.github.holgerbrandl:kutils:0.12")

import de.mpicbg.scicomp.kutils.evalBash

val result = evalBash("date")
println(result.exitCode)
println(result.sout())

include の利用

@file:Include("utils.kt") のように書くこともできるので、例えば、依存関係をコピペして維持するのがダルいって場合にはこれをつかって共通化することも可能だ。

ローカルパスの他に URL を指定することができる。

see also