Hatena::ブログ(Diary)

駄文生産所 このページをアンテナに追加 RSSフィード

2013-05-31

リンク集からRSS/Atomフィードを収集するWebアプリ/スクリプト

Feed TrawlerというWebアプリ作成した。


「Feed Trawler」

http://feedtrawler.devgoodies.net/


ブログリンク集的なページのURLから、一段階分リンクをたどり、RSSAtomフィードがあった場合、「ページのタイトル」「リンクURL」「フィードURL」を抜き出し、リストする。

こんなものを使うのは、ろくでなしに決っているので、広告の配置もろくでもない感じにした。一度やってみたかった。


環境は、Groovy/Grails + Heroku。それに、GParsとjsoup。

すぐにHerokuの制限であるメモリ512MB、30秒の制限に引っかかってしまうのが残念なところ。

GParsで30スレッド並行で処理しようとすると、メモリが溢れ、強制停止されてしまった。いまはとりあえず5スレッドに設定している。


やると決めて制作に一日。作業時間は、コア部分、Web部分、見た目調整、雑務で、各3時間程度。

以下は最初期に殴り書きした整理前のスクリプトGroovyの実行環境が準備できるなら、これで十分役には立つ。



import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import groovyx.gpars.GParsPool

def url = "http://www.j-pfa.or.jp/blog"

Document rootDoc = Jsoup.connect(url).get()
Elements rootAnchors = rootDoc.getElementsByTag('a')

List candidates = []
rootAnchors.each {Element elem ->
    def text =  elem.text()
    def link = elem.attr("href")

    if (link =~ /^https?\:\/\//) {
        candidates << link
    }
}
candidates.unique() // 副作用

// 若いインデックスのものほど優先
List feedTypes = [
    'application/atom+xml',
    'application/rss+xml',
    'application/rdf+xml',
    'application/x.atom+xml',
    'application/xml',
    'text/xml',
]

def selectFeed(List types, List elements) {
    def selected = null
    types.each {type ->
        if (selected == null) {
            selected = elements.find {Element elem -> elem.attr('type') == type}
        }
    }
    return selected
}

GParsPool.withPool(30) {
    candidates.eachParallel {String linkUrl ->
        try {
            Document doc = Jsoup.connect(linkUrl).get()
            Elements links = doc.head().getElementsByTag('link').findAll {Element elem -> elem.attr('rel') == 'alternate'}

            List feedLinks = links.findAll {Element elem -> feedTypes.any {type -> elem.attr('type') == type}}
            feedElem = selectFeed(feedTypes, feedLinks)

            if (feedElem) {
                String feedUrl = feedElem.attr('href')
                if (! feedUrl.startsWith('http')) {
                    URL u = new URL(linkUrl)
                    def protocol = u.protocol
                    def host = u.host
                    feedUrl = "${protocol}://${host}${feedUrl}"
                }

                println ("${doc.title()}\t${linkUrl}\t${feedUrl}")
            }
        } catch (e) {
            // println(e.message)
        }
    }
}

2013-05-28

Groovy/Grailsでフィードアグリゲータを作成し、Herokuで運用してみた

ここ20日間ほど、Groovy/Grailsフィードアグリゲータを作っていた。

これにどれほどの需要があるかはさておき、自分の見たい情報が整理されて出てくるところまでは来た。


ハンドボールナビ」

http://handballnavi.com


JVMで動くアクション型のフレームワークを調査するところから始めて、練習課題的にフィードアグリゲータを設定し、10日後に公開、その後10日で肉付けして、現在に至る、というのはそう悪いペースではないだろう。


以下、チラシの裏


Webフレームワーク

もともとやりたいことは別にあって、それにはSeasideフレームワークが向いていないことは分かっていた。Smalltalk環境を活かした生産性は抜群で、実行速度も十分(Javaの1/5程度の速度)だと思うが、コンポーネントベースフレームワークは内向きのプロダクトに向いていて、外向きにはイマイチだ。現状、SmalltalkWeb開発となると、実質Seaside一択になってしまうのは辛いところで、WAFの自作からやるのは遠すぎる。


Webフレームワークは、以下の条件で調べていた。


Railsは実行速度で、PHP系統クロージャで外した。残るのはJVMGroovyScalaV8JavaScriptといったところだったが、JVMを選択した。JavaScriptクロージャは気に入らない。


Grails、Play、SpringMVC、SAStrutsの資料を眺め、いくらかサンプルを書いたところでGrailsに決めた。

SpringMVC、SAStrutsGroovyScalaから使うには冗長で、他の2つに比べて設定記述も多く、面倒な印象を受けた。

PlayはViewテンプレートは素晴らしいが、Javaから使うには冗長な記述で、Scalaの構文は読みにくかった。なにより検索結果にPlay1とPlay2の情報が錯綜するのに参った。

消去法的にGrails。受けが広い気がした。


そこそこの数の資料を見て回ったが、特に意志決定を後押ししたのは以下の3つ。

Grails vs. Play Framework comparison」にあった、 "Scala is for writing, not for reading"には、まぁそうよね、とPlayのサンプルを読み書きした後に軽く同意した。


Grails

ドメインオブジェクトリファクタリングに起因すると思われるHibernate周りの異常動作、変更時の再起動必要性の分かりにくさ、CoC(Conversion of Control)なフレームワーク記述が少ない反面「覚えゲー」なのではないかという疑問等々、引っかかる部分があるにはあったが、全体に良いモノだった。Seasideのように、問題発生時は潜って自力解決しつつ仕組みを徐々におさえていく、ということができないのは残念だが、公式ドキュメント書籍Blog、StackOverflow等々、日本語では少ないものの、必要情報がたいてい手に入るのは素晴らしい。


Grails in Action」にちゃんと目を通すのが良いようだが、必要なところだけつまんで、今のところはサボっている。

数年前にデブサミで頂いた「Grails徹底入門」は、M、V、Cで別々の章立てになっている関係や、コード解説が部分部分になっているためか、少々全体像が把握しづらかった。特にWebFlowのあたり。一緒に頂いたサンプル入りCD-ROMを紛失してしまったのが悔やまれる。


IDEにはIntelliJ Idea Ultimateを使用した。他は試していない。大層賢くて、Grails名前解決な部分もちゃんと補完し、リファクタリング時も追随してくれる。昨年使用していたEclipseに比べると雲泥の差だ。2日前に199ドルを支払ったが、応分価値はある。

とはいえ、デバッグ環境はイマイチだ。Java系はどれも及第点を与えられない。

昨年作成したGInspectorがそれなりに役立った。


Heroku

公開しないか、VPSのお試し期間で試すかで迷っていたが、Heroku無償で使えることが分かったのでそこに乗らせてもらった。コンパイル時にメモリオーバーでコケたり、ログが最新100行分しかとれなかったり(注:ちゃんとコマンドを調べれば取れることがわかる)、デプロイに失敗するがappを作り直すと同じコード成功するとか、なにかと不可解で厳しいこともあったが、無償には代えられない。


データは基本オンメモリ設定のH2 Databaseに保持し、残したいものは、これもHerokuRESTサーバをたて、PostgreSQLのTEXTにJSONを突っ込んでいる。無償範囲は10000行に制限されているが、データ容量の制限は明記されていないのでこのようにした。運用が面倒だ。失って困るデータはないので、あまり真剣対応していない。


Herokuは1時間アクセスのないサービスアイドリング状態にする。それを避けるため、cronで定期アクセスするのが定番のようだが、これもGrailsのquartz pluginを使用し、設定されたURLに定期アクセスするサーバを2つ立て、相互にアクセスさせるようにした。オーバーだが、Grailsに慣れる過程でやってみた。それなりに役立ってはいるし、WorkerDynoも要らない。


本体の更新は、2つのappに交互にデプロイし、初期設定と動作確認を終えた時点でDNSの設定を切り替えている。


Grails on Heroku無償

イニシャルの状態でもアプリサイズ(Slug)が80MBを超え、コンパイル時のメモリ使用量も制限の512MBを超えるのは難。

アプリサイズ最大200MB、メモリも基本512MB以内という制限があり、メモリ使用量がこの1.2倍(だったか?)を超えるとプロセスが止められてしまい、なにかとイライラさせられる。Railsだとアプリサイズで30MB(某所でちょっと目にしただけだが)程度に収まるようなので、当たり前だが、Ruby向けに作られたサービスということだろう。


Herokuの印象

ちょっと触っただけで言うのもなんだが、遠くて、小さい。

なにかと割り切りが必要で、継続的な前進や、日常的なメンテ必要となるシステムに、Herokuは使いにくい。

枠をはみ出す毎に課金必要になる。課金必要な程度にシステムが膨れた場合は、国内VPSクラウドサービスを借りるのが現実的だと思う。

フリーライダーがどの口で言うか、という話ではあるが印象はそうだ。


終わり

  • Grails/Groovyは良いバランスだと思う。Javaの肩に乗れるのも良い
    • Hibernateは気に入らない。テーブル3つJoinして、、、な処理は、いまのところあきらめて無駄SQLを発行している。本当に必要ならMyBatisを使う
  • Herokuは限定的に、割り切って使う

2013-05-22

キーワードにマッチした2chスレのリストを取得するGroovyスクリプト

Groovyで、キーワードマッチした2chスレリストを取得するスクリプトを書いた。スクレイピングにjsoup、並行処理にGParsを使っている。

板の数は800を超えるが、wait無しの複数スレッドでドカンとやれば、パズーの身支度より早く終る。ただし、調子に乗ると規制されてアクセスできなくなるので注意すること。

えーとその、もうしないので許してください。ゴメンナサイ。


import org.jsoup.*
import org.jsoup.nodes.*
import org.jsoup.select.*
import groovyx.gpars.GParsPool

List keywords = ['iPhone', 'アイフォン', 'アイフォーン']


String bbsTable = 'http://menu.2ch.net/bbstable.html'
List ignoreBBSList = [
    '2ch総合案内', '地震ヘッドライン', '地震速報', '臨時地震', '臨時地震+', '緊急自然災害@超臨時',
    'テレビ番組欄',
    '2chプロジェクト',
    'いろいろランキング',
    '削除依頼', '批判要望',
]

Document doc = Jsoup.connect(bbsTable).get()
Elements anchors = doc.getElementsByTag('a')

List bbsList = []
String reg = /^http:\/\/\w+\.2ch\.net\/\w+\/$/
anchors.each {Element elem ->
    String name = elem.text()
    String url = elem.attr('href')
    if ((url ==~ reg) &&  (! ignoreBBSList.contains(name))) {
        bbsList << [name, url]
    }
}


List threads = []
GParsPool.withPool(1) { // 規制上等で一気に取得したい場合は増やす
    bbsList.eachParallel {String bbsName, String bbsUrl ->
        sleep(1000) // ここも調整
        println("取得中: " + bbsName)

        try {
            String subjects = new URL(bbsUrl + 'subject.txt').getText('SHIFT_JIS')
            subjects.eachLine {String line ->
                (line =~ /^(\d+)\.dat<>(.+)\(\d+\)$/).each { String all, String threadId, String threadName ->
                    if (keywords.any {String keyword -> threadName.matches(/(?i).*$keyword.*/)}) {
                        threads << [bbsName:bbsName, bbsUrl:bbsUrl, threadName:threadName, threadId:threadId]
                    }
                }
            }
        } catch (e) {
            println("!!! ERROR !!! " + [bbsName, bbsUrl, e.message])
        }
    }
}


println()
println()
threads.each {
    println it
}

取得中: 一人暮らし
取得中: プロバイダー
取得中: インスタント麺
取得中: 美容
取得中: 癒し
取得中: Walker+
取得中: 面白ネタnews
    <snip>
取得中: 天国
取得中: ニー速


[bbsName:プロバイダー, bbsUrl:http://engawa.2ch.net/isp/, threadName:【IP電話】Vivafon(iPhone/Win用)【Skype/アジルフォン】 , threadId:1321980902]
[bbsName:面白ネタnews, bbsUrl:http://kohada.2ch.net/be/, threadName:iPhoneぶっ壊れたwwwww , threadId:1356962936]
[bbsName:面白ネタnews, bbsUrl:http://kohada.2ch.net/be/, threadName:iPhoneアプリ「笑えるコピペ」に採用されそうな面白い話を教えて! , threadId:1338139697]
[bbsName:ビジネスnews+, bbsUrl:http://anago.2ch.net/bizplus/, threadName:【決算】KDDI、前3月期は増収増益 iPhone効果生かす[13/04/30] , threadId:1367305621]
[bbsName:ビジネスnews+, bbsUrl:http://anago.2ch.net/bizplus/, threadName:【携帯】iPhone、日本の携帯電話出荷1位に[13/04/25] , threadId:1366859028]
    <snip>
[bbsName:天国, bbsUrl:http://anago.2ch.net/heaven4vip/, threadName:妹が風呂入ってたからiPhoneで写真撮ったら , threadId:1368793734]

2012-10-05

デバッグパースペクティブの拡張をあきらめる

GInspectorを、EclipseデバッグパースペクティブのVariablesビューから起動できるようにと、プラグインを書いたのだが、これがまるで使えないことが判明。

ソースコードをあまり汚さずにオブジェクトを捕まえられてHappyかと思いきや、捕まえればデバッグ中のスタックフレームを壊し、副作用のあるメッセージを送ればハングアップと、二進も三進も行かない状況となってしまった。

JVMのつくりの問題と思われるので、これにて終了とする。

2012-10-04

Groovyの、バグなのか仕様なのか分からない事象3つ

いずれもGroovy2.0.4で再現する。


@Singletonで定義したSingletonクラスのgetInstanceメソッドに、Javaクラスからアクセスできない

JavaSingletonClientはgetInstanceの解決ができず、コンパイルエラーとなってしまう。AST Browserやjavapを見るとpublic staticなgetInstanceメソッドがあるようだが、何か間違っているのだろうか?

SomeSingletonに、明示的にstaticなgetInstanceメソッドを定義すればコンパイルは通る。

// SomeSingleton.groovy
package example

@Singleton
class SomeSingleton {
}
// GroovySingletonClient.groovy
package example

class GroovySingletonClient {
    static main(args) {
        println SomeSingleton.getInstance()
    }
}
// コンパイル不可
// JavaSingletonClient.java
package example;

public class JavaSingletonClient {
    public static void main(String[] args) {
        System.out.println(SomeSingleton.getInstance());
    }
}

@Singletonと@Mixinを同時に使用することができない

このケースでは、@Singletonが有効で、@Mixinは無効。

一緒にしても構わないAST変換の組み合わせと、ダメな組み合わせがあるようだ。前もって判別する方法は無いのだろうか?

package example2

class Hello {
    def hello() {
        println 'Hello'
    }
}

@Singleton
@Mixin(Hello)
class Greeter {
}

Greeter.instance.hello() // Caught: groovy.lang.MissingMethodException: No signature of method: example2.Greeter.hello() is applicable for argument types: () values: []
new Greeter().hello()    // Caught: java.lang.RuntimeException: Can't instantiate singleton example2.Greeter. Use example2.Greeter.instance

A.mixin(B)としたとき、BのtoStringが優先される場合がある

主体はAであるから、Bが表に出てくるべきではない。おそらくtoStringに限らない。

<追記>

Booleanクラスのメソッドを、BooleanCategoryのメソッドで上書きするのだから、BooleanCategory(Object)>>toStringが適用される。Traitsのように、バッティングしたメソッドを解決する手段が無いのが問題。

package example3

println '#pre'
println true
println true.toString()
println()

class BooleanCategory {
    boolean not() {
        return ! this
    }
}
Boolean.mixin(BooleanCategory)

println '#post'
println true
println true.toString()
// 実行結果
#pre
true
true

#post
true
BooleanCategory@7ca336 //<---true となって欲しい

2012-09-02

GInspector: an object inspector for Groovy

Groovyで書いたオブジェクトインスペクタ。GroovyJavaオブジェクトを捕まえて、変数を覗いたり、メッセージを送ったり、メソッドセレクタを一覧したりすることができる。

なお、捕まえたオブジェクト自身を指す擬変数(的なものとして)は「this」ではなく、「self」または「_this」を使用している。メッセージ送信(画面下部のテキスト領域で指揮を評価)の際は注意すること。

https://github.com/kaminami/GInspector

f:id:kaminami:20120902212855p:image


EclipseRCPを使用したアプリの副産物で、Groovy標準のGroovyConsoleに不満があったので自作。SWTを使用しているのは、EclipseRCPの勉強用でもあったため。

VisualWorks SmalltalkのInspectorを横目に見ながら作成した。


これを、EclipseやIntelliJの、デバッガビューの変数リストから起動できると開発効率がグッと上がるはずなのだが、JDI(Java Debug Interface)経由でプロキシ越しにうまくやる方法に辿り着けていない。

2010-01-02

Groovyのmixinを試す その2

以下のエントリーをなぞりながら、mixinでクロージャにメソッドを追加してみた。

結論としては、可能だが記法が少々ダサく見え、(計測していないのだが)全体的にかなり遅くなるらしい。

見た目はまぁ許容範囲、使えるかどうかは速度次第、といったところか。

// クロージャを拡張する場合はフラグを立てる
// かなり遅くなるらしい
ExpandoMetaClass.enableGlobally()

class ClosureMix {
    def whileTrue(repeatClosure) {
        while (this.call()) {
            repeatClosure.call()
        }
    }
}

Closure.mixin(ClosureMix)

def idx = 1
def sum = 0

// セミコロンで区切る必要アリ
// 無い場合、"0"のcallメソッドの引数としてクロージャが渡されてしまう?
;

// "->"を入れておかないとクロージャではなく、ブロックとして認識されてしまう
{->idx <= 10}.whileTrue{ 
    sum += idx
    idx++
}

assert sum == 55

2010-01-01

Groovyのmixinを試す

Groovyならmixinで既存クラスの拡張もイケる!!」ってのがどの程度のものなのか。

純粋オブジェクト指向で、動的束縛で、ファーストクラスなクロージャがあって、既存クラスの拡張アリアリ、要素的にGroovyと被る部分の多いSmalltalkから拝借したサンプルでちょいと試してみる。

以下は上手いこと動いた例。

// 追加するメソッドを定義
class BooleanMix {
    def not() { ! this }

    def ifTrueIfFalse(trueClosure, falseClosure) {
        if (this) { 
            trueClosure.call() 
        } else { 
            falseClosure.call()
        }
    }

    def ifTrue(trueClosure, ifFalse={}) {
        this.ifTrueIfFalse(trueClosure, ifFalse)
    }

    def ifFalse(falseClosure, ifTrue={}) {
        this.ifTrueIfFalse(ifTrue, falseClosure)
    }
}


class ObjectMix {
    def isNull() { false }
    def notNull() { this.isNull().not() }
    def ifNull(nullClosure) { null }
    def ifNotNull(nonNullClosure) { nonNullClosure.call(this) }
}

class NullObjectMix {
    def isNull() { true }
    def ifNull(nullClosure) { nullClosure.call() }
    def ifNotNull(nonNullClosure) { null }
}


// mixin実行
Object.mixin(ObjectMix)
Boolean.mixin(BooleanMix)
org.codehaus.groovy.runtime.NullObject.mixin(NullObjectMix)


// テストケース
assert true.not() == false
assert true.ifTrue{"t"} == "t"
assert true.ifFalse{"f"} == null
assert true.ifTrue({"t"}, ifFalse={"f"}) == "t"
assert true.ifFalse({"f"}, ifTrue={"t"}) == "t"

assert false.not() == true
assert false.ifTrue{"t"} == null
assert false.ifFalse{"f"} == "f"
assert false.ifTrue({"t"}, ifFalse={"f"}) == "f"
assert false.ifFalse({"f"}, ifTrue={"t"}) == "f"

assert "obj".isNull() == false
assert "obj".notNull() == true
assert "obj".ifNull{ "n" } == null
assert "obj".ifNotNull{ (it as String) + (it as String) } == "objobj"

assert null.isNull() == true
assert null.notNull() == false
assert null.ifNull{ "n" } == "n"
assert null.ifNotNull{ (it as String) + (it as String) } == null