2012-12-18 18 views
11

Używam cURL do przeciągania strony z serwera. Przekażę go do Tidy i wyrzucę dane wyjściowe do DOMDocument. Wtedy zaczyna się kłopot.Jak poprawić wydajność iteracji DOMDocument?

Strona zawiera około trzech tysięcy (yikes) znaczników tabeli, a ja z nich pobierane są dane. Istnieją dwa rodzaje tabel, w których jeden lub więcej typów B jest typu A.

Sprofilowałem swój skrypt, używając wywołań microtome(true). Umieściłem połączenia przed i po każdym etapie mojego skryptu i odejmowałem czasy od siebie nawzajem. Tak więc, jeśli prześledzisz mnie za pomocą mojego kodu, wyjaśnię to, podzielę się wynikami z profilu i wskażę, gdzie jest problem. Może nawet pomożecie mi rozwiązać problem. Oto:

Po pierwsze, dołączam dwa pliki. Jeden obsługuje trochę analizowania, a drugi definiuje dwie klasy "struktury danych".

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

Obejmuje nieistotne, o ile wiem, a więc przejdźmy do importu cURL.

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

Skonfigurowałem cURL tak, aby nie przekraczał limitu czasu i aby opublikować pewne dane nagłówka, które są niezbędne do uzyskania sensownej odpowiedzi. Następnie oczyszczam dane, aby przygotować je do 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; 

Do tej pory kod trwał około dziewięć sekund. Biorąc pod uwagę, że jest to praca crona, która działa nieczęsto, nie mam z tym nic wspólnego. Jednak kolejna część kodu naprawdę barfuje. Tutaj biorę to, co chcę z HTML i wrzucam je do moich klas niestandardowych. (. Mam zamiar wepchnąć to do bazy danych MySQL też, ale jest to pierwszy krok)

// 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; 
    } 
} 

Dla porównania, oto funkcji użytkowych, które nazywam:

// 
// 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; 
} 

Kod ten trwa 63 sekund . To ponad minuta, aby skrypt PHP mógł pobierać dane ze strony internetowej. Do licha!

Polecono mi podzielić pracę mojej głównej pętli roboczej, ale biorąc pod uwagę jednorodny charakter moich danych, nie jestem do końca pewien jak. Wszelkie sugestie dotyczące ulepszenia tego kodu są bardzo doceniane.

Co mogę zrobić, aby poprawić czas wykonywania kodu?

+2

Szybsze może być użycie 'foreach ($ tables as $ table)', ponieważ wywołujesz '$ tables-> item ($ i)' w tej pętli. Nie jestem pewien, ale * może * może wykonywać liniowe przemieszczenie połączonej listy, aby znaleźć indeks za każdym razem. 'foreach' powinien definitywnie wyliczyć listę w kolejności. –

+2

Problem jest najprawdopodobniej taki, jak @mootinator mówi ... notatki na tej stronie zawierają pewne informacje na ten temat http://php.net/manual/en/domnodelist.item.php – Eliezer

+0

Tak więc 'foreach' rzeczywiście poprawia przetwarzanie razy, ale użycie pętli while i wyeliminowanie wywołań '-> item()' jest jeszcze szybsze. Zobacz moją odpowiedź. – Moshe

Odpowiedz

10

Okazuje się, że moja pętla jest strasznie nieefektywna.

Przy użyciu czasu obcięcia od połowy do około 31 sekund. Ale to nie było wystarczająco szybkie. Więc wymyśliłem kilka splajnów i przeprowadziłem burzę mózgów z około połową programistów, których wiem, jak wyłudzać online. Oto, co znaleźliśmy:

Korzystanie z akcesora DOMNodeList item() jest liniowe, co powoduje wykładniczy powolny czas przetwarzania w pętlach. Usunięcie pierwszego elementu po każdej iteracji powoduje, że pętla jest szybsza. Teraz zawsze mamy dostęp do pierwszego elementu listy. To doprowadziło mnie do 8 sekund.

Po zagraniu, zdałem sobie sprawę, że właściwość ->length z DOMNodeList jest tak samo zła jak item(), ponieważ powoduje również liniowy koszt.Więc zmieniłem do pętli to:

$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); 
} 

Zauważ, że zrobiłem kilka innych optymalizacji pod kątem kierowania danych chcę, ale istotne jest to, jak sobie radzić postępuje z jednej pozycji do drugiej.

+1

To rozwiązanie skróciło czas wykonywania mojego skryptu z około 1 dnia do około 25 minut! –

Powiązane problemy