これを行う効率的な方法は見つかりませんでした。RecursiveIteratorIteratorは良い候補でしたが、残念ながら、最終ノードへの完全なパスではなく、キーとして単一の値しか提供しませんでした。
最終的に、これとまったく同じことを行うクラスを開発しました。このクラスは、任意の XML ドキュメントを取り、「配列/オブジェクト」の性質を持つ CSV を作成します。
/**
* This class converts a multidimentional XML file into a CSV file.
* First XML level are distinct CSV lines
* Last XML level are values
* Path from the first to the last XML levels are CSV column names.
*
* @todo May be conflicts with XML nodes with names finishing by [n].
* => Case <x>test</x><x>test</x><x[0]>test</x[0]>
* will generate 2 times x[0] on CSV file.
*
* @author ninsuo <ninsuo at gmail dot com>
*/
class Xml2Csv
{
/**
* An array that looks like :
*
* array(
* 'simple.name' => stdClass(
* ->names => array('real.name[0]', 'real.name[1]', ...)
* ->positions => array('position-in-rows-1', 'position-in-rows-2', ...)
* ),
* ...
* )
*
* Allow us to create dynamic column names according to
* content disposition.
*
* @access private
* @var array
*/
private $columnNames;
/**
* Rows of CSV file
*
* @access private
* @var array
*/
private $rows;
/**
* Current row number
*
* @access private
* @var int
*/
private $rowNumber;
public function convert($xmlSource, $csvTarget)
{
$this->_checkSourceAndTarget($xmlSource, $csvTarget);
libxml_use_internal_errors();
try
{
$tree = new SimpleXMLIterator($xmlSource, 0, true);
}
catch (Exception $e)
{
throw new Exception("Can't load XML : " . $e->getMessage());
}
libxml_clear_errors();
$this->_reset();
$this->_browseXMLTree($tree);
unset($tree);
$this->_writeCSV($csvTarget);
$this->_reset();
}
/**
* Checks if $source file exists and is readable.
* Checks if $target file is writable
*
* @access private
* @param string $source
* @param string $target
* @throws Exception
*/
private function _checkSourceAndTarget($source, $target)
{
if ((!is_file($source)) || (!is_readable($source)))
{
throw new Exception("Source file does not exist or is not readable.");
}
if (((is_file($target)) && (!is_writable($target))) || (!is_writable(dirname($target))))
{
throw new Exception("Target file is not writable.");
}
}
/**
* Reset attributes (avoid taking huge amount of memory when converting big files)
*
* @access private
*/
private function _reset()
{
$this->columnNames = array ();
$this->rows = array ();
$this->rowNumber = 0;
}
/**
* First XML-level are CSV rows
*
* @access private
* @param SimpleXMLIterator $tree
*/
private function _browseXMLTree($tree)
{
foreach ($tree as $node)
{
if (count($node) > 0)
{
$this->rows[$this->rowNumber] = array ();
$this->_browseXMLNode($node);
$this->rowNumber++;
}
}
}
/**
* Browsing next XML levels until a node has no child (CSV value)
*
* @access private
* @param type $node
* @param array $path
*/
private function _browseXMLNode($node, array &$path = array ())
{
array_push($path, $node->getName());
foreach ($node as $key => $child)
{
if (count($child) > 0)
{
$this->_browseXMLNode($child, $path);
}
else
{
$this->_addValue(implode('.', $path) . '.' . $key, strval($child));
}
}
array_pop($path);
}
/**
* Create a CSV column if it does not exist.
* Add a value to the given CSV column.
*
* @access private
* @param string $path
* @param string $value
*/
private function _addValue($column, $value)
{
if (array_key_exists($column, $this->columnNames))
{
$columnInfo = $this->columnNames[$column];
foreach ($columnInfo->positions as $position)
{
if (array_key_exists($position, $this->rows[$this->rowNumber]) == false)
{
$this->rows[$this->rowNumber][$position] = $value;
return;
}
}
if (count($columnInfo->positions) == 1)
{
$columnInfo->names[0] .= '[0]';
}
$columnPosition = $this->_countCSVColumns();
array_push($columnInfo->names, $column . '[' . count($columnInfo->positions) . ']');
array_push($columnInfo->positions, $columnPosition);
$this->columnNames[$column] = $columnInfo;
$this->rows[$this->rowNumber][$columnPosition] = $value;
}
else
{
$columnPosition = $this->_countCSVColumns();
$columnInfo = new stdClass();
$columnInfo->names[0] = $column;
$columnInfo->positions[0] = $columnPosition;
$this->columnNames[$column] = $columnInfo;
$this->rows[$this->rowNumber][$columnPosition] = $value;
}
}
/**
* Return current number of columns in the CSV file.
* Used to get index of a new column.
*
* @access private
* @return int
*/
private function _countCSVColumns()
{
$count = 0;
foreach ($this->columnNames as $columnInfo)
{
$count += count($columnInfo->positions);
}
return $count;
}
/**
* Write CSV file
*
* @access private
* @param string $csvTarget
*/
private function _writeCSV($csvTarget)
{
$columns = $this->_getCSVColumns();
if (($handle = fopen($csvTarget, 'w')) === false)
{
throw new Exception("Cannot open target file : fopen() failed.");
}
$this->_writeCsvRow($handle, $columns);
$columnPositions = array_keys($columns);
$columnNumber = count($columnPositions);
for ($currentRow = 0; ($currentRow < $this->rowNumber); $currentRow++)
{
$csvRow = array ();
for ($currentColumn = 0; ($currentColumn < $columnNumber); $currentColumn++)
{
$position = $columnPositions[$currentColumn];
if (array_key_exists($position, $this->rows[$currentRow]) == false)
{
$csvRow[$position] = '';
}
else
{
$csvRow[$position] = $this->rows[$currentRow][$position];
}
}
$this->_writeCsvRow($handle, $csvRow);
}
fclose($handle);
}
/**
* Return CSV columns as a single array
*
* @access private
* @return array
*/
private function _getCSVColumns()
{
$columns = array ();
foreach ($this->columnNames as $columnInfo)
{
foreach ($columnInfo->names as $key => $name)
{
$columns[$columnInfo->positions[$key]] = $name;
}
}
ksort($columns);
return $columns;
}
/**
* Write a row into CSV file
*
* @access private
* @param resource $handle
* @param array $csvRow
* @throws Exception
*/
private function _writeCsvRow($handle, $csvRow)
{
if (fputcsv($handle, $csvRow, "\t", '"') === false)
{
fclose($handle);
throw new Exception("Cannot write target file, fputcsv() failed.");
}
}
}
デモ :
1/ demo.xml ファイルを作成します。
<items>
<item>
<sku>abc 1</sku>
<title>a book 1</title>
<price>42 1</price>
<attributes>
<attribute>
<name>Number of pages 1</name>
<value>123 1</value>
</attribute>
<attribute>
<name>Author 1</name>
<value>Rob dude 1</value>
</attribute>
</attributes>
<contributors>
<contributor>John 1</contributor>
<contributor>Ryan 1</contributor>
</contributors>
</item>
<item>
<sku>abc 2</sku>
<title>a book 2</title>
<price>42 2</price>
<attributes>
<attribute>
<name>Number of pages 2</name>
<value>123 2</value>
</attribute>
<attribute>
<name>Author 2</name>
<value>Rob dude 2</value>
</attribute>
</attributes>
<contributors>
<contributor>John 2</contributor>
<contributor>Ryan 2</contributor>
</contributors>
</item>
</items>
2/これをあなたの仕事のどこかに入れてください
$service = new Xml2Csv();
$service->convert('demo.xml', 'demo.csv');
3/ "demo.csv" の出力を確認します。
item.sku item.title item.price item.attributes.attribute.name[0] item.attributes.attribute.value[0] item.attributes.attribute.name[1] item.attributes.attribute.value[1] item.contributors.contributor[0] item.contributors.contributor[1]
"abc 1" "a book 1" "42 1" "Number of pages 1" "123 1" "Author 1" "Rob dude 1" "John 1" "Ryan 1"
"abc 2" "a book 2" "42 2" "Number of pages 2" "123 2" "Author 2" "Rob dude 2" "John 2" "Ryan 2"
注:これは簡単に書かれていますが、convert()
メソッドにいくつかのパラメーターを追加することで、CSVセパレーターや必要なものを変更できます。
楽しみ。