Home PageКаталог Изменения НовыеКомментарии Пользователи Регистрация
CakePHP: Code/Components/SeveralFlows ...

Загрузка в несколько потоков с multi_curl и PHP

Автор Владимир Лучанинов


Когда нужно скачать сотню страниц, то можно обойтись моим компонентом Browser. Но недавно мне надо было скачать столько информации, что нужно было ждать неделю. Проблема в том, что компонент загружает страницы по очереди. Пришлось придумывать как заставить его качать в несколько потоков.


Оказалось, что самое простое решение – это multi_curl. Но его на форумах очень критикуют за то, что он ест слишком много памяти, глючит и вообще безобразничает. Есть ещё вариант – неблокирующие сокеты, но мне они показались более сложными. Ну, а самое правильное, решение – это pkg_delete php, pkg_add perl :) Потому что Perl для этого задуман, а PHP – нет. Но ради единоразовой операции, хоть и длительной, пересиливать свою необъяснимую нелюбовь к Perl я не стал. Тем более пишут, что если ставить немного потоков (до 100), то multi_curl будет нормально работать.


К счастью, я не сторонник выдумывания велосипедов, поэтому взял библиотеку Вадима Тимофеева для работы с multi_curl. Чтобы она работала с компонентом Browser, скачайте последнюю версию, разархивируйте файл Multi Curl.class?.php в папку vendors вашего CakePHP-проекта и переименуйте его в multi_curl.php.


Папка vendors в CakePHP позволяет использовать сторонние разработки, которые не созданы специально для CakePHP. Эти файлы подключаются с помощью функции vendor()

Класс MultiCurl довольно интересно реализован. Он является абстрактным классом. Чтобы использовать его в своей программе, надо создать его наследника, в котором переопределить событие, которое происходит при загрузке. Вот что пришлось дописать в начало компонента Browser.


vendor('multi_curl');
class BrowserComponentMultiCurl extends MultiCurl {
    var $Browser = null;
    protected function onLoad($url, $content, $info) {
        $s = (serialize($info) . "\r\n\r\n" . $content);
        file_put_contents($this->Browser->_getCacheFilename($url), $s);
    }
}


Тут необходимо пояснить принцип работы multi_curl. При скачивании в несколько потоков multi_curl ставит в очередь закачки несколько страниц и возращает управление основной программе только после того как закачает все страницы. Но после закачки каждого файла выполняется callback-функция.


Я решил упростить себе задачу и сделал вместо метода getMulti($urls), метод cacheMulti($urls). Поэтому не забудьте созадать папку для кеширования – APP/tmp/cache/browser


/**
 * Downloads URLs in multiple threads
 *
 * @param array $urls a(url1, url2, ...)
 * @return boolean
 */
function cacheMulti($urls) {
    try {
        $mc = new BrowserComponentMultiCurl();
        $mc->Browser = $this;
        $mc->setMaxSessions(5); // limit 5 parallel sessions (by default 10)
        //$mc->setMaxSize(10240); // limit 10 Kb per session (by default 10 Mb)
        foreach ($urls as $url) {
            if (!$this->_isCached($url)) {
                $mc->addUrl($url);
            }
        }
        $mc->wait();
    } catch (Exception $e) {
        // dirty style, but good enough for my tasks
        echo('<h3 style="color:red">'.$e->getMessage().'</h3>');
        @flush();ob_flush();
    }
    return true;
}


Из основной программы этот метод я хотел вызвать сначала так:


$this->Browser->cacheMulti($urls);
foreach ($urls as $url) {
    $html = $this->Browser->get($url); // уже закешировано, поэтому выдаст сразу
    // process $html
}


Но не тут то было. Так как URLов действительно очень много, то ни Firefox, ни Lynx (я сервисные скрипты запускаю часто через него) не могли дождаться ответа от сервера и писалось 408 Request Timeout. Тогда я добавил в httpd.conf настройку Timeout 300000. Ошибка 408 Request Timeout всё равно показывалась, но насколько я понял, Apache продолжал выполнять скрипт ещё 300000 секунд. Я не нашёл правильного решения этой проблемы, поэтому немного подкорректировал скрипт, разбив список URLов на части.


$queueLength = 30;
for ($i=0; $i<count($urls); $i+=$queueLength) {
    $queue = array_slice($urls, $i, $queueLength);
    $this->Browser->cacheMulti($queue);
    foreach ($queue as $url) {
        $html = $this->Browser->get($url); // уже закешировано, поэтому выдаст сразу
        // process $html
    }
    echo(((!empty($i)?', ':'')) . $i); // показываем обработанный номер
    @flush();ob_flush(); // принудительное отображение. по одной работать не хотят. собака в начале строки - неизбежное зло
    usleep(500); // нечего пререгружать сильно чужие сервера - забанят
}


Можно было бы поставить обработку информации в onLoad, но мне так удобнее :)


Вот полная версия обновлённого компонента Browser

<?
vendor
('multi_curl');
class 
BrowserComponentMultiCurl extends MultiCurl {
    var 
$Browser null;
    protected function 
onLoad($url$content$info) {
        
$s = (serialize($info) . "\r\n\r\n" $content);
        
file_put_contents($this->Browser->_getCacheFilename($url), $s);
    }
}
/**
 * Emulation of browser
 *
 * @version 1.3 (24 Oct 2007)
 * @author Vladimir Luchaninov - http://php.southpark.com.ua
 *
 */
class BrowserComponent extends Object {
    var 
$handle;
    var 
$header;
    var 
$body;
    
/**
     * Name of browser you want to emulate. If 'random' then it will select from the large list.
     *
     * @var string
     */
    
var $userAgent 'random';
    
// if you need http auth
    
var $username null;
    var 
$password null;
    var 
$proxy ''// 'ip:port'
    
var $referer 'http://www.google.com/';
    var 
$timeout 30;
    
/**
     * if you want to cache your requests you need to create folder APP/tmp/cache/browser
     *
     * @var string
     */
    
var $cacheFolder null;
    var 
$symbolsNotFile = array( '~',  '!',  '@',  '#',  'http://''/',  "\\"':',  '*',  '?',  '"',  '<',  '>',  '|');
    var 
$symbolsFile = array('~~''!!''@@''##''#~',      '~!''~@''~#''!~''!@''!#''@~''@!''@#'); // still reserved '#!', '#@'
    /**
     * Init handle for connection
     *
     * @param AppController $controller
     */
    
function startup(&$controller) {
        
$cacheFolder APP 'tmp' DS 'cache' DS 'browser' DS;
        if (
is_dir($cacheFolder)) {
            
$this->cacheFolder $cacheFolder;
        }
        
$this->_initUserAgent();
        
$this->handle curl_init();
    }
    
/**
     * Convert URL to the filename for caching
     *
     * @param string $url Like http://php.southpark.com.ua
     * @return string Filename of the cache file (withour full path)
     */
    
function urlToFilename($url) {
        return 
r($this->symbolsNotFile$this->symbolsFile$url).'.txt';
    }
    
/**
     * Convert filename from cache to URL
     *
     * @param string $filename Filename of cached file (without full path)
     * @return string URL
     */
    
function filenameToUrl($filename) {
        return 
r($this->symbolsFile$this->symbolsNotFilesubstr($filename0strlen($filename)-4));
    }
    
/**
     * Extract header and body from response to $this->header and $this->body
     *
     * @param string $response
     * @return string
     */
    
function _setHeaderBody($response) {
        
// You should see responses from some strange web-services
        // Check for \r\n\r\n is really not enough
        
$regex '/(.*?)\n[\r\n]*?\n+(.*)/sm';
        
$this->header '';
        if (!
preg_match($regex$response$m)) {
            
$this->body $response;
        } else {
            
$this->header $m[1];
            
$this->body ltrim($m[2], "\r");
            
// sometimes there are several headers
            
while (strpos($this->body'HTTP/')===&& preg_match($regex$this->body$m)) {
                
$this->header .= "\n\n" $m[1];
                
$this->body ltrim($m[2], "\r");
            }
        }
        return 
true;
    }
    
/**
     * Get cache file filename for $url if possible. Otherwise null
     *
     * @param string $url Like http://php.southpark.com.ua
     * @return string Cache file filename with full path
     */
    
function _getCacheFilename($url) {
        if (!empty(
$this->cacheFolder) && empty($postvars)) {
            return 
$this->cacheFolder $this->urlToFilename($url);
        } else {
            return 
null;
        }
    }
    
/**
     * Check if $url is already downloaded and saved to cache file
     *
     * @param string $url Like http://php.southpark.com.ua
     * @return boolean True if $url exist in cache
     */
    
function _isCached($url) {
        
$cacheFile $this->_getCacheFilename($url);
        return (!empty(
$cacheFile) && file_exists($cacheFile));
    }
    
/**
     * List all cached URLs
     *
     * @return array a(url1, url2, ...)
     */
    
function getCachedUrls() {
        
$folder = new Folder($this->cacheFolder);
        
$files $folder->find('.*\.txt');
        
$urls = array();
        foreach (
$files as $filename) {
            
$urls[] = $this->filenameToUrl($filename);
        }
        return 
$urls;
    }
    
/**
     * Main function
     *
     * @param string $url
     * @param array $postvars
     * @return string body
     * after execution $this->header is accessible if needed
     */
    
function get($url$postvars=null) {
        
$cacheFile $this->_getCacheFilename($url);
        if (
$this->_isCached($url)) {
            
$response file_get_contents($cacheFile);
        } else {
            
$this->prepare($url$postvars);
            
$response curl_exec($this->handle);
            if (!empty(
$cacheFile)) {
                
file_put_contents($cacheFile$response);
            }
        }
        
$this->referer $url;
        
$this->_setHeaderBody($response);
        return 
$this->body;
    }
    
/**
     * Downloads URLs in multiple threads
     *
     * @param array $urls a(url1, url2, ...)
     * @return boolean
     */
    
function cacheMulti($urls) {
        try {
            
$mc = new BrowserComponentMultiCurl();
            
$mc->Browser $this;
            
$mc->setMaxSessions(5); // limit 5 parallel sessions (by default 10)
            //$mc->setMaxSize(10240); // limit 10 Kb per session (by default 10 Mb)
            
foreach ($urls as $url) {
                if (!
$this->_isCached($url)) {
                    
$mc->addUrl($url);
                }
            }
            
$mc->wait();
        } catch (
Exception $e) {
            
// dirty style, but good enough for my tasks
            
echo('<h3 style="color:red">'.$e->getMessage().'</h3>');
            @
flush();ob_flush();
        }
        return 
true;
    }
    
/**
     * Set default options of curl
     *
     * @param string $url
     * @param array $postvars
     */
    
function prepare($url$postvars=false){
        
curl_setopt($this->handleCURLOPT_PROXY$this->proxy);
        
curl_setopt($this->handleCURLOPT_REFERER$this->referer);
        
curl_setopt($this->handleCURLOPT_USERAGENT$this->userAgent);
        
curl_setopt($this->handleCURLOPT_URLstr_replace('&','&',$url));
        
curl_setopt($this->handleCURLOPT_HEADER1);
        
curl_setopt($this->handleCURLOPT_FOLLOWLOCATION,1);
        
curl_setopt($this->handleCURLOPT_RETURNTRANSFER1);
        
curl_setopt($this->handleCURLOPT_TIMEOUT$this->timeout);
        
curl_setopt($this->handleCURLOPT_SSL_VERIFYPEERfalse);
        
curl_setopt($this->handleCURLOPT_SSL_VERIFYHOST,  2);
        
curl_setopt($this->handleCURLOPT_COOKIEJARAPP.'tmp/cookie.txt');
        
curl_setopt($this->handleCURLOPT_COOKIEFILEAPP.'tmp/cookie.txt');
        if (!empty(
$postvars)){
            
curl_setopt($this->handleCURLOPT_POST1);
            
curl_setopt($this->handleCURLOPT_POSTFIELDS$postvars);
        }
        if (!empty(
$this->username)) {
            
curl_setopt($this->handleCURLOPT_HTTPAUTHCURLAUTH_ANY);
            
curl_setopt($this->handleCURLOPT_USERPWD$this->username.':'.$this->password); // $auth should be [username]:[password]
        
}
        return 
true;
    }
    
/**
     * Close current connection
     *
     */
    
function close() {
        
curl_close($this->handle);
        return 
true;
    }
    
/**
     * Clears cache
     */
    
function clearCache() {
        if (empty(
$this->cacheFolder)) return false;
        
$dir dir($this->cacheFolder);
        while ((
$file $dir->read()) !== false) {
            if (
in_array($file, array('''.''..'))) continue;
            
unlink($dir->path $file);
        }
        return 
true;
    }
    
/**
     * What browser should be emulated
     *
     * @return string browser name
     */
    
function _initUserAgent() {
        if (
$this->userAgent!='random') return true;
        
$browsers = array(
            
'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows 98)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.0.3705)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Avant Browser; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.10',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; FunWebProducts)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; MRA 4.8 (build 01709); Maxthon; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.50',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.54',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; MAXTHON 2.0)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 1.1.4322; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; InfoPath.2; .NET CLR 1.1.4322; MAXTHON 2.0)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.2)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.7 (build 01670); .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.7 (build 01670); InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.8 (build 01709))',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.8 (build 01709); .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.8 (build 01709); .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.8 (build 01709); Maxthon; .NET CLR 2.0.50727; .NET CLR 1.1.4322; .NET CLR 3.0.04506.30)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MyIE2; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MyIE2; MRA 4.8 (build 01709); .NET CLR 1.1.4322; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 1.1.4322; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser; Avant Browser; .NET CLR 1.1.4322)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon; Avant Browser; InfoPath.2)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon; MyIE2; .NET CLR 1.0.3705; .NET CLR 2.0.50727; InfoPath.2)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.6 (build 01425); InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.8 (build 01709))',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.8 (build 01709); .NET CLR 1.1.4322; InfoPath.1)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.8 (build 01709); Avant Browser)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.9 (build 01863); .NET CLR 2.0.50727)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MyIE2)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MyIE2; .NET CLR 2.0.50727; InfoPath.1; .NET CLR 1.1.4322; MEGAUPLOAD 1.0)',
            
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; InfoPath.2; .NET CLR 1.1.4322)',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; bg; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4',
            
'Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.7.12) Gecko/20050919 Firefox/1.0.7',
            
'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.1) Gecko/20060313 Fedora/1.5.0.1-9 Firefox/1.5.0.1 pango-text',
            
'Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.8) Gecko/20060112 ASPLinux/1.5-1.2am Firefox/1.5',
            
'Opera/8.54 (Windows NT 5.1; U; en)',
            
'Opera/9.00 (Windows NT 5.1; U; ru)',
            
'Opera/9.01 (Windows NT 5.1; U; ru)',
            
'Opera/9.02 (Windows NT 5.0; U; ru)',
            
'Opera/9.02 (Windows NT 5.1; U; ru)',
            
'Opera/9.02 (Windows NT 5.2; U; en)',
            
'Opera/9.10 (Windows NT 5.1; U; ru)',
            
'Opera/9.20 (Windows NT 5.1; U; en)',
            
'Opera/9.20 (Windows NT 5.1; U; ru)',
            
'Opera/9.21 (Windows NT 5.1; U; ru)',
            );
        
$this->userAgent $browsers[array_rand($browsers)];
        return 
true;
    }
}
?>


 
Комментарии

Интересное решение.

78-36-17-81.dynamic.murmansk.dslavangard.ru (2008-10-26 21:34:53)
Зачем perl – можно python
ip-92-50-100-106.unitymediagroup.de (2010-01-16 12:44:27)
multicurl не работает с CURLOPT_TIMEOUT. К сожалению.
mail.intermost.ru (2010-02-24 16:04:37)

ссылка ведёт не на последнюю версию.
dsluplink-152-54.intelbi.ru (2010-05-10 06:53:28)
Класс от Тимофеева на самом деле не использует многопоточность.
Если посмотреть на исходники, то легко заметить, что при каждом вызове метода addUrl
вызывается curl_multi_init(). То есть автор просто не понимает как мультикурл работает.
ppp91-78-237-136.pppoe.mtu-net.ru (2010-05-19 21:26:49)
Добавить комментарий:

Файлов нет. [Показать файлы/форму]