Autor Zpráva
Sylar
Profil
Zdravím,
lze nějakým způsobem omezit velikost načítaných dat nebo jejich konkrétní počet u funkce simplexml_load_file() ?
Zpracovávám jedno obrovské XML od klienta a potřeboval bych si jej rozdělit na několik menších částí a zpracovat jej poustupně.
Keeehi
Profil
Jednoduchá odpověď je ne. Simplexml_load_file() dokáže načíst vždy jen celý soubor.

Řešení tu ale je. Nejdříve ten soubor rozdělíš do několika menších a ty pak budeš načítat. Ovšem to rozdělení musíš udělat chytře, aby to byly zase validní xml soubory.
Sylar
Profil
problém je s tím samotným rozdělením, ať hledám jak hledám, všechny funkce, které dokáží číst soubor po řádcích nebo znacích potřebují nejprve fopen(), která ale do paměti načte celý soubor najednou a už máme problém s memory_limitem. To samé např. u curl, tam je to obdobné s curl_exec().

Já ten soubor bohužel nemohu fyzicky rozdělit na více, protože jej dostávám od jiného uživatele.

Jediné co funguje je výpis souboru přes curl, když není nastavené RETURNTRANSFER na true, pak se soubor pouze vypíše na obrazovku, ale zase jej nemám v žádné proměnné, takže s ním stejně nemohu pracovat.
ShiraNai7
Profil
Sylar:
nejprve fopen(), která ale do paměti načte celý soubor najednou

fopen() vrací resource jako ukazatel na soubor (pomocí kterého jde soubor číst/zapisovat) .V žádném případě nenačítá celý soubor do paměti!
Keeehi
Profil
$handle = fopen("soubor.xml", "r");
$i=1;
while (!feof($handle)) {
  file_put_contents("soubor_$i.txt" fread($handle, 1000));
  $i++;
}
fclose($handle);
Takto se dá soubor rozdělit do více souborů po 1000 bytech. Je to ovšem hloupé řešení, jelikož vám to rozbije strukturu xml souboru. Vy tedy budete v těch 1000 bytech muset nalézt poslední uzavírací element a mezi ním a dalším otevíracím to rozdělit, to co je před uložit a to co následuje si zapamatovat a v dalším průchodu přidat před těch nových 1000 bytů. Realizace je však na vás.
Sylar
Profil
ShiraNai7:
máš pravdu, moje chyba. fopen() bohužel lze použít pouze na lokální soubory, nikoliv externí, fsockopen zase není na serveru povolené ...
Keeehi
Profil
fopen lze použít i na externí soubory. Záleží to však na nastavení allow_url_fopen. Ovšem pokud to je zakázané, stejně se k tomu souboru nedostanete. Pokud máte k dispozici cURL, tak výstup se dá i uložit do spuboru místo do proměnné nebo přímého výstupu. CURLOPT_FILE
Sylar
Profil
Keeehi:
tak jsem to tak nakonec vyřešil. celé xml si přes curl stáhnu na local, tam si jej rozdělím na několik menších a ty poté zpracuju klasicky přes simplexml_load_file() .

děkuji všem za pomoc
Alphard
Profil
Sylar:
Alternativou je obětovat simplexml a použít pro zpracování jiné metody, které nenačítají celý soubor do paměti. Pak je sice problematické se v souboru pohybovat, ale jednoduchý průchod je bezproblémový.

Mimochodem, když zveřejníte script, který rozdělí velké xml do menších (je-li aspoň trochu obecný), časem to určitě někdo ocení.
[#10] Sylar Děkuji.
Sylar
Profil
určitě, ještě jsem ho trošku upravoval. pokud byste tam měl někdo námět na zlepšení, asi nejen já za něj budu rád :)

    // uložení feedu na local
    $c = curl_init($cesta_ke_xml_feedu);
    $f = fopen('xml.txt', 'w');
    curl_setopt($c, CURLOPT_FILE, $f);
    curl_setopt($c, CURLOPT_TIMEOUT, 60);
    if (!curl_exec($c)) {
      $xmls = false;
      fclose($f);
    } else {
      fclose($f);
      
      // zpracování feedu locálně
      $f = fopen( 'xml.txt', 'r');
      $fir_line = fgets($f);
      $sec_line = fgets($f);
      $las_line = "</XML_TAG>"; // uzavírací tag xml feedu
  
      $file = "";
      $i = 0;
      $j = 0;
      $max = 10000; // maximální počet položek vzatých z velkého xml
      $xmls = array();
      
      // procházíme velký xml feed
      while (!feof($f)) {
        $line = fgets($f);
        if (stripos($line, "</KONCOVY_TAG_POLOZKY>") !== false) $i++; // pokud aktuální řádka obsahuje koncovou značku položky xml feedu, iterujeme počet položek
        $file .= $line;
        if ($i == $max) { // dosáhli jsme maximálního počtu položek
          $fs = fopen('xml_' . $j . '.txt', 'w'); // vytvoříme malý xml feed
          fwrite($fs, $fir_line . $sec_line . $file . $las_line); // přidáme k němu první, druhý a za něj i poslední řádek, aby byl feed kompletní
          fclose($fs);
        
          $xmls[] = 'xml_' . $j . '.txt'; // cestu k malému xml feedu si uložíme do pole
          $j++;
          $file = "";
          $i = 0;
        }
      }
      fclose($f);
    
      $fs = fopen('xml_' . $j . '.txt', 'w'); // zbývající řádky z velkého xml feedu uložíme do nového, malého xml
      fwrite($fs, $fir_line . $sec_line . $file);
      fclose($fs);
      $xmls[] = 'xml_' . $j . '.txt';
    
    }
    
    unlink('xml.txt'); // smažeme velký xml feed
    
    foreach($xmls as $val) {
      $xml = simplexml_load_file($val);
      unlink($val); // smazání malých xml feedu
      
      // zpracování malých xml feeedu
    }

Samozřejmě není nutné si cesty k malým xml ukládat do pole, ale zpracovávat je rovnou ve while a poslední část ihned pod ním. Je ale potřeba myslet na to, že tam kód pro zpracování bude dvakrát. Pokud je zpracování na více řádků (provádí se před uložením do db hodně operací) je toto dle mého výhodnější, právě kvůli neredundantnosti kódu.
Keeehi
Profil
Tak jsem na to vytvořil třídu xmlCutter. Ovládání je velmi jednoduché. Třemi metodami se nastaví vstupní soubor, výstupní složka a maska pro název výstupních souborů. Pak už se jen proces dělení spustí.
Třída je čistotná. Po skončení maže dočasné soubory, nebo pokud během dělení nastal problém, maže i zatím úspěšně vytvořené soubory.

inputFile($inputFile) - vstupní soubor, může to být jak lokální soubor, tak i url adresa.
outputFolder($outputFolder) - výstupní složka, kam se uloží vstupní soubor rozdělený na menší části
outputFilenameMask($mask) / outputFilenameMask($first, $second) - řetězec podle kterého se budou vytvářet jména výstupních souborů. Buď má formát, aby se dal použít ve funkci sprintf() s jednou číselnou proměnnou, nebo jsou očekávány dva obyčejné řetězce mezi které se vloží číslo a to bude jméno souboru. Tato metoda je nepovinná. Pokud se nezavolá, použije je jméno vstupního souboru.
cut($type, $limit) - Druhý parametr určuje, typ dělení. Buď podle počtu položek (xmlCutter::BY_ITEMS) nebo podle velikosti výstupního souboru (xmlCutter::BY_BYTES), aby nikdy nepřesáhla zadanou hodnotu. První parametr určuje právě počet položek, nebo velikost v bytech. Oba parametry jsou nepovinné. Výchozí hodnoty jsou $limit = 100, $type = BY_ITEMS. Návratovou hodnotou je pole s názvy vygenerovaných souborů.

Přesto že jsem třídu otestoval, doporučuji ji před nasazením řádně otestovat na předpokládaných datech. Pokud objevíte nějakou chybu, budu rád za zpětnou vazbu.

Příklad použití:
header('Content-type: text/html; charset=utf-8');
$xmlCutter = new xmlCutter();
$xmlCutter->inputFile ("feed.xml");
$xmlCutter->outputFolder ("./out/");
$xmlCutter->outputFilenameMask ("feed_%d.xml");
try {
    var_dump($xmlCutter->cut(5, xmlCutter::BY_ITEMS));
} catch (Exception $e) {
    echo 'Caught exception: ',  $e->getMessage(), "\n";
}



Kód třídy:
class xmlCutter {
    private $inputFile;
    private $outputFolder;
    private $outputFilenameMask;
 
    const BY_BYTES = 0;
    const BY_ITEMS = 1;
 
    //internal settings
    const BLOCK = 4096;
    const DOWNLOAD_TIMEOUT = 60;
 
    /**
     * @param string $inputFile name of the file to read
     */
    public function inputFile ($inputFile) {
        $this->inputFile = $inputFile;
 
        if ( $this->outputFilenameMask === null ) {
            preg_match('~^[\.\w\d-_]*~',basename($inputFile),$matchName);
            if ( ($pos = strrpos($matchName[0], ".")) !== false ) {
                $this->outputFilenameMask = substr($matchName[0], 0, $pos)."-part-%d".substr($matchName[0], $pos);
 
            } else {
                $this->outputFilenameMask = "$matchName[0]-part-%d";
            }
        }
    }
 
    /**
     * @param string $outputFolder the directory where the output files will be created
     */
    public function outputFolder ($outputFolder) {
        $this->outputFolder = rtrim($outputFolder, "\\/").DIRECTORY_SEPARATOR;
    }
 
    /**
     * @param string $mask  mask for output file or the first part of filename
     * @param string $after the second part of filename
     */
    public function outputFilenameMask ($mask, $after=null) {
        if($after === null) {
            $this->outputFilenameMask = $mask;
        }
        else {
            $this->outputFilenameMask = $mask."%d".$after;
        }
    }
   
    /**
     * @param  int $type  cutting type
     * @param  int $limit limit
     * @throws Exception
     * @return array created files
     */
    public function cut ($limit = 100, $type = self::BY_ITEMS) {
        if ( strpos($this->outputFilenameMask, "%d") === false ) {
            throw new Exception("Mask \"$this->outputFilenameMask\" for output filenames has bad format, \"%d\" is missing.");
        }
 
        if ( !is_dir($this->outputFolder) ) {
            throw new Exception("Folder \"$this->outputFolder\" does not exist.");
        }
 
        if ( preg_match('~^(http|ftp)://~',$this->inputFile) ) {
            $sourceFile = $this->downloadFile();
            $downloadedSource = true;
        }
        else {
            if ( !is_file($this->inputFile) ) {
                throw new Exception("File \"$this->inputFile\" does not exist.");
            }
            $sourceFile = $this->inputFile;
            $downloadedSource = false;
        }
 
        $hSource = fopen( $sourceFile, 'r');
        $root = $content = "";
        $found = false;
        while (!feof($hSource)) {
            $content .= fread($hSource, self::BLOCK);
            if ( preg_match('~<([^\.\d-"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s][^"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s]*)(?:[\x20\x09\x0D\x0A]+[^"\']+[\x20\x09\x0D\x0A]*=[\x20\x09\x0D\x0A]*(?:"[^"]*"|\'[^\']*\'))*[\x20\x09\x0D\x0A]*/?>~', $content, $matchRoot, PREG_OFFSET_CAPTURE) ) {
                preg_match('~(?:<\?xml.*?\?>)?(?:<!--.*?-->|<\?.*?\?>|[\x20\x09\x0D\x0A]+)*(?:<!DOCTYPE[^>\[]*(?:\[.*?\])?.*?>)?(?:<!--.*?-->|<\?.*?\?>|[\x20\x09\x0D\x0A]+)*~', $content, $matchProlog);
 
                $prolog = $matchProlog[0];
                $root = $matchRoot[0][0];
                $shortRoot = $matchRoot[1][0];
                preg_match('~[\x0D\x0A]*[\x20\x09]*(?=<[^\.\d-"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s][^"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s]*)~',$content,$matchSpace);
                $spacesBeforeRoot = !empty($matchSpace) ? $matchSpace[0] : "";
 
                $offset = $matchRoot[0][1]+strlen($matchRoot[0][0]);
                $found = true;
 
                break;
            }
        }
 
        if ( empty($root) ) {
            fclose($hSource);
             if ( $downloadedSource ) {
                unlink($sourceFile);
            }
            throw new Exception ("Root element was not found.");
        }
 
        try {
            $saver = new saver ($this->outputFolder, $this->outputFilenameMask, $root, "$spacesBeforeRoot</$shortRoot>", $prolog, $type == self::BY_BYTES ? saver::BY_BYTES : saver::BY_ITEMS, $limit);
        } catch (Exception $e) {
            fclose($hSource);
            throw new Exception ($e->getMEssage());
        }
 
        $content = substr($content, $offset);
 
        while( true ) {
            if ( preg_match('~[\x20\x09\x0D\x0A]*<([^\.\d-"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s][^"#!$%&\'\\\()*+,/;<=>?@\[\]^`{|}\~\s]*)(?:[\x20\x09\x0D\x0A]+[^"\']+[\x20\x09\x0D\x0A]*=[\x20\x09\x0D\x0A]*(?:"[^"]*"|\'[^\']*\'))*[\x20\x09\x0D\x0A]*/?>~', $content, $matchElement, PREG_OFFSET_CAPTURE) ) {
                $matchLength = strlen($matchElement[0][0]);
                if ( $matchElement[0][0][$matchLength-2] == "/" ) {
                    $offset = $matchLength;
                    $data = $matchElement[0][0];
                } else {
                    if ( preg_match('~</'.$matchElement[1][0].'[\x20\x09\x0D\x0A]*>~',$content,$matchEndElement, PREG_OFFSET_CAPTURE) ) {
                        $offset = $matchEndElement[0][1] + strlen($matchEndElement[0][0]);
                        $data = substr($content, 0, $offset);
                    } else {
                        if ( feof($hSource) ) {
                            break;
                        } else {
                            $content .= fread($hSource, self::BLOCK);
                        }
                    }
                }
 
                try {
                    $saver->save($data);
                } catch (Exception $e) {
                    fclose($hSource);
                    $fileNames = $saver->finalize();
                    foreach ( $fileNames as $fileName ) {
                        unlink($filename);
                    }
                    throw new Exception ($e->getMEssage());
                }
 
                $content = substr($content, $offset);
            } else {
                if ( feof($hSource) ) {
                    break;
                } else {
                    $content .= fread($hSource, self::BLOCK);
                }
            }
        }
 
        $fileNames = $saver->finalize();
 
        if ( $downloadedSource ) {
            unlink($sourceFile);
        }
 
        return $fileNames;
    }
 
    private function downloadFile () {
        $tmpName = null;
        if( $hRead = @fopen($this->inputFile, "r") ) {
            $ch = curl_init($this->inputFile);
            if ( !($tmpName = tempnam($this->outputFolder,"")) ) {
                fclose($hRead);
                throw new Exception("Can not create temp file.");
            }
            $hWrite = fopen($tmpName, "w");
            $start = time();
            while (!feof($hRead) && self::DOWNLOAD_TIMEOUT + $start - time() > 0 ) {
                stream_set_timeout($hRead, self::DOWNLOAD_TIMEOUT + $start - time());
                fwrite($hWrite, fread($hRead, self::BLOCK));
            }
            if(!feof($hRead)) {
                fclose($hRead);
                fclose($hWrite);
                unlink($tmpName);
                throw new Exception("File \"$this->inputFile\" was not downloaded. Timeout expired.");
            }
            fclose($hRead);
            fclose($hWrite);
        } elseif ( function_exists('curl_init') ) {
            $ch = curl_init($this->inputFile);
            if ( !($tmpName = tempnam($this->outputFolder,"")) ) {
                fclose($hRead);
                throw new Exception("Can not create temp file.");
            }
            $hWrite = fopen($tmpName, "w");
            curl_setopt($ch, CURLOPT_FILE, $hWrite);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, self::DOWNLOAD_TIMEOUT);
            if (!curl_exec($ch)) {
                curl_close($ch);
                fclose($hWrite);
                unlink($tmpName);
                throw new Exception("File \"$this->inputFile\" was not downloaded. Timeout expired.");
            }
            curl_close($ch);
            fclose($hWrite);
        } else {
            throw new Exception("File \"$this->inputFile\" was not downloaded. Stream opening functions failed.");
        }
        return $tmpName;
    }
}



class saver {
    private $folder;
    private $filenameMask;
    private $type;
    private $limit;
    private $prolog;
    private $root;
    private $endRoot;
    private $counter = 0;
    private $hSave = null;
    private $fileIndex = 0;
    private $fileNames = array();
 
    const BY_BYTES = 0;
    const BY_ITEMS = 1;
 
    public function saver ($folder, $filenameMask, $root, $endRoot, $prolog = "", $type = self::BY_ITEMS, $limit = 100) {
        if ( !is_dir($folder) ) {
            throw new Exception("Folder \"$folder\" does not exist.");
        }
 
        if ( strpos($filenameMask, "%d") === false ) {
            throw new Exception("Mask \"$filenameMask\" for output filenames has bad format, \"%d\" is missing.");
        }
 
        if ( $type == self::BY_BYTES and strlen($prolog) + strlen($root) + strlen($endRoot) >= $limit ) {
            throw new Exception("Bytes limit is smaller than initial data.");
        }
 
        $this->folder = $folder;
        $this->filenameMask = $filenameMask;
        $this->type = $type;
        $this->limit = $limit;
        $this->prolog = $prolog;
        $this->root = $root;
        $this->endRoot = $endRoot;
    }
 
    public function save ($data) {
        if ( $this->hSave == null ) {
            $this->openFile();
        }
 
        if ( $this->type == self::BY_BYTES ) {
            if ( strlen($data) + strlen($this->endRoot) + $this->counter <= $this->limit  ) {
                $this->counter += fwrite($this->hSave, $data);
            } else {
                if ( $this->counter == strlen($this->prolog.$this->root) ) {
                    $this->closeFile();
                    throw new Exception("Can not write data. Data are too long.");
                } else {
                    $this->closeFile();
                    $this->openFile();
 
                    if ( strlen($data) + strlen($this->endRoot) + $this->counter <= $this->limit  ) {
                        $this->counter += fwrite($this->hSave, $data);
                    } else {
                        $this->closeFile();
                        throw new Exception("Can not write data. Data are too long.");
                    }
                }
            }
        } else {
            fwrite($this->hSave, $data);
            if ( ++$this->counter == $this->limit ) {
                $this->closeFile();
            }
        }
    }
 
    public function finalize () {
        if ( $this->hSave != null ) {
            $this->closeFile();
        }
        return $this->fileNames;
    }
 
    private function openFile () {
        if ( $this->hSave != null ) {
            $this->closeFile();
        }
        $this->fileNames[$this->fileIndex] = $this->folder.sprintf($this->filenameMask, $this->fileIndex);
        $this->hSave = fopen( $this->fileNames[$this->fileIndex++], 'w');
        fwrite($this->hSave, $this->prolog.$this->root);
        if ( $this->type == self::BY_BYTES ) {
            $this->counter = strlen($this->prolog.$this->root);
        } else {
            $this->counter = 0;
        }
    }
 
    private function closeFile () {
        if ( $this->hSave != null ) {
            fwrite($this->hSave, $this->endRoot);
            fclose($this->hSave);
        }
        $this->hSave = null;
        $this->counter = 0;
    }
}
Pavel Dumbrovský
Profil
Zkoušel jsem to na tomto: http://b2b.heliosrace.cz/exp/xml_feed_sklad_zaklad_cz.xml
Soubory to vytváří ok, ovšem obsah je místama rozbitej, chybí uzavírací značka </SHOPITEM> , takže pro další použití se to nedá.. :(

Vaše odpověď

Mohlo by se hodit

Odkud se sem odkazuje


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: