2012-12-18 13 views
11

Sto usando cURL per estrarre una pagina web da un server. Lo passo a Tidy e getti l'output in un DOMDocument. Quindi inizia il problema.Come migliorare le prestazioni durante l'iterazione di un DOMDocument?

La pagina Web contiene circa tremila tag di tabelle (yikes) e sto recuperando i dati da essi. Esistono due tipi di tabelle, in cui uno o più tipi B seguono un tipo A.

Ho profilato il mio script utilizzando le chiamate microtome(true). Ho effettuato chiamate prima e dopo ciascuna fase del mio script e sottratto le ore l'una dall'altra. Quindi, se mi seguirai attraverso il mio codice, lo spiegherò, condividerò i risultati del profilo e indicherò dove si trova il problema. Forse puoi persino aiutarmi a risolvere il problema. Andiamo:

In primo luogo, includo due file. Uno gestisce alcune analisi e l'altro definisce due classi "struttura dati".

// Imports 
include('./course.php'); 
include('./utils.php'); 

Include sono irrilevanti per quanto ne so, e quindi cerchiamo di procedere all'importazione cURL.

// Execute cURL 
$response = curl_exec($curl_handle); 

Ho configurato cURL per non scadere e per pubblicare alcuni dati di intestazione, necessari per ottenere una risposta significativa. Successivamente, pulisco i dati per prepararli per DOMDocument.

// Run about 25 str_replace calls here, to clean up 
// then run tidy. 



$html = $response; 

// 
//  Prepare some config for tidy 
// 
     $config = array(
        'indent'   => true, 
        'output-xhtml' => true, 
        'wrap'   => 200); 

    // 
    // Tidy up the HTML 
    // 

    $tidy = new tidy; 
    $tidy->parseString($html, $config, 'utf8'); 
    $tidy->cleanRepair(); 

    $html = $tidy; 

Fino ad ora, il codice ha richiesto circa nove secondi. Considerando che si tratta di un lavoro cron, eseguito raramente, sto bene con quello. Tuttavia, la prossima parte del codice è davvero in discussione. Ecco dove prendo ciò che voglio dall'HTML e lo inserisco nelle mie classi personalizzate. (. Ho intenzione di roba questo in un database MySQL troppo, ma questo è un primo passo)

// Get all of the tables in the page 

$tables = $dom->getElementsByTagName('table'); 

// Create a buffer for the courses 

$courses = array(); 

// Iterate 

$numberOfTables = $tables->length; 

for ($i=1; $i <$numberOfTables ; $i++) { 

    $sectionTable = $tables->item($i); 
    $courseTable = $tables->item($i-1); 

    // We've found a course table, parse it. 

    if (elementIsACourseSectionTable($sectionTable)) { 

     $course = courseFromTable($courseTable); 
     $course = addSectionsToCourseUsingTable($course, $sectionTable);    

     $courses[] = $course; 
    } 
} 

Per riferimento, ecco le funzioni di utilità che io chiamo:

// 
// Tell us if a given element is 
// a course section table. 
// 

function elementIsACourseSectionTable(DOMElement $element){ 

     $tableHasClass = $element->hasAttribute('class'); 
     $tableIsCourseTable = $element->getAttribute("class") == "coursetable"; 

     return $tableHasClass && $tableIsCourseTable; 
} 

// 
// Takes a table and parses it into an 
// instance of the Course class. 
// 

function courseFromTable(DOMElement $table){ 

    $secondRow = $table->getElementsByTagName('tr')->item(1); 
    $cells = $secondRow->getElementsByTagName('td'); 

    $course = new Course; 

    $course->startDate = valueForElementInList(0, $cells); 
    $course->endDate = valueForElementInList(1, $cells);   
    $course->name = valueForElementInList(2, $cells); 
    $course->description = valueForElementInList(3, $cells); 
    $course->credits = valueForElementInList(4, $cells); 
    $course->hours = valueForElementInList(5, $cells); 
    $course->division = valueForElementInList(6, $cells); 
    $course->subject = valueForElementInList(7, $cells); 

    return $course; 

} 


// 
// Takes a table and parses it into an 
// instance of the Section class. 
// 

function sectionFromRow(DOMElement $row){ 

    $cells = $row->getElementsByTagName('td'); 

    // 
    // Skip any row with a single cell 
    // 

    if ($cells->length == 1) { 
     # code... 
     return NULL; 
    } 

    // 
    // Skip header rows 
    // 

    if (valueForElementInList(0, $cells) == "Section" || valueForElementInList(0, $cells) == "") { 
     return NULL; 
    } 


    $section = new Section; 

    $section->section = valueForElementInList(0, $cells); 
    $section->code = valueForElementInList(1, $cells); 
    $section->openSeats = valueForElementInList(2, $cells);  
    $section->dayAndTime = valueForElementInList(3, $cells);   
    $section->instructor = valueForElementInList(4, $cells);   
    $section->buildingAndRoom = valueForElementInList(5, $cells); 
    $section->isOnline = valueForElementInList(6, $cells); 

    return $section; 

} 

// 
// Take a table containing course sections 
// and parse it put the results into a 
// give course object. 
// 

function addSectionsToCourseUsingTable(Course $course, DOMElement $table){ 

    $rows = $table->getElementsByTagName('tr'); 
    $numRows = $rows->length; 

    for ($i=0; $i < $numRows; $i++) { 

     $section = sectionFromRow($rows->item($i)); 

     // Make sure we have an array to put sections into 

     if (is_null($course->sections)) { 
      $course->sections = array(); 
     } 

     // Skip "meta" rows, since they're not really sections 

     if (is_null($section)) { 
      continue; 
     } 

     $course->addSection($section); 
    } 

    return $course; 
} 

// 
// Returns the text from a cell 
// with a 
// 

function valueForElementInList($index, $list){ 
    $value = $list->item($index)->nodeValue; 
    $value = trim($value); 
    return $value; 
} 

Questo codice richiede 63 secondi . È più di un minuto che uno script PHP recuperi i dati da una pagina web. Sheesh!

Mi è stato consigliato di suddividere il carico di lavoro del mio ciclo di lavoro principale, ma considerando la natura omogenea dei miei dati, non sono del tutto sicuro di come. Qualsiasi suggerimento su come migliorare questo codice è molto apprezzato.

Cosa posso fare per migliorare il tempo di esecuzione del codice?

+2

Potrebbe essere più veloce usare 'foreach ($ tables come $ table)' perché stai chiamando '$ tables-> item ($ i)' in quel ciclo. Non sono sicuro, ma * potrebbe * fare una traversata lineare di una lista collegata per trovare l'indice ogni volta. 'foreach' dovrebbe sicuramente solo enumerare la lista in ordine. –

+2

Il problema è molto probabile come dice @mootinator ... le note su questa pagina hanno alcune informazioni su di esso http://php.net/manual/en/domnodelist.item.php – Eliezer

+0

Quindi 'foreach' migliora effettivamente l'elaborazione volte, ma usare un ciclo while ed eliminare le chiamate '-> item()' è ancora più veloce. Vedi la mia risposta. – Moshe

risposta

10

Si scopre che il mio ciclo è terribilmente inefficiente.

Utilizzo di un tempo di interruzione foreach da metà a circa 31 secondi. Ma non era abbastanza veloce. Quindi ho reticolato alcune spline e ho fatto un brainstorming con circa la metà dei programmatori che so come colpire online. Ecco cosa abbiamo trovato:

L'utilizzo dell'accessorio di DOMNodeList item() è lineare, producendo tempi di elaborazione esponenzialmente lenti nei loop. Quindi, rimuovere il primo elemento dopo ogni iterazione rende il ciclo più veloce. Ora, accediamo sempre al primo elemento dell'elenco. Questo mi ha portato a 8 secondi.

Dopo aver giocato un po 'di più, mi sono reso conto che la proprietà ->length di DOMNodeList è pessima come item(), poiché comporta anche costi lineari.Così ho cambiato il mio ciclo for per questo:

$table = $tables->item(0); 

while ($table != NULL) { 

    $table = $tables->item(0); 

    if ($table === NULL) { 
     break; 
    } 

    // 
    // We've found a section table, parse it. 
    // 

    if (elementIsACourseSectionTable($table)) { 

     $course = addSectionsToCourseUsingTable($course, $table);   
    } 

    // 
    // Skip the last table if it's not a course section 
    // 

    else if(elementIsCourseHeaderTable($table)){ 
     $course = courseFromTable($table); 
     $courses[] = $course; 
    } 

    // 
    // Remove the first item from the list 
    // 

    $first = $tables->item(0); 
    $first->parentNode->removeChild($first); 

    // 
    // Get the next table to parse 
    // 

    $table = $tables->item(0); 
} 

Nota che ho fatto alcune altre ottimizzazioni in termini di mira i dati che voglio, ma la parte rilevante è come gestire progredendo da una voce all'altra.

+1

Questa soluzione ha ridotto il tempo di esecuzione del mio script da circa 1 giorno a circa 25 minuti! –

Problemi correlati