php で xpath

結構前に、Liner NoteさんのPHP汎用スクレイピングライブラリを作ってみたで公開されているソースを元に改造したxpathルーチンを作ったのでおいときます。いろいろあって作ってから1年ぐらい経過したけど。。。
ライセンスは、派生元が修正BSDなんで修正BSDでおながいします。

ダウソする↓
http://rtilabs.net/files/2009_09_02/simple_scraper.zip

メリット。
firefox xpatherぽい値を返してくれるよう最善の努力をしている。
つまり、努力賞ってことね。

文字コードutf-8でお願いします。

<?
	//
	//htmlをxpathでぬはぬはするルーチン
	//FireFoxのxpatherコンパチを目指して挫折しています。
	//
	//派生元 http://note.openvista.jp/2008/php-scraing-library/
	//ライセンスは、派生元が修正BSDなんで修正BSDでおながいします。
	//
	//このソフトウェア上記 scraper.Class.php の内容を改変したものが含まれています。
	class SimpleScraper
	{
		//読み込んだHTML
		var $XML = FALSE;
		
		//おまけ ファイルまたは、HTTP GET
		public function LoadURL($url,$encode = NULL)
		{
			return $this->LoadHTML(file_get_contents($url),$encode);
		}
		//HTTP POST
		public function LoadURLPost($url,$data,$encode = NULL)
		{
			$context = array(
				 "http" => array(
				 "method"  => "POST"
				,"header"  => array(
						 "Content-Type: application/x-www-form-urlencoded"
						,"Content-Length: ".strlen($data)
					)
				,"content" => $data
			)
			);

			return $this->LoadHTML(file_get_contents($url, false, stream_context_create($context)),$encode);
		}
		//文字列から読み込む
		public function LoadHTML($html,$encode = NULL)
		{
			//UTF-8にする
			if ($encode == NULL)
			{
				$html = mb_convert_encoding($html,'UTF-8',$this->getContentType($html));
			}
			else if ($encode = 'pass')
			{
				//nop
			}
			else
			{
				$html = mb_convert_encoding($html,'UTF-8',$encode);
			}

			// コメントを削除
			$html = preg_replace("/<!--.*?-->/iusm", "", $html);
			$html = preg_replace("/&/iusm", "&amp;", $html);
			
			//scriptタグを消滅させる
			$html = preg_replace("/<script[^>]*?>.*?<\/script>/iusm", "", $html);
			$html = preg_replace("/<noscript[^>]*?>.*?<\/noscript>/iusm", "", $html);
			
			//styleタグを消滅させる
			$html = preg_replace("/<style[^>]*?>.*?<\/style>/iusm", "", $html);

			//DOMDocumentの文字化け対策1 charsetを消す
			$html = preg_replace('/<meta[^>]*?charset[^>]*?>/uis',
				'',
				$html);

			//DOMDocumentの文字化け対策2 headの直後に utf8のcharsetを入れる
			$html = preg_replace('/<head[^>]*?>/uis',
				'<meta http-equiv="content-type" content="text/html; charset=utf-8">',
				$html);
			//DOCTYPE が付いていないならつける
			if (!preg_match("/<!DOCTYPE /ius",$html))
			{
				$html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' ."\r\n". $html;
			}
			$html = preg_replace('/xml:lang="ja"/uis','',$html);

			//DOMに読み込ませる
			$dom = new DOMDocument("1.0", "utf-8");
			@$dom->loadHTML($html);

			// DOMをSimpleXMLへ変換
			$ret = simplexml_import_dom($dom);
			$html = $ret->asXML();

			// XML宣言付与
			if (true !== preg_match('/^<\\?xml version="1.0"/uis', $html)) 
			{
				$html = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $html;
			}
			
			//xmlnsの消去  ネームスペースの関係でうまく動作しないことがある
			//xmlnsだけ消すのは大変だから、htmlタグ自体を交換しちゃうよ!
			$html = preg_replace("/<html [^>]*?xmlns=[^>]*?>/uis",'<html>',$html);

			$this->XML = simplexml_load_string($html);
			
			return  ($this->XML !== FALSE);
		}

		//xpathで取得した結果をテキストでもらう
		//xpather の Textに相当する内容
		public function XPathText($xpath)
		{
			assert($this->XML !== FALSE);

			$retNodes = array();
			$isAttribute = $this->IsAttributeXPath($xpath);

			$arr = $this->XML->xpath($xpath);
			if (!is_array($arr))
			{
				return array();
			}

			foreach($arr as $node)
			{
				if ($isAttribute)
				{
					$h = (string)$node;
				}
				else
				{
					$h = preg_replace('/<.*?>/uis' , '' , (string)$node->asXML());
				}
				//& -> &asm; となってしまうので戻す.
				$retNodes[] = $this->EscapeCodeReverse($h);
			}


			return $retNodes;
		}

		//xpathで取得した結果をInnerHTMLでもらう
		//xpather の InnerHTMLに相当する内容
		public function XPathInnerHTML($xpath)
		{
			assert($this->XML !== FALSE);

			$retNodes = array();
			$isAttribute = $this->IsAttributeXPath($xpath);
			
			$arr = $this->XML->xpath($xpath);
			if (!is_array($arr))
			{
				return array();
			}

			foreach($arr as $node)
			{
				if ($isAttribute)
				{//属性だと取れません
					$retNodes[] = NULL;
				}
				else
				{
					$tagName = $node->getName();
					$h = preg_replace('/^<'.$tagName.'[^>]*?>(.*?)<\/'.$tagName.'[^>]*?>$/uis'
									,'\1',
									(string)$node->asXML()
									);
					//& -> &asm; となってしまうので戻す.
					$retNodes[] = $this->EscapeCodeReverse($h);
				}
			}
			return $retNodes;
		}

		//xpathで取得した結果をOuterHTMLでもらう
		//InnerHTMLがあるなら OuterHTMLもほしいぢゃん。
		public function XPathOuterHTML($xpath)
		{
			assert($this->XML !== FALSE);

			$retNodes = array();
			$isAttribute = $this->IsAttributeXPath($xpath);

			$arr = $this->XML->xpath($xpath);
			if (!is_array($arr))
			{
				return array();
			}

			foreach($arr as $node)
			{
				if ($isAttribute)
				{//属性だと取れません
					$retNodes[] = NULL;
				}
				else
				{
					$retNodes[] = $this->EscapeCodeReverse((string)$node->asXML());
				}
			}
			return $retNodes;
		}

		//xpathで指定したデータを削除する.
		public function XPathRemove($xpath)
		{
			foreach( $this->XPathNode($xpath) as $node )
			{
				$node->parentNode->removeChild($node);
			}
			return TRUE;
		}

		//xpathで取得した結果をそのままNodeでもらう
		public function XPathNode($xpath)
		{
			assert($this->XML !== FALSE);
			return $this->XML->xpath($xpath);
		}

		//文字コード判別
		private function getContentType($html)
		{
			$matches = array();
			preg_match("/<meta.*?charset=(?:\"')?([0-9A-Za-z\-_]+)(?:\"')?/uis",$html ,$matches );
			if (! isset($matches[1]) )
			{
				mb_detect_order("sjis-win,eucjp-win,euc-jp,UTF-8");
				return mb_detect_encoding($html);
			}
			else
			{
				return $matches[1];
			}
		}

		//属性を求めるxpathですか?
		private function IsAttributeXPath($xpath)
		{
			//  /span/@class みたいなヤツ?
			if ( preg_match('/\/@[^\/]+$/uis',$xpath) )
			{
				return true;
			}
			return false;
		}
		
		//XMLの使用上エスケープされてしまう文字列を元に戻す
		private function EscapeCodeReverse($text)
		{
			$text = preg_replace("/&gt;/uis","<",$text);
			$text = preg_replace("/&lt;/uis",">",$text);
			$text = preg_replace("/&amp;/uis","&",$text);
			
			return $text;
		}
	};
?>

使い方

<?
	//このソースはサンプルなので、
	//ソースで echo なんてサンプルソースしかできないダーティな技をたくさん利用していますwww

	//ライブラリのロード
	require_once('simple_scraper.inc');


	//構築
	$ss = new SimpleScraper();
	//読み込み 文字コードは自動推測してUTF-8に変換してくれます。
	$ret = $ss->LoadURL("http://www.yahoo.co.jp/");
	if (!$ret)
	{
		echo "パースできませんでした。";
		exit;
	}

	//textだけよこせ
	echo "<h2>textだけよこせ</h2>";		
	$nodes = $ss->XPathText("//table");
	foreach($nodes as $n)
	{
		echo htmlspecialchars($n,ENT_QUOTES) . "<br /><br />";
	}

	//innerHTMLでよこせ
	echo "<h2>innerHTMLでよこせ</h2>";
	$nodes = $ss->XPathInnerHTML("//table");
	foreach($nodes as $n)
	{
		echo htmlspecialchars($n,ENT_QUOTES) . "<br /><br />";
	}

	//OuterHTMLでよこせ
	echo "<h2>OuterHTMLでよこせ</h2>";
	$nodes = $ss->XPathOuterHTML("//table");
	foreach($nodes as $n)
	{
		echo htmlspecialchars($n,ENT_QUOTES) . "<br /><br />";
	}

	//meta の Description値
	echo "<h2>meta の description値</h2>";
	$r = $ss->XPathText("//meta[@name='description']/@content");
	echo $r[0];
?>

バグがあったらごめんね。
EDFはバグを恐れない。