Skip to content

February 1, 2011

8

PHP İle Çok Büyük Boyutlu XML Dosyalarını Açma (Parse Etme)

PHP Büyük XML Dosyası Parse Etme

Merhaba arkadaşlar, bu yazımızda php ile çok büyük boyutlu xml dosyalarını sorunsuz bir şekilde nasıl açacağımıza ve nasıl işleyeceğimize bakacağız.

Neden Gerekli ?

Bazen PHP ‘de büyük boyutlu XML dosyalarıyla çalışmamız gerekebiliyor. Belirli bir boyuta kadar (40-50 mb) simplexml kütüphanesiyle rahatlıkla parse işlemi yapabiliyoruz.  Ancak gb seviyelerinde parse işleminde simplexml hata vereceği gibi sunucudan size ayrılan kaynağı çoktan aşmış olabilirsiniz. Örneklerimizde olabildiğinde gerçek hayattan örnekler vermeye çalışacağız. Böylelikle hem daha iyi anlaşılmasını hemde daha sonra kullanabileceğiniz kod olmasını amaçlıyoruz.

Ne Boyutta Bir Dosya İle Çalışacağız ?

Örneğimizde otomatik olarak oluşturduğumuz 2gb lık bir xml dosyasını açıp içindeki verileri işleyeceğiz. Ve bu işlemi yaparken kb seviyelerinde yada 1mb kadar (isteğe bağlı) RAM kullanacağız.

Hangi Yöntem Kullanılacak ?

Bu yöntem aslında yeni değil. JAVA dan gelen bir çok özellik gibi (Javadoc -> phpDoc, JUnit => PHPUnit, OOP => OOP) buda Java ‘nın javax.xml.parsers.SAXParser sınıfının bir özelliği.

Olay (event) bazlı yöntem olarak adlandırılan bu parsing yöntemi kısaca şöyle çalışıyor: Fonksiyona parça parça yada tamamı verilen (tamamını vermek büyük dosyalar için performans kaybı yaşatır) datalar parse edilirken Element (tag) açılışı olayı, veri (data/cdata) olayı ve element (tag) kapanma olayı şeklinde 3 event tetikliyerek verileri yönetmenizi sağlıyor. Tüm veriyi ram’ a atmadığından sunucumuzu neredeyse hiç yormayarak (ram kullanımı) performans kazanımı sağlıyor.

İlk Örneğimiz

Şimdi ilk örneğimizde yöntemi incelemeye başlayalım. Örnekte Google Adwords ad grup performans raporunu barındıran bir dosya üzerinden gideceğiz. Bu dosyayı seçmemizin sebebi Google Adwords ‘ün dataları attribute olarak ataması ve hiç bir satır sonu karakteri basmaması. Yani Google Adwords performans raporundan aldığınız 500mb lık bir xml dosyasının tamamı tek satırdan oluşur.

Eğer size gelen xml dosyasının bir şeması/dökümanı yoksa bazen bunu öğrenmek zor olabilir. Yazacağınız kod otomatik çalışacak bir kod olsa bile bir defaya mahsus dosyadaki ilk 10kb kadar veriyi şemasını görmek için görüntülemek isteyebilirsiniz.

<?php

// Dosyanın ilk 10kb lık kısmını oku
$filename = "data.xml";
$handle = fopen($filename, "rb");
echo fread($handle, 10240);
fclose($handle);
?>

Dosyanın Çıktısı  (xml çıktısı olduğundan tarayıcıdan çalıştırıyorsanız sayfa kaynağına bakmanız gerekebilir):

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><report><report-name name='Reklam grubu raporu'/><date-range date='Tüm Zamanlar'/><table><columns><column name='day' display='Gün'/><column name='adGroup' display='Reklam grubu'/><column name='clicks' display='Tıklamalar'/><column name='impressions' display='Gösterimler'/><column name='ctr' display='TO'/><column name='avgCPC' display='Ort. TBM'/><column name='cost' display='Maliyet'/><column name='avgPosition' display='Ort. konum'/><column name='conv1PerClick' display='Dönş. (tıklama başına 1)'/><column name='costConv1PerClick' display='Maliyet / dönüşüm (tıklama başına 1)'/><column name='convRate1PerClick' display='Dönş. oranı (tıklama başına 1)'/><column name='viewThroughConv' display='Görüntüleme dönş.'/><column name='avgCPM' display='Ort. BGBM'/><column name='convManyPerClick' display='Dönş. (tıklama başına birden çok)'/><column name='costConvManyPerClick' display='Maliyet / dönüşüm (tıklama başına birden çok)'/><column name='convRateManyPerClick' display='Dönş. oranı (tıklama başına birden çok)'/><column name='totalConvValue' display='Toplam dönüşüm değeri'/><column name='valueConv1PerClick' display='Değer / dönüşüm (tıklama başına bir)'/><column name='valueConvManyPerClick' display='Değer / dönüşüm (tıklama başına birden çok)'/></columns><row day='2007-08-16' adgroup='make a short link 2yp0xr5qam' clicks='802' impressions='21247' ctr='% 0,04' avgcpc='1,34' cost='802,00' avgposition='4,37' conv1perclick='0' costconv1perclick='0,00' convrate1perclick='% 0,00' viewthroughconv='0' avgcpm='37,75' convmanyperclick='0' costconvmanyperclick='0,00' convratemanyperclick='% 0,00' totalconvvalue='0,0' valueconv1perclick='0,0' valueconvmanyperclick='0,0'/>

Dosyanın Çıktısı (formatlı):

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<report>
    <report-name name='Reklam grubu raporu'/>
    <date-range date='Tüm Zamanlar'/>
    <table>
        <columns>
            <column name='day' display='Gün'/>
            <column name='adGroup' display='Reklam grubu'/>
            <column name='clicks' display='Tıklamalar'/>
            <column name='impressions' display='Gösterimler'/>
            <column name='ctr' display='TO'/>
            <column name='avgCPC' display='Ort. TBM'/>
            <column name='cost' display='Maliyet'/>
            <column name='avgPosition' display='Ort. konum'/>
            <column name='conv1PerClick' display='Dönş. (tıklama başına 1)'/>
            <column name='costConv1PerClick' display='Maliyet / dönüşüm (tıklama başına 1)'/>
            <column name='convRate1PerClick' display='Dönş. oranı (tıklama başına 1)'/>
            <column name='viewThroughConv' display='Görüntüleme dönş.'/>
            <column name='avgCPM' display='Ort. BGBM'/>
            <column name='convManyPerClick' display='Dönş. (tıklama başına birden çok)'/>
            <column name='costConvManyPerClick' display='Maliyet / dönüşüm (tıklama başına birden çok)'/>
            <column name='convRateManyPerClick' display='Dönş. oranı (tıklama başına birden çok)'/>
            <column name='totalConvValue' display='Toplam dönüşüm değeri'/>
            <column name='valueConv1PerClick' display='Değer / dönüşüm (tıklama başına bir)'/>
            <column name='valueConvManyPerClick' display='Değer / dönüşüm (tıklama başına birden çok)'/>
        </columns>
        <row day='2007-08-16' adgroup='make a short link 2yp0xr5qam' clicks='802' impressions='21247' ctr='% 0,04' avgcpc='1,34' cost='802,00' avgposition='4,37' conv1perclick='0' costconv1perclick='0,00' convrate1perclick='% 0,00' viewthroughconv='0' avgcpm='37,75' convmanyperclick='0' costconvmanyperclick='0,00' convratemanyperclick='% 0,00' totalconvvalue='0,0' valueconv1perclick='0,0' valueconvmanyperclick='0,0'/>
        ...

Dosyayı inceledik ve parse etmemiz gereken yerin <row ile başlayan element (tag) olduğunu anlıyoruz. Yukarıda bahsettiğimiz gibi row elementinin içeriği değil sadece attribute lari var.

Parser fonksiyonumuza söylememiz gereken şeyi kodu yazmadan önce çıkartırsak işimiz çok kolaylaşacaktır. Bunlar;

  • Element açıldı” olayı tetiklendiğinde element adı row ise bir değişkende tutacağız ve attribute lar üzerinde bir işlem yapacaksak yapıp bir attribute değerlerini (array) değişkene atacağız
  • Veri” olayı açıldığında hiçbir işlem yapmayacağız (bu örnek için) çünkü içi boş.
  • Element kapandı” olayı tetiklendiğinde veriyi veritabanına kaydedebilir yada ne amaçla parse ettiysek o işlemi gerçekleştirebiliriz (bu örnek için birşey yapmayacağız).

Parser Sınıfımız (AdwordsReportParser.class.php)

<?php

/**
 * Google Adwords Ad Grup Raporlarını Parse Edip Veritabanına Kaydeder
 *
 * @author mustafa.kirimli <[email protected]>
 */
class AdwordsReportParser {

    /**
     * XML parser kaynağı
     * @var resource a XML parser resource
     */
    private $rSax = null;
    /**
     * Google Adwords raporunun bulunduğu dosyanın adı
     * @var string Parse edilecek xml dosyası
     */
    public $sReportFile = null;
    /**
     * Parse işlemi esnasında sax_start metodu tarafından duruma bağlı set edilir
     * @var boolean parse esnasında row elementinin açık olup olmadığını tutar
     */
    private $bRowOpened = false;
    /**
     * Adwords ad grouplarının bir günlük rapor verisi
     * @var array rapor dizisi
     */
    private $aData = null;

    /**
     *
     * @param string $sFileName Parse edilecek xml dosyası
     */
    function __construct($sFileName) {
        //
        /**
         * XML parse nesnesi oluştur
         * @see http://www.php.net/manual/tr/function.xml-parser-create.php
         */
        $this->rSax = xml_parser_create();
        /**
         * parse esnasında xml element adları büyük harfe dönüştürülmesin
         * @see http://www.php.net/manual/tr/function.xml-parser-set-option.php
         */
        xml_parser_set_option(&$this->rSax, XML_OPTION_CASE_FOLDING, false);
        /**
         * boşluk karakterlerinden oluşan verileri gözardı et
         * @see http://www.php.net/manual/tr/function.xml-parser-set-option.php
         */
        xml_parser_set_option(&$this->rSax, XML_OPTION_SKIP_WHITE, true);
        /**
         * Element olayları tetiklendiğinde kullanılacak metodları kaydet
         * @see http://www.php.net/manual/tr/function.xml-set-element-handler.php
         */
        xml_set_element_handler(&$this->rSax, array(&$this, 'sax_start'), array(&$this, 'sax_end'));
        /**
         * Veri olayı tetiklendiğinde kullanılacak metodu kaydet
         * @see http://www.php.net/manual/tr/function.xml-set-character-data-handler.php
         */
        xml_set_character_data_handler(&$this->rSax, array(&$this, 'sax_cdata'));
        // parse edilecek dosyayı sınıf üyesine aktar
        $this->sReportFile = $sFileName;
    }

    /**
     * Element açıldı olayı tetiklendiğinde (xml parser tarafından) kullanılacak metod
     * @param resource $sax xml parser nesnesi
     * @param string $tag element adı
     * @param array $attr attributelar dizisi
     */
    function sax_start($sax, $tag, $attr) {
        // element adı row ise row elementinin açıldığını belirt ve attributeları al
        if ($tag == 'row') {
            $this->bRowOpened = true;
            $this->aData = $attr;
            // verileri veritabanına kaydet ve değişkeni boşalt
            // işlemler ..
            $this->aData = "";
        } else {
            $this->bRowOpened = false;
        }
    }

    /**
     * Veri olayını tetiklendiğinde (xml parser tarafından) kullanılacak metod
     * @param resource $sax  xml parser nesnesi
     * @param string $data element verisi
     */
    function sax_cdata($sax, $data) {
        // bu örneğimizde veri içeren elementlerle çalışmıyoruz
    }

    /**
     * Element kapandı olayı tetiklendiğinde (xml parser tarafından) kullanılacak metod
     * @param resource $sax
     * @param string $tag
     */
    function sax_end($sax, $tag) {
        // bu örneğimizde attributes kullanıldığından bu özelliği kullanmıyoruz
    }

    /**
     * Dosya parse işlemine başla (800 kb lık parçalar halinde oku)
     *
     * throws Exception
     */
    function parse() {
        // rapor dosyasını binary okuma modunda aç
        $fp = fopen($this->sReportFile, 'rb');

        // dosya açılırken hata oluştuysa istisna fırlat
        if (!$fp) {
            throw new Exception("Dosya Açılamadı!");
        }

        // dosyayı sonuna ulaşıncaya kadar 800 kb lık parçalar halinde oku
        while (!feof($fp)) {
            // veriyi al ve xml parser 'a gönder
            xml_parse($this->rSax, fread($fp, 819200));
            // 200 mikrosaniye bekle
            usleep(200);
        }

        // xml parser' a dosyanın sonuna gelindiğini bildir
        xml_parse($this->rSax, "", true);
        // rapor dosyasını kapat
        fclose($fp);
        // xml parser 'ı kapat
        xml_parser_free($this->rSax);
    }

}

?>

Nasıl Çalışır ?

parse metodu dosyayı 800kb lık parçalar halinde okuyor. Ve okuduğu veriyi parser ‘a gönderir. Parser verilerin içinde element başlangıcı, veri (cdata) veya element bitişi (kapanışı) varsa bizim atadığımız metodları (sax_start, sax_cdata, sax_end) çağırır.

Parse Yapan Dosyamız (parser.php)

<?php

/*
 * Google Adwords Raporunu Parse Et
 */

// kodun maksimum çalışma süresini 1 saat yap
set_time_limit(3600);

// Google Adwords parser sınıfını dahil et
require 'AdwordsReportParser.class.php';

try {
    // parser 'ın çalışma süresini ölçmek için parser 'ı başlatmadan önceki zamanı al
    $start = microtime(true);
    // parser 'ın ram kullanımını öğrenmek için parser 'ı başlatmadan önceki kullanım miktarını al
    $startMem = memory_get_peak_usage();

    // parser sınıfına parse edilecek dosyayı ver
    $adwordsReportParser = new AdwordsReportParser("data.xml");
    // parse işlemine başla
    $adwordsReportParser->parse();
    // parse bitti.
    //
    // çalışma süresini hesapla
    echo number_format(microtime(true) - $start, 6) . "<br/>";
    // ram kullanımını hesapla
    echo number_format((memory_get_peak_usage() - $startMem) / 1024 / 1024) . " mb";
} catch (Exception $exc) {
    echo $exc->getMessage();
}
?>

Bu dosyamızda parse işlemini gerçekleştirmiş oluyoruz (AdwordsReportParser sınıfının içindeki sax_start metodundaki bölüme veriyi işlemek için kendi kodunuzu yazabilirsiniz).

Dosyalar (Download)

php_sax php dosyaları

php_sax veri dosyası (küçük boyutta)

php_sax veri dosyası (büyük boyut: 2gb)

CPU Performansı ?

Bu işlemi yaparken CPU üzerinde yük oluşuyor. Ancak işlemci tipi, işletim sistemi, işlemcinin  Multithreading ‘e bakış açıcı vb. unsurlara göre değişiklik göstermesine rağmen işlemler arasında mikro saniye cinsinden bekletme koyduğumuzdan CPU araya istek alabilecek ve sunucu üzerinde hiçbir işlem beklemeye geçmeyecektir.

CPU performansı, uzmanı olduğumuz yada farklı platformlarda denediğimiz bir konu değil. Sadece fikir oluşması açısından paylaşmış bulunuyoruz.

Toplam 8 Yorum Yorum Yaz
  1. zafer
    Feb 5 2011

    Eline sağlık Mustafa,

    Bir ara böyle bir şeye ihtiyacım olmuştu. Not alıyorum =)

    Reply
  2. Feb 6 2011

    Güle güle kullan Zafer =),

    Bende SEM konsolu yazarken Google Adwords ‘den aldığım XML formatlı reklam istatistikleri için ihtiyacım olmuştu. Toplamda 6 gb falandı çünkü. Ve şuanda hergün 400mb boyutlu dosyayı otomatik işliyor.

    Görüşmek üzere.

    Reply
    • Cemo
      Mar 11 2011

      Merhaba Mustafa. Bir sorum olacak. Burada XML’den bahsetmişsiniz ama. Veritabanı bazlı olarak düşündüğümüzde üyelik sisteminde 1.000.000 üye veya daha fazla üyeye ulaşan, ne bileyim facebook gibi bir üye sayısı olan(tahminime göre 100 milyon) bir projede veritabanı yedeklemesi veya bakımı, veri çekme işlemi, update işlemi nasıl olur.

      Mesela 1 milyon kayıtta; sadece Aktif=1 gibi basit bir güncelleme kaç saniyede biter. Ya da böyle bir işlem olabilir mi.

      Fikirlerini bekliyorum.
      Teşekkürler…

      Reply
    • Mar 19 2011

      Merhaba Cemo,

      Sizinde bahsettiğiniz gibi bu sadece XML ‘i açıklayan bir yazı. Ancak iyi bir noktaya değinmişsiniz. Bunun öyle tek bir cevabı yok. 1 milyon kayıt çokta fazla değil bence. Ayrıca benim duyduğum 500milyon kadar üyesi var Facebook un.

      Sorgu performansına gelince yaptığınız sorguya gore değişir performans ve optimizasyon. Örneğin SELECT “in hızını artırmak için en basit olarak PARTITIONING ve REPLICATION kullanabilirsin.

      Mesela MySQL IN yerine JOIN yazısında 40-50 milyon sorgu üzerinde IN yerine JOIN önerme sebebini tartışmışız.

      Konuyla alakalı tartışmaya açacağınız spesifik konuları bilgimiz dahilinde cevaplamaya çalışırız veya araştırıp üzerinden gidebiliriz.

      İlginize teşekkürler.

      Reply
  3. Monex
    Aug 6 2011

    Note that.these data sets are very small and that you need more training data to create a useful parsing model..To train a default parsing model with MaltParser type the following at the command line prompt ..prompt java -jar malt.jar -c test -i examples data talbanken05 train.conll -m learn..This line tells MaltParser to create a parsing model named test.mco also know as a Single Malt configuration file from the data .in the file examples data talbanken05 train.conll. The parsing model gets its name from the configuration name which is specified .by the option flag -c without the file suffix .mco.

    Reply
  4. İbrahim Gündüz
    Sep 19 2011

    Son derece yararlı ve anlaşılır bir makale olmuş. Ellerinize sağlık, tebrik ederim.

    Reply
  5. selman tunç
    Dec 22 2012

    helal olsun üşenmeden bilgilerini yazıyorsun yaa hocam eline sağlık ben zaman bulum kendi bloguma bişey giremiyorum …

    Reply
  6. Macit SARI
    Sep 24 2016

    Merhabalar, öncelik uzun süre önce dahi olsa yararlı bir anlatım olmuş elinize sağlık. Elimde günde bir defa güncellenen 50 mb’lik bir xml var ürün çekmek için kullanıyorum. Dosya boyutu büyük olduğu için sürekli time out oluyor bu yüzden bu xml dosyasını partlara bölmek istiyorum yardımcı olur musunuz?

    Reply

Leave a Reply to Macit SARI

(gerekli)
(gerekli)