Когда нужно скачать сотню страниц, то можно обойтись моим компонентом Browser. Но недавно мне надо было скачать столько информации, что нужно было ждать неделю. Проблема в том, что компонент загружает страницы по очереди. Пришлось придумывать, как заставить его качать в несколько потоков.
Оказалось, что самое простое решение — это multi_curl. Но его на форумах очень критикуют за то, что он ест слишком много памяти, глючит и вообще безобразничает. Есть ещё вариант — неблокирующие сокеты, но мне они показались более сложными. Ну, а самое правильное, решение — это pkg_delete php, pkg_add perl. Потому что Perl для этого задуман, а PHP — нет. Но ради единоразовой операции, хоть и длительной, пересиливать свою необъяснимую нелюбовь к Perl я не стал. Тем более пишут, что если ставить немного потоков (до 100), то multi_curl будет нормально работать.
К счастью, я не сторонник выдумывания велосипедов, поэтому взял библиотеку Вадима Тимофеева для работы с multi_curl. Чтобы она работала с компонентом Browser, скачайте последнюю версию, разархивируйте файл MultiCurl.class.php в папку vendors вашего CakePHP-проекта и переименуйте его в multi_curl.php.
Папка vendors в CakePHP позволяет использовать сторонние разработки, которые не созданы специально для CakePHP. Эти файлы подключаются с помощью функции vendor()
Класс MultiCurl довольно интересно реализован. Он является абстрактным классом. Чтобы использовать его в своей программе, надо создать его наследника, в котором переопределить событие, которое происходит при загрузке. Вот что пришлось дописать в начало компонента Browser.
PHP:
-
vendor('multi_curl');
-
class BrowserComponentMultiCurl extends MultiCurl {
-
var $Browser = null;
-
-
protected function onLoad($url, $content, $info) {
-
file_put_contents($this->Browser->_getCacheFilename($url), $s);
-
}
-
}
Тут необходимо пояснить принцип работы multi_curl. При скачивании в несколько потоков multi_curl ставит в очередь закачки несколько страниц и возращает управление основной программе только после того, как закачает все страницы. Но после закачки каждого файла выполняется callback-функция.
Я решил упростить себе задачу и сделал вместо метода getMulti($urls), метод cacheMulti($urls). Поэтому не забудьте создать папку для кеширования — APP/tmp/cache/browser.
PHP:
-
/**
-
* 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;
-
}
Из основной программы этот метод я хотел вызвать сначала так:
PHP:
-
$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ов на части.
PHP:
-
$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(); // принудительное отображение. по одной работать не хотят. собака в начале строки - неизбежное зло
-
}
Можно было бы поставить обработку информации в onLoad, но мне так удобнее.
Вот полная версия обновлённого компонента Browser.
PHP:
-
<?
-
-
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
-
*
-
*/
-
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 https://www.seoded.ru
-
* @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->symbolsNotFile, substr($filename, 0, strlen($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/')===0 && 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 https://www.seoded.ru
-
* @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 https://www.seoded.ru
-
* @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->handle, CURLOPT_PROXY, $this->proxy);
-
curl_setopt($this->handle, CURLOPT_REFERER, $this->referer);
-
curl_setopt($this->handle, CURLOPT_USERAGENT, $this->userAgent);
-
curl_setopt($this->handle, CURLOPT_URL, str_replace('&','&',$url));
-
curl_setopt($this->handle, CURLOPT_HEADER, 1);
-
curl_setopt($this->handle, CURLOPT_FOLLOWLOCATION,1);
-
curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
-
curl_setopt($this->handle, CURLOPT_TIMEOUT, $this->timeout);
-
curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, false);
-
curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 2);
-
-
curl_setopt($this->handle, CURLOPT_COOKIEJAR, APP.'tmp/cookie.txt');
-
curl_setopt($this->handle, CURLOPT_COOKIEFILE, APP.'tmp/cookie.txt');
-
-
if (!empty($postvars)){
-
curl_setopt($this->handle, CURLOPT_POST, 1);
-
curl_setopt($this->handle, CURLOPT_POSTFIELDS, $postvars);
-
}
-
-
if (!empty($this->username)) {
-
curl_setopt($this->handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
-
curl_setopt($this->handle, CURLOPT_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;
-
}
-
}
-
-
?>
Автор: Владимир Лучанинов.