phpxpathdomxpath

PHP Xpath: How to get table headers where another table is inside the first row?


I am trying to extract the headers table from this url: https://www4.bcb.gov.br/pec/poupanca/poupanca.asp

enter image description here

Unfortunately, they use a "non-standard" html table where instead of rowspan they used a (bizarre) table inside the first row 😩...

So I would like to extract dynamically this header in an array of 8 items like this (merging the the headers with two rows size):

$columns_extracted_result = [
    'Data',
    'DataFim',
    'Depósitos até 03.05.2012 - Remuneração básica ',
    'Depósitos até 03.05.2012 - Remuneração adicional',
    'Depósitos até 03.05.2012 - Remuneração total',
    'Depósitos a partir de 04.05.2012 (*) -Remuneração básica',
    'Depósitos a partir de 04.05.2012 (*) - Remuneração adicional',
    'Depósitos a partir de 04.05.2012 (*) - Remuneração total'
];

And after that, create a array where the keys will be the $columns_extracted_result like:

$table = [
    [
        'Data' => '26/04/2022',
        'DataFim' => '26/05/2022',
        'Depósitos até 03.05.2012 - Remuneração básica' => '0,1538'
    //...
    ],
    [
        'Data' => '27/04/2022',
        'DataFim' => '27/05/2022',
        'Depósitos até 03.05.2012 - Remuneração básica' => '0,1568'
    //...
    ]
];

How Can I achieve this using DomXpath ?


Solution

  • Solution

    get_columns

    The get_columns function returns a list of columns as defined in the table headers:

    array(8) {
      [0]=>
      string(4) "Data"
      [1]=>
      string(8) "Data fim"
      [2]=>
      string(50) "Depósitos até 03.05.2012 - Remuneração básica"
      [3]=>
      string(52) "Depósitos até 03.05.2012 - Remuneração adicional"
      [4]=>
      string(48) "Depósitos até 03.05.2012 - Remuneração total"
      [5]=>
      string(61) "Depósitos a partir de 04.05.2012 (*) - Remuneração básica"
      [6]=>
      string(63) "Depósitos a partir de 04.05.2012 (*) - Remuneração adicional"
      [7]=>
      string(59) "Depósitos a partir de 04.05.2012 (*) - Remuneração total"
    }
    

    get_rows

    The get_rows function returns the complete data set, see code and output:

    Complete code

    <?php
    
    $doc = new \DOMDocument();
    @$doc->loadHTMLFile("https://www4.bcb.gov.br/pec/poupanca/poupanca.asp");
    $xpath = new DOMXpath($doc);
    
    $data = get_rows($xpath, get_columns($xpath));
    var_dump($data);
    
    function get_inner_html(DOMElement $node) : string 
    {
        $innerHTML= '';
        $children = $node->childNodes;
        foreach ($children as $child) {
            $innerHTML .= $child->ownerDocument->saveXML( $child );
        }
    
        return $innerHTML;
    }
    
    function get_clean_value(DomElement $text) : string 
    {
        $html = get_inner_html($text);
        $decode = html_entity_decode($html);
        $replace = str_replace(["<br/>", "\t", PHP_EOL, " ", "\xc2\xa0"], " ", $decode);
        $stripped = strip_tags($replace);
        return trim($stripped);
    }
    
    function get_columns(DOMXPath $xpath) : array 
    {
        $tds = $xpath->query("(/html/body/table/tbody/tr)[1]/td");
        $columns = [];
        foreach($tds as $td) {
            $tdFirst = $xpath->query('(table)[1]/tr/td', $td);
            $tdSecond = $xpath->query("(table)[2]/tr/td", $td);
            if($tdFirst->count() > 0 && $tdSecond->count() > 0) {
                $columnPrefix = get_clean_value($tdFirst[0]);
                foreach($tdSecond as $item) {
                    $columns[] = $columnPrefix . " - " . get_clean_value($item);
                }
            } else {
                $columns[] = get_clean_value($td);
            }
        }
        return $columns;
    }
    
    function get_rows(DOMXPath $xpath, array $columns) : array 
    {
        $result = [];
        $trs = $xpath->query("/html/body/table/tbody/tr");
        foreach($trs as $count=>$tr) {
            if($count === 0) continue;
            $tds = $xpath->query("td", $tr);
            $row = [];
            foreach($tds as $td) {
                $row[] = get_clean_value($td);
            }
            $result[] = array_combine($columns, $row);
        }
        return $result;
    }
    

    Output

    array(45) {
      [0]=>
      array(8) {
        ["Data"]=>
        string(10) "27/04/2022"
        ["Data fim"]=>
        string(10) "27/05/2022"
        ["Depósitos até 03.05.2012 - Remuneração básica"]=>
        string(6) "0,1568"
        ["Depósitos até 03.05.2012 - Remuneração adicional"]=>
        string(6) "0,5000"
        ["Depósitos até 03.05.2012 - Remuneração total"]=>
        string(6) "0,6576"
        ["Depósitos a partir de 04.05.2012 (*) - Remuneração básica"]=>
        string(6) "0,1568"
        ["Depósitos a partir de 04.05.2012 (*) - Remuneração adicional"]=>
        string(6) "0,5000"
        ["Depósitos a partir de 04.05.2012 (*) - Remuneração total"]=>
        string(6) "0,6576"
      }
    ...