2011/02/17

ContentExtractorのプラグインの仕様

NILScriptに同梱の「Notebook.ng」でWebページから本文を抽出するために用意されている「ContentExtractor」では、本文の抽出方法をプラグインとして追加できるようになっています。
現在のところ、はてなダイアリーやFC2ブログなどの代表的なブログサービスや、Vector、窓の杜、ITMediaなどのIT系情報サイトに対応するプラグインなどが用意されていますが、各自でプラグインを作成すればどんなサイトにでも対応させられます。
今回は、ContentExtractorプラグインの仕様について説明します。

基本的な定義

ContentExtractorのプラグインは、NILScriptのディレクトリ配下の「plugins\ContentExtractor」ディレクトリ内にプラグイン名のディレクトリを作って格納します。
各プラグインのディレクトリには、プラグイン名に拡張子「.ng」を付けたファイル名でプラグインのスクリプトファイルを格納します。
スクリプトファイル内には、以下のようなオブジェクト定義を記述します。

({
    description:String(<![CDATA[
        http://wiredvision.jp/の巡回と記事本文抽出。
    ]]>),
    level:50,
    rules:{
        atom:{
            urlPattern:/^http:\/\/(wiredvision\.jp|rss\.rssad\.jp\/rss\/wiredvision)\/.*\/atom\.xml$/i,
            inherit:"std/atom",
            childContentBodyPattern:true,
            replace:['NoTracker'],
        },
        article:{
            urlPattern:/^http:\/\/wiredvision\.jp\/.*\.html$/i,
            contentPattern:/<!--\s*google_ad_section_start\(name=s2\)\s*-->([\s\S]*?)<!--\s*google_ad_section_end\(name=s2\)\s*-->/i,
            sectionLinkPattern:/(<p><a href="http:\/\/wiredvision\.jp\/[^">]+">[^<]*へ続く<\/a><\/p>)/i,
            replace:[
                
            ],
        },
    },
})

オブジェクトの「rules」メンバの下にページの解析ルールを列挙し、各ルールのurlPatternメンバで、そのルールが対応しているページのURLにマッチする正規表現を指定します。
上記の「atom」ルールの「inherit:"std/atom"」は、stdプラグインで定義されている「atom」ルールを継承し上書きすることを示しています。
std/atomルールは、Atomフィードから各entry要素のlinkやcontentを子アイテムとして抽出するためのルールです。他に「std/rss」や「std/rdf」もあります。
これらのルールでフィードから項目を抽出する際には、アクセス解析用の中継URLをリダイレクト先のURLに置換したり広告項目を除去する機能などが自動的に適用されるようになっています。
「childContentBodyPattern:true」は、content要素を無視してlinkが示すWebページから対応するルールで抽出した本文を本文として使用することを示しています。
これは、フィードに含まれている本文が途中で終って「続きを読む」のリンクになっている場合に本当の本文を保存させるためなどに使用します。
「replace:['NoTracker']」は、フィードのsummary要素に仕込まれているアクセス解析用の不可視の画像を取り除く処理を定義しています。replaceの詳細については後で説明します。
このようなフィード用定義は、urlPatternとinheritの値以外はほとんど使い回せるので、コピペ・改編して利用するといいでしょう。

「article」ルールでは、記事のページから本文を抽出するルールを定義しています。
ここでは、「contentPattern」に指定された正規表現に従って、ページのHTMLから本文の部分が抽出されます。この時、正規表現中に「()」が使われている場合は、最初の括弧にマッチした部分が抜き出し結果となり、括弧がない場合はマッチ範囲全体が使われます。これは、前後の部分を目印に使いつつ、抜き出し結果には含めたくない場合などのための仕様です。途中に「(p|div)」のような括弧を使ったパターンを含めたい場合は、「(?:p|div)」のように、キャプチャ無し括弧を使ってください。
上記の例のように、コンテキストマッチング型広告サービスを利用しているサイトでは、本文の範囲を認識させるために埋め込まれたコメントタグを利用するのが手っ取り早いでしょう。

正規表現で抜き出しパターンを指定する際に特に注意すべきなのは「.」は改行にマッチしないということです。改行を含むあらゆる文字列にマッチさせるためには「[\s\S]」などと指定しなければなりません。
また、量指定子「*」や「+」は、そのままでは「貪欲」にマッチングを行うため、[\s\S]と組み合わせて使用する場合は、「?」を付けて「無欲」にする必要があります。
これを忘れて「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1><p>BBBB</p>」のようなHTMLに対して「/<h1>.*<\/h1>/i」のようなマッチングを行うと、「<h1>aaa</h1><p>AAAA</p><h1>bbb</h1>」までが一致してしまったりします。
今回のcontentPatternの場合、終了タグが1回しか出現しないので貪欲でも問題はありませんが、無欲の方が余計な検索が行われず高速に動作するはずです。

「sectionLinkPattern」では、本文が複数のページに分かれているときに、以降のページにリンクしている部分を抽出・結合するためのパターンを指定します。
また、この例では登場しませんが、ページのタイトルをtitleタグ以外から抽出したい場合に指定する「titlePattern」や、ページの概要を抽出する「summaryPattern」などもあります。

これらの抜き出しパターンでは、正規表現以外にもいくつかの抽出方法が指定できます。
HTMLの要素に付けられたid属性で抽出するには、「contentPattern:['#contentBody']」のように指定します。
「contentPattern:function(html){/*何らかの処理*/;return(content);}」のように、HTML文字列を引数に受取り抜き出した文字列を返す関数を指定することも可能です。
他にもいくつかの方法がありますが、この3つを覚えておけばほとんどの状況には対応できるでしょう。


HTMLからの記事一覧の抽出

RSSフィードなどを配信していないサイトで、記事一覧のページから記事を抽出したい場合は、「childContentPattern」を使用します。
以下は、「vector」プラグインで定義されているカテゴリ別ソフト一覧のページの定義例です。

        soft_category:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/vpack\/filearea\/(?:win|mac)\/.*$/i,
            childContentPattern:/(<LI>\s*<A HREF="[^">]*\/se\d+\.html">[^<]*<\/A>\s*<IMG[^>]*>[\s\S]*?<BR>[\s\S]*?<\/LI>)/ig,
            childContentTitlePattern:/<a href="[^">]*\/se\d+\.html">(.*?)<\/a>/i,
            childContentInternalLinkPattern:/(<a href="[^">]*\/se\d+\.html">.*?<\/a>)/i,
            childContentSummaryPattern:/<\/A>([\s\S]*?)<\/LI>/i,
            childContentBodyPattern:true,
            
            sectionLinkPattern:/<div class="pagenav">([\s\S]*?)<\/div>/i,
            replace:[
                'NoImageLink',
                [/<img\b[^>]*alt="([^">]*)"[^>]*>([\s\S]*?)<br[^>]*>/ig,'[$1] $2'],
            ],
        },

まず、「childContentPattern」で各子アイテムのHTMLを抜き出します。
「childContentTitlePattern」、「childContentInternalLinkPattern」、「childContentSummaryPattern」は、抜き出した子アイテムのHTMLからタイトル、ページ本体へのリンク、概要を抜き出す定義です。
一覧の項目に本文が丸ごと含まれている場合は、「childContentBodyPattern」で本文の抜き出し定義を指定しますが、この例では本文はリンク先のページから抜き出す必要があるため、「childContentBodyPattern:true」を指定しています。
また、複数ページに分かれた一覧を全て取り出すため、「sectionLinkPattern」も指定しています。
std/atomなどのルールも、このようにして定義されています。


replaceによる置換ルール

contentPatternなどで抜き出した範囲に広告やブログパーツなどの不要部分が含まれてしまう場合は、「replace」に置換ルールの配列を記述して削除などを行います。
配列の要素として単なる正規表現を指定すると、その正規表現に一致する箇所が空文字列に置換されて削除されます。この時、全ての出現箇所を削除するには、正規表現に「g」オプションを付ける必要があることに注意してください。
削除ではなく別の内容に置換したい場合には、「[/<b>([\s\S]*?)<\/b>/ig,'$1']」のように、正規表現と置換文字列の2要素からなる配列を指定します。第2要素には、文字列のreplace()メソッドの第2引数と同じように、関数を指定することも可能です。
よく使われる置換ルールは、「HTMLRewriter」という機能のプラグインで定義されており、ルール名を単に文字列として指定するだけで使用できます。
標準で用意されている置換ルールの一覧と説明は、「plugins\HTMLRewriter\Cleanup\Cleanup.ng」内に書かれています。



画像の保存

contentPatternなどで抽出された本文にimgタグで埋め込まれている画像は、抽出時に自動的に保存されますが、aタグでリンクされているだけの画像は、そのままでは保存されません。
画像が保存されるようにするには、以下のようにして明示的にダウンロードすべきコンテンツのURLであることを指定します。
        image:{
            urlPattern:/^http:\/\/blog-imgs-\d+-origin\.fc2\.com\/.*\.(?:jpe?g|png|gif|bmp)$/i,
            download:true,
        },

本文からリンクされているのが画像ファイル本体でなく画像を表示するためのHTMLである場合には、URLの変換定義を用意することで、画像本体のみを保存し直接リンクさせられます。
URLの変換定義の方式には、「urlRewrite」と「urlRedirect」の2つがあります。
単なるURLの正規表現置換で画像本体のURLが得られ、HTMLにアクセスしていなくても画像に直接アクセス出来る場合は、以下のように「urlRewrite」で置換文字列を指定します。置換後のURLを保存させるための定義も別途必要であることに注意してください。

        image_html:{
            urlPattern:/^(http:\/\/japan\.cnet\.com\/)image\/l\/(.*\.(?:jpg|jpeg|png|gif|bmp))$/i,
            urlRewrite:"$1$2",
        },
        image:{
            urlPattern:/^http:\/\/japan\.cnet\.com\/(?:story_image|image|storage)\/.*\.(jpg|jpeg|png|gif|bmp)$/i,
            download:true,
        },

HTMLのURLから画像本体のURLが定まらない場合や、HTMLにアクセスしてCookieの取得などを行わないと画像本体へのアクセスが出来ない場合などは、以下のように「urlRedirect」に、HTML内から画像本体のURLが含まれる部分を抽出する定義を記述します。この定義で抜き出された範囲に最初に出現するhref=やsrc=のリンク先URLが、変換後のURLとなります。

        softnews_ss:{
            urlPattern:/^http:\/\/www\.vector\.co\.jp\/magazine\/softnews\/\d+\/n\d+_pic\.html$/i,
            urlRedirect:/<td [^>]*>\s*<a href="[^">]*">(<img [^>]*>)<\/a>/i,
        },

また、フィードから抜き出されたURLに「?ref=rss」のようなゴミがくっついている場合なども、urlRewriteによるURLの置換を定義しておくと良いでしょう。


ContentExtractorのルール定義の主要部分の説明は以上です。
まだ説明されていない機能やテクニックもいくつかあるので、「doc\ContentExtractor.txt」の説明や、「plugins\ContentExtractor\」内の標準プラグイン内の実際の定義なども参照してみてください。

出来上がったプラグインを配布したい場合は、ZIPで圧縮してアップロードするなどしてください。そのうち、プラグインの配布や入手、アップデートなどを行う機能も実装される予定です。

HTTPオブジェクトのCookieを保存しておいて次回実行時に読み込む

HTTPオブジェクトでCookieを扱う際、受取ったCookieをファイルに保存しておいて、次にスクリプトを実行したときに読み込んで利用したい場合があるかも知れません。
このような場合は、HTTPオブジェクトのコンストラクタの引数に以下のようなオプションオブジェクトを指定してください。

try{
    var http=new (require('HTTP').HTTP)({
        cookies:Main.scriptDirectory.file('cookies.json'),
        saveCookies:true
    });
    //何らかの処理
}finally{
 free(http);
}

オプションオブジェクトの「cookies」メンバにファイルパスやFileオブジェクトを指定すると、そのファイルからCookieが読み込まれます。この時、ファイルが存在しなくてもエラーになったりはせず、単に何も読み込まずに初期化されます。
また、cookiesに加えて「saveCookies:true」を指定すれば、オブジェクトをfree()で解放する際に、cookiesで指定したファイルにCookieが保存されます。
ファイルから読み込まずに保存だけを行いたい場合は、cookiesを指定せずにsaveCookiesにファイルパスやFileオブジェクトを指定します。

このCookie保存機能では、ブラウザを閉じると破棄されてしまうセッションCookieも保存できます。これにより、ブラウザをずっと開いたままにしているように装うことも可能となります。
オプションオブジェクトに「discardSessionCookies:true」を指定することで、セッションCookieは保存せず破棄させることも可能です。

2011/02/16

HTTPオブジェクトで受信したCookieを使用したアクセスを行う

会員制のサイトなどのコンテンツにアクセスするには、IDとパスワードを入力してログインしなければならない場合があります。
このようなサイトでは、サーバが正しいログイン情報を受取った時にユーザーを識別するセッションIDを生成して「Set-Cookie」というレスポンスヘッダで送信し、それを受取ったブラウザなどのクライアントプログラムが次回以降のアクセス時に「Cookie」というリクエストヘッダでセッションIDを送信することで認証済みであることを証明するという方式がよく用いられています。

NILScriptのHTTPオブジェクトには、受取ったCookieを保持して次回アクセス時に送信するという機能が用意されており、このような認証が必要なサイトのコンテンツにも簡単にアクセス出来ます。
以下に示すのは、mixiにログインして指定コミュニティのトップページのテキストを抽出・表示するというスクリプト例です。
var user="user@example.com",pass="password";
var url="https://mixi.jp/view_community.pl?id=38328";
try{
    var http=new (require('HTTP').HTTP)();
    http.getText({
        url:"https://mixi.jp/login.pl",
        method:"post",
        body:{
            next_url:"/home.pl",
            email:user,
            password:pass,
            sticky:"",
        }
    });
    println(String((new (require('LooseXML').HTML)(http.getText(url)))
    .$0('#communityIntro')).replace(/]*>/ig,'\n').replace(/<.*?>/ig,''));
}finally{
    free(http);
}
HTTPクラスには「HTTP.getText()」のようなクラスメソッドが用意されていますが、Cookieを用いたアクセスを行うには、new HTTP()でインスタンスを生成する必要があります。
次に、このHTTPオブジェクトのgetText()メソッドでログイン情報を送信します。送信先URLとパラメータ名などは、ログインフォームのHTMLなどから探してください。

ログインに成功すれば、このHTTPオブジェクトにCookieが保存されます。
なお、リクエストオプションオブジェクトで「ignoreCookies:true」を指定すれば、受取ったCookieを無視させることも出来ます。
以降は、普通にこのHTTPオブジェクトのgetText()メソッドやsaveTo()メソッドを呼び出せば、アクセス先サーバから過去に受取って保存されたCookieが送信され、ログイン済みでないとアクセス出来ないコンテンツなどにもアクセス出来ます。

HTTPオブジェクトには次回アクセス時に再利用するためにKeep-Alive状態になっているTCPStreamオブジェクトなども保持されているので、使い終ったらfree(http)のようにして解放する必要があります。

なお、この機能はずっと実装するのを忘れたまま放置されていて、つい最近実装されました。使用するには、最新版にアップデートしてください。

2011/02/08

NotebookのScan機能でサイト上の全ページを保存

NILScriptに同梱されている「Notebook.ng」では、手動でページを保存したり自動巡回で新着ページを保存するだけでなく、リンクを辿ってサイト上のページを全て保存させることも可能です。
この機能を利用するには、リンクを辿る基点にしたいページでブックマークレットの「ScanForm」を実行するか、サイドバーの「Scan from Website」からURL未入力のフォームに進んでください。
すると、以下のような画面で使用するプラグインの選択などを行なえます。
巡回登録の場合と同様に、説明を参考に使用するプラグインを選択してください。

「Start」ボタンを押すと、以下のような画面で進捗状況が表示されます。サイトの規模によってはかなりの時間がかかるので注意してください。

Notebook.ngの巡回機能でWebページの更新チェック

NILScriptに同梱の「Notebook.ng」に用意された巡回機能では、RSSリーダーのように新着記事を収集するだけでなく、特定のWebページの内容の変化をチェックすることも可能です。
この機能を利用するには、巡回ルールを「blog/body」や「std/htmlBody」などにして巡回登録してください。
これらのルールでは、子アイテムの抽出は行われませんが、対象URL自体の内容が抽出され、変化があれば保存されます。更新が検出されたページは、Notebookのメイン画面の一覧の上位に表示されます。


ページに更新があると、Notebookのメイン画面の新着ページ一覧の上位に表示されます。この時、一覧項目の下部の「rev.」の部分の番号が更新され、リンクとしてクリックできるようになります。

リビジョンの部分のリンクをクリックすると、このような画面で前のリビジョンとの差分が表示されます。
色の薄い部分が変化の無かった部分、赤みがかった文字色で打ち消し線が引かれているのは削除された箇所、普通の文字の部分が新たに追加された部分です。(追加された文章を読みやすいように、新着部分が普通の配色になっています。)
更に、サイドバーの「This Page」にある「History」のリンクからは、過去のリビジョン一覧や各リビジョンの内容、任意のリビジョン間の差分なども表示できます。

この機能を利用すれば、RSSなどの配信が行われておらず、対応するContentExtractorルールも用意されていないページも、他のサイトと一緒に更新チェックを行なえるでしょう。

なお、この機能で利用されている差分検出の処理は「Diff」というユニットで定義されています。
自作のスクリプトで差分処理を利用したい場合は、「doc」ディレクトリ内の「Diff.txt」の説明を参照してください。

Notebook.ngの巡回機能でブログなどの新着記事を自動収集

NILScriptに同梱されている「Notebook.ng」には、ブログなどの新着記事を自動収集する巡回機能が用意されています。
プラグインで定義したルールに従ってページの本文などを抜き出す「ContentExtractor」機能により、必要な部分だけを保存して、快適に読み進めることが可能です。

Notebookに巡回対象サイトを登録するには、巡回したいサイト上で「AddCrawl」ブックマークレットを実行してください。
すると、以下のような新規巡回項目設定画面が表示されます。



登録対象ページにRSSやAtomのフィードが用意されている場合は、URL欄の下に「rss feed」などとしてリンクが表示されます。このリンクをクリックすれば、フィードURLを対象とした巡回登録画面に移行します。

その下には、インストールされているContentExtractorプラグインとルールの一覧が表示されます。対応URL認識機能によって対応しているとみなされたルールがあると、リストの上位に表示されます。ルール名の下の説明に従って、使用したいルールを選択してください。

ルール名の横の「Test」のリンクからは、そのルールを使用した場合の抽出結果のプレビュー画面に進めます。「blog/rss」などの汎用的なルールを使用する場合は、上手く抽出できるか確認してから登録すると良いでしょう。

Options以下の部分では、高度な巡回オプションを指定できます。一般的なRSS系のルールの場合は、そのままでも問題ありません

「Filters」では、子アイテムに対して除外や改変などのフィルタリングを行うルールを定義できます。この機能はまだ仮のものなので、準備が整い次第別途説明します。

「Interval」は巡回間隔です。たまにしか更新されないページでは大きめの値にすることで、他の巡回をスムーズに出来る場合があります。

「Content Update Check」では、子アイテムごとの更新チェックを行うかどうかと、更新チェック時にHTMLタグ部分を無視するかどうかを指定できます。追記などがほとんどないサイトでは「No」のままでよいでしょう。画像URLのみの更新なども検出したい場合は「Including HTML Tags」を、テキストの更新のみを検出したい場合は「Without Tags」を指定します。

「Children Order」では子アイテムの並び順を指定できます。「No」を指定すると、常に全ての子アイテムがチェックされますが、記事が更新日時順に並んでいることが分かっているサイトでは、適切なオプションを指定する事で、処理を高速化させられます。

「Depth」は、列挙された子ページを更に巡回登録するかどうかのオプションです。子ページの巡回オプションは、その下の「Options for Children」などで指定します。これらは、掲示板のスレッド一覧などを巡回するための機能です。説明部分に「"Depth"を"Crawl Children"にすることで――」のような記載があるルール以外では、設定してもあまり意味がないので「Crawl this URL only」のままにしておいてください。

フォームの一番下の「Save」ボタンを押せば、巡回設定が登録されます。登録された巡回項目の設定変更や削除などは、サイドバーの「All Crawls」で表示される一覧から行なえます。

サイドバーの「Crawl: Disabled」となっている部分の右の「Start」をクリックして「Crawl: Enabled」にしておくと、定期的に巡回が実行されます。収集されたページは、Notebookのメイン画面の最近更新されたページ一覧などで閲覧可能です。

Webページなどを保存して閲覧・検索できる「Notebook.ng」

NILScriptの最近のバージョンから同梱されるようになった「Notebook.ng」は、Webページなどを保存して閲覧・検索できるスクリプトです。
ページの追加はブックマークレットなどから手動で行う他、RSSなどを巡回登録して自動で新着記事を取り込むことも可能です。
NILScriptにはWebページからの本文抽出処理をプラグイン方式で定義できる「ContentExtractor」機構が用意されており、ブログのサイドバーなどの不要部分を除いた本文だけを保存して快適に閲覧できます。更に、検索時にサイドバーのカテゴリ一覧などにマッチして関係のないページがヒットしてしまうという問題も避けられます。

NILScriptは元々このスクリプトを実装することを第一目標として開発を始めたものですが、ようやく主要な機能を実装し仕様も固まってきたので、そろそろ使い方の説明をしたいと思います。

Notebook.ngを使用するには、Notebook.ngをng.exeかngw.exeで実行します。スタートアップなどへの登録は手動で行ってください。
最初に実行すると、データの保存先などを設定するページがブラウザで開かれます。ここでData Directoryに「Portable」を指定すれば、NILScriptのディレクトリをUSBメモリなどで持ち出して複数のPCで利用することも可能です。


初期設定を行うと、以下のようなメイン画面が表示されます。このURLをブラウザのブックマークなどに登録しておいてください。

次に、サイドバーの「Bookmarklets」のリンクからブックマークレット一覧のページに行き、使用したいブックマークレットをブラウザのブックマークに登録します。

基本的なページの保存を行うには、「Scrap」ブックマークレットを使用します。保存したいページを表示した状態で登録したブックマークレットを開くと、範囲選択を行っている場合は選択範囲の内容が、そうでない場合はサイドバーなどの不要部分を自動的に取り除いた部分が、Notebookに新規項目として保存されます。この際、ページに埋め込まれている画像も保存されるため、ページがサーバから削除されてしまっても、支障なく閲覧できます。


ページの保存が行われると、以下のように保存されたページが表示されます。
ここでは、左のサイドバーから「タグ」としてキーワードを関連付けたり、ページの重要度を表す「スコア」を変更したりできます。

サイドバーの検索ボックスに検索ワードを入力すれば、該当するページを検索できます。キーワードの前に「:」を付けると、タグのみを検索対象に出来ます。スペースや「:」を含む語句を検索したい場合は「""」で囲んでください。他のもいくつか検索オプション構文が用意されていますが、詳しくは同梱の「doc\Notebook.txt」を参照してください。

なお、Notebook.ngはまだテスト段階のため、不具合や不便な点が残っていると思われます。見つけた場合は、BBSやTwitterなどでお知らせください。