Как я могу очистить содержимое веб-сайта в PHP от веб-сайта, который требует логин с помощью cookie?
Моя проблема в том, что он не просто требует базовый файл cookie, а запрашивает файл cookie сеанса и случайно сгенерированные идентификаторы. Я думаю, это означает, что мне нужно использовать эмулятор веб-браузера с файлом cookie?
Я пытался использовать Snoopy, Goutte и несколько других эмуляторов веб-браузера, но пока не смог найти учебники о том, как получать куки. Я немного отчаялся!
Кто-нибудь может привести пример того, как принимать куки в Snoopy или Goutte?
Заранее спасибо!
2 ответа
Объектно-ориентированный ответ
Мы реализуем как можно больше предыдущего ответа в одном классе под названием Browser
это должно обеспечить нормальные навигационные функции.
Тогда мы сможем поместить специфичный для сайта код в очень простой форме в новый производный класс, который мы называем, скажем, FooBrowser
, который выполняет очистку сайта Foo
,
Браузер, наследующий класс, должен предоставлять некоторые специфичные для сайта функции, такие как path()
функция, позволяющая хранить специфичную для сайта информацию, например
function path($basename) {
return '/var/tmp/www.foo.bar/' . $basename;
}
abstract class Browser
{
private $options = [];
private $state = [];
protected $cookies;
abstract protected function path($basename);
public function __construct($site, $options = []) {
$this->cookies = $this->path('cookies');
$this->options = array_merge(
[
'site' => $site,
'userAgent' => 'Mozilla/5.0 (Windows NT 5.1; rv:16.0) Gecko/20100101 Firefox/16.0 - LeoScraper',
'waitTime' => 250000,
],
$options
);
$this->state = [
'referer' => '/',
'url' => '',
'curl' => '',
];
$this->__wakeup();
}
/**
* Reactivates after sleep (e.g. in session) or creation
*/
public function __wakeup() {
$this->state['curl'] = curl_init();
$this->config([
CURLOPT_USERAGENT => $this->options['userAgent'],
CURLOPT_ENCODING => '',
CURLOPT_NOBODY => false,
// ...retrieving the body...
CURLOPT_BINARYTRANSFER => true,
// ...as binary...
CURLOPT_RETURNTRANSFER => true,
// ...into $ret...
CURLOPT_FOLLOWLOCATION => true,
// ...following redirections...
CURLOPT_MAXREDIRS => 5,
// ...reasonably...
CURLOPT_COOKIEFILE => $this->cookies,
// Save these cookies
CURLOPT_COOKIEJAR => $this->cookies,
// (already set above)
CURLOPT_CONNECTTIMEOUT => 30,
// Seconds
CURLOPT_TIMEOUT => 300,
// Seconds
CURLOPT_LOW_SPEED_LIMIT => 16384,
// 16 Kb/s
CURLOPT_LOW_SPEED_TIME => 15,
]);
}
/**
* Imports an options array.
*
* @param array $opts
* @throws DetailedError
*/
private function config(array $opts = []) {
foreach ($opts as $key => $value) {
if (true !== curl_setopt($this->state['curl'], $key, $value)) {
throw new \Exception('Could not set cURL option');
}
}
}
private function perform($url) {
$this->state['referer'] = $this->state['url'];
$this->state['url'] = $url;
$this->config([
CURLOPT_URL => $this->options['site'] . $this->state['url'],
CURLOPT_REFERER => $this->options['site'] . $this->state['referer'],
]);
$response = curl_exec($this->state['curl']);
// Should we ever want to randomize waitTime, do so here.
usleep($this->options['waitTime']);
return $response;
}
/**
* Returns a configuration option.
* @param string $key configuration key name
* @param string $value value to set
* @return mixed
*/
protected function option($key, $value = '__DEFAULT__') {
$curr = $this->options[$key];
if ('__DEFAULT__' !== $value) {
$this->options[$key] = $value;
}
return $curr;
}
/**
* Performs a POST.
*
* @param $url
* @param $fields
* @return mixed
*/
public function post($url, array $fields) {
$this->config([
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
]);
return $this->perform($url);
}
/**
* Performs a GET.
*
* @param $url
* @param array $fields
* @return mixed
*/
public function get($url, array $fields = []) {
$this->config([ CURLOPT_POST => false ]);
if (empty($fields)) {
$query = '';
} else {
$query = '?' . http_build_query($fields);
}
return $this->perform($url . $query);
}
}
Теперь очистить FooSite:
/* WWW_FOO_COM requires username and password to construct */
class WWW_FOO_COM_Browser extends Browser
{
private $loggedIn = false;
public function __construct($username, $password) {
parent::__construct('http://www.foo.bar.baz', [
'username' => $username,
'password' => $password,
'waitTime' => 250000,
'userAgent' => 'FooScraper',
'cache' => true
]);
// Open the session
$this->get('/');
// Navigate to the login page
$this->get('/login.do');
}
/**
* Perform login.
*/
public function login() {
$response = $this->post(
'/ajax/loginPerform',
[
'j_un' => $this->option('username'),
'j_pw' => $this->option('password'),
]
);
// TODO: verify that response is OK.
// if (!strstr($response, "Welcome " . $this->option('username'))
// throw new \Exception("Bad username or password")
$this->loggedIn = true;
return true;
}
public function scrape($entry) {
// We could implement caching to avoid scraping the same entry
// too often. Save $data into path("entry-" . md5($entry))
// and verify the filemtime of said file, is it newer than time()
// minus, say, 86400 seconds? If yes, return file_get_content and
// leave remote site alone.
$data = $this->get(
'/foobars/baz.do',
[
'ticker' => $entry
]
);
return $data;
}
Теперь фактический код будет следующим:
$scraper = new WWW_FOO_COM_Browser('lserni', 'mypassword');
if (!$scraper->login()) {
throw new \Exception("bad user or pass");
}
foreach ($entries as $entry) {
$html = $scraper->scrape($entry);
// Parse HTML
}
Обязательное уведомление: используйте подходящий парсер для получения данных из необработанного HTML.
Вы можете сделать это в cURL без внешних "эмуляторов".
Код ниже извлекает страницу в переменную PHP для анализа.
сценарий
Есть страница (назовем это HOME), которая открывает сессию. Серверная сторона, если она в PHP, это та, которая (на самом деле, любая) вызывает session_start()
в первый раз. На других языках вам нужна определенная страница, которая будет выполнять все настройки сеанса. Со стороны клиента это страница, предоставляющая куки-файл идентификатора сессии. В PHP все сессионные страницы делают; на других языках это сделает целевая страница, все остальные проверит наличие файла cookie и, если его нет, вместо создания сеанса перенаправят вас в HOME.
Существует страница (LOGIN), которая генерирует форму входа и добавляет в сеанс критическую информацию - "Этот пользователь вошел в систему". В приведенном ниже коде эта страница запрашивает идентификатор сессии.
И, наконец, есть N страниц, где хранятся лакомства.
Итак, мы хотим нажать ДОМОЙ, затем ЛОГИН, затем ХОРОШО, один за другим В PHP (и на самом деле других языках), опять же, HOME и LOGIN вполне могут быть одной страницей. Или все страницы могут иметь один и тот же адрес, например, в одностраничных приложениях.
Код
$url = "the url generating the session ID";
$next_url = "the url asking for session";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
// We do not authenticate, only access page to get a session going.
// Change to False if it is not enough (you'll see that cookiefile
// remains empty).
curl_setopt($ch, CURLOPT_NOBODY, True);
// You may want to change User-Agent here, too
curl_setopt($ch, CURLOPT_COOKIEFILE, "cookiefile");
curl_setopt($ch, CURLOPT_COOKIEJAR, "cookiefile");
// Just in case
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$ret = curl_exec($ch);
// This page we retrieve, and scrape, with GET method
foreach(array(
CURLOPT_POST => False, // We GET...
CURLOPT_NOBODY => False, // ...the body...
CURLOPT_URL => $next_url, // ...of $next_url...
CURLOPT_BINARYTRANSFER => True, // ...as binary...
CURLOPT_RETURNTRANSFER => True, // ...into $ret...
CURLOPT_FOLLOWLOCATION => True, // ...following redirections...
CURLOPT_MAXREDIRS => 5, // ...reasonably...
CURLOPT_REFERER => $url, // ...as if we came from $url...
//CURLOPT_COOKIEFILE => 'cookiefile', // Save these cookies
//CURLOPT_COOKIEJAR => 'cookiefile', // (already set above)
CURLOPT_CONNECTTIMEOUT => 30, // Seconds
CURLOPT_TIMEOUT => 300, // Seconds
CURLOPT_LOW_SPEED_LIMIT => 16384, // 16 Kb/s
CURLOPT_LOW_SPEED_TIME => 15, //
) as $option => $value)
if (!curl_setopt($ch, $option, $value))
die("could not set $option to " . serialize($value));
$ret = curl_exec($ch);
// Done; cleanup.
curl_close($ch);
Реализация
Прежде всего мы должны получить страницу входа.
Мы используем специальный User-Agent, чтобы представиться, чтобы быть узнаваемыми (мы не хотим противодействовать веб-мастеру), а также чтобы обмануть сервер, отправив нам конкретную версию сайта, адаптированную для браузера. В идеале, мы используем тот же User-Agent, что и любой браузер, который мы будем использовать для отладки страницы, плюс суффикс, чтобы было понятно, кто бы ни проверял, что это автоматический инструмент, на который они смотрят (см. Комментарий Halfer),
$ua = 'Mozilla/5.0 (Windows NT 5.1; rv:16.0) Gecko/20100101 Firefox/16.0 (ROBOT)';
$cookiefile = "cookiefile";
$url1 = "the login url generating the session ID";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url1);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiefile);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiefile);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
curl_setopt($ch, CURLOPT_NOBODY, False);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, True);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, True);
$ret = curl_exec($ch);
Это вернет страницу с запросом имени пользователя / пароля. Осматривая страницу, мы находим нужные поля (в том числе скрытые) и можем их заполнить. FORM
тег говорит нам, нужно ли нам продолжать с POST или GET.
Возможно, мы захотим проверить код формы, чтобы настроить следующие операции, поэтому мы просим cURL вернуть содержимое страницы как есть в $ret
и сделать возвращение тела страницы. Иногда, CURLOPT_NOBODY
установлен в True
все еще достаточно для запуска создания сеанса и отправки файлов cookie, и если это так, то это быстрее. Но CURLOPT_NOBODY
("нет тела") работает путем выдачи HEAD
запрос, а не GET
; а иногда HEAD
запрос не работает, потому что сервер будет реагировать только на полный GET
,
Вместо того, чтобы извлекать тело таким способом, можно также войти в систему, используя настоящий Firefox, и прослушать содержимое формы, публикуемое с помощью Firebug (или Chrome с Chrome Tools); некоторые сайты будут пытаться заполнить / изменить скрытые поля с помощью Javascript, чтобы отправляемая форма не была той, которую вы видите в коде HTML.
Веб-мастер, который хочет, чтобы его сайт не был удален, может отправить скрытое поле с отметкой времени. Человеку (которому не помогает слишком умный браузер - есть способы сказать браузерам, что они не должны быть умными; в худшем случае, каждый раз, когда вы меняете имя пользователя и пропускаете поля), заполнение формы занимает не менее трех секунд. Сценарий cURL принимает ноль. Конечно, задержка может быть смоделирована. Это все в тени...
Мы также можем быть готовы к появлению формы. Веб-мастер может, например, создать форму, спрашивая имя, адрес электронной почты и пароль; а затем, используя CSS, переместите поле "электронная почта", где вы ожидаете найти имя, и наоборот. Таким образом, реальная отправляемая форма будет иметь "@" в поле с именем username
ни один в поле не называется email
, Сервер, ожидающий этого, просто снова инвертирует два поля. "Скребок", созданный вручную (или спамбот), сделает то, что кажется естественным, и отправит электронное письмо в email
поле. И, поступая так, он выдает себя. Работая через форму один раз с реальным браузером, поддерживающим CSS и JS, отправляя значимые данные и анализируя то, что на самом деле отправляется, мы могли бы преодолеть это конкретное препятствие. Возможно, потому что есть способы усложнить жизнь. Как я уже сказал, shadowboxing.
Возвращаясь к рассматриваемому случаю, в этом случае форма содержит три поля и не имеет оверлея Javascript. У нас есть cPASS
, cUSR
, а также checkLOGIN
со значением "Проверить логин".
Поэтому мы готовим форму с соответствующими полями. Обратите внимание, что форма должна быть отправлена как application/x-www-form-urlencoded
, что в PHP cURL означает две вещи:
- мы должны использовать
CURLOPT_POST
- опция CURLOPT_POSTFIELDS должна быть строкой (массив будет сигнализировать cURL для отправки в виде
multipart/form-data
, который может работать... или не может).
Поля формы, как говорится, urlencoded; для этого есть функция.
Мы читаем action
поле формы; это URL, который мы должны использовать для отправки нашей аутентификации (которая у нас должна быть).
Так что все готово...
$fields = array(
'checkLOGIN' => 'Check Login',
'cUSR' => 'jb007',
'cPASS' => 'astonmartin',
);
$coded = array();
foreach($fields as $field => $value)
$coded[] = $field . '=' . urlencode($value);
$string = implode('&', $coded);
curl_setopt($ch, CURLOPT_URL, $url1); //same URL as before, the login url generating the session ID
curl_setopt($ch, CURLOPT_POST, True);
curl_setopt($ch, CURLOPT_POSTFIELDS, $string);
$ret = curl_exec($ch);
Теперь мы ожидаем "Привет, Джеймс - как насчет хорошей игры в шахматы?" стр. Более того, мы ожидаем, что сеанс, связанный с файлом cookie, сохраненным в $cookiefile
была предоставлена критическая информация - "пользователь аутентифицирован".
Таким образом, все последующие запросы страницы сделаны с использованием $ch
и к тому же банку cookie будет предоставлен доступ, что позволит нам довольно легко "чистить" страницы - просто не забудьте установить режим запроса обратно GET
:
curl_setopt($ch, CURLOPT_POST, False);
// Start spidering
foreach($urls as $url)
{
curl_setopt($ch, CURLOPT_URL, $url);
$HTML = curl_exec($ch);
if (False === $HTML)
{
// Something went wrong, check curl_error() and curl_errno().
}
}
curl_close($ch);
В цикле у вас есть доступ к $HTML
- HTML-код каждой страницы.
Велик соблазн использования регулярных выражений. Сопротивляйся этому ты должен. Чтобы лучше справляться с постоянно меняющимся HTML, а также быть уверенным, что вы не увидите ложных срабатываний или ложных отрицаний, когда макет остается прежним, но содержимое меняется (например, вы обнаружите, что у вас есть прогнозы погоды в Ницце, Туррет-Левенс, Кастанье, но никогда не Asprémont или Gattières, и не правда ли это?), Лучший вариант - использовать DOM: