XMLパーサーの作成

背景


私に提示されたホスティングで小さなサービスを開始することに決めたので、そこには単一のXMLパーサーはなく、SimpleXMLもDOMXMLもなく、libxmlとxml-rpcだけであることがわかりました。 考え直すことなく、自分で書くことにしました。 複雑でないrssフィードを解析する必要があったので、xml =>配列クラスだけで十分でした。 [1]

しかし、興味深い記事のために、これは明らかに十分ではなかったので、今度はSimpleXMLの代わりを書きます。 そして同時に、PHP 5の多くの興味深い機能を検討します。

問題の声明


要素へのアクセスは、クラスのプロパティへのアクセス、たとえば$ xml-> elementとして提供され、要素の属性へのアクセスは配列として、これらの$ xml-> element [ 'attr' ] 、属性の存在チェックも使用しますisset ()およびforeachを使用して要素を反復処理します。 それでは、始めましょう。


ちょっとした魔法?


PHP 5では、特定の「マジック」メソッドがクラスに定義され、ダブルアンダースコア「__」で始まり、特定のアクションが発生したときに呼び出されます。 [2]次のものが必要です。


SPL


標準PHPライブラリ-C ++の世界のSTLのような標準PHPライブラリは、典型的な問題を解決するための開発者ツールを提供するために作成されました。 [3]
次のインターフェイスを実装する必要があります。


XMLとexpat


これらは、XMLを操作し、XMLパーサーを作成するための標準ライブラリです。 [4]問題を解決するために必要なもの。 興味を引くために、XMLファイルの解析を手動で、たとえば正規表現で書くことができます。
expatで最も重要なのは、次の関数です。

注:phpのコールバックは、文字列または2つの値を持つ配列として渡される関数の名前です-最初はクラスの名前で、2番目はこのクラスのメソッドの名前です。

ポインタ


PHPのポインターは、CまたはC ++のようには機能しません。 [5]実際、 $ a =& $ bの構築は $ aが$ bと同じデータを持つエリアを指し、 $ b$ aを指すアドレスを変更すること不可能であることを意味するだけであり、アドレスの変更には、1レベルのネストがあります。
5番目のバージョンから、PHPではすべての変数がポインターによって関数に渡されますが、値を変更するとすぐに、新しい変数にメモリが割り当てられます。 この場合、ポインターは親要素を指すのに便利です。

コーディング


理論が完了したら、パーサーの作成に直接進みましょう。
各オブジェクトは1つのxml要素を表すため、タグ名、属性、データ、親への参照、子孫を持つ配列などのプロパティが必要になります。さらに、現在の要素へのポインタ変数が必要になります。 メソッドのうち、すべてのインターフェイスを実装し、子孫を追加し、親への参照を設定し、要素のコンテンツとパーサーに必要な3つの関数(タグの開閉と要素のコンテンツの受信)を割り当てる必要があります。
将来のクラスをスケッチしましょう:
クラス XMLはArrayAccess IteratorAggregate Countable {

プライベート$ポインター ;

private $ tagName ;


private $ attributes = array();

プライベート$ cdata ;

プライベート$親 ;

private $ childs = array();



パブリック 関数 __construct $ data ){}




パブリック 関数 __toString (){return; }



パブリック 関数 __get $ name ){return; }




パブリック 関数 offsetGet $ offset ){return; }



パブリック 関数 offsetExists $ offset ){return; }




パブリック 関数 offsetSet $ offset $ value ){return; }

パブリック 関数 offsetUnset $ offset ){return; }




パブリック 関数 count (){return; }



パブリック 関数 getIterator (){return; }




パブリック 関数 appendChild $ tag $ attributes ){return; }



パブリック 関数 setParent XML $ parent ){}




パブリック 関数 getParent (){return; }



パブリック 関数 setCData $ cdata ){}




プライベート 関数 解析 $ data ){}



プライベート 関数 tag_open $ parser $ tag $ attributes ){}



プライベート 関数 cdata $ parser $ cdata ){}


プライベート 関数 tag_close $ parser $ tag ){}


}

それでは、関数の実装に取り​​掛かりましょう。 順番に、コンストラクターから始めましょう。 私たちの場合、PHPの異なるパラメータを持つ1つのメソッドのオーバーロードがないため、文字列(xml)または2つの要素の配列(要素名、属性)の2種類の値を取ることができます-型を手動で確認する必要があります
パブリック 関数 __construct $ data ){

if( is_array $ data )){

list( $ this- > tagName $ this- > attributes )= $ data ;


} else if( is_string $ data ))

$ this- > parse $ data );

}

既に述べたように、 __ toString()マジックメソッドの助けを借りて、ユーザーは要素データを文字列として取得し、それを必要な型に変換することができます。
同時に、次のマジックメソッド__get( $ nameを分析し、それを使用して現在の要素の子孫にアクセスします。 子孫が1つしかない場合は、配列の0インデックスにアクセスする必要なく、すぐに子孫を返すのが論理的です。 例: $ xml-> rss-> channel-> item [ 5 ]-> urlの代わりに、 $ xml-> rss [ 0 ]-> channel [ 0 ]-> item [ 5 ]-> url [ 0 ]の場合、要素rss、channel、およびurlは、ネストレベルで単一のインスタンスに存在します。
パブリック 関数 __toString (){

$ this- > cdataを 返し ます

}



パブリック 関数 __get $ name ){


if(isset( $ this- > childs [ $ name ])){

if( count $ this- > childs [ $ name ])== 1


return this this- > childs [ $ name ] [ 0 ];

他に

return $ this- > childs [ $ name ];


}

新しい 例外を スロー 「UFOが[$ name]を盗む!」 );

}


offsetGetoffsetExistsoffsetSet、およびoffsetUnset関数は、 ArrayAccessインターフェイスを実装して、オブジェクトを配列としてアクセスします。 これを使用して、要素の属性にアクセスします。 offsetSetoffsetUnsetは、今のところスタブとして残されます。
パブリック 関数 offsetGet $ offset ){

if(isset( $ this- > attributes [ $ offset ]))


$ this- > attributes [ $ offset ];を 返し ます。

新しい 例外を スロー 「聖なる牛![$オフセット]属性はありません!」 );

}




パブリック 関数 offsetExists $ offset ){

return isset( $ this- > attributes [ $ offset ]);

}


そして今、私たちは最近の決定により問題に直面しています。 突然単一の要素でforeachサイクルを開始したい場合、xmlオブジェクト自体で開始します! したがって、要素属性にforeachを使用し、 getAttributes()メソッドを実装するには、 簡単な方法で機会を犠牲にする必要があります。 そして、呼び出された要素が属する要素の配列のイテレータと要素数を返します。親がない場合は、現在の要素の配列に対するイテレータを返します。 したがって、 IteratorAggregateおよびCountableインターフェースが実装されます。
パブリック 関数 カウント (){

if( $ this- > parent != null

return count $ this- > parent- > childs [ $ this- > tagName ]);


1を 返し ます。

}



パブリック 関数 getIterator (){

if( $ this- > parent != null


新しい ArrayIteratorを返す $ this- > parent- > childs [ $ this- > tagName ]);

新しい ArrayIterator (array( $ this ));


}


子孫の追加は単純な関数です;唯一の興味深い点は、要素を追加した後、それへのリンクを返すことです。
パブリック 関数 appendChild $タグ $属性 ){

$ element = new XML (array( $ tag $ attributes ));


$要素 -> setParent $ this );

$ this- > childs [ $ tag ] [] = $ element ;

$要素を 返し ます


}

次に、パーサー自体を実装します。 ツリー構造を作成するには、現在の要素へのポインターを使用します。 最初は、現在の要素、タグが開かれたとき、開いた要素に直接インストールされ、その中に含まれるすべての要素がその子孫に追加され、タグが閉じられたときに親要素に追加されます。
プライベート 関数 parse $ data ){

$ this- > pointer =& $ this ;

$パーサー = xml_parser_create ();


xml_set_object $ parser $ this );

xml_parser_set_option $ parser XML_OPTION_CASE_FOLDING false );

xml_set_element_handler $ parser "tag_open" "tag_close" );


xml_set_character_data_handler $ parser "cdata" );

xml_parse $ parser $ data );

}



プライベート 関数 tag_open $ parser $ tag $ attributes ){


$ this- > pointer =& $ this- > pointer- > appendChild $ tag $ attributes );

}


プライベート 関数 cdata $ parser $ cdata ){


$ this- > ポインター -> setCData $ cdata );

}


プライベート 関数 tag_close $ parser $ tag ){


$ this- > pointer =& $ this- > pointer- > getParent ();

}


それだけです パーサーの準備が整いました。 記事をこれ以上膨らませないために、Googleドキュメントに関するコメント と使用例も含め完全なソースコードをアップロードしました。 [6]

次は?


これは、まだSimpleXMLの完全な代替ではありません;パーサーは、その中のデータからxmlドキュメントを作成する方法をまだ知りません。 必要な機能を追加するのは難しい作業ではないので、宿題のやり方に興味がある人に任せます。

参照資料


1) xml => array parserの最初のバージョン。
2) マジックメソッドに関するドキュメント(eng)( rus )。
3) SPLドキュメント
4) xmlパーサーの機能の説明。
5) サイン (eng) による文書化rus )。
6) パーサー最終バージョンと簡単な使用例

Source: https://habr.com/ru/post/J30353/


All Articles