5ちゃんねるをスクレイピングするライブラリを書いた


repository

https://github.com/yuki-eto/pot-collector
5ちゃんねる…というより前身の2ちゃんねる…といえば壺のイメージがあったのでこういう名前にしてみました。
テストはまだ書いてません :-p

使い方

基本的には sample/load_thread_articles.go に書いてある通り書けば使える…はず。
(おわかりかと思いますが、5ちゃんねる側で仕様変更があった場合はその都度修正が必要かもです…)


package main

import (
    "fmt"
    "strings"

    "github.com/yuki-eto/pot-collector"
)

func main() {
    const threadListURL = "https://krsw.5ch.net/idolmaster/subback.html"
    const threadBaseURL = "https://krsw.5ch.net/test/read.cgi/idolmaster/"
    const containsString = "本田未央"

    // *Board を作って
    board := potCollector.NewBoard()
    // *goquery.Document をもらう
    doc, err := board.LoadThreadListDocument(threadListURL)
    if err != nil {
        panic(err)
    }

    // HTML をパースして Board.Threads.List にスレッドを入れる
    if err := board.LoadThreads(doc); err != nil {
        panic(err)
    }

    // *Thread を受け取って bool を返すフィルタリング関数で Threads.List を
    // Threads.List をある程度弄ることが可能
    // 下記はスレッド名に特定の文字列が入ってるスレッドだけを取り出す例
    board.Threads.FilterThread(func(t *potCollector.Thread) bool {
        return strings.Contains(t.Title, containsString)
    })

    if board.Threads.Count == 0 {
        fmt.Println("do nothing...")
        return
    }

    for _, thread := range board.Threads.List {
        // 差分だけを取り込みたいときは、LoadArticleDocument() を呼び出す前に
        // thread.LastReadArticleID に1より大きい数を入れるとそこからの差分を取得できる
        // (何らかの手段で最後に読み込んだ番号を記録しておく必要がある)
        thread.LastReadArticleID = 1

        // スレッドURLを生成してアクセスし、*goquery.Document を受け取る
        // 取得するスレッドが多い場合はこのへんで goroutine とかを使うのもアリかもしれない
        // (節度を守って適切な並列数でやるべし)
        doc, err := thread.LoadArticleDocument(threadBaseURL)
        if err != nil {
            panic(err)
        }

        // HTML をパースして thread.Articles.List に書込内容を入れる
        if err := thread.LoadArticles(doc); err != nil {
            panic(err)
        }

        fmt.Printf("%d: %s (%d)\n", thread.ID, thread.Title, thread.LastArticleID)
        for _, article := range thread.Articles.List {
            if article.IsOver1000 {
                continue
            }
            fmt.Printf("%d: %s %v ID:%s\n", article.ID, article.Name, article.Date, article.UID)
            fmt.Println(article.Text)
            fmt.Println()
        }
    }
}

苦労した点

適切な設計がわからない

処理自体の実装はスラスラ進んだのだけど、ある程度進んでライブラリっぽい体裁を整える段階で、どういう粒度でファイルを分けるとか、どのポインタメソッドにどの処理を当てるかとかで結構悩んだ。

見た感じ golang の小さめのライブラリだと、ルートディレクトリに全部おいて同じ package にして、サンプルでディレクトリ切って package main にするみたいな感じが多そうだったのでそれに合わせた。

golang 書いててどのへんからちゃんと package 分けるとか未だに悩む気がしている(PHP の名前空間での同じような悩みを思い出した)。

とりあえず行き当たりばったりで自分だけが使えればいいやと思って作り始めたので、そもそも設計もクソもないというのはさておき。

goquery

*Document.Find() で jQuery 風なクエリが使えるってだけで、(当たり前だけど)他の部分は完全に golang の世界観なんで、当然 jQuery かじった程度で扱えるというわけではない。
むしろ Find() で受け取った *Selection を扱うところからが本番で、GoLand なかったら完全に詰んでた。
タグがある場合とそうでない場合のケース分けがけっこう難しく、最終的には HTML を文字列でそのまま受け取り、必殺奥義 regexp で解決したところも結構ある。