以前、officeドキュメントの技術調査しながら、xmllintを学習していたんですが、案件のオーダーが入ったのでしばらく置き去りにしておいた作業をまた再開しました。
それはOfficeドキュメントを自由自在にコントロールできるコンバーターツールなんですが、RPAの一貫で独自ツールを作っている人も多いし、それをブログに掲載している人も多いようです。
でも肝心のXML操作について書いてくれている人があまりいないのか、ドンピシャな記事に出会えなかったので、備忘録を兼ねて残しておきたいと思います。
Officeドキュメント(今回はpptx)のXMLについて
XMLについての扱い方やxmllintマニュアル、PHPのsimpleXMLなどのリファレンスサイトはたくさんあるんですが、どれもnamespaceについての記述が少なく、操作する時に結構苦労してしまいます。
ちなみに、pptxのxmlドキュメント(slideデータのみ)は以下のような感じです。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
<p:cSld>
<p:spTree>
<p:nvGrpSpPr>
<p:cNvPr id="1" name=""/>
<p:cNvGrpSpPr/>
<p:nvPr/>
</p:nvGrpSpPr>
<p:grpSpPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="0" cy="0"/>
</a:xfrm>
</p:grpSpPr>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="2" name="Title 1"/>
<p:cNvSpPr>
<a:spLocks noGrp="1"/>
</p:cNvSpPr>
<p:nvPr>
<p:ph type="title"/>
</p:nvPr>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:r>
<a:rPr lang="en-US" dirty="0" smtClean="0"/>
<a:t>{page-1:title}</a:t>
</a:r>
<a:endParaRPr lang="en-US" dirty="0"/>
</a:p>
</p:txBody>
</p:sp>
<p:sp>
<p:nvSpPr>
<p:cNvPr id="3" name="Content Placeholder 2"/>
<p:cNvSpPr>
<a:spLocks noGrp="1"/>
</p:cNvSpPr>
<p:nvPr>
<p:ph idx="1"/>
</p:nvPr>
</p:nvSpPr>
<p:spPr/>
<p:txBody>
<a:bodyPr/>
<a:lstStyle/>
<a:p>
<a:pPr marL="0" indent="0">
<a:buNone/>
</a:pPr>
<a:r>
<a:rPr lang="en-US" dirty="0" smtClean="0"/>
<a:t>{page-1:txt}</a:t>
</a:r>
<a:endParaRPr lang="en-US" dirty="0"/>
</a:p>
</p:txBody>
</p:sp>
<p:pic>
<p:nvPicPr>
<p:cNvPr id="4" name="Picture 3" descr="favicon_64.png"/>
<p:cNvPicPr>
<a:picLocks noChangeAspect="1"/>
</p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rId2">
<a:extLst>
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
</a:ext>
</a:extLst>
</a:blip>
<a:stretch>
<a:fillRect/>
</a:stretch>
</p:blipFill>
<p:spPr>
<a:xfrm>
<a:off x="1975090" y="3022600"/>
<a:ext cx="812800" cy="812800"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:pic>
<p:pic>
<p:nvPicPr>
<p:cNvPr id="5" name="Picture 4" descr="website-3407280_1280.jpg"/>
<p:cNvPicPr>
<a:picLocks noChangeAspect="1"/>
</p:cNvPicPr>
<p:nvPr/>
</p:nvPicPr>
<p:blipFill>
<a:blip r:embed="rId3">
<a:extLst>
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
</a:ext>
</a:extLst>
</a:blip>
<a:stretch>
<a:fillRect/>
</a:stretch>
</p:blipFill>
<p:spPr>
<a:xfrm>
<a:off x="3994603" y="2438546"/>
<a:ext cx="4192198" cy="2793707"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</p:spPr>
</p:pic>
</p:spTree>
<p:extLst>
<p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}">
<p14:creationId xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="1487470405"/>
</p:ext>
</p:extLst>
</p:cSld>
<p:clrMapOvr>
<a:masterClrMapping/>
</p:clrMapOvr>
</p:sld>
p,r,aというnamespaceを持っていて、中には属性にまでnamespaceがついているものもあります。
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
とりあえず、このxmlに対して、情報検索やデータ取得、DOM追加、削除などを操作できなければ先に進むことができないため、Try&Attachで試しながらコーディングしてみたので、それぞれのやり方を書き残しておきます。
simpleXMLでのnamespace操作法
文字列からxmlデータの読み込み
<?php
// 既に読み込まれているxmlデータをパースする
$xmlstr = file_get_contents("slide.xml");
$xml = new SimpleXMLElement($xmlstr);
$ns = $xml->getNamespaces(true);
print_r($xml);
foreach($xml->children($ns["p"]) as $key => $val){
echo $key .":";
print_r($val);
echo PHP_EOL;
}
xmlファイルからの読み込み
$xmlstr = file_get_contents("slide.xml");
$xml = new SimpleXMLElement($xmlstr);
// 上記プログラムの部分を次のように変更するだけ
$xml = simplexml_load_file("slide.xml");
SimpleXMLElement Object
(
)
cSld:SimpleXMLElement Object
(
[spTree] =&gt; SimpleXMLElement Object
(
[nvGrpSpPr] =&gt; SimpleXMLElement Object
(
[cNvPr] =&gt; SimpleXMLElement Object
(
)
[cNvGrpSpPr] =&gt; SimpleXMLElement Object
(
)
[nvPr] =&gt; SimpleXMLElement Object
(
)
)
[grpSpPr] =&gt; SimpleXMLElement Object
(
)
[sp] =&gt; Array
(
[0] =&gt; SimpleXMLElement Object
(
[nvSpPr] =&gt; SimpleXMLElement Object
(
[cNvPr] =&gt; SimpleXMLElement Object
(
)
[cNvSpPr] =&gt; SimpleXMLElement Object
(
)
[nvPr] =&gt; SimpleXMLElement Object
(
[ph] =&gt; SimpleXMLElement Object
(
)
)
)
[spPr] =&gt; SimpleXMLElement Object
(
)
[txBody] =&gt; SimpleXMLElement Object
(
)
)
[1] =&gt; SimpleXMLElement Object
(
[nvSpPr] =&gt; SimpleXMLElement Object
(
[cNvPr] =&gt; SimpleXMLElement Object
(
)
[cNvSpPr] =&gt; SimpleXMLElement Object
(
)
[nvPr] =&gt; SimpleXMLElement Object
(
[ph] =&gt; SimpleXMLElement Object
(
)
)
)
[spPr] =&gt; SimpleXMLElement Object
(
)
[txBody] =&gt; SimpleXMLElement Object
(
)
)
)
[pic] =&gt; Array
(
[0] =&gt; SimpleXMLElement Object
(
[nvPicPr] =&gt; SimpleXMLElement Object
(
[cNvPr] =&gt; SimpleXMLElement Object
(
)
[cNvPicPr] =&gt; SimpleXMLElement Object
(
)
[nvPr] =&gt; SimpleXMLElement Object
(
)
)
[blipFill] =&gt; SimpleXMLElement Object
(
)
[spPr] =&gt; SimpleXMLElement Object
(
)
)
[1] =&gt; SimpleXMLElement Object
(
[nvPicPr] =&gt; SimpleXMLElement Object
(
[cNvPr] =&gt; SimpleXMLElement Object
(
)
[cNvPicPr] =&gt; SimpleXMLElement Object
(
)
[nvPr] =&gt; SimpleXMLElement Object
(
)
)
[blipFill] =&gt; SimpleXMLElement Object
(
)
[spPr] =&gt; SimpleXMLElement Object
(
)
)
)
)
[extLst] =&gt; SimpleXMLElement Object
(
[ext] =&gt; SimpleXMLElement Object
(
)
)
)
clrMapOvr:SimpleXMLElement Object
(
)
プログラムを実行すると、pタグの情報を取得することができますが、ネストになっている内部のnamespace情報はこれだけでは見ることができないので、逐一namespaceアクセスをする必要があります。
そして、namespaceは以下のようにするとアクセスができるようになります。
$ns = $xml->getNamespaces(true);
$xml->children($ns["p"])
$nsは、xmlに定義されているnamespaceを連想配列で取得することが出来ます。
またxpathを使う場合は以下のようにピンポイントにアクセスすることもできます。
print_r($xml->xpath("//a:t"));
>>> Result
SimpleXMLElement Object
(
)
Array
(
[0] => SimpleXMLElement Object
(
[0] => {page-1:title}
)
[1] => SimpleXMLElement Object
(
[0] => {page-1:txt}
)
)
namespace構造の要素の追加
サンプルxmlの任意箇所に要素を追加する場合は以下のようにします。
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="0" cy="0"/>
<a:chOff x="0" y="0"/>
<a:chExt cx="0" cy="0"/>
</a:xfrm>
xml内部(上の方)にあるこの箇所に<a:hoge cx="0" cy="0"/>という要素を追加してみたいと思います。
<?php
// xmlファイルから直接読み込む
$xml = simplexml_load_file("slide.xml");
$ns = $xml->getNamespaces(true);
// 追加処理
foreach($xml->xpath("//a:xfrm") as $xfrm){
$newXml = $xfrm->addChild("a:hoge" , "" , $ns["a"]);
$newXml->addAttribute("a" , "0");
$newXml->addAttribute("b" , "0");
}
print_r($xml->xpath("//a:xfrm/a:hoge"));
>>> Result
Array
(
[0] => SimpleXMLElement Object
(
[@attributes] => Array
(
[a] => 0
[b] => 0
)
)
[1] => SimpleXMLElement Object
(
[@attributes] => Array
(
[a] => 0
[b] => 0
)
)
[2] => SimpleXMLElement Object
(
[@attributes] => Array
(
[a] => 0
[b] => 0
)
)
)
addChildに$ns["a"]を付けているところがミソですね。
要素の削除
最後に要素の削除ですが、基本形は、要素を抽出して、そのparentNodeからremoveCHildするというJavascriptと同じやり方でいいのですが、foreachなどの内部で行いたいときがありますが、その場合は、削除していくと、順番が変わってkey値がずれていくので、よくトラブルになりがちなので、僕の場合は以下のようにしています。
<?php
// xmlファイルから直接読み込む
$xml = simplexml_load_file("slide.xml");
$ns = $xml->getNamespaces(true);
// 削除対象の要素を抽出
$del = [];
foreach($xml->xpath("//a:xfrm") as $xfrm){
$del[] = $xfrm;
}
// 抽出した要素を削除
for($i=count($del)-1; $i>=0; $i--){
$target = dom_import_simplexml($del[$i]);
$target->parentNode->removeChild($target);
echo "deleted ! (".$i.")".PHP_EOL;
}
print_r($xml->xpath("//a:xfrm"));
>>> Result
deleted ! (2)
deleted ! (1)
deleted ! (0)
Array()
無事にデータが削除できていれば完了です。
とりあえず、本日はこのへんまで・・・
namespaceは本当にめんどくさいとしか思えない仕様なのですが、セキュリティも含めてこの仕様の利点が何も思い浮かびません。
でも、MS企画に対応するしかないでしょ。
また、作業が進んできたら、備忘録を残していきたいと思います。
参考サイト
PHP マニュアル
基本的な SimpleXML の使用法
SimpleXMLElement クラス
PisukeCode - Web開発まとめ
PHPプログラミング解説
LIG : 一手間必要。PHPでのSimpleXML関数のパース処理
0 件のコメント:
コメントを投稿