feezch.infoで使われているあやしい技術を解説します

feezch.infoで使われている一部の技術を解説しようと思います。
Railsやnginxの設定の話は、僕が書くことでもないので、一般的なウェブサービスではあまり使われてなさそうな2ちゃんねるに関連した技術やあやしめ技術をいくつか選んでみました。

主に画像関連です。
クローラー周りも、とてもよく考えられたすばらしい仕組みを持っているのですが、特にあやしくなく本当にすごいので、ここでは省略します。
以下がもくじです。(リンクはつけ方が分からなかったので、ついてないです)

  1. 画像URLのルール集『ImageViewURLReplace.dat』を使って投稿内容から画像URLを抽出しダウンロードする
  2. グロ画像ブラックリスト『NGFiles.txt』でグロ画像をブロックする
  3. ウェブブラウザがリファラを送らないように画像にリンクする
  4. グーグルの新機能『Search by Image』を使ってサムネイルとインターネットのどこかにある元画像をリンクする

feezch.infoとは

まずfeezh.infoとは何かというと、最近僕が作って公開している2ちゃんねるのパートスレをフィードリーダーで読むためのウェブツールです。
とても便利であり、使っていない人はちょっとどうかと思うので、知らない方はfeezch: ヘルプをぜひ読んでみてみてください。

画像URLのルール集『ImageViewURLReplace.dat』を使って投稿内容から画像URLを抽出しダウンロードする

feezch.infoでは、2ちゃんねるの投稿に含まれる画像URLを抽出して、各画像からサムネイル(プレビュー用のとても小さな画像)を作成しています。
投稿から画像のURLを抽出する単純な方法として、

  • 拡張子が画像っぽいURLを正規表現で抽出する

といった方法があります。しかし、画像アップローダーの中には拡張子だけ見ても画像と分からないURLを生成するものが多くあり、たとえば、Twitterなどでよく見かけるtwitpicやyfrogも意味としては画像を指したリンクですが、単純な画像拡張子の正規表現では抽出できない形式をしています。こういう場合には、単純な正規表現による抽出では一部のURLを落としてしまいます。

また、

  • Cookieリファラを与えないと画像が取得できないことがある(取得できないとサムネイルが作成できない)
  • スクレイピングをしないと画像のURLが分からないことがある

ということもあり、URL抽出だけでなく、ダウンロード時の設定も必要になってきます。
さらに、JaneStyleなどの2ちゃんねるブラウザに慣れていると、

  • 動画のURLやアマゾンの商品URLなどからもサムネイルを表示して欲しい

などといった要求もでてきます。JaneStyleでは設定を行えばこういったことができているからです。

ImageViewURLReplace.dat

2ちゃんねるブラウザではできている――ではどうやっているのかというと、ImageViewURLReplace.datというファイルにしたがって画像のURLの抽出、ダウンロードを行っています。
これには、

  1. 画像を指すURLの正規表現
  2. 抜き出したURLを画像URLに変換するルール(正規表現による置換またはスクレイピングによる抽出)
  3. 画像URLにアクセスするときのルール(CookieRefererの指定)
  4. アクセスしてはいけないURL(グロ, おまんちんなど)

が定義されています。
このファイルは、2ちゃんねる2ちゃんねるブラウザのコミュニティによって編集され、まとめたものがインターネット上で公開されています。
歴史的な経緯はよく知りませんが、たぶんオープンソース2ちゃんねるブラウザ OpenJane に実装されて、その派生ブラウザであるJaneViewやJaneStyleなどで拡張されていったのではないかと思います。注意することは、ImageViewURLReplace.datと呼ばれているファイルにも、ブラウザの実装によってフォーマットや機能に違いがあるということです。僕の確認している範囲では、4種類におよぶ1行コメントの書き方があります。

JaneStyleでの仕様は、ImageViewURLReplace.dat [2ちゃんねる専用ブラウザ「Jane Style」オンラインヘルプ]で確認できます。JaneStyleの仕様(と思われる)でのImageViewURLReplace.datは、StreamingPlayer3で配布されています。
OpenJaneでの仕様は、ImageViewURLReplace.datで確認できます。OpenJane(実際にはその派生のJaneView)での仕様のImageViewURLReplace.datは、scriptstuffで配布されています。

詳細は、上記URLを見てほしいのですが、処理内容がイメージできるように簡単に説明します。

ファイルの各行は

URLの正規表現\t画像URLの置換パターン\tRefererの置換パターン\t拡張1\t拡張2...

(\tはタブ)
となっています。
たとえば、youtubeのサムネイルを抜き出すルールは

http://(?:[^/]+\.)?(youtube\.com/)(?:(?:verify_age\?next_url=/)?(?:w/)?(?:watch)?/?(?:\?|%3F|#).*v(?:=|%3D)|(?:.+?/)?v/|.+?[?&]video_id=)([\-\w]{11})	http://img.$1vi/$2/hqdefault.jpg

となっています。これにhttp://www.youtube.com/watch?v=b3d2o6caTMAを入力すると、

  1. URLが最初の正規表現にマッチ
  2. $& => http://www.youtube.com/watch?v=b3d2o6caTMA, $1 => youtube.com/, $2 => b3d2o6caTMA, などと変数がセットされる
  3. 2番目の引数 http://img.$1vi/$2/hqdefault.jpg を置換 => http://img.youtube.com/vi/b3d2o6caTMA/hqdefault.jpg
  4. 画像取得

といった処理を行います。3番目の引数はないので、Refererは必要ありません。3番目がある場合は、2番目と同じルールで変換を行った後、Refererにセットして、画像URLにアクセスします。

4番目の引数に$EXTRACTが指定してある場合は、RefererのURLをスクレイピングして画像URLを得ます。たとえば、twitpicのルールは、

http://twitpic\.com/(?!(?:photos|tag|show/large)/)(\w+)	$EXTRACT	$&/full#/$1	$EXTRACT	src="(http://[^"]+)

となっています。

これにhttp://twitpic.com/4fdrc5を入力すると、

  1. URLが最初の正規表現にマッチ
  2. $& => http://twitpic.com/4fdrc5, $1 => 4fdrc5などと変数がセットされる
  3. 2番目の引数 $&/full#/$1 を置換 => http://twitpic.com/4fdrc5/full#/4fdrc5
  4. 5番目の引数 src="(http://[^"]+) を置換 => src="(http://[^"]+) (変数がないのでそのまま)
  5. http://twitpic.com/4fdrc5/full#/4fdrc5 にアクセスしてCookieとコンテンツを取得
  6. コンテンツから5番目の正規表現 src="(http://[^"]+) でマッチング
  7. $EXTRACT => http://s3.amazonaws.com/twitpic/photos/full/267700901.jpg?AWSAccessKeyId=AKIAJF3XCCKACR3QDMOA&Expires=1318099070&Signature=lVFULcrM4BUS0WKKRYtj6zAv5Z4%3Dなどが変数にセットされる
  8. 2番目の引数 $EXTRACT を置換 => http://s3.amazonaws.com/twitpic/photos/full/267700901.jpg?AWSAccessKeyId=AKIAJF3XCCKACR3QDMOA&Expires=1318099070&Signature=lVFULcrM4BUS0WKKRYtj6zAv5Z4%3D
  9. スクレイピング時に受け取ったCookieと置換したRefererをセットして画像を取得

といった動きになります。置換パターンの変数$EXTRACT,$EXTRACT1〜9は、5番目の正規表現でマッチングした際の$1,$1〜$9に対応しています。

さて……
レガシーっぽくてなんだか面倒くさそうですが、どうやら、この処理系を書けばやりたいことが実現できそうです。

feezch.infoでは、ImageViewURLReplace.datのRubyライブラリを書いて、クローラーに組み込んでいます。基本的には、JaneStyle系の仕様にあわせていますが、せっかく配布されているのでJaneView系のファイルも読めるようにしています。完全に対応しているわけではないので、注意は必要ですが、上記のURLで配布されている両ファイルは適当にマージしてそのまま使うことができます。

Zch::ImageURL

Zchというのは、feezch.infoのために僕が作った超高性能なヒミツの2ちゃんねるRubyライブラリです。
ImageViewURLReplace.datの処理系もこの一部として実装されていて、本当はヒミツなのですが、特別にこの部分だけgithubにて配布することにしました。好きにしてよいです。

ImageViewURLRepalce.datのRuby実装 — Gist

ImageViewURLReplace.datの読み込み

デフォルトでは、ライブラリのファイルと同じディレクトリにある"ImageViewURLReplace.dat"というファイルを読みます。
変更したい場合は、はじめに

Zch::ImageURL.load("./ImageViewURLReplace.dat")

と呼べば、指定されたファイルを使うようになります。

画像URLの抽出

Zch::ImageURL.extractで抽出できます。

content = <<CONTENT
すごい
tp://www.youtube.com/watch?v=b3d2o6caTMA

すごい
ttp://twitpic.com/4fdrc5

CONTENT

Zch::ImageURL.extract(content).each do |m|
  pp({
       :match_url => m.match_url,
       :url => m.url,
       :referer => m.referer,
       :image_url => m.image_url,
       :ng => m.ng?
     })
end
{:match_url=>"tp://www.youtube.com/watch?v=b3d2o6caTMA",
 :url=>"http://www.youtube.com/watch?v=b3d2o6caTMA",
 :referer=>nil,
 :image_url=>"http://img.youtube.com/vi/b3d2o6caTMA/hqdefault.jpg",
 :ng=>false}
{:match_url=>"ttp://twitpic.com/4fdrc5",
 :url=>"http://twitpic.com/4fdrc5",
 :referer=>"http://twitpic.com/4fdrc5/full#/4fdrc5",
 :image_url=>
  "http://s3.amazonaws.com/twitpic/photos/full/267700901.jpg?AWSAccessKeyId=AKIAJF3XCCKACR3QDMOA&Expires=1318100850&Signature=zVcPfY3cy%2B9z5Zet8XUumlSi%2Fg0%3D",
 :ng=>false}

となります。match_urlにマッチしたURLそのまま、urlには、正規化されたURLが入っています。image_urlは$EXTRACTモードの場合は、ネットワークにアクセスするので注意が必要です。事前にネットワークにアクセスするか(ブロックしたりネットワークのエラーで例外が発生したりするか)チャックしたい場合は、network_required?でチェックできます。またng?がtrueの場合、NG指定のURLです。

画像データの取得

Zch::ImageURL.extractで抽出したあと、fetchメソッドで取得できます。

content = <<CONTENT
すごい
tp://www.youtube.com/watch?v=b3d2o6caTMA

すごい
ttp://twitpic.com/4fdrc5

CONTENT

Zch::ImageURL.extract(content).each do |m|
  content, header = m.fetch
  if (content)
    p m.url
    p "content-type: #{header['content-type']}, size: #{content.length}"
  end
end
"http://www.youtube.com/watch?v=b3d2o6caTMA"
"content-type: image/jpeg, size: 10926"
"http://twitpic.com/4fdrc5"
"content-type: image/jpeg, size: 203497"

となります。取得した内容とレスポンスヘッダーを返します。
fetchで使うメソッドは、デフォルトでは、

Zch::ImageURL.fetcher = Proc.new do |url, http_options|
  content = nil
  headers = {}
  if (RUBY_VERSION >= "1.9.0")
    OpenURI.open_uri(url, "r:binary", http_options) do |f|
      f.meta.each do |k,v|
        headers[k] = v
      end
      content = f.read
    end
  else
    OpenURI.open_uri(url, http_options) do |f|
      f.meta.each do |k,v|
        headers[k] = v
      end
      content = f.read
    end
  end
  [content, headers]
end

となっています。インターフェースをあわせれば、置き換えることができます。
テキトウに使う分にはこのままでいいと思いますが、ウェブサービスなどで使う場合には置き換えたほうがいいと思います。
feezch.infoでは、クローラー用のfetchメソッドで置き換えて使用しています。
たとえば、

  1. 接続タイムアウトの設定
  2. 受信タイムアウトの設定
  3. 最大受信サイズの設定
  4. ローカルネットワークへのアクセス拒否
  5. タイムアウトするホストへ連続アクセスしないための制限
  6. これらのエラーに対して適切な例外を投げる

などの機能を追加しています。

グロ画像のブラックリスト『NGFiles.txt』によるグロ画像判定

2ちゃんねるでは、よく嫌がらせとしてグロ画像(首がもげたり顔が爆発したりしている死体などの画像)が投稿されています。僕はできるだけグロ画像を見たくないので、サムネイルにする前の段階でグロ画像を判定してブロックする仕組みがあるとうれしいです。

画像認識がある程度分かる自分としては、グロ画像を機械学習して云々と考え始めてしまうのですが、グロ画像というのは、全裸の老婆から昆虫までさまざま種類があり、実用レベルの精度を出すのは無理だろと思います(たとえば、後述のNGFiles.txtから抜き出したグロ画像の説明一覧を見てください)。
また、2ちゃんねるに投稿されるグロ画像はほとんどが使いまわしのコピペなので、ハッシュ値の完全一致でもかなりブロックできるだろうという予想があります。

NGFiles.txt

NGFiles.txtとは、2ちゃんねるブラウザJaneStyleが使っているブラックリストです。OpenJane由来のものなのかなど、歴史的なことはよく知りません。
JaneStyleでは、画像を選んで「対象をNGファイルに追加」を選択すると、画像のハッシュ値がNGFiles.txtに追加されます。NGFiles.txtに存在している画像はビューワーで表示されなくなり、グロ画像等を見なくてもよくなるという仕組みです。
このファイルもまた、2ちゃんねるブラウザのコミュニティによって編集され、まとめたものがインターネット上で公開されています。
たとえば、JaneView用ツールまとめ@wiki - NGFiles.txtにあります。

feezch.infoではグロ画像ブロッキングの最初の処理として、このNGFiles.txtを使ったブラックリスト方式でグロ画像の判定を行っています。

NGFiles.txtのフォーマット
009R8F7EL0LGTSMOQOB2HB3VE5=*刃物刺さりまくり惨殺死体|グロ|死体
00C4N25VHBALTFGUN4JF6RD7B4=*頭部が縦に割れてる
00DAMRQA5JM108NQGUKBCDNQQ2=*うんこ

ようになっています。=*が区切り文字に見えますが、=が区切り文字で*は何かの印だと思います(よく分かっていない)。
最初のカラムがハッシュ値で、2番目のカラムがコメントです。
この短いハッシュ値は独自のハッシュアルゴリズムで、MD5をスクランブルして作ります。(ぎゃー)

feezch.infoでのNgFileクラス

ハッシュは独自のものですが、nghash.zipC言語での実装があります。
feezch.infoでは、これをRuby 1.9.2 + Rails 3.0用に移植して、ActiveRecordのModelとして使っています。

コードは、
NGFile.txt用のActiveRecordのModel (Ruby 1.9.2) — Gist
においてあります。

Rails用ですが、MD5からNGFiles用のハッシュ値を得る部分は、適当に切り出せると思います。その部分だけあれば、適当に使うことができます。

rails runner NgFile.pull('./NGFiles_01.txt')
rails runner NgFile.pull('./NGFiles_02.txt')
...

で差分をデータベースに取り込んで、あとは、

md5 = Digest::MD5.hexdigest("画像データ")
if (ngfile = NgFile.find_by_md5(md5))
  # ブロッキングの処理
end

と判定に使っています。

ウェブブラウザがリファラを送らないように画像にリンクする

feezch.infoのフィードをlivedoor ReaderGoogle Readerなどウェブアプリケーション型のフィードリーダーで読んでいる場合、フィードはウェブブラウザ上で表示され、フィードには画像へリンクが張られていることがあります。このような画像へのリンクをクリックしたとき、リファラとしてフィードリーダーのアドレスを送ってしまうと画像を置いているホストからアクセス拒否され、画像を表示できない場合があります。こういう挙動をするホストで有名なところにFC2があります。

feezch.infoでは、この問題を回避するために、ウェブブラウザがリファラを送らないように画像にリンクする2種類のあやしいテクニックを使っています。

Meta refreshよるRefererのクリア

IEやFirefoxでは、

<meta http-equiv="refresh" content="0; url='http://example.com/'" />

のようなMETAタグでジャンプした場合に、Refererが送信されない実装になっています。
ただし、ChromeではRefererが送信されてしまいます。

Data URI scheme + Meta refreshによるRefererのクリア

FirefoxChromeに実装されているData URI schemeを使うと、URIの中にデータを埋め込むことができます。
データの種類としてtext/htmlを指定すると、URIからHTMLページを生成して、そのページをブラウザで開くことができます。
そして、このData URI schemeで生成したHTMLページからのリンクにRefererが送信されない実装になっています。

// ブラウザのアドレスバーへコピペすると確認くんへのリンクを作ります
data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3Ca%20href%3D%22http%3A%2F%2Fwww.ugtop.com%2Fspill.shtml%22%3Elink%3C%2Fa%3E%3C%2Fbody%3E%3C%2Fhtml%3E

この性質を使うと、

  1. JavaScriptURIに埋め込んだMeta refreshを生成
  2. location.hrefに代入しData URIへジャンプ
  3. 埋め込まれたページにあるMeta refreshでさらにジャンプ

という多段のジャンプによってリファラを送らないリダイレクタを作ることができます。

UAによってリダイレクトの方法を分ける

あまりたくさんのブラウザで調べていないし、動かないブラウザも結構ありそうですけど、いまのところ、こんな感じのviewでリダイレクタを作っています。フィードから外部の画像にリンクするときは、jump/noref?url=URLでリンクします。

<!-- noref.html.erb -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="ja">
  <head>
    <title>
      redirecting to <%= params[:url] %>..
    </title>
    <% if request.user_agent.include?('MSIE') %>
      <meta http-equiv="refresh" content="0; url='<%= raw encode_uri(params[:url]) %>'" />
    <% else %>
      <script type="text/javascript">
        var html = "<html><head><meta http-equiv=\"refresh\" content=\"0; url='<%= raw encode_uri(params[:url]) %>'\" /></head></html>";
        location.href = "data:text/html;charset=utf-8," + encodeURIComponent(html);
      </script>
    <% end %>
  </head>
  <body>
    redirecting to <%= link_to params[:url], params[:url] %>..
  </body>
</html>

Data URI Schemeが使えそうにないIEだったらMeta refresh、それ以外ならData URI Schemeを使ってparams[:url]へジャンプします。
encode_uriはこんなのです。('もエンコードしてることに注意)

  def encode_uri(s)
    URI.escape(s, /[^-_.!~*()a-zA-Z\d;\/?:@&=+$,#]/n)
  end

ここのエスケープの処理はちょっと自信が無いので、まずそうだったら教えてください。

グーグルの新機能『Search by Image』を使ってサムネイルとインターネットのどこかにある元画像をリンクする

feezch.infoを作ろうと思いたったのは、1年以上も前なのですが、どうしても気になる超重要な問題があって、手が出ずにいました。

  • フィードを配信してLDRで読みたい=インターネット上で公開するってこと(プライベートにできない)
  • どうせインターネット上で公開するなら過去ログも有効に使いたい
  • 画像スレも読みたいよね。サムネイルも出てほしい
  • しかしうpロダの画像はすくに消えてしまう
    • つまり画像へのリンクが失われてしまうのだ
    • 画像へのリンクが失われた画像スレの過去ログはゴミに等しい
  • だからといって画像自体は持ちたくない
    • ハードディスク容量が足りなくなりそう
    • グロや無修正や児童ポルノなどが張られることがあって運用が面倒くささそう
  • でも画像スレの過去ログをゴミにしたくない
  • あばばばばb

なんらか元画像へアクセスできる可能性は残したいけど、自分のところで画像をホスティングするのは避けたい、といった自分勝手な悩みですが、これがどうにかできない限り、feezch.infoは運用できないと考えていました。
最初に考えていたのは、GazouMe: 画像アップローダーというサイトを作って、ここに画像を投げるようにして、アクセスの多い人気の画像を残すようにする+報告のあった画像を自動的に消して二度と登録できないようにする、ということでした。問題を薄くするというか運用をできるだけ楽にして、一部の画像だけでもリンクを残そうというものです。
でも…これでもアレガー・・・・・うー・・・あー…逮・・・オー

となっていたときに、グーグルの新機能
Search by Image – Inside Search – Google
が現れました。
これはコンテンツベースの画像検索エンジンです。日本語で言うと、「画像による画像の検索」です。つまり画像をクエリに画像が検索できる。普通は同じものが写っている別の画像を探すときに使うものですが、Googleの実装だと違うサイズの同じ画像や別のホストにある同じ画像も探すことができます。
つまりどういうことかというと、

  • サムネイルをクエリにインターネットのどこかにある元画像が検索できる

わけです。
feezch.infoは画像のサムネイル(プレビュー用のとても小さな画像)を保持していますから、たとえうpロダの元画像が消えたとしても、元画像がインターネット上に存在する限り、サムネイルをクエリにGoogle検索すると見つかるのです。ゆるいリンクはずっと繋がったままなわけです。(本当にマズイ画像や人気のない画像はインターネットから消えていくでしょう)
というわけで、feezch.infoのサムネイルには『この画像でググる』という検索アイコンが付いています。

Search by ImageのAPIを勝手に使う方法

Search by Imageを使えば、いいということは分かりましたが、出たばかりのサービスだしAPIが公開されていないようです。そもそもGoogleは昔クエリつきのリンクを張るなと言っていた気がするし、公開されないかもしれません。
と思っていたところ目に付いたのが、Search by ImageのFirefox拡張です。
これをインストールすると、ウェブページ上の全ての画像に検索アイコンが現れ、アイコンをクリックすることで、画像をクエリにググれるようになっていました。まさに僕がやりたいことです。この拡張をリバースエンジニアリングすればAPIの使い方が分かりそうです。

さて、だんだんタイトルどおり、あやしくなってきました。僕としては、専用のFirefox拡張を作ってまで使ってほしそうなので、ぜひ有効に使わせてもらいましょう、という自分勝手な解釈により勝手に使っています。使おうと思う方は自分が何をしているのかよく考えてからにしてください。

Firefoxの拡張形式であるxpiは、zipフォーマットなので解凍できます。中身はJavaScriptで実装されているので、特にあやしいことをすることなくソースコードが見れました。

画像のURLからクエリを送っている箇所は、quimby.jsの192行目辺りにあります。

    var newTab = gBrowser.addTab(
        'http://' + SERVER + '/searchbyimage?' +
        FIREFOX_EXTENSION_VERSION_PARAMETER_NAME + '=' +
        FIREFOX_EXTENSION_VERSION +
        '&image_url=' + encodeURIComponent(src));
    gBrowser.selectedTab = newTab;

サーバーの/searchbyimageにimage_url=URLというパラメーターを渡せばいいだけです。
いくつか試したところ、ホストは、google.co.jpでも可能(日本語表示になる)、バージョンはパラメーターの互換性のためについていると思うのですが、image_urlのパラメーターは多分変わらないだろうと思うので、外しています。

ということで、

http://www.google.co.jp/searchbyimage?image_url=http://www.st-hatena.com/users/ul/ultraist/user_p.gif
とすれば勝手に使えます。

まとめ

feezch.infoの画像周りで使われている2ちゃんねるブラウザ由来の技術、各種あやしい技術を突然妙なテンションで解説しました。長い。

ImageViewURLReplace.datはサイトのHTMLが変更されたりで結構使えなくなっているパターンが入っているので、使いながら修正する必要があります。スレを見てマージするのがメンドイので、AutoPagerizeのSITEINFOようにWedataとか使ってみんなで共有したら便利そうです。

NGFiles.txtは思ったよりもヒットしないです。まとめを使うよりも自分ひとりが3日くらいかけてグロ画像を集めまくったほうがいいリストが作れそうです。これもWedataで共有すると便利そう。

2ちゃんねるの周辺ツールは全体的に時代遅れに見えますけど、ユーザーが多くて十分なデータがあるので、互換性を持たせた実装を作るといろいろ便利になるものも多いと思います。

at your own risk
welcome to underground