Как я могу очистить содержимое веб-сайта в 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:

Получение атрибута href элемента A

Другие вопросы по тегам