我用 php 创建了一个脚本来将 xml 转换为 csv,但是所有结果都是垂直的,而不是在一行中带有标题

Posted

技术标签:

【中文标题】我用 php 创建了一个脚本来将 xml 转换为 csv,但是所有结果都是垂直的,而不是在一行中带有标题【英文标题】:I create a script with php to convert xml to csv, but all results is vertical instead in one line with headers 【发布时间】:2022-01-09 21:51:40 【问题描述】:

我想通过 php 创建一个将 xml 转换为 csv 的脚本。我从 url 获取 xml,并使用以下代码制作了一个 csv。问题是该字段是垂直的而不是水平的。

例如我的xml是这样的:

<product>
   <id>1001</id>
   <sku>product1</sku>
   <name>Product 1 Name</name>
   <manufacturer>My Company</manufacturer>
</product>
<product>
   <id>1002</id>
   <sku>product2</sku>
   <name>Product 2 Name</name>
   <manufacturer>My Company</manufacturer>
</product>
<product>
   <id>1003</id>
   <sku>product3</sku>
   <name>Product 3 Name</name>
   <manufacturer>My Company</manufacturer>
</product>

我得到类似的东西:

id,1001
sku,product1
name,"product 1"
manufacturer,My Company
id,1002
sku,product2
name,"product 2"
manufacturer,My Company
id,1003
sku,product3
name,"product 3"
manufacturer,My Company

而不是这个(我想要的)

"id","sku","name","manufactuer"
"1001","product1","Product 1","My Company"
"1002","product2","Product 2","My Company"
"1003","product3","Product 3","My Company"

我现在的代码是

file_put_contents("products.xml", fopen("https://xml.mysite.com/get.asp?xml=products&key=myxml", 'r'));

    if (file_exists('products.xml'))
       $xml = simplexml_load_file('products.xml');
       file_put_contents("products.csv", "");
       $f = fopen('products.csv', 'w');
       createCsv($xml, $f);
       fclose($f);
    

    function createCsv($xml,$f)
        foreach ($xml->children() as $item) 
           $hasChild = (count($item->children()) > 0)?true:false;
            if(!$hasChild)
                $put_arr = array($item->getName(),$item); 
                fputcsv($f, $put_arr ,',','"');
             else 
                createCsv($item, $f);
            
        
     

请问我能做什么?

【问题讨论】:

查看第一个comment here 了解将 XML 转换为数组的非常简单的方法,只要您的 XML 相对简单就可以使用 【参考方案1】:

SimpleXML(和 DOM)可以使用 Xpath 从 XML 中获取元素。行需要一个表达式,列需要一个表达式列表。

function readRowsFromSimpleXML(
  SimpleXMLElement $element, string $rowExpression, array $columnExpressions
): Generator 
    foreach ($element->xpath($rowExpression) as $rowNode) 
        $row = [];
        foreach ($columnExpressions as $column => $expression) 
            $row[$column] = (string)($rowNode->xpath($expression)[0] ?? '');
        
        yield $row;
    


$rows = readRowsFromSimpleXML(
  simplexml_load_file('products.xml'), 
  '//product',
  $columns = [
      'id' => './id',
      'sku' => './sku',
      'name' => './name',
      'price' => './price',
      'manufacturer' => './manufacturer'
  ]
);

readRowsFromSimpleXML(...) 将返回一个Generator。它还不会读取数据。只有当你解决它时才会发生这种情况 - 例如使用foreach()

显式处理行和列数据可以使输出更加稳定。如果缺少元素,它甚至可以工作。我添加了一个price 列来显示这一点。

要将其放入 CSV,您必须迭代生成器:

$fh = fopen('php://stdout', 'w');
fputcsv($fh, array_keys($columns));
foreach ($rows as $row) 
    fputcsv($fh, array_values($row));

输出:

id,sku,name,price,manufacturer
1001,product1,"Product 1 Name",,"My Company"
1002,product2,"Product 2 Name",,"My Company"
1003,product3,"Product 3 Name",,"My Company"

这也适用于更复杂的表达式。例如读取价格元素的货币属性或多张图片:

$columns = [
    'id' => './id',
    'sku' => './sku',
    'name' => './name',
    'manufacturer' => './manufacturer',
    'price' => './price',
    'price' => './price/@currency',
    'image0' => '(./image)[1]',
    'image1' => '(./image)[2]'
]';

如果需要聚合值,请在列定义中添加回调。

function readRowsFromSimpleXML(
  SimpleXMLElement $element, string $rowExpression, array $columnExpressions
): Generator 
    foreach ($element->xpath($rowExpression) as $rowNode) 
        $row = [];
        foreach ($columnExpressions as $column => $options) 
            if (is_array($options)) 
                [$expression, $callback] = $options;
             else 
                $expression = $options;
                $callback = null;
            
            $values = $rowNode->xpath($expression);
            if ($callback) 
                $row[$column] = $callback($values);
             else 
                $row[$column] = (string)($rowNode->xpath($expression)[0] ?? '');
            
        
        yield $row;
    


$rows = readRowsFromSimpleXML(
  simplexml_load_file('products.xml'), 
  '//product',
  $columns = [
      'id' => './id',
      'sku' => './sku',
      // ...
      'categories' => [ './category', fn ($values) => implode(',', $values) ]
  ]
);

复杂的配置数组难以维护。一个更封装的方法是一个类。以下类适用于 SimpleXML 和 DOM。字段/列是通过方法添加的。

class XMLRecordsReader implements \IteratorAggregate 
    
    private $_source;
    private $_expression = './*';
    private $_fields = [];
    
    public function __construct($source) 
        if ($source instanceof \SimpleXMLElement) 
            $this->_source = dom_import_simplexml($source);
            return;
         
        if ($source instanceof \DOMNode) 
            $this->_source = $source;
            return;
        
        throw new \InvalidArgumentException('Need SimpleXMLElement or DOMNode $source.');
    
    
    public function setExpression(string $expression): self 
        $this->_expression = $expression;
        return $this;
    
    
    public function addField(string $name, string $expression, callable $mapper = null): self 
        $this->_fields[$name] = [$expression, $mapper];
        return $this;
    
    
    public function getIterator(): \Generator 
        $xpath = new DOMXpath(
            $this->_source instanceof DOMDocument ? $this->_source : $this->_source->ownerDocument
        );
        foreach ($xpath->evaluate($this->_expression, $this->_source) as $node) 
            $record = [];
            foreach ($this->_fields as $field => $options) 
                [$expression, $mapper] = $options;
                $values = $xpath->evaluate($expression, $node);
                if ($mapper) 
                    $record[$field] = $mapper($values);
                 else if ($values instanceof DOMNodeList) 
                    $value = $values[0] ?? null;
                    $record[$field] = $value->textContent ?? '';
                 else 
                    $record[$field] = (string)($values ?? '');
                
            
            yield $record;
        
    


$reader = new XMLRecordsReader(
  simplexml_load_file('products.xml'), 
);
$reader
  ->addField('id', './id')
  ->addField('sku', './sku')
  ->addField('name', './name')
  ->addField('manufacturer', './manufacturer')
  ->addField('price', './price')
  ->addField('currency', './price/@currency')
  ->addField('image0', '(./image)[1]')
  ->addField('image1', '(./image)[2]')
  ->addField(
      'categories', 
      './category', 
      fn (\DOMNodeList $values) => implode(
          ',', 
          array_map(
              fn (\DOMNode $node) => $node->textContent,
              iterator_to_array($values)
          )
      )
  );

var_dump(iterator_to_array($reader));

【讨论】:

谢谢。有什么方法可以自动获取字段名称?因为现在例如我写了 5 列。在实际项目中,我有超过 50 个而不是静态的。 (在一个 xml 中,我可能有 45 个,其他 60 个,其他 30 个) 当然,您可以从 XML 中读取标签名称并从中构建表达式,但我建议使用固定定义。想一想,XML 可能会发生变化,这会直接传递到您的出口——使其不稳定。您甚至无法记录导出,因为您无法控制它。 非常感谢我的朋友。最后一个问题。在一个xml标签里面的结构是Url 1st photoUrl 2st photoUrl 3rd photo等...。上面的代码中是否有任何方法可以在名为“image0”=> 1st url、“image1”=> 2nd url 等的 csv 动态字段中创建?谢谢 当然,我为更复杂的表达式添加了一个示例。

以上是关于我用 php 创建了一个脚本来将 xml 转换为 csv,但是所有结果都是垂直的,而不是在一行中带有标题的主要内容,如果未能解决你的问题,请参考以下文章

将 .CSV 文件转换为 .XML 的 PHP 脚本

管道输入到脚本中

在 PHP 中将 JSON 转换为 XML,但在 XML 中为 JSON 数组创建一个容器元素

访问对象属性(在没有数组转换的情况下向下钻取)[重复]

将多个json文件转换为xml文件

php 脚本为导入对象mcity创建xml文件