ダメ人間オンライン

あまり信用しないほうがいい技術メモとか備忘録とかその他雑記

Puppeteerが強い💪💪💪

github.com

ヘッドレスChromeでスクレイピングが簡単にできる時代がきました。
スクロールしたら追加でコンテンツを読み込んで〜みたいなページをスクレイピングしたい機運がちょうど高まってたのでPuppeteer使ってみたんですがめっちゃ簡単に実現できて完全に最高だった。

ローカル(mac)でもサーバー(CentOS8)でも動かすの楽だったし環境によって困るみたいなこともなかった。
CentOS6で動かすのは相当大変というかほぼ無理では?という感じなのでおとなしくCentOS7or8で楽に動かしましょう。他のOSは知りません。

インストールとか動かし方とかは調べたら大量に出てくるのでそちらを見るのがいいでしょう。
簡単なサンプルと注意点だけ載せておきます。

特定のwebサイトにアクセスしてちょっとだけスクロールしてからコンテンツ取得してログに出す。実際はDBに書き込むなりどこかにPOSTしたりとかやってるけどそれは省略。

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const device = devices['iPhone 6']; // スマートフォン用のページを表示させたい時とか指定できるよ

(async () => {
    const browser = await puppeteer.launch({
        headless: true // false にすると実際にChromeが起動してデバッグやりやすいよ
    })
    const page = await browser.newPage()
    try {
        await page.emulate(device)

        // これは sample url なのでスクロールしても何も要素追加されないよ
        await page.goto('https://blog.dameninngenn.com', {waitUntil: "domcontentloaded"})

        // 雑にスクロールさせるよ
        await page.evaluate( () => {
            window.scrollBy(0, 2000);
        });
        await page.waitFor(1000)

        // 欲しい要素見つけてテキストだけとるよ
        var item = await page.$('.footer-address > a');
        var itemText = await (await item.getProperty('textContent')).jsonValue();
        console.log(itemText);
    } catch (e) {
        console.error(e);
    } finally {
        // 必ず閉じよう!!!!!!!!!!!!!!!!!!!!!!!
        await browser.close()
    }
})()

すごい雑なコードだけど雑にやっても良い感じにできる。
1つだけ注意しないといけないことがあって、必ず try-catch で包んで finally で必ず browser.close() すること。
browser.close() しないとプロセスが残ったままになってしまう。
こうやって書いたコードを daemon 化して動かしたり cron で動かしたりすることもあると思うんですがそれでうっかり browser.close() せず抜けてしまうようになってると無限に chrome プロセスが作られて残ったままになってリソースを食い潰して地獄みたいになります。

build.gradle.kts で良い感じに repositories をまとめたかった

build.gradle.kts のサンプルとかを見てると

buildScript {
    repositories {
        google()
        jcenter()
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

のように書いてるのを良く見るけど repositories に指定するものは一箇所でまとめて書いておきたい。

従来の build.gradle だと

buildScript {
    ext {
        repos = {
            google()
            jcenter()
        }
    }
    repositories repos
}

allprojects {
    repositories repos
}

のように書けた。

build.gradle.kts の場合は

buildScript {
    val repos by extra {
        fun RepositoryHandler.() {
            google()
            jcenter()
        }
    }
    repositories(repos)
}

allprojects {
    val repos: RepositoryHandler.() -> Unit by project
    repositories(repos)
}

とすればよい。

ここで大事なのが

val repos: RepositoryHandler.() -> Unit by project

としてるとこで、

val repos: RepositoryHandler.() -> Unit by extra

とするとダメだった。

rootProject.extra["repos"] となるので by project でやらないといけない。

assertjで例外発生のテストをシュッと書く

Exception を throw する可能性のある method のテストを書く時今までは

@Test(expected = NantokaException.class)

とアノテーションをつけたり

try {
    nantokaService.doNantoka();
} catch (NantokaException e) {
    assertThat(e.getMessage()).isEqualTo("Nanka");
}

として確認していたけど、前者は exception の中身を確認することはできないし後者は exception が発生しなかった場合にスルーされてしまうって問題があってちゃんと確認したいなら両方書くとかしてた。

んで、java8以後 + 最近のassertjだともっといいやり方あるよーって教えてもらって下記のように書けることを知った。

assertThatThrownBy(() -> nantokaService.doNantoka())
        .isInstanceOfSatisfying(NantokaException.class, e -> {
            assertThat(e.getMessage()).isEqualTo("Nanka");
        });

シュッとしてて良い。

gradle の warning がどこで出てるのかわからない時の調べ方

$ ./gradlew build

とかして

Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
See https://docs.gradle.org/4.5.1/userguide/command_line_interface.html#sec:command_line_warnings

こんなのが返ってきてどれが原因なのか教えてくれなかったけど、

$ ./gradlew build -Dorg.gradle.warning.mode=all

みたいに -Dorg.gradle.warning.mode=all をつけてやれば

> Task :hoge-fuga:generateProto
Using TaskInputs.file() with something that doesn't resolve to a File object has been deprecated and is scheduled to be removed in Gradle 5.0. Use TaskInputs.files() instead.

みたいに教えてくれた。

Spring Framework で空JSONを返したい

{}

やんごとなき事情でこういう空っぽのJSONを返す必要がある場合には空っぽの response 用の class を用意して JsonSerialize annotaion をつけてやるのが楽。

@JsonSerialize
public class NantokaResponse {
}
@RestController
public class NantokaController {
    @GetMapping("/wei")
    public NantokaResponse wei() {
        return new NantokaResponse();
    }
}