Как улучшить производительность, повторяя DOMDocument?
Я использую cURL, чтобы вытащить веб-страницу с сервера. Я передаю его в Tidy и выдаю вывод в DOMDocument. Затем начинается проблема.
Веб-страница содержит около трех тысяч (yikes) табличных тегов, и я извлекаю данные из них. Существует два типа таблиц, в которых один или несколько типов B соответствуют типу A.
Я профилировал свой script с помощью вызовов microtome(true)
. Я размещал вызовы до и после каждого этапа моего script и вычитал время друг от друга. Итак, если вы последуете за мной через мой код, я объясню это, поделись результатами профиля и укажу, где проблема. Может быть, вы даже можете помочь мне решить проблему. Здесь мы идем:
Во-первых, я включаю два файла. Один обрабатывает некоторый синтаксический анализ, а другой определяет два класса структуры данных.
// Imports
include('./course.php');
include('./utils.php');
Включая, как мне известно, несущественны, поэтому давайте перейдем к импорту cURL.
// Execute cURL
$response = curl_exec($curl_handle);
Я сконфигурировал cURL, чтобы не выходить из игры, и публиковать некоторые данные заголовка, необходимые для получения значимого ответа. Затем я очищаю данные, чтобы подготовить их к 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;
До сих пор код занял около девяти секунд. Учитывая, что это работа cron, работающая нечасто, я в порядке с этим. Тем не менее, следующая часть кода действительно закрывается. Здесь, где я беру то, что хочу от HTML, и вставляю его в свои пользовательские классы. (Я планирую использовать это в базе данных MySQL, но это первый шаг.)
// 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;
}
}
Для справки, здесь функции утилиты, которые я вызываю:
//
// 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;
}
Этот код занимает 63 секунды. Это за минуту для PHP script для вывода данных с веб-страницы. Sheesh!
Мне было рекомендовано разделить нагрузку на мой основной рабочий цикл, но, учитывая однородность моих данных, я не совсем уверен, как это сделать. Любые предложения по улучшению этого кода приветствуются.
Что я могу сделать, чтобы улучшить время выполнения кода?
Ответы
Ответ 1
Оказывается, моя петля ужасно неэффективна.
Использование времени foreach
сокращает время от половины до примерно 31 секунды. Но это было недостаточно быстро. Таким образом, я сформулировал несколько сплайнов и провел мозговой штурм примерно с половиной программистов, которые я знаю, как тыкать онлайн. Здесь мы обнаружили:
Использование DOMNodeList item()
accessor является линейным, производя экспоненциально медленное время обработки в циклах. Таким образом, удаление первого элемента после каждой итерации делает цикл более быстрым. Теперь мы всегда получаем доступ к первому элементу списка. Это привело меня к 8 секундам.
После того, как я сыграл еще несколько, я понял, что свойство ->length
DOMNodeList
так же плохо, как item()
, так как оно также несет линейную стоимость. Поэтому я изменил цикл for на следующее:
$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 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);
}
Обратите внимание, что я сделал некоторые другие оптимизации с точки зрения таргетинга на данные, которые я хочу, но важная часть - это то, как я обрабатываю переход от одного элемента к другому.