404 motivation not found | t_ishidaのブログ

11月/09

12

こんなん書いてた

ヘッダコメントの通りです。

課題:
アトリビュート内にonclick=”this.innerHTML=”<h1>hogehoge</h1^>’”とか、やられるとパースしちゃう。タグ構造壊れている奴を上手くパースできないかも。

<?php
/*************************************************
 * HTMLっぽい文字列をパースっぽいことをする
 * @todo クエリの結果に対してattrやタグ名の操作を、
 *       やっても現在別オブジェクトなのでDOM操作的な
 *       ことが出来るようにした方が良いのかね?
 *************************************************/
class Tag {
  private $Elements = array();
  private $ExcludeList = array(
    'onload',
    'onunload',
    'onabort',
    'onerror',
    'onmove',
    'onresize',
    'ondragdrop',
    'onfocus',
    'onblur',
    'onsubmit',
    'onreset',
    'onclick',
    'ondblclick',
    'onkeydown',
    'onkeypress',
    'onkeyup',
    'onmousedown',
    'onmouseup',
    'onmouseover',
    'onmouseout',
    'onmousemove',
    'onchange',
    'onselect',
    array( 'target'=> 'href', 'pattern' => '#^javascript:#' ) ,
    );

  private $ExcludeTag = array(
    'script',
    );

  /*******************************************
   * コンストラクタ
   ******************************************/
  public function __construct( $html = null ){
    if ( is_array( $html ) )                      $this->Elements = $html;
    elseif( preg_match( '#^https?://#', $html ) ) $this->Elements = $this->parse( file_get_contents( $html) );
    elseif( is_scalar( $html ) )                  $this->Elements = $this->parse( $html );
  }

  /******************************************************
   * アトリビュートを取得する
   * @param アトリビュート名
   * @return アトリビュート値
   ******************************************************/
  public function getAttribute( $name ){
    return $this->Elements['attrs'][$name];
  }

  /******************************************************
   * アトリビュートを設定する
   * @param アトリビュート名
   * @param 値
   ******************************************************/
  public function setAttribute( $name, $value ){
    $this->Elements['attrs'][$name] = $value;
  }

  /******************************************************
   * タグ名を取得する
   * @param 文字列(Path構文)
   ******************************************************/
  public function getTagName(){
    return $this->Elements['tagName'];
  }

  /******************************************************
   * タグ名を変更する
   * @param 文字列(Path構文)
   ******************************************************/
  public function setTagName( $tagName ){
    $this->Elements['tagName'] = $tagName;
  }

  public function getChildNode( $idx ){
    $class = __CLASS__;
    return new $class( $this->Elements['childNodes'][$idx] );
  }

  public function getTextNode(){
    $result = '';
    foreach( $this->Elements['childNodes'] as $child ) {
      if( $child['tagName'] == 'textNode' ) $result .= $child['attrs']['value'];
    }
    return $result;
  }

  /******************************************************
   * Path構文でHTMLをツリーから対象のタグを取得する
   * @param "/" 区切りでid => "#",class => ".", タグ名で取得する
   * @return 対象タグの配列とか
   ******************************************************/
  public function query( $q ){
    $path = preg_split( '#/#', $q );
    $current = array( $this );
    foreach( $path as $x ){
      if( !$x ) continue;
      $next = array();
      foreach( $current as $elm ){
        if ( preg_match( '/^#(\S+)$/', $x, $tmp ) ){
          $buf = $elm->getElementById( $tmp[1] );
          if( $buf ) $next[] = $buf;
        } elseif( preg_match( '#^\.(\S+)$#', $x, $tmp ) ){
          $buf = $elm->getElementsByClassName( $tmp[1] );
          if( $buf ) $next = array_merge( $next, $buf );
        } elseif( preg_match( '#^(\S+?)(?:([\#\.])(\S+))?$#', $x, $tmp ) ) {
          $buf = $elm->getElementsByTagName( $tmp[1] );
          if( $buf ){
            if( $tmp[2] ){
              $buf2 = array();
              foreach( $buf as $elm ){
                if( $elm->getAttribute( $tmp[2] == '.' ? 'class' : 'id' ) == $tmp[3] ){
                  $buf2[] = $elm;
                }
              }
              $buf = $buf2;
            }
            $next = array_merge( $next, $buf );
          }
        }
      }
      $current = $next;
    }
    if( !$current )                     return array();
    if( preg_match('|^#[^/]+$|', $q ) ) return $current[0];
    return $current;
  }

  /*************************************************
   * IDアトリビュートからタグオブジェクトを取得する
   *************************************************/
  public function getElementById( $id, $tag = null ){
    if( !$tag ) $tag = $this->Elements;
    $class = __CLASS__;
    if( $tag['attrs']['id'] == $id ) return new $class( $tag );
    foreach( $tag['childNodes'] as $child ){
      $result = $this->getElementById( $id, $child );
      if( $result ) return $result;
    }
    return null;
  }

  /*************************************************
   * タグ名からタグオブジェクトの配列を取得する
   *************************************************/
  public function getElementsByTagName( $tag_name,  $tag = null ){
    if( !$tag ) $tag = $this->Elements;
    $class = __CLASS__;
    $result = array();
    if( $tag['tagName'] == $tag_name ) $result[] = new $class( $tag );
    foreach( $tag['childNodes'] as $child ){
      $result2 = $this->getElementsByTagName( $tag_name, $child );
      if( $result2 ) $result = array_merge( $result, $result2 );
    }
    return $result;
  }

  /*************************************************
   * クラス名からタグオブジェクトの配列を取得する
   *************************************************/
  public function getElementsByClassName( $class, $tag = null ){
    if( !$tag ) $tag = $this->Elements;
    $c = __CLASS__;
    $result = array();
    if( $tag['attrs']['class'] == $class ) $result[] = new $c( $tag );
    foreach( $tag['childNodes'] as $child ){
      $result2 = $this->getElementsByClassName( $class, $child );
      if( $result2 ) $result = array_merge( $result, $result2 );
    }
    return $result;
  }

  /***********************************************************
   * HTMLツリーを再帰的に遡ってアトリビュートを削除する
   * @param array() 削除したりアトリビュートの名前
   * @return 対象タグのオブジェクト
   **********************************************************/
  public function removeAttribute( $targets = array() ){
    if( !$targets ) $targets = $this->ExcludeList;
    $class = __CLASS__;
    return new $class( $this->__removeAttribute( $this->Elements, $targets ) );
  }

  /***********************************************************
   * HTMLツリーを再帰的に遡ってアトリビュートを削除する(再起のための受け皿)
   * @param @tag
   * @param array() 削除したりアトリビュートの名前
   * @return 対象タグの配列とか
   **********************************************************/
  private function __removeAttribute( $tag, $targets ){
    foreach( $targets as $t ){
      if( is_array( $t ) ){
        if( preg_match( $t['pattern'], $tag['attrs'][$t['target']] ) ) unset( $tag['attrs'][$t['target']] );
      } else {
        unset( $tag['attrs'][$t] );
      }
    }
    if( $tag['childNodes'] ){
      foreach( $tag['childNodes'] as $key => $value ) {
        $tag['childNodes'][$key] = $this->__removeAttribute( $tag['childNodes'][$key], $targets );
      }
    }
    return $tag;
  }

  /***********************************************************
   * HTMLツリーを再帰的に遡ってタグを削除する(再起のための受け皿)
   * @param @tag
   * @param array() 削除したりタグの名前
   * @return 対象タグの配列とか
   **********************************************************/
  public function removeTag( $targets = array() ){
    if( !$targets ) $targets = $this->ExcludeTag;
    $class = __CLASS__;
    if( in_array( $this->Elements['tagName'], $targets ) ) return new $class( array() );
    return new $class( $this->__removeTag( $this->Elements, $targets ) );
  }

  /***********************************************************
   * HTMLツリーを再帰的に遡ってタグを削除する(再起のための受け皿)
   * @param @tag
   * @param array() 削除したいタグの名前
   * @return 対象タグの配列とか
   **********************************************************/
  private function __removeTag( $tag, $targets ){
    foreach( $tag['childNodes'] as $key => $value ) {
      if( in_array( $tag['childNodes'][$key]['tagName'], $targets ) ) unset( $tag['childNodes'][$key] );
    }
    return $tag;
  }

  /*****************************************
   * HTMLツリーを再帰的にHTML化する
   * @return 出力結果のHTML
   *****************************************/
  public function build( $flg = false ){
    $html = '';
    // rootは出さない
    if( $this->Elements['tagName'] == 'root' ){
      foreach( $this->Elements['childNodes'] as $child ) $html .= $this->__build( $child );
    } else{
      $html = $this->__build( $this->Elements, $flg );
    }
    return $html;
  }

  /*****************************************
   * HTMLツリーを再帰的にHTML化する(再起のための受け皿)
   * @param 対象のタグ
   * @return 出力結果のHTML
   *****************************************/
  private function __build( $tag ){
    if( !$tag ) return '';
    if( $tag['tagName'] == 'comment') {
      $html = '<!--';
      $html .= $tag['attrs']['value'];
      $html .= '-->';
    } elseif( $tag['tagName'] == 'cdata') {
      $html = '<![CDATA[';
      $html .= $tag['attrs']['value'];
      $html .= ']]>';
    } elseif( $tag['tagName'] == 'textNode' ){
      $html = htmlspecialchars( $tag['attrs']['value'], ENT_QUOTES );
    } else {
      $html = '<' . $tag['tagName'];
      if( $tag['attrs'] ) $html .= $this->buildAttributes( $tag['attrs'] );
      if( $tag['childNodes'] ){
        $html .=  '>';
        foreach( $tag['childNodes'] as $child ) $html .= $this->__build( $child );
        $html .= '</' . $tag['tagName'] . '>';
      } else {
        $html .= ' />';
      }
    }
    return $html;
  }

  /******************************************************
   * HTMLをツリーハッシュにビルド
   * @param 文字列
   * @return ハッシュ
   ******************************************************/
  public static function parse( $html ){
    $in_tag = false;
    $buf = '';
    $length  = count( $chars );
    $parent  = array();
    $current =  array( 'tagName' => 'root', 'childNodes' => array(), 'attrs' => array() );
    $stack   = array();
    foreach( preg_split( '//', mb_convert_encoding( $html, 'utf8', 'euc-jp,sjis' ) ) as $char ){
      // タグ開始のお知らせ
      if( $char === '<' ){
        if( !$in_tag ){
          $in_tag = true;
          if( $buf ){
            $current['childNodes'][] = array(
              'tagName'    => 'textNode',
              'attrs'      => array( 'value' => html_entity_decode( $buf ) ),
              'childNodes' => array()
              );
          }
          $buf = '';
        }
        continue;
      }

      // タグ終了のお知らせ
      if( ( preg_match( '#^!\[CDATA\[(.*)\]\]$#s', $buf ) && $char === '>' ) ||
          ( preg_match( '#^!--(.*)--$#s'         , $buf ) && $char === '>' ) ||
          ( !preg_match( '#^(?:!\[CDATA\[|!--)#' ,$buf )  && $char === '>' )  ){

        if( $in_tag ) {
          $in_tag = false;

          // コメント
          if( preg_match( '#^!--(.*)--$#s', $buf, $tmp ) ){
            $current['childNodes'][] = array(
              'tagName'    => 'comment',
              'attrs'      => array( 'value' => $tmp[1] ),
              'childNodes' => array(),
              );
          }

          // CDATA
          elseif( preg_match( '#^!\[CDATA\[(.*)\]\]$#s',$buf, $tmp ) ) {
            $current['childNodes'][] = array(
              'tagName'    => 'cdata',
              'attrs'      => array( 'value' => $tmp[1] ),
              'childNodes' => array(),
              );

          }

          // 想定通りの閉じタグ
          elseif( preg_match( '#^/' . $current['tagName'] . '#i', $buf ) ) {
            $parent['childNodes'][] = $current;
            $current = $parent;
            if( $stack  ) $parent = array_pop( $stack );
          }

          // 想定外の閉じタグ
          elseif( preg_match( '#^/#', $buf ) ){
            ///
            /// @todo: 満足したらスタックを遡って補完するようにする
            /// まだまだ想定外がありそうなので、とりあえずはデバッグモードにしておく
            ///
            print mb_convert_encoding( $html, 'utf8', 'euc-jp,sjis' );
            print "debug:$buf\n\n";
            var_dump( $parent );
            var_dump( $current );
            var_dump( $stack );
            return ;
          }

          // 単体のタグ
          elseif( preg_match( '#^(\S+)(.+)/$#s', $buf, $tmp ) ||
                  preg_match( '#^(input|br|img|hr|meta|link|!DOCTYPE|option|base|area)(.*)$#si',$buf, $tmp ) ) {
            $current['childNodes'][] = array(
              'tagName'    => strtolower( $tmp[1] ),
              'attrs'      => self::parseAttributes( $tmp[2] ),
              'childNodes' => array()
              );

          }

          // 通常の開始タグ
          elseif( preg_match( '#^([a-zA-Z0-9]+)(.*)$#s', $buf, $tmp ) ){
            if( $parent ) $stack[] = $parent;
            $parent = $current;
            $current = array(
              'tagName' => strtolower( $tmp[1] ),
              'attrs'   => self::parseAttributes( $tmp[2] ),
              'childNodes'  => array(),
              );
          }
          ///
          /// 超例外
          ///
          elseif( preg_match( '#^(.+)/([a-zA-Z0-9]+)$#', $buf, $tmp ) && $tmp[2] == $current['tagName'] ) {
            $current['childNodes'][] = array( 'tagName' => 'textNode', 'attrs' => array( 'value' => $tmp[1] ), 'childNodes' => array() );
            $current = $parent;
            if( $stack  ) $parent = array_pop( $stack );
          } else {
            /// textNodeとして"<"が含まれてるとか色々あるようだ
            continue;
          }
          $buf = '';
        }
        continue;
      }
      $buf .= $char;
    }
    return $current;
//    return $parent;
  }

  /******************************************************
   * タグ名を除いたタグの中身をAttributeのハッシュに変換
   * @param 文字列
   * @return ハッシュ
   ******************************************************/
  public static function parseAttributes( $chars ){
    $in_quote = 0;
    $esc      = 0;
    $attr     = '';
    $elms     = array();
    foreach( preg_split( '//', trim( $chars ) ) as $char ){
      if( $esc === 1 ){
        $esc = 0;
        $attr .= $char;
      } elseif( !$in_quote && !trim( $char ) ){
        list( $label, $value ) = self::parseAttribute( trim($attr ) );
        $attr && $elms[$label] = $value;
        $attr = '';
      } else {
        if    (  $in_quote === $char )                             $in_quote = null;
        elseif( !$in_quote && ( $char === "'" || $char === '"' ) ) $in_quote = $char;
        $char === '\\' && $esc = 1;
        $attr .= $char;
      }
    }
    return $elms;
  }

  /********************************************
   * attributeっぽい文字列をlabelとvalueに分割
   * @param 文字列
   * @return array( $label, $value );
   *********************************************/
  public static function parseAttribute( $chars ){
    $label = '';
    $value = '';
    $in_quote = 0;
    $esc      = 0;
    $buf = '';
    foreach( preg_split( '//', trim( $chars ) ) as $char ){
      if( !$in_quote  && $char === ' ' ) continue;
      if( $esc === 1 ){
        $esc = 0;
        $buf = $buf . $char;
      } else{
        if( $in_quote === $char ){
          $in_quote = null;
        } else{
          if( $in_quote ){
            if( $char === '\\' )  $esc  = 1;
            else                  $buf = $buf . $char;
          } else {
            if( $char === "'" || $char === '"' ) {
              $in_quote = $char;
            } elseif( $char === '=' ) {
              $label  =  $buf;
              $buf    = '';
            } else {
              $buf = $buf . $char;
            }
          }
        }
      }
    }
    $value =  $buf;
    return array( $label, $value );
  }

  /**********************************
   * ハッシュをアトリビュート化する
   * @param ハッシュ
   * @return アトリビュートっぽい文字列
   **********************************/
  private function buildAttributes( $attr ){
    $str = '';
    foreach( $attr as $key => $value ){
      if( $value == '' ) continue;
      $str .= ' ' .  $key .'="';
      if( is_array( $value ) ){
        // まあ、あんまり使わないで
        $str .= htmlspecialchars( join( ' ',  $value, ENT_QUOTES ) );
      } else {
        $str .= htmlspecialchars( $value, ENT_QUOTES );
      }
      $str .='"';
    }
    return $str;
  }
}

///
/// 弾さんのブログを試しにスクレイピングするとこうなる
///

// ob_start();
//$tag = new Tag('http://blog.livedoor.jp/dankogai/archives/51321141.html');
//print "title:" .  array_shift( $tag->query('title') )->getChildNode(0)->getAttribute('value') . "\n\n";
//print "date:"  .  trim( array_shift( $tag->query('h2.date') )->removeTag( array('a', 'span') )->getTextNode() ) . "\n\n";
//print "body:\n";
//print array_shift( $tag->query('div.blogbody') )->removeTag()->removeAttribute()->build();
//print mb_convert_encoding( ob_get_clean(), 'sjis', 'utf8' );
Share and Enjoy:
  • Digg
  • del.icio.us
  • Google Bookmarks
  • Tumblr
  • email
  • Facebook
  • FriendFeed

RSS Feed

コメントはまだありません。

Leave a comment!

<< 先日のチロッと直した

SSHの色々 >>

Find it!

Theme Design by devolux.org

Tag Cloud