sesiones - set cookie php no funciona
¿Cómo puedo raspar el contenido del sitio web en PHP desde un sitio web que requiere un inicio de sesión de cookies? (2)
Mi problema es que no solo requiere una cookie básica, sino que solicita una cookie de sesión y una ID generada aleatoriamente. Creo que esto significa que necesito usar un emulador de navegador web con un contenedor de cookies.
Intenté usar Snoopy, Goutte y un par de emuladores de otros navegadores web, pero aún no he podido encontrar tutoriales sobre cómo recibir cookies. Me estoy poniendo un poco desesperado!
¿Alguien puede darme un ejemplo de cómo aceptar las cookies en Snoopy o Goutte?
¡Gracias por adelantado!
Respuesta orientada a objetos
Implementamos tanto como sea posible la respuesta anterior en una clase llamada Browser
que debe proporcionar las características de navegación normales.
Entonces deberíamos poder poner el código específico del sitio, en una forma muy simple, en una nueva clase derivada que llamamos, digamos, FooBrowser
, que realiza raspado del sitio Foo
.
El navegador de derivación de clase debe proporcionar alguna función específica del sitio, como una función de path()
que permita almacenar información específica del sitio, por ejemplo
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);
}
}
Ahora para raspar 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;
}
Ahora el código de raspado real sería:
$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
}
Aviso obligatorio: utilice un analizador adecuado para obtener datos del HTML sin procesar .
Puede hacerlo en cURL sin necesidad de ''emuladores'' externos.
El código a continuación recupera una página en una variable de PHP para ser analizada.
Guión
Hay una página (llamémoslo HOME) que abre la sesión. El lado del servidor, si está en PHP, es el que (en realidad alguien) llama a session_start()
por primera vez. En otros idiomas necesita una página específica que hará toda la configuración de la sesión. Desde el lado del cliente, es la página que proporciona la cookie de ID de sesión. En PHP, todas las páginas de sesión lo hacen; en otros idiomas, la página de destino lo hará, todos los demás verificarán si la cookie está allí, y si no la hay, en lugar de crear la sesión, lo llevarán a HOME.
Hay una página (LOGIN) que genera el formulario de inicio de sesión y agrega información crítica a la sesión: "Este usuario ha iniciado sesión". En el siguiente código, esta es la página que solicita la ID de la sesión.
Y finalmente hay N páginas donde residen las golosinas para ser rasguños.
Entonces queremos presionar HOME, luego LOGIN, luego GOODIES uno tras otro. En PHP (y en otros idiomas en realidad), de nuevo, HOME y LOGIN podrían ser la misma página. O todas las páginas pueden compartir la misma dirección , por ejemplo, en Aplicaciones de una sola página.
El código
$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);
Implementación
Antes que nada, tenemos que obtener la página de inicio de sesión.
Utilizamos un User-Agent especial para presentarnos, tanto para ser reconocible ( no queremos antagonizar con el webmaster) como para engañar al servidor para que nos envíe una versión específica del sitio que está hecho a medida del navegador. Idealmente, utilizamos el mismo agente de usuario como cualquier navegador que vamos a usar para depurar la página, más un sufijo para dejar en claro a quien compruebe que es una herramienta automatizada que están viendo ( ver el comentario de 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);
Esto recuperará la página solicitando usuario / contraseña. Al inspeccionar la página, encontramos los campos necesarios (incluidos los ocultos) y podemos completarlos. La etiqueta FORM
nos dice si necesitamos continuar con POST o GET.
Es posible que deseemos inspeccionar el código del formulario para ajustar las siguientes operaciones, por lo que pedimos a cURL que devuelva el contenido de la página como está en $ret
, y que devuelva el cuerpo de la página. A veces, CURLOPT_NOBODY
establecido en True
todavía es suficiente para activar la creación de sesiones y el envío de cookies, y si es así, es más rápido. Pero CURLOPT_NOBODY
("sin cuerpo") funciona emitiendo una solicitud HEAD
, en lugar de un GET
; y a veces la solicitud HEAD
no funciona porque el servidor solo reaccionará ante un GET
completo.
En lugar de recuperar el cuerpo de esta manera, también es posible iniciar sesión utilizando un Firefox real y olfatear el contenido del formulario que se publica con Firebug (o Chrome con Chrome Tools); algunos sitios intentarán rellenar / modificar los campos ocultos con Javascript, de modo que el formulario que se envíe no sea el que ve en el código HTML.
Un webmaster que quería que su sitio no fuera copiado podría enviar un campo oculto con la marca de tiempo. Un ser humano (no ayudado por un navegador demasiado listo) hay maneras de decirle a los navegadores que no sean inteligentes, en el peor de los casos, cada vez que cambie el nombre del usuario y pase campos, toma al menos tres segundos para completar un formulario. Un script cURL toma cero. Por supuesto, se puede simular un retraso. Es todo shadowboxing ...
También es posible que deseemos estar atentos a la apariencia del formulario. Un webmaster podría, por ejemplo, crear un formulario pidiendo nombre, correo electrónico y contraseña; y luego, mediante el uso de CSS, mueva el campo "correo electrónico" donde esperaría encontrar el nombre, y viceversa. Entonces, el formulario real que se presente tendrá una "@" en un campo llamado username
, ninguno en el campo llamado email
. El servidor, que espera esto, simplemente invierte nuevamente los dos campos. Un "raspador" construido a mano (o un spambot) haría lo que parece natural y enviará un correo electrónico en el campo de email
. Y al hacerlo, se traiciona a sí mismo. Al trabajar una vez con el formulario con un navegador real CSS y JS, enviar datos significativos y olfatear lo que realmente se envía, podríamos superar este obstáculo en particular. Podría , porque hay formas de dificultar la vida. Como dije, shadowboxing .
Volviendo al caso que nos ocupa, en este caso el formulario contiene tres campos y no tiene superposición de Javascript. Tenemos cPASS
, cUSR
y checkLOGIN
con un valor de ''Comprobar inicio de sesión''.
Así que preparamos el formulario con los campos adecuados. Tenga en cuenta que el formulario debe enviarse como application/x-www-form-urlencoded
, que en PHP cURL significa dos cosas:
- debemos usar
CURLOPT_POST
- la opción CURLOPT_POSTFIELDS debe ser una cadena (una matriz enviaría una señal a cURL para que envíe como
multipart/form-data
, que podría funcionar ... o no).
Los campos del formulario son, como se dice, urlencoded; hay una función para eso.
Leemos el campo de action
de la forma; esa es la URL que debemos usar para enviar nuestra autenticación (que debemos tener).
Entonces todo está listo ...
$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);
Esperamos ahora un "Hola, James, ¿qué tal un buen juego de ajedrez?" página. Pero más que eso, esperamos que la sesión asociada a la cookie guardada en $cookiefile
se haya suministrado con la información crítica: "el usuario está autenticado" .
Por lo tanto, se otorgará acceso a todas las solicitudes de páginas siguientes realizadas con $ch
y el mismo contenedor de cookies, lo que nos permitirá ''rozar'' páginas con bastante facilidad; solo recuerde volver a establecer el modo de solicitud en 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);
En el ciclo, tiene acceso a $HTML
, el código HTML de cada página.
Gran la tentación de usar expresiones regulares es. Resístalo debes. Para lidiar mejor con el siempre cambiante HTML, así como también asegurarse de no mostrar falsos positivos o falsos negativos cuando el diseño permanece igual pero el contenido cambia (por ejemplo, descubre que tiene las previsiones meteorológicas de Nice, Tourrette-Levens, Castagniers, pero nunca Asprémont o Gattières, ¿y no es tan cómico?), La mejor opción es usar DOM: