404 motivation not found | t_ishidaのブログ

11月/09

16

先日のチロッと直した

先日のを昼休みにチロッと直した

<?php
/*************************************************
 * HTMLっぽい文字列をパースっぽいことをする
 * @todo クエリの結果に対してattrやタグ名の操作を、
 *       やっても現在別オブジェクトなのでDOM操作的な
 *       ことが出来るようにした方が良いのかね?
 * @todo タグ比べるところは性能の問題あるし正規表現じゃなくてsubstrにしよう
 *************************************************/
class fwwTag {
  private $Elements = array();

  /// removeTagのデフォルト値
  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:#' ) ,
    );

  /// removeTagのデフォルト値
  private $ExcludeTag = array(
    'script',
    );

  /// PCDATA扱いにするもの
  private $isPCDATA  = array( 'style', '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;
  }

  /******************************************************
   * 配下ノードに添え字でアクセス
   * @param 添え字
   * @return 対象ノードのオブジェクト
   ******************************************************/
  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アトリビュートからタグオブジェクトを取得する
   * @param id
   * @return 対象オブジェクト
   *************************************************/
  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;
  }

  /*************************************************
   * タグ名からタグオブジェクトの配列を取得する
   * @param タグ名
   * @return 対象オブジェクトの配列
   *************************************************/
  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;
  }

  /*************************************************
   * クラス名からタグオブジェクトの配列を取得する
   * @param クラス名
   * @return 対象オブジェクトの配列
   *************************************************/
  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] );
      else                                                            $tag['childNodes'][$key] = $this->__removeTag( $tag['childNodes'][$key], $targets );
  
    }
    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, $no_esc = false ){
    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 = $no_esc ? $tag['attrs']['value'] : htmlspecialchars( $tag['attrs']['value'], ENT_QUOTES );
    } else {
      $html = '<' . $tag['tagName'];
      if( $tag['attrs'] ) $html .= $this->buildAttributes( $tag['attrs'] );
      if( $tag['childNodes'] ){
        $html .=  '>';
        $no_esc = in_array( $tag['tagName'], $this->isPCDATA );
        foreach( $tag['childNodes'] as $child ) $html .= $this->__build( $child, $no_esc );
        $html .= '</' . $tag['tagName'] . '>';
      } else {
        $html .= ' />';
      }
    }
    return $html;
  }

  /******************************************************
   * HTMLをツリーハッシュにビルド
   * @param 文字列
   * @return ハッシュ
   ******************************************************/
  public function parse( $html ){
    $in_tag     = false;
    $in_comment = false;
    $in_cdata   = false;
    $in_quote   = false;
    $in_pcdata  = false;
    $is_esc     = 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,utf8,auto' ) ) as $char ){
      // タグ開始のお知らせ
      if( !$in_tag ){
        if( $char === '<' ){
          $in_tag = true;
          if( $buf ){
            $current['childNodes'][] = array(
              'tagName'    => 'textNode',
              'attrs'      => array( 'value' => html_entity_decode( $buf ) ),
              'childNodes' => array()
              );
          }
          $buf = '';
          continue;
        }
      }
      /// タグの中かどうか
      else {
        // "<"の後だけど、これはタグじゃねーだろの場合
        if( !$buf && ( !$in_comment && !$in_pcdata  && !$in_cdata ) && !preg_match( '#^[\!/a-zA-Z0-9]#', $char ) ){
          $in_tag = false;
          $buf = '<' . $char;
          continue;
        }

        // クォートの中
        elseif( $in_quote ){
          if( $is_esc )                $is_esc = false;
          elseif( $in_quote == $char ) $in_quote = false;
          elseif( $char     == '\\' )  $is_esc = true;
          $buf .= $char;
          continue;
        }
        // quoteの開始
        elseif( $char === "'" || $char === '"' ) {
          $in_quote = $char;
          $buf .= $char;
          continue;
        }

        // コメント開始
        elseif( $buf . $char == '!--' ){
          $in_comment = true;
          $buf .= $char;
          continue;
        }

        // CDATA 開始
        elseif( strtoupper( $buf . $char ) == '![CDATA[' ){
          $in_cdata = true;
          $buf .= $char;
          continue;
        }

        /// タグ閉じ
        elseif( $char === '>' && (
          ( $in_comment  && mb_substr( $buf, -2 ) == '--' )  ||
          ( $in_cdata    && mb_substr( $buf, -2 ) == ']]' )  ||
          ( $in_pcdata   && "/$in_pcdata"         == trim( strtolower( $buf ) ) ) ||
          ( !$in_comment && !$in_pcdata  && !$in_cdata  )
          ) ) {
          $in_tag = false;

          // コメントの閉じ
          if( $in_comment && mb_substr( $buf, -2 ) == '--' ){
            $current['childNodes'][] = array(
              'tagName'    => 'comment',
              'attrs'      => array( 'value' =>  mb_substr( $buf, 3, -2 ) ),
              'childNodes' => array(),
              );
            $in_comment = false;
            $buf = '';
            continue;
          }

          // CDATAの閉じ
          elseif( $in_cdata && mb_substr( $buf, -2 ) == ']]' ) {
            $current['childNodes'][] = array(
              'tagName'    => 'cdata',
              'attrs'      => array( 'value' => mb_substr( $buf, 8, -2 ) ),
              'childNodes' => array(),
              );
            $in_cdata = false;
            $buf = '';
            continue;
          }

          // 想定通りの閉じタグ
          elseif(  '/' . $current['tagName'] == strtolower( trim( $buf ) ) ) {
            if( $in_pcdata == $current['tagName'] ) $in_pcdata = false;
            $parent['childNodes'][] = $current;
            $current = $parent;
            if( $stack  ) $parent = array_pop( $stack );
            $buf = '';
            continue;
          }

          // 想定外の閉じタグ
          elseif( preg_match( '#^/#', $buf ) ){
            print "不整合: debug: $buf ". $current['tagName'] . "\n\n";
            die;
          }

          // 単体のタグ
          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'      => $this->parseAttributes( $tmp[2] ),
              'childNodes' => array()
              );
            $buf = '';
            continue;
          }

          // 通常の開始タグ
          elseif( preg_match( '#^([a-zA-Z0-9]+)(.*)$#s', $buf, $tmp ) ){
            if( in_array( strtolower( $tmp[1] ), $this->isPCDATA ) ) $in_pcdata = strtolower( $tmp[1] );
            if( $parent ) $stack[] = $parent;
            $parent = $current;
            $current = array(
              'tagName' => strtolower( $tmp[1] ),
              'attrs'   => $this->parseAttributes( $tmp[2] ),
              'childNodes'  => array(),
              );
            $buf = '';
            continue;
          }
        } // タグ終了のお知らせ
      }   // タグの中かどうか
      $buf .= $char;
    } // end foreach;
    return $current;
  }

  /******************************************************
   * タグ名を除いたタグの中身をAttributeのハッシュに変換
   * @param 文字列
   * @return ハッシュ
   ******************************************************/
  public 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 ) = $this->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 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 fwwTag('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!

<< フレームワーク(笑) ver0.2の固定ページ

こんなん書いてた >>

Find it!

Theme Design by devolux.org

Tag Cloud