Повышение эффективности скребка HTML с помощью вилки pcntl()
С помощью двух предыдущих вопросов у меня теперь есть рабочий HTML-скребок, который загружает информацию о продукте в базу данных. То, что я сейчас пытаюсь сделать, - это эффективно улучшить, обернув свой мозг вокруг того, чтобы заставить мой скребок работать с pcntl_fork
.
Если я разделю свой скрипт php5-cli на 10 отдельных блоков, я значительно улучшу общее время выполнения, поэтому я знаю, что я не связан с вводом-выводом или процессором, а просто ограничен линейным характером моих функций очистки.
Использование код, который я собрал из нескольких источников, у меня есть этот рабочий тест:
<?php
libxml_use_internal_errors(true);
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
set_time_limit(0);
$hrefArray = array("http://slashdot.org", "http://slashdot.org", "http://slashdot.org", "http://slashdot.org");
function doDomStuff($singleHref,$childPid) {
$html = new DOMDocument();
$html->loadHtmlFile($singleHref);
$xPath = new DOMXPath($html);
$domQuery = '//div[@id="slogan"]/h2';
$domReturn = $xPath->query($domQuery);
foreach($domReturn as $return) {
$slogan = $return->nodeValue;
echo "Child PID #" . $childPid . " says: " . $slogan . "\n";
}
}
$pids = array();
foreach ($hrefArray as $singleHref) {
$pid = pcntl_fork();
if ($pid == -1) {
die("Couldn't fork, error!");
} elseif ($pid > 0) {
// We are the parent
$pids[] = $pid;
} else {
// We are the child
$childPid = posix_getpid();
doDomStuff($singleHref,$childPid);
exit(0);
}
}
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
// Clear the libxml buffer so it doesn't fill up
libxml_clear_errors();
В связи с чем возникают следующие вопросы:
1) Учитывая, что мой hrefArray содержит 4 URL-адреса - если массив должен был содержать, скажем, 1000 URL-адресов продуктов, этот код породил бы 1000 дочерних процессов? Если да, то каков наилучший способ ограничить количество процессов, скажем, 10, и снова 1000 URL-адресов в качестве примера разделите рабочую нагрузку на дочерних до 100 продуктов на каждого ребенка (10 х 100).
2) Я узнал, что pcntl_fork создает копия процесса и всех переменных, классов и т.д. Что я хотел бы сделать, так это заменить переменную hrefArray запросом DOMDocument, который создает список продуктов для очистки, а затем передает их дочерним процессам для выполнения обработки - таким образом, распределяя нагрузку между 10 дочерними работниками.
Мой мозг говорит, что мне нужно сделать что-то вроде следующего (очевидно, это не работает, поэтому не запускайте его):
<?php
libxml_use_internal_errors(true);
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
set_time_limit(0);
$maxChildWorkers = 10;
$html = new DOMDocument();
$html->loadHtmlFile('http://xxxx');
$xPath = new DOMXPath($html);
$domQuery = '//div[@id=productDetail]/a';
$domReturn = $xPath->query($domQuery);
$hrefsArray[] = $domReturn->getAttribute('href');
function doDomStuff($singleHref) {
// Do stuff here with each product
}
// To figure out: Split href array into $maxChilderWorks # of workArray1, workArray2 ... workArray10.
$pids = array();
foreach ($workArray(1,2,3 ... 10) as $singleHref) {
$pid = pcntl_fork();
if ($pid == -1) {
die("Couldn't fork, error!");
} elseif ($pid > 0) {
// We are the parent
$pids[] = $pid;
} else {
// We are the child
$childPid = posix_getpid();
doDomStuff($singleHref);
exit(0);
}
}
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
// Clear the libxml buffer so it doesn't fill up
libxml_clear_errors();
Но чего я не могу понять, так это как построить свой hrefsArray[] в только главный/родительский процесс и передайте его дочернему процессу. В настоящее время все, что я пробовал, вызывает циклы в дочерних процессах. Т.Е. мой hrefsArray создается в главном и в каждом последующем дочернем процессе.
Я уверен, что я делаю все это совершенно неправильно, поэтому был бы очень признателен, если бы вы просто подтолкнули меня в правильном направлении.
2 answers
Кажется, что я предлагаю это ежедневно, но вы смотрели на Гирмана? Существует даже хорошо документированный класс PECL.
Gearman - это система рабочих очередей. Вы бы создали работников, которые подключаются и прослушивают задания, и клиентов, которые подключаются и отправляют задания. Клиент может либо дождаться завершения запрошенного задания, либо уволить его и забыть. По вашему усмотрению работники могут даже отправлять обновления статуса и указывать, насколько далеко они продвинулись в процессе.
В другими словами, вы получаете преимущества нескольких процессов или потоков, не беспокоясь о процессах и потоках. Клиенты и работники могут даже находиться на разных машинах.
Введение
pcntl_fork()
это не единственный способ повысить производительность HTML scraper
, хотя, возможно, было бы неплохо использовать Message Queue
, предложенный Charles
, но вам все еще нужен более быстрый эффективный способ получить этот запрос в вашем workers
Решение 1
Используйте curl_multi_init
... завиток на самом деле быстрее, и использование нескольких завитков дает вам параллельную обработку
Из документа PHP
Curl_multi_init Позволяет обрабатывать несколько дескрипторов завитков в параллель.
Поэтому вместо того, чтобы использовать $html->loadHtmlFile('http://xxxx');
для загрузки файлов несколько раз, вы можете просто использовать curl_multi_init
для загрузки нескольких URL-адресов одновременно
Вот несколько интересных реализаций
- php - Самый быстрый способ проверить наличие текста во многих доменах (выше 1000)
- php получает все изображения с URL-адреса, ширина и высота которого >=200 больше быстрее
- Как предотвратить перегрузку сервера во время запросов Curl в PHP
Решение 2
Вы можете использовать pthreads для использования многопоточности в PHP
Пример
// Number of threads you want
$threads = 10;
// Treads storage
$ts = array();
// Your list of URLS // range just for demo
$urls = range(1, 50);
// Group Urls
$urlsGroup = array_chunk($urls, floor(count($urls) / $threads));
printf("%s:PROCESS #load\n", date("g:i:s"));
$name = range("A", "Z");
$i = 0;
foreach ( $urlsGroup as $group ) {
$ts[] = new AsyncScraper($group, $name[$i ++]);
}
printf("%s:PROCESS #join\n", date("g:i:s"));
// wait for all Threads to complete
foreach ( $ts as $t ) {
$t->join();
}
printf("%s:PROCESS #finish\n", date("g:i:s"));
Вывод
9:18:00:PROCESS #load
9:18:00:START #5592 A
9:18:00:START #9620 B
9:18:00:START #11684 C
9:18:00:START #11156 D
9:18:00:START #11216 E
9:18:00:START #11568 F
9:18:00:START #2920 G
9:18:00:START #10296 H
9:18:00:START #11696 I
9:18:00:PROCESS #join
9:18:00:START #6692 J
9:18:01:END #9620 B
9:18:01:END #11216 E
9:18:01:END #10296 H
9:18:02:END #2920 G
9:18:02:END #11696 I
9:18:04:END #5592 A
9:18:04:END #11568 F
9:18:04:END #6692 J
9:18:05:END #11684 C
9:18:05:END #11156 D
9:18:05:PROCESS #finish
Используемый класс
class AsyncScraper extends Thread {
public function __construct(array $urls, $name) {
$this->urls = $urls;
$this->name = $name;
$this->start();
}
public function run() {
printf("%s:START #%lu \t %s \n", date("g:i:s"), $this->getThreadId(), $this->name);
if ($this->urls) {
// Load with CURL
// Parse with DOM
// Do some work
sleep(mt_rand(1, 5));
}
printf("%s:END #%lu \t %s \n", date("g:i:s"), $this->getThreadId(), $this->name);
}
}