2008年10月26日日曜日

Proxomitronでよく使われる表現でクオートされた "<>" に誤爆する問題に対処する

元ネタは「Proxomitron フィルター作成スレッド Part10」の45氏
(ISP規制で2chに投稿できないので、こちらに書きました。)

問題の概要

45氏が提起された問題を以下にまとめます。

  • Proxomitronでよく使われる表現「<a\s[^>]++href=」は「<a onclick="for(i=0;10>i;i++){...}">」に誤爆する
  • &;lt;a title="<test>">」は「&;lt;>」をHTMLエンティティ化すれば対処できる
  • JavaScriptコードはHTMLエンティティ化すると動作しない。どうしよう?

解決法:初めからクオートされた "&;lt;>" に対応できるフィルタにする

<a\s[^>]++href=<a(\s[a-z]+(=$AV(*)|(^=)))++\shref= に置換することで、対応します。

[Patterns]
Name = "Kill <a> ad (anti quoted <>) [2008/10/26] test1"
Active = TRUE
URL = "$TYPE(htm)"
Limit = 256
Match = "$NEST(<a(^(^\s)),"
        "(\s[a-z]+(=$AV(*)|(^=)))++\shref=$AV(http://$LST(AdList)*)*"
        ",</a>)"

この記法、汎用性はありますが美しいコードではないのがネックです。

そもそも属性値に "&;lt;>" と書いているサイトはどれぐらいあるのか

「>」と書いているサイト、「&gt;」と書いているサイト、どちらも見たことがありますが、母数が少ないためにどちらが多いとは断定し難い状態です。
(どちらかといえば、しっかりしたHTMLが増えてきていて 「&gt;」と書いているサイトが多い印象はあります。)

JavaScriptに限定すれば、<a onclick="func();"> (関数埋め込み型) や <a id="TEST"> (外部スクリプトでイベントハンドラを割り当てる) と比べて、<a onclick="for(i=0;10>i;i++){...}"> が圧倒的に少ない、のは間違いないと思います。
JavaScriptライブラリの影響で、一昔前と比べて手打ちでコードを入力する機会が減っているのかも。

結論:結局どう対応すればいいのか

完全対応を目指すなら、既存のフィルタをクオートされた "&;lt;>" に対応できるフィルタに修正するのが一番。
ですが、すべてのフィルタを修正していくのは大変です。
経験上、クオートされた "<>" を見る機会がほとんどないので、問題が発生したときに原因となるフィルタを対処療法的に修正していくだけで十分だと思います。

ラベル: ,

2008年9月28日日曜日

AutoPagerize用SITEINFO + SITEINFOの書き方 まとめ

かねてより試してみたかったGreaseMonkeyスクリプト「AutoPagerize」が非常に便利だったのでWedata未登録のSITEINFOを書いてみました。

AutoPagerize用SITEINFO

var SITEINFO = [
 {
  name:         'ブラック会社に勤めてるんだが、もう俺は限界かもしれない',
  url:          '^http://ueharasan\.y\.ribbon\.to/html/',
//  Unicodeエスケープ前の文字列 (Wedata登録時はエスケープしなくてもOK)
//  nextLink:     '//a[starts-with(text(),"次") or contains(text(),"進む")][1]',
  nextLink:     '//a[starts-with(text(),"\u6B21") or contains(text(),"\u9032\u3080")][1]',
  pageElement:  '//a[starts-with(text(),"\u6B21") or contains(text(),"\u9032\u3080")]/preceding-sibling::*[self::img or self::br or self::hr]',
  insertBefore: '/html/body/hr[last()]',
  exampleUrl:   'http://ueharasan.y.ribbon.to/html/1to1.htm'
 },
 {
  name:         'ふたばちゃんねる',
  url:          '^http://[^\.]+\.2chan\.net/[^/]+/[\da-z]+\.htm$',
  nextLink:     '//form[substring(@action, string-length(@action) - string-length(".htm") + 1) = ".htm"][last()]',
  pageElement:  '//hr/following-sibling::table[@align="right"]/preceding-sibling::node()[not(self::center or self::p or self::hr and @width)]',
  insertBefore: '//hr/following-sibling::table[@align="right"]',
  exampleUrl:   'http://nov.2chan.net/q/futaba.htm'
 },
]

SITEINFOの適用方法

  • "C:\Documents and Settings\\Application Data\Mozilla\Firefox\Profiles\****\gm_scripts\autopagerize\autopagerize.user.js" の var SITEINFO に記入する
  • AutoPagerize - wedata にSITEINFOを登録する

上記いずれかの方法でSITEINFOを適用できます。

ちなみに、この記事に書いたSITEINFOはwedataに登録済みなので、autopagerize.user.js を編集しないでも使えます。
SITEINFO作成時には「autopagerize.user.js でテストして、正常ならWedataに登録する」という流れでOKなようです。

XPathの参考URL

XPath Checker :: Firefox Add-ons
ページ上で右クリックすると、選択したオブジェクトを指定するためのXPathを出力してくれるFirefox拡張です。
右クリック後に現れたウインドウでXPathを指定すると、指定したオブジェクトのみを表示してくれるので、XPath構文をテストする用途に向いています。
AutoPagerize用のXPathをかんたんに作るためのブックマークレット AutoPagerize IDE - bits and bytes
このブックマークレットを使用すると、ページ上で選択したオブジェクトのXPathを出力してくれます。
XPathを指定したオブジェクトにボーダー線で付けて装飾することもできます。
XPath Tutorial
XPathの構文例に対応したツリー構成のXMLソースを表したチュートリアル。
英語が苦手な私でも、結果が一目瞭然なのが有り難かったです。
Latest topics &gt; CSS3セレクタとXPathでの表現の対応表 - outsider reflex
CSSセレクタに対応したXPathを表にした記事。
CSS習得済みの人にとってはわかりやすいと思います。
XPath リファレンス
MSDNのXPathリファレンス。
XPathを全般に渡って解説されているので、ここで全体を把握してキーワードを元にGoogle検索で詳細を探すと良いと思います。

Tips/FAQ 的なこと

"autopagerize.user.js" に記入するXPathはマルチバイト文字をUnicodeエスケープしないと働かない

マルチバイト文字は Text Escaping and Unescaping in JavaScript 等で \uXXXX の形式にしないと働かないので、注意してください。

XPath Checker ではマルチバイト文字をUnicodeエスケープすると働かない

"autopagerize.user.js" と正反対の性質なので、要注意です。
「XPath CheckerでXPath構文をテスト → "autopagerize.user.js" にUnicodeエスケープしたXPathを入力してテスト」という流れが良いと思います。

Wedata登録時にマルチバイト文字がUnicodeエスケープされていない場合は、自動的にエスケープされる

Wedata登録時にはUnicodeエスケープしていないXPath構文をそのまま入力すると、Wedata(が出力するJSON)にはUnicodeエスケープされたデータが登録されます。
もっとも、その前にSITEINFO制作者は "autopagerize.user.js" でテストしているでしょうから、この機能に意味があるかどうかに疑問は残りますが…。

/preceding-sibling::* は要素ノードのみにマッチし、/preceding-sibling::node() で全ノードにマッチする

/preceding-sibling::node() でテキストノードを含めたすべてのノードを取得できます。

反面、node()はパフォーマンスが若干悪くなるそうなので、必要なときだけ node() を使う方針でいいと思います。

ブックマークレット「AutoPagerize IDE」は一部のXPath構文で指定されたオブジェクトを強調表示しない

AutoPagerize IDEはテキストノード、hr要素ノード、/preceding-sibling::* のオブジェクトを枠で囲って強調表示しません。
複雑なXPathを指定する時は XPath Checker を使う必要があります。

備考

Autopagerizeを導入して2日目ですので、おかしな記述があったり、用語の使い方が間違っているかもしれません。
XPathも今回初めて触れたので、覚え間違いがあるなら早めに修正したいところです。

コメントはお気軽にどうぞ。

ラベル: , , ,

2008年3月20日木曜日

[Prx] Googleが持つほとんどのURLにマッチさせる方法

現実的な路線は、以下のようになると思います。

([^/]++.|)google.co(m|.jp)
[^.]+.google.co(m|.jp)

どうして上のような表現になるのか?
これを読み解くにはドメインについて理解を深める必要があります。

Googleが持っているサブドメイン

以下のURLを例にとってみましょう。

http://www.google.com/

このURL中の www.google.com (以下、この部分をホスト名と呼びます) は3つのドメインに分割することができます。

第1レベルドメイン (トップレベルドメイン)
  .com
第2レベルドメイン (セカンドレベルドメイン)
  .google
第3レベルドメイン (サードレベルドメイン)
  www.

ホスト名は第2レベルドメイン以上を持ってアドレスが与えられます。
だから、もし誰かがドメインを取得しようとしたら、少なくとも第2レベルドメインまでの名前を持ったURLを得ることになります。

Googleは google.com のドメインを持っており、第3レベルドメイン以降のドメインはサブドメインと呼ばれるものです。(www. はサブドメイン)
サブドメインはGoogleが自由に作ることができるので、Googleはサブドメインを作ることでいくらでもURLを増やせます。

http://mail.google.com/
http://groups.google.com/
http://maps.google.com/

これらにマッチさせる表現は、([^/]++.|)google.com[^.]+.google.com となるでしょう。
前者はサブドメインがないURL (google.com) にマッチしますが、後者はマッチしません。

Googleが持っているトップレベルドメイン

http://www.google.com/http://www.google.co.jp/ はURLこそ違いますが、ページに表示される内容は同じです。
よくみると、.com と .co.jp が違うことがわかりますね。

.jp は日本を表すドメイン名です。
Googleは世界中にサーバを設置しているため、google.us (アメリカ)、google.fr (フランス)など様々な国のドメインを持っています。
第1レベルドメインはトップレベルドメイン一覧にあるように、非常にたくさんの数があります。
Proxomitronで第1レベルドメイン全てを網羅しようとするなら、パフォーマンスの観点からリストを使うことになりますが、そこまでしなくても自分が利用している第1レベルドメインだけ網羅すれば十分かもしれません。

まとめ

google.com と google.co.jp のサブドメインを含むURLにマッチする表現は

([^/]++.|)google.co(m|.jp)
[^.]+.google.co(m|.jp)

となります。
前者はサブドメインがないURL (google.com) にマッチしますが、後者はマッチしません。

これでほとんどのURLに対応できると思いますが、Googleは500個以上のドメインを持っているそうなので、全ドメインに対応するのはまず無理でしょう。
サブドメインとトップレベルドメインの仕組みを理解した上で、必要なドメインを取捨選択するのが妥当だと思います。

ラベル: ,

2008年2月21日木曜日

addEventListener(), attachEvent() を使ってaddLoadEvent()

addLoadEvent() は少し古い技術なので、DOM操作で使われるaddEventListener()attachEvent() を利用して書き換えてみました。

function addLoadEvent(func) {
 if(typeof window.addEventListener == 'function'){ // addEventListenerが使えるなら
  window.addEventListener('load', func, false);
  return true;
 } else if(typeof window.attachEvent == 'object'){ // attachEventが使えるなら(IE用)
  window.attachEvent('onload', func);
  return true;
 }

 var oldonload = window.onload; // どちらも使えないなら
 if (typeof window.onload != 'function') {
  window.onload = func;
 } else {
  window.onload = function() {
   oldonload();
   func();
  }
 }
}

addEventListener() と attachEvent()

addEventListener() と attachEvent() は、DOMで扱われるノードに対するイベント(マウスクリックとか)をトリガーとして、メソッドを追加します。
addEventListener() はW3Cで定義された標準メソッドで、attachEvent() はIEの独自拡張です。IEは addEventListener() を利用することができません。
attachEvent() は追加されたメソッドの処理順がめちゃくちゃなんですが、window.onload よりは使い勝手がよいので採用しました。

window.onloadだけで作られたaddLoadEvent()との違い

まず、次のJavaScriptコードを実行してみてください。

<script type='text/javascript'>
function addLoadEvent(func) {
 var oldonload = window.onload;
 if (typeof window.onload != 'function') {
  window.onload = func;
 } else {
  window.onload = function() {
   oldonload();
   func();
  }
 }
}

function hello(){
 alert('Hello, World!');
}

function yes(){
 alert('Yes, my master.');
}

addLoadEvent(hello);
window.onload = yes;
</script>

上記コードを実行すると、yes() が実行され、hello()が実行されない結果に終わります。
addEventListener() で window.onload 追加したメソッドが window.onload = yes; によって上書きされてしまっているのです。

上書きされないようにするためには addLoadEvent() を addEventListener() を利用したコードに書き換えます。

<script type='text/javascript'>
function addLoadEvent(func) {
 if(typeof window.addEventListener == 'function'){
  window.addEventListener('load', func, false);
  return true;
 } else if(typeof window.attachEvent == 'object'){
  window.attachEvent('onload', func);
  return true;
 }

 var oldonload = window.onload;
 if (typeof window.onload != 'function') {
  window.onload = func;
 } else {
  window.onload = function() {
   oldonload();
   func();
  }
 }
}

function hello(){
 alert('Hello, World!');
}

function yes(){
 alert('Yes, my master.');
}

addLoadEvent(hello);
window.onload = yes;
</script>

これで hello(), yes() 両方の関数が有効になりました。
addEventListener() は window.onload に関数を格納しているわけではないので、window.onload = yes; が実行されて機能が上書きされる心配はありません。

どんな状況で addLoadEvent() が役に立つのか

  • 他人が書いたコードに機能を追加するとき
  • Proxomitron, Greasemonkey 等で既存サイトに機能を追加するとき

# 逆から見れば、一からJavaScriptコードを書くときには、addLoadEvent() を使わず addEventListener() を使った方がわかりやすいかもしれません。

ラベル: ,

2008年2月20日水曜日

JavaScriptでinclude

JavaScriptはVer.2.0から include が実装されるそうですが、それまでは長いコードを一つのJSファイルにまとめると管理が大変です。かといって、複数ファイルにコードを分割して、script要素を複数宣言するのも手間がかかります。
そこで、includeの代用となる関数を作りました。

// 外部JavaScriptをインクルード
function include_javascript(jspath){
 jspath = jspath.toString();

 var scriptElm = document.createElement('script');
 scriptElm.type = 'text/javascript';
 scriptElm.charset = 'utf-8';
 scriptElm.src = jspath;
 var head = document.getElementsByTagName('head')[0];
 head.appendChild(scriptElm);
}

// 外部JavaScript群をインクルード
function include_javascripts(jspaths){

 for(var key in jspaths){
  include_javascript(jspaths[key]);
 }
}

関数の使い方

1つの外部JavaScriptをincludeするには、以下のように書きます。

include_javascript('./js/hogehoge.js');

複数の外部JavaScriptをincludeするには、以下のように書きます。

var js_path = [
 './js/hogehoge1.js',
 './js/hogehoge2.js'
];
include_javascripts(js_path);

注意点

引数のJavaScriptパスはHTML文書からの相対パスか絶対パスを指定してください。(関数を呼び出したJavaScriptファイルからの相対パスではありません)

仕組み

include_javascript() は引数で指定された外部JavaScriptファイルを呼び出すscript要素を生成します。
include_javascripts() は引数で渡された配列の要素数だけ include_javascript() を繰り返します。

ラベル: ,

2008年2月19日火曜日

JavaScriptでExcelデータをtable要素に変換

(X)HTMLのtable要素は多量のデータを整理してくれる優れものですが、(X)HTMLソースからは直感的に全体像が見て取れず、それ故、編集が面倒、という理由から今まで敬遠していました。
この欠点を解消するため、Excelの表データをtable要素に変換するJavaScriptを書いてみました。

使い方

  1. Excelファイルを開き、table要素に変換したい部分を範囲選択してコピー (マウスでぐぐ~っと選択すればOK)
  2. 変換スクリプトのフォーム上に貼り付け
  3. [変換] ボタンをクリック!
  4. table要素に変換されたHTMLソースが出力される

内部動作 (アルゴリズム)

Excelからクリップボードにコピーされたデータは「タブ文字区切りのCSVフォーマット」となっており、これを array[行][列] で構成される多次元配列に変換します。
(各セルにあるデータはタブ文字で区切られているので split("\t") で良さそうに思えますが、実際にはセルデータに改行が入っているデータをコピーした時にセルデータ全体をダブルクォートで括られるケースがあり、これに対応するために少し複雑な処理をしています。)
多次元配列に変換したら、配列データを元にtable要素に変換して終わりです。

# 特殊なことはしていないつもりですが、平均よりちょっとユーザー定義関数が多めかもしれません。
# preg_replace() とか hreg_replace() は個人的趣向がかなり入っているので、人によっては読みづらいかもしれません……。

更新履歴

2008/02/19 2:00
 - 初版
2008/02/20 1:23
 - ダブルクォートで括られたセルデータが改行区切りで指定されていたとき、一つのセルデータとして扱う場合があった不具合を修正 ("hogehoge"\nhoge2")

既知の不具合/仕様

  • 結合されたセルがtable要素に反映されない。(クリップボードに格納されたデータにセルの結合を示す情報がないので、対応できません。)

ToDo

  • 1列目を項目名として扱い、[1列目のデータをth要素に変換する] オプションを追加
  • table要素からExcelデータに戻せるように変換

ラベル:

2008年2月11日月曜日

右クリック禁止を解除するProxomitronフィルタ

右クリックメニューの表示を禁止しているサイトで右クリックメニューを表示させます。
以下の3つのフィルタで実現している機能全てを含んでおり、それ以上の対策も施してあります。

  • Allow right mouse click
  • Allow right mouse click 2
  • Allow right mouse click plus
[Patterns]
Name = "Allow contextmenu event [js] [2008/02/11] test2"
Active = TRUE
URL = "$TYPE(htm)"
Limit = 8
Match = "(^(^</head>))$STOP()"
Replace = "\r\n<script type='text/javascript'>\r\n"
          "function addLoadEvent(func) {"
          " if(typeof window.addEventListener == 'function'){"
          "  window.addEventListener('load', func, false);"
          "  return true;"
          " } else if(typeof window.attachEvent == 'object'){"
          "  window.attachEvent('onload', func);"
          "  return true;"
          " }"
          ""
          " var oldonload = window.onload;"
          " if (typeof window.onload != 'function') {"
          "  window.onload = func;"
          " } else {"
          "  window.onload = function() {"
          "   oldonload();"
          "   func();"
          "  }"
          " }"
          "}"
          "function killContextmenuEvent(){"
          " if(typeof document.oncontextmenu == 'function'){"
          "  document.oncontextmenu = null;"
          " }"
          ""
          " if(typeof document.onmousedown == 'function'){"
          "  document.onmousedown = null;"
          " }"
          ""
          " var tag = document.getElementsByTagName('*');"
          ""
          " for(i = 0, L = tag.length; i < L ; i++){"
          "  tag[i].oncontextmenu = null;"
          "  if(navigator.appName == 'Microsoft Internet Explorer'){"
          "   tag[i].onmousedown = null;"
          "  }"
          " }"
          "}"
          "addLoadEvent(killContextmenuEvent);"
          "\r\n</script>\r\n"

動作検証

実際に動作しているか確認するためのテストページも作ってみました。

onmousedown について

onmousedown はマウスボタンが押されたときに発動します。
このことから、onmousedown は右クリック禁止以外の用途で使われる事があると想像でき、onmousedownに関する処理を無効化すると、右クリック禁止以外の場面で使用している重要な機能を失ってしまう可能性があります。
私の検証した範囲ではonmousedown が右クリックに反応するのはIEのみという結果だったので、このフィルタではonmousedown の無効化処理をIE限定にしています。
(マウスイベントが右クリックによるものかの判定できなかったので、やむなくブラウザの名前で判定しました)

ただし、通常の使い方で表示領域全体を監視する document.onmousedown を使用する必要性はまずないと思いますので、こちらは機能が使えれる環境であれば有効になる処理にしてあります。
もっとも、documentオブジェクトからonmousedownへのアクセスはIEの独自拡張っぽいです。

addEventListenerへの対策

addEventListenerにどうやって対策したものか、考えあぐねています。
addEventListener で追加したイベントハンドラはremoveEventListenerで解除できるようですが、メソッド名がわからなければ手が出せません。
悩んだ末に考えついた対策は次の2通り。どちらも形になるところまで進んでいません。

  • addEventListener でデフォルトアクションを発生させる処理を追加する
  • removeEventListener ですべてのイベントハンドラを削除する

attachEvent対応

IE対応を謳うならattachEventにも対応しなくてはなりませんが、力尽きたのでいずれ。
希望があれば、声をかけてください。

ラベル: ,