2006年08月17日
HTMLを整形式のXML文書に修正するPHPクラス
ガーロ解任記念(無関係)。夏休みの宿題(嘘)。
HTMLなどにありがちなズサンなマークアップを、整形式のXMLドキュメントに補正するPHP用のクラスライブラリです。
一般的なXHTML変換処理にみられる、タグ書式の修正や不適切に用いられている解析対象記号の置換などに加えて、DTD的構成ルールを用いて、欠落している要素の補完や、要素の親子関係の補正をある程度行なうことができます。構成ルールは任意に指定可能で、これにより例えば、HTMLソースから不要な要素や属性を除去したり、別の要素に置換したりといったことも行えます。
各種のプログラミング言語において様々に提供されているマークアップの補正機能は、PHPでは、PECL拡張モジュール版のTidyなどを利用できますが、残念ながらこれらは、現在の一般的なレンタルサーバのPHP環境では必ずしも用意されているわけではありません。このスクリプトベースのクラスライブラリは、このようなバイナリ拡張モジュールが利用できない環境で、マークアップの補正を行う際の補助となることを意図したものです。
てゆうか、まあこいつのために作ったんだけど。
このクラスライブラリは、Free Software FoundationによるGNU Lesser General Public License(LGPL)のバージョン2.1あるいはそれ以降のバージョンの規約に従いライセンスされます。
応用例
» HTMLをXML化してDOMやXPathで操作するWebスクレイピング用PHPクラス
仕様とか制限事項
- このクラスライブラリを利用するにはPEARのXML_HTMLSax3、及びバージョン4.3.0以降のPHPが必要です。また、XML_HTMLSax3が扱える文字エンコーディングはUTF-8のみなので、mbstringなどの文字エンコーディング変換機能が必要かもしれません。
- 要素の補完や親子関係の補正には限界があり、必ずしも望ましい結果が得られるとは限りません。
- XML宣言やDOCTYPE宣言、名前空間などの補正は行いません。
- コメントやCDATAセクション以外のマークアップ記号はすべて解析対象です。即ち、XMPやPLAINTEXT、そして子要素がコメントまたはCDATAセクションではないSCRIPTなどがパース対象のドキュメントに含まれる可能性がある場合は、事前の処理が必要かもしれません。
- 任意の構成ルールを指定できますが、省略はできません。リリースパッケージには、XHTML 1.0 TransitionalのDTDに基づいて作成した構成ルールのサンプルが付属しますが、HTML以外のマークアップに対して利用する場合は構成ルールを独自に用意する必要があります。
- パフォーマンスを考慮した結果、パース結果は構造体ではなく文字列に構築されます。そのため、例えばドキュメントツリーを操作するようなインタフェースはありません(ドキュメントツリーを操作する必要がある場合は、出力をDOMオブジェクトに読み込むなどしてください)。
リリース
» 1.2.1 (stable) released 2007/05/17
※バージョン1.1からは、それ以前のバージョンと異なり、HTML_Sax3クラスのオブジェクトを別途に作成する必要がなくなりました。
| ファイル名 | 概要 |
|---|---|
| HTMLParser.class.php | クラスライブラリ本体。 |
| xhtml1-transitional_dtd.inc.php | XHTML 1.0 TransitionalのDTDを基に作成した、サンプルの構成ルールを表す連想配列を返す外部PHPファイル。 |
| sample.php | HTMLを読み込み、補正結果を表示するサンプルアプリケーション。 ファイル中の、ターゲットのURIを指定している箇所を編集して、お好みのウェブページを表示してみてください。また、冒頭にあるふたつの変数を、output_xmlをTRUEに、output_xmlnsをFALSEにすると、Mozilla FirefoxなどではXMLドキュメントツリーを確認できると思います。 |
概要
このクラスライブラリは、SAXベースのXMLパーサである、PEARのXML_HTMLSax3用のイベントハンドラを定義したものです(従ってその利用にはXML_HTMLSax3が必要です)。XML_HTMLSax3は、PHPのネイティブXMLパーサと異なり、不適切なマークアップに対しても処理を継続します。XML_HTMLSax3は、一般的なSAXパーサと同様に、パース対象のドキュメント中に開始タグや終了タグ、キャラクターデータなどの要素を検出する都度、各要素に対応するハンドラを呼び出します。このクラスライブラリの各ハンドラメソッドは、XML_HTMLSax3から引き渡された各要素を、指定の構成ルールに従って文字列に構築していきます。
サンプルプログラム
以下のダメダメHTMLをパースする簡単なサンプルプログラムを見ていきます。
以下のHTMLはShift_JISエンコーディングで、sample.htmlという名称のファイルで存在しているものとします。
<META NAME=DESCRIPTION CONTENT=Foo&Bar> <H1> <DIV ALIGN=CENTER>Foo&Bar</DIV> </H1> <HR NOSHADE> </BODY> <P>Foo<BR>Bar <TABLE FOO=BAR> <TD>Foo&Bar</TR></TR></TR></TR> </TABLE> <LI>Foo <LI>Bar <FOO> <HTML>
まず、このHTMLを読み込み、UTF-8エンコーディングに変換しておきます(XML_HTMLSax3が扱える文字エンコーディングはUTF-8のみです)。
$source = file_get_contents('./sample.html');
mb_convert_variables('UTF-8', 'Shift_JIS', $source);
このクラスのオブジェクトを作成し、アーカイブに付属している、サンプルの構成ルールを表す連想配列を返す外部PHPファイル(xhtml1-transitional_dtd.inc.php)を指定します。このサンプルの構成ルールはXHTML 1.0 TransitionalのDTDを基に作成したものです。setRuleFileメソッドは外部ファイルを指定しますが、これに替えて、setRuleメソッドで配列を指定することもできます。
require_once('./HTMLParser.class.php');
$parser = new HTMLParser;
$parser->setRuleFile('./xhtml1-transitional_dtd.inc.php');
このクラスのオプションを指定します。
- 要素の親子関係の補正を行う際には、構成ルールに定義される、各タグの「既定の親(default_parent)」が参照される場合があります。例えば、サンプルの構成ルールでは、「meta」の項には「'default_parent' => 'head'」という記述があり、上のsample.htmlで欠落しているHEAD要素は、この定義に従い補完されます。HTMLでは、多くのタグについてこの「既定の親」は「body」ですが、これらのタグについて構成ルールに逐次「既定の親」を記述するのは効率が悪いので、「既定の親」が未定義のタグの、既定の「既定の親」を指定するのがsetGenericParentメソッドです。
- setRootメソッドで指定する値はドキュメントのルート要素(HTMLの場合は「html」)の指定であり、パース対象のドキュメントに(上のsample.htmlのように)ルート要素が欠落している場合に補完されるものです。
パーサから開始タグのハンドラに最初に渡されるタグ名(上のsample.htmlでは「meta」)がsetRootメソッドで指定した名前と合致しない場合は、ここで指定した値に基づく要素が補完され、ハンドラから渡された要素は、その子要素として評価されます(なお、属性値は評価対象ではありません。名前さえ合致していれば、その属性値に関わらずハンドラから渡された要素がそのまま利用されます)。
以下の指定により、上のsample.htmlのパース結果にはまず「<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">」というタグが補完され、次いでMETAが、その子要素として評価されます(当然METAはHTMLの直接の子要素ではないので、さらに補完が行われますが)。
$parser->setGenericParent('body');
$parser->setRoot('html',
array('xmlns' => 'http://www.w3.org/1999/xhtml', 'xml:lang' => 'ja')
);
パースを実行します。
$parser->parse($source);
パース結果の出力を得ます。オプションの文字エンコーディングが指定されている場合、mbstringが利用可能であれば、文字列は指定した文字エンコーディングに変換されます。オプションが未指定の場合、及びmbstringが利用不可の場合、文字エンコーディングはUTF-8となります。
$result = $parser->dump('Shift_JIS');
この出力は以下のようになります(改行とインデントは見易いように追加したもので、実際はこの通りではありません)。
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta name="DESCRIPTION" content="Foo&Bar" />
</head>
<body>
<div align="CENTER">
<h1>Foo&Bar</h1>
</div>
<hr noshade="noshade" />
<p>Foo<br />Bar</p>
<table>
<tr><td>Foo&Bar</td></tr>
</table>
<ul>
<li>Foo</li>
<li>Bar</li>
</ul>
</body>
</html>
この例では以下のような処理が施されています。
- タグ書式の修正(要素名と属性名の小文字化、空要素の終了記号の追加)。
- 終了タグの補完。
- 欠落している要素の補完(HTML、HEAD、BODY、TR、UL)。
- 要素の親子関係の補正(H1とDIV)。
- 不正な要素の除去(「FOO」タグ)。
- 不正な属性の除去(TABLEの「FOO=BAR」)。
- HTMLでは最小化が可能な属性の補正(「NOSHADE」→「noshade="noshade"」)。
- 不適切に用いられている解析対象記号の置換(「&」→「&」)。
しかし一方、これを例えばXHTML 1.0 TransitionalのDTDで検証した場合、以下のような不備があり、これらは、このクラスライブラリの処理の限界の例です。
- XML宣言とDOCTYPE宣言の欠落。
- 必須要素の欠落(TITLE、及びContent-Typeを指定するMETA)。
- 必須属性の欠落(TABLEのsummary)。
- 不適切な属性値(「align="CENTER"」)
なお、ドキュメントのパース結果は文字列として返されますが、幾つかのタグについてはより利便性の高い形式でその属性値を取得したい場合があるかもしれません。例えば、HTMLを解析する際にはしばしばMETAやLINKの属性値を得る必要がありますが、これらをパース結果の文字列から再度取得し直すのは非効率的です。
このクラスライブラリでは、あらかじめ指定されたタグについて、その属性値を連想配列に保存し、パース結果とは別途にこれを取得する手段を用意しています。
まず、パースを開始する前に、setTagsToSaveメソッドで属性値を保存するタグ名を指定しておきます。タグ名は、以下のような文字列、または配列のいずれでも指定でき、両者の混在も可能です。
$parser->setTagsToSave('meta', 'link');
パース後にgetSavedTagsメソッドで保存された連想配列を得ることができます。オプションの文字エンコーディングが指定されている場合、mbstringが利用可能であれば、文字列は指定した文字エンコーディングに変換されます。オプションが未指定の場合、及びmbstringが利用不可の場合、文字エンコーディングはUTF-8となります。
$saved_tags = $parser->getSavedTags('Shift_JIS');
以下は例として、取得した配列からMobile Link DiscoveryのURIを得るものです。
if (isset($saved_tags['link'])) {
foreach ($saved_tags['link'] as $value) {
if (isset($value['href'])
and isset($value['rel'])
and isset($value['media'])
and strtolower($value['rel']) == 'alternate'
and strtolower($value['media']) == 'handheld') {
$mobile_link = $value['href'];
break;
}
}
}
if (isset($mobile_link)) {
var_dump($mobile_link);
}
リファレンス
- void HTMLParser->setRuleFile ( string filename )
- 構成ルールを表す連想配列を返す外部PHPファイルを指定します。
連想配列及び外部ファイルの詳細はアーカイブに付属のサンプルを参照してください。 - void HTMLParser->setRule ( array )
- 構成ルールを表す連想配列を指定します。
連想配列の詳細はアーカイブに付属のサンプルを参照してください。 - string HTMLParser->dump ( [string to_encoding] )
- パース結果の文字列を返します。オプションの文字エンコーディングが指定されている場合、mbstringが利用可能であれば、文字列は指定した文字エンコーディングに変換されます。オプションが未指定の場合、及びmbstringが利用不可の場合、文字エンコーディングはUTF-8となります。
- void HTMLParser->setRoot ( string tag_name [, array tag_attributes] )
- ドキュメントのルート要素を指定します。パーサから開始タグのハンドラに最初に渡されるタグ名がここで指定した名前と合致しない場合は、指定の値に基づく要素が補完され、ハンドラから渡された要素は、その子要素として評価されます(なお、属性値は評価対象ではありません。名前さえ合致していれば、その属性値に関わらずハンドラから渡された要素がそのまま利用されます)。
- void HTMLParser->setGenericParent ( string tag_name )
- 要素の親子関係の補正を行う際には、構成ルールに定義される、各タグの「既定の親(default_parent)」が参照される場合があります。HTMLでは、多くのタグについてこれは「body」ですが、これらのタグについて構成ルールに逐次「既定の親」を記述するのは効率が悪いので、「既定の親」が未定義のタグについては、ここで指定したタグが「既定の親」となります。
- void HTMLParser->setTagsToSave ( mixed tag_name )
- このメソッドによって指定したタグの属性値は連想配列に保存され、パース後にgetSavedTagsメソッドによってアクセスすることができるます。タグの指定は、タグがひとつなら文字列で、複数ある場合はカンマ区切りの文字列または配列で行い、文字列と配列は混在することができます。
- array HTMLParser->getSavedTags ( string to_encoding )
- setTagsToSaveメソッドで指定したタグの属性値を保存した連想配列を返します。オプションの文字エンコーディングが指定されている場合、mbstringが利用可能であれば、文字列は指定した文字エンコーディングに変換されます。オプションが未指定の場合、及びmbstringが利用不可の場合、文字エンコーディングはUTF-8となります。
ACKNOWLEDGMENT
The idea of this software is in fact inspired by PEAR XML_DTD maintained by Thomas V.V.Cox and Stephan Schmidt. And, of course, this software is greately depends on PEAR XML_HTMLSax3 maintained by Harry Fuecks. I would like to thank them all.
Category: ウェブ制作
Posted 2006年08月17日 21:54
トラックバック
このエントリーのトラックバックURL:
http://www.rcdtokyo.com/mt/mt-rcdtokyo5428-tb.cgi/770
このリストは、次のエントリーを参照しています: HTMLを整形式のXML文書に修正するPHPクラス:
» [PHP][雑記]PHPで街を育てる from Do You PHP はてな
via. Rauru Blog » Blog Archive » Ruby とか Perl とかで街を育てる 空前のMyMiniCi... [続きを読む]
Pinged at 2007/12/20 13:14
» [11.開発日記][php][HTMLScraping][RSS]String could not be parsed as XMLの対策 from [Mi]みたいなもの
String could not be parsed as XMLの原因について、肝心の対策を書いてなかったので書いておきます。 (HTMLScrapi... [続きを読む]
Pinged at 2009/07/16 12:31
» Googleæ¤ç´¢é ä½åå¾ï¼PHPã§ãGoogleæ¤ç´¢ã®é ä½åå¾ãã試ãã¦ã¿ãããããã¾ã便å©ãªã¯ã©ã¹ã©ã¤ãã©ãªãå©ç¨ ã»ã»ã» ãPHPã from åµãmetaboy
ãã¦ãå
æ¥
ãGoogleã®ãã¼ã¸ã©ã³ã¯ã調ã¹ããã¼ã«ãåµã£... [続きを読む]
Pinged at 2009/07/30 18:02
コメント
便利!ですので、使わせてもらっていますが、当初エラーが出たので、なぜだ?と思ったら、preg_match_all()に使っている定数PREG_OFFSET_CAPTUREがPHP4.3.3以前では使えないようです。(PHP公式マニュアルでは4.3.0から使えることになってますが誤りの用です)。PHPをバージョンアップしたら使えました。一応ご報告でした。
Posted by 通りすがりさん at 2007/08/07 12:19
ああ、すいません4.3.0とか書いてますねえ…。
PCサイト→携帯変換スクリプトの制作初期に、PREG_OFFSET_CAPTUREの罠にはまったことがあったっけと、今思いだしますた(笑)
Posted by ucbさん at 2007/08/07 23:19