2010年12月9日

「『作曲できる奴ちょっとこい』のデータで遊ぶ」の3日間

「作曲できる奴ちょっとこい」のデータで遊ぶ
http://sdc.oov.ch/

「埋もれた歌詞の救済」というテーマを元にこんなサイトを作ってみた。
実際の作業自体は大体3日ぐらいで終わった。実作業時間は半日とちょっとぐらい。

1日目 ----

ネタ元はもちろん少し前に話題になったJ-POP ジェネレータ
それを見た友人が「作曲スレのデータでできないかな」と言っていて、作業しようと思い立ったのがこの日。

まず最初に歌詞のデータを集めてくる必要があった。
いくつか抜けている部分もあるもののIDは既に19000を越えているようなので、1秒ずつ HTTP アクセスを掛けたとしても19000秒。
図書館事件も記憶に新しい今日この頃なので、サーバのレスポンス速度も確認してデータ収集中はほぼ常時対象サーバもクライアントも監視していた。

収集で使用したのは概ね以下のようなコード。golang で書いた。
package main
import (
    "http"
    "io"
    "os"
    "log"
    "fmt"
    "time"
)
func main(){
    for i:=0; ; i++ {
        time.Sleep(1e9)
        r, _, err := http.Get(fmt.Sprintf("http://www9.atpages.jp/stewmusic/akadb/songlist.php?key=No:%d", i))
        if err != nil {
            log.Printf("http.Get error: %s", err.String())
            i-- //リトライ
            continue
        }
        dir := fmt.Sprintf("akadb/%02x", i & 0xff)
        filename := fmt.Sprintf("%s/%04x.html", dir, i)
        err = os.MkdirAll(dir, 0755)
        if err != nil {
            log.Printf("os.MkdirAll error: %s", err.String())
            r.Body.Close()
            continue
        }
        file, err := os.Open(filename, os.O_WRONLY | os.O_CREAT, 0644)
        if err != nil {
            log.Printf("os.Open error: %s", err.String())
            r.Body.Close()
            continue
        }
        written, err := io.Copy(file, r.Body)
        if err != nil {
            log.Printf("cannot write to file: %s", err.String())
            r.Body.Close()
            file.Close()
            continue
        }
        file.Close()
        r.Body.Close()
        log.Printf("%04d written %d bytes.", i, written)
    }
}
これを自宅のマシンで動かした。
恐らくインデックスが張られているであろうID基準での検索なのでレスポンスも速く順調に進んでデータの収集は難なく終了した。

2日目 ----

次にローカルに落としたファイルから歌詞の部分を抜き出す処理を書こうと思ったのだが、Go言語のパッケージにある html 解析周辺は絶賛開発中で上手く動かなかった(使っているのがreleaseブランチではなかったせいだと思う)のでここの解析処理は Python で BeautifulSoup を使った。
# -*- coding: utf-8 -*-
import sys, string, sqlite3
from BeautifulSoup import BeautifulSoup

def main(begin, end):
    db = sqlite3.connect("akadb.sqlite3")
    dbc = db.cursor()
    for i in xrange(begin, end):
        filename = "akadb/%02x/%04x.html" % (i & 0xff, i)
        soup = BeautifulSoup(open(filename, "rb").read())
        outline = soup.find('div', id="contents_outline")
        if outline == None:
            continue
        inlines = outline.findAll('div', id="contents_inline")

        #最後の contents_inline は歌詞ではない可能性が高いので
        #調べて必要ないなら消す
        last = inlines[len(inlines)-1].renderContents()
        if last.find("<!--//") != -1 or last.find("<a href") != -1:
            inlines.pop()

        for inline in inlines:
            body = inline.renderContents()
            #brタグのみを改行としてその他の改行は取り除く
            body = body.replace("\r", "").replace("\n", "")
            body = body.replace("<br />", "\n").rstrip() #末尾の改行は消す
            body = body.replace("&quot;", "\"").replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
            dbc.execute("insert into lyric (parent_id, body) values (?,?)", (i, body))
            print "%d processed." % i
    db.commit()

if __name__ == "__main__":
    main(int(sys.argv[1], 10), int(sys.argv[2], 10))
ひとつの記事に対して複数の歌詞が存在するケースがしばしばあるので、そこも考慮して SQLite3 に突っ込んでいく。
入門段階で終わっていた Python を久々に触ると色々忘れててやばい感じだったが無事に収集できた。

テキストの処理がやっつけ仕事なので若干漏れたデータがあるかも知れない。
まあでも取りあえず次進んじゃいましょうということでこのステップもさくっと終了。

3日目 ----

準備は整ったのであとは実際に歌詞を解体して組立て直すだけでいい。ここは再び golang で。
SQLite3 の DB に突っ込んだデータを gosqlite でランダムに30件程度抜き出してきて、その中のテキストを行単位とブロック単位で切り分け。
サビっぽい箇所にブロックの1行目を持ってきたりとか、歌詞っぽくするために適宜改行入れたりとか、そういうわりと単純な処理をいくつかやって完成。

この辺は自分で考えるのが楽しいところで晒すのは色々アレなので端折るが、それでも230行程度。
その中で http サーバを立てて JSON 形式でデータを返すような処理も行っている。

ここまで出来上がったらあとは実際に Web サイトとして仕上げるための作業。
ドメインを当てたり HTML とか JavaScript とか CSS を書いたりとか。
さり気なくこの前衝動的に買った紙・布・テクスチャー素材集も使ってみた。いい感じ。だと思う。

一応 JavaScript が動いてない環境でも最低限の挙動は出来るようサーバサイドでも PHP を使って多少処理をした。
Web サーバの nginx とランダム歌詞を返す golang 製サーバは HAProxy を経由して両方ともポート80番でアクセスできるようにした。

そして公開。埋もれた歌詞が発掘されるきっかけになればいいなあと思う。
Clip to Evernote