Ir al contenido principal

Solicitudes HTTP (+trabajo con cookies, proxy, sesiones)

Métodos de la clase base

Para recopilar datos de una página web, es necesario realizar una solicitud HTTP. En la JavaScript API v2 de A-Parser se ha implementado un método de ejecución de solicitudes HTTP fácil de usar, que devuelve un objeto JSON como respuesta dependiendo de los argumentos del método especificados. A continuación, aprenderá: cómo se realiza una solicitud HTTP, qué argumentos y opciones tiene el método, los resultados de dichas opciones, cómo especificar la condición de éxito de una solicitud HTTP, y más. Estos métodos se heredan de BaseParser y son la base para crear sus propios extractores. Obtención de una respuesta HTTP mediante una solicitud; se especifican como argumentos:

  • method - método de la solicitud (GET, POST...)

  • url - enlace para la solicitud

  • queryParams - hash con parámetros get o hash con el cuerpo de la solicitud post solicitudes HTTP o guardar para que otro hilo las ejecute utilizando el Gestor de sesiones.

Estos métodos se heredan de BaseParser y son la base para crear extractores propios

await this.request(method, url[, queryParams][, opts])

await this.request(method, url, queryParams, opts)

Obtención de una respuesta HTTP mediante una solicitud; se especifican como argumentos:

  • method - método de la solicitud (GET, POST...)
  • url - enlace para la solicitud
  • queryParams - hash con parámetros get o hash con el cuerpo de la solicitud post
  • opts - hash con opciones de la solicitud

opts.check_content

check_content: [ condición1, condición2, ...] - matriz de condiciones para verificar el contenido recibido; si la verificación no se cumple, la solicitud se repetirá con otro proxy.

Posibilidades:

  • uso de cadenas como condiciones (búsqueda por coincidencia de cadena)
  • uso de expresiones regulares como condiciones
  • uso de funciones de verificación propias, a las que se pasan los datos y los encabezados de la respuesta
  • se pueden establecer varios tipos de condiciones a la vez
  • para la negación lógica, coloque la condición en una matriz, es decir, check_content: ['xxxx', [/yyyy/]] significa que la solicitud se considerará exitosa si los datos recibidos contienen la subcadena xxxx y, al mismo tiempo, la expresión regular /yyyy/ no encuentra coincidencias en la página

Para una solicitud exitosa, deben cumplirse todas las verificaciones especificadas en la matriz

Ejemplo (en los comentarios se indica qué se necesita para que la solicitud se considere exitosa):

let response = await this.request('GET', set.query, {}, {
check_content: [
/<\/html>|<\/body>/, // en la página recibida debe activarse esta expresión regular
['XXXX'], // en la página recibida no debe estar esta subcadena
'</html>', // en la página recibida debe estar esta subcadena
(data, hdr) => {
return hdr.Status == 200 && data.length > 100;
} // esta función debe devolver true
]
});

opts.decode

decode: 'auto-html' - detección automática de la codificación y conversión a utf8

Valores posibles:

  • auto-html - basado en encabezados, etiquetas meta y el contenido de la página (opción recomendada óptima)
  • utf8 - indica que el documento está en codificación utf8
  • <encoding> - cualquier otra codificación

opts.headers

headers: { ... } - hash con encabezados, el nombre del encabezado se especifica en minúsculas, se puede incluir cookie.

Ejemplo:

headers: {
accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'accept-encoding': 'gzip, deflate, br',
cookie: 'a=321; b=test',
'user-agent' 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36'
}

opts.headers_order

headers_order: ['cookie', 'user-agent', ...] - permite redefinir el orden de clasificación de los encabezados

opts.onlyheaders

onlyheaders: 0 - determina la lectura de data; si está activado (1), solo obtiene los encabezados

opts.recurse

recurse: N - número máximo de saltos por redirecciones, por defecto 7, use 0 para desactivar el seguimiento de redirecciones

opts.proxyretries

proxyretries: N - número de intentos de ejecución de la solicitud, por defecto se toma de la configuración del extractor

opts.parsecodes

parsecodes: { ... } - lista de códigos de respuesta HTTP que el extractor considerará exitosos, por defecto se toma de la configuración del extractor. Si se especifica '*': 1, todas las respuestas se considerarán exitosas.

Ejemplo:

parsecodes: {
200: 1,
403: 1,
500: 1
}

opts.timeout

timeout: N - tiempo de espera de la respuesta en segundos, por defecto se toma de la configuración del extractor

opts.do_gzip

do_gzip: 1 - determina si se debe usar compresión (gzip/deflate/br), por defecto activado (1); para desactivarlo se debe establecer el valor 0

opts.max_size

max_size: N - tamaño máximo de la respuesta en bytes, por defecto se toma de la configuración del extractor

opts.cookie_jar

cookie_jar: { ... } - hash con cookies. Ejemplo de hash:

"cookie_jar": {
"version": 1,
".google.com": {
"/": {
"login": {
"value": "true"
},
"lang": {
"value": "ru-RU"
}
}
},
".test.google.com": {
"/": {
"id": {
"value": 155643
}
}
}

opts.attempt

attempt: N - indica el número del intento actual; al usar este parámetro, se ignora el controlador de intentos integrado para esta solicitud

opts.browser

browser: 1 - emulación automática de encabezados de navegador (1 - activado, 0 - desactivado)

opts.use_proxy

use_proxy: 1 - redefine el uso de proxy para una solicitud individual dentro del extractor JS por encima del parámetro global Use proxy (1 - activado, 0 - desactivado)

opts.noextraquery

noextraquery: 0 - desactiva la adición de Extra query string a la URL de la solicitud (1 - activado, 0 - desactivado)

opts.save_to_file

save_to_file: file - permite descargar un archivo directamente al disco, omitiendo la escritura en memoria. En lugar de file, se especifica el nombre y la ruta bajo la cual guardar el archivo. Al usar esta opción, se ignora todo lo relacionado con data (la verificación de contenido en opts.check_content no se realizará, response.data estará vacío, etc.)

opts.bypass_cloudflare

bypass_cloudflare: 0 - evasión automática de la protección JavaScript de CloudFlare utilizando el navegador Chrome (1 - activado, 0 - desactivado)

El control de Chrome Headless en este caso se realiza mediante los ajustes del extractor bypassCloudFlareChromeMaxPages y bypassCloudFlareChromeHeadless, que deben especificarse en static defaultConf y static editableConf:

static defaultConf: typeof BaseParser.defaultConf = {
version: '0.0.1',
results: {
flat: [
['title', 'Title'],
]
},
max_size: 2 * 1024 * 1024,
parsecodes: {
200: 1,
},
results_format: "$title\n",
bypass_cloudflare: 1,
bypassCloudFlareChromeMaxPages: 20,
bypassCloudFlareChromeHeadless: 0
};

static editableConf: typeof BaseParser.editableConf = [
['bypass_cloudflare', ['textfield', 'bypass_cloudflare']],
['bypassCloudFlareChromeMaxPages', ['textfield', 'bypassCloudFlareChromeMaxPages']],
['bypassCloudFlareChromeHeadless', ['textfield', 'bypassCloudFlareChromeHeadless']],
];

async parse(set, results) {
const {success, data, headers} = await this.request('GET', set.query, {}, {
bypass_cloudflare: this.conf.bypass_cloudflare
});
return results;
}

opts.follow_meta_refresh

follow_meta_refresh: 0 - permite seguir las redirecciones declaradas a través de la etiqueta meta HTML:

<meta http-equiv="refresh" content="time; url=..."/>

opts.redirect_filter

redirect_filter: (hdr) => 1 | 0 - permite establecer una función de filtrado para el seguimiento de redirecciones; si la función devuelve 1, el extractor seguirá la redirección (teniendo en cuenta el parámetro opts.recurse); si devuelve 0, el seguimiento de redirecciones se detendrá:

redirect_filter: (hdr) => {
if (hdr.location.match(/login/))
return 1;
return 0;
}

opts.follow_common_rediects

opts.follow_common_rediects: 0 - determina si se deben seguir las redirecciones estándar (por ejemplo, http -> https y/o www.domain.com -> domain.com); si se especifica 1, el extractor seguirá las redirecciones estándar sin tener en cuenta el parámetro opts.recurse

opts.http2

opts.http2: 0 - determina si se debe utilizar el protocolo HTTP/2 al realizar solicitudes; por defecto se utiliza HTTP/1.1

opts.randomize_tls_fingerprint

opts.randomize_tls_fingerprint: 0 - esta opción permite evitar el bloqueo de sitios por huella digital TLS (1 - activado, 0 - desactivado)

opts.tlsOpts

tlsOpts: { ... } – permite pasar configuraciones para conexiones https ​

await this.cookies.*

Trabajo con cookies para la solicitud actual

.getAll()

Obtención de una matriz de cookies

await this.cookies.getAll();
Ejemplo del resultado de obtener una matriz de cookies

.setAll(cookie_jar)

Establecimiento de cookies; se debe pasar como argumento un hash con las cookies

async parse(set, results) {
this.logger.put("Start scraping query: " + set.query);

await this.cookies.setAll({
"version": 1,
".google.com": {
"/": {
"login": {
"value": "true"
},
"lang": {
"value": "ru-RU"
}
}
},
".test.google.com": {
"/": {
"id": {
"value": 155643
}
}
}
});

let cookies = await this.cookies.getAll();

this.logger.put("Cookies: " + JSON.stringify(cookies));

results.SKIP = 1;
return results;
}
Ejemplo del resultado de establecer una matriz de cookies

.set(host, path, name, value)

await this.cookies.set(host, path, name, value) - establecimiento de una sola cookie.

El ámbito de visibilidad de la cookie depende directamente del formato del dominio especificado, por lo que en host se tiene en cuenta la presencia de un punto antes del host:

  • si se especifica el punto (this.cookies.set('.domain.com', ...)), la cookie se utilizará para todos los subdominios (por ejemplo, a.domain.com, b.a.domain.com)
  • si el host se especifica sin punto delante (this.cookies.set('site.com', ...)), la cookie se utilizará estrictamente para el host especificado (host-only cookie) y no se transmitirá a los subdominios
información

Esta distinción es críticamente importante, ya que la existencia simultánea de cookies con y sin punto puede llevar a su duplicación y a un funcionamiento impredecible del sitio. Para una emulación correcta, verifique siempre cómo establece las cookies el sitio de destino (con el atributo Domain o sin él) y utilice el formato correspondiente.

async parse(set, results) {
this.logger.put("Start scraping query: " + set.query);

await this.cookies.set('.a-parser.com', '/', 'Test-cookie-1', 1);
await this.cookies.set('.a-parser.com', '/', 'Test-cookie-2', 'test-value');

let cookies = await this.cookies.getAll();

this.logger.put("Cookies: " + JSON.stringify(cookies));

results.SKIP = 1;
return results;
}
Ejemplo del resultado de establecer una cookie individual

await this.proxy.*

Trabajo con proxy

.next()

Cambiar el proxy al siguiente; el proxy antiguo ya no se utilizará para la solicitud actual

.ban()

Cambiar y banear el proxy (es necesario usarlo cuando el servicio bloquea el trabajo por IP); el proxy será baneado por el tiempo especificado en la configuración del extractor (proxybannedcleanup)

.get()

Obtener el proxy actual (el último proxy con el que se realizó la solicitud)

.set(proxy, noChange?)

await this.proxy.set('http://127.0.0.1:8080', true) - establecer un proxy para la siguiente solicitud. El parámetro noChange es opcional; si se establece en true, el proxy no cambiará entre intentos. Por defecto noChange = false

await this.sessionManager.*

Métodos para trabajar con sesiones. Cada sesión almacena obligatoriamente el proxy y las cookies utilizados. También se pueden guardar adicionalmente datos arbitrarios. Para utilizar sesiones en un extractor JS, primero es obligatorio inicializar el Gestor de sesiones. Esto se hace mediante el método await this.sessionManagerinit() en init()

.init(opts?)

Inicialización del Gestor de sesiones. Como argumento se puede pasar un objeto (opts) con parámetros adicionales (todos los parámetros son opcionales):

  • name - permite redefinir el nombre del extractor al que pertenecen las sesiones; por defecto es igual al nombre del extractor en el que ocurre la inicialización
  • waitForSession - indica al extractor que espere por una sesión hasta que aparezca (esto es relevante solo cuando funcionan varias tareas, por ejemplo, una genera sesiones y la segunda las utiliza), es decir, .get() y .reset() siempre esperarán por una sesión
  • domain - indica buscar sesiones entre todas las guardadas para este extractor (si no se establece valor), o bien solo para un dominio específico (es necesario indicar el dominio con un punto delante, por ejemplo, .site.com)
  • sessionsKey - permite establecer manualmente el nombre del almacenamiento de sesiones; si no se establece, el nombre se forma automáticamente basado en name (o el nombre del extractor si name no se establece), el dominio y el proxychecker
  • expire - establece el tiempo de vida de la sesión en minutos; por defecto es ilimitado

Ejemplo de uso:

async init() {
await this.sessionManager.init({
name: 'JS::test',
expire: 15 * 60
});
}

.get(opts?)

Obtención de una nueva sesión; debe llamarse antes de realizar la solicitud (antes del primer intento). Devuelve un objeto con datos arbitrarios guardados en la sesión. Como argumento se puede pasar un objeto (opts) con parámetros adicionales (todos los parámetros son opcionales):

  • waitTimeout - posibilidad de especificar cuántos minutos esperar a que aparezca la sesión; funciona independientemente del parámetro waitForSession en .init() (lo ignora); al expirar se utilizará una sesión vacía
  • tag - obtención de una sesión con una etiqueta determinada; se puede usar, por ejemplo, el nombre del dominio para vincular las sesiones a los dominios de los que se obtuvieron

Ejemplo de uso:

await this.sessionManager.get({
waitTimeout: 10,
tag: 'test session'
})

.reset(opts?)

Limpieza de cookies y obtención de una nueva sesión. Debe usarse si la solicitud no fue exitosa con la sesión actual. Devuelve un objeto con datos arbitrarios guardados en la sesión. Como argumento se puede pasar un objeto (opts) con parámetros adicionales (todos los parámetros son opcionales):

  • waitTimeout - posibilidad de especificar cuántos minutos esperar a que aparezca la sesión; funciona independientemente del parámetro waitForSession en .init() (lo ignora); al expirar se utilizará una sesión vacía
  • tag - obtención de una sesión con una etiqueta determinada; se puede usar, por ejemplo, el nombre del dominio para vincular las sesiones a los dominios de los que se obtuvieron

Ejemplo de uso:

await this.sessionManager.reset({
waitTimeout: 5,
tag: 'test session'
})

.save(sessionOpts?, saveOpts?)

Guardado de una sesión exitosa con la posibilidad de guardar datos arbitrarios en la sesión. Admite 2 argumentos opcionales:

  • sessionOpts - datos arbitrarios para almacenar en la sesión; puede ser un número, cadena, matriz u objeto
  • saveOpts - objeto con parámetros de guardado de la sesión:
    • multiply - parámetro opcional; permite multiplicar la sesión; se debe especificar un número como valor
    • tag - parámetro opcional; establece una etiqueta para la sesión guardada; se puede usar, por ejemplo, el nombre del dominio para vincular las sesiones a los dominios de los que se obtuvieron

Ejemplo de uso:

await this.sessionManager.save('some data here', {
multiply: 3,
tag: 'test session'
})

.count()

Devuelve la cantidad de sesiones para el Gestor de sesiones actual

Ejemplo de uso:

let sesCount = await this.sessionManager.count();

.removeById(sessionId)

Elimina todas las sesiones con un id determinado. Devuelve la cantidad de sesiones eliminadas. El id de la sesión actual se encuentra en la variable this.sessionId Ejemplo de uso:

const removedCount = await this.sessionManager.removeById(this.sessionId);

Ejemplo complejo de uso del Gestor de sesiones

async init() {
await this.sessionManager.init({
expire: 15 * 60
});
}

async parse(set, results) {
let ses = await this.sessionManager.get();

for(let attempt = 1; attempt <= this.conf.proxyretries; attempt++) {
if(ses)
this.logger.put('Data from session:', ses);
const { success, data } = await this.request('GET', set.query, {}, { attempt });
if(success) {
// process data here
results.success = 1;
break;
} else if(attempt < this.conf.proxyretries) {
const removedCount = await this.sessionManager.removeById(this.sessionId);
this.logger.put(`Removed ${removedCount} bad sessions with id #${this.sessionId}`);
ses = await this.sessionManager.reset();
}
}

if(results.success) {
await this.sessionManager.save('Some data', { multiply: 2 });
this.logger.put(`Total we have ${await this.sessionManager.count()} sessions`);
}

return results;
}
Ejemplo de guardado de datos arbitrarios y su posterior obtención

Métodos de solicitud await this.request

Método GET

Se pueden pasar los parámetros de la solicitud directamente en la cadena de consulta https://a-parser.com/users/?type=staff:

const { success, data, headers } = await this.request('GET', 'https://a-parser.com/users/?type=staff');

O como un objeto en queryParams, donde key: value es igual a param=value:

const { success, data, headers } = await this.request('GET', 'https://a-parser.com/users/', {
type: 'staff'
});

Método POST

Si se utiliza el método POST, el cuerpo de la solicitud se puede pasar de dos maneras:

  • Enumerar los nombres de las variables y sus valores en queryParams, por ejemplo:

    {
    "key": set.query,
    "id": 1234,
    "type": "text"
    }
  • Enumerarlos en opts.body, por ejemplo:

    body: 'key=' + set.query + '&id=1234&type=text'

Si el cuerpo de la solicitud se pasa como un objeto, se convierte automáticamente al formato form-urlencoded; asimismo, si se especifica body y no se especifica el encabezado content-type, se asignará automáticamente content-type: application/x-www-form-urlencoded:

const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {
title: 'foo,',
body: 'bar',
userId: 1
});

Si el cuerpo de la solicitud POST es una cadena o un búfer, se transmite tal cual:

// solicitud con cadena
const string = 'title=foo&body=bar&userId=1';
const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {}, {
body: string
});

// solicitud con buffer
const string = 'title=foo&body=bar&userId=1';
const buf = Buffer.from(string, 'utf8');
const { success, data, headers } = await this.request('POST', 'https://jsonplaceholder.typicode.com/posts', {}, {
body: buf
});

Carga de archivos

Envío de un archivo mediante una solicitud POST utilizando el módulo form-data:

const file = fs.readFileSync('pathToFile');
const FormData = require('form-data');
const format = new FormData();
format.append('file', file, 'fileName.ext');

const { success, data, headers } = await this.request('POST', 'https://file.io', {}, {
headers: format.getHeaders(),
body: format.getBuffer()
});

Ejemplo de envío de un archivo en una solicitud POST con el tipo de contenido multipart/form-data:

const EOL = '\r\n';
const file = fs.readFileSync('pathToFile');
const boundary = '----WebKitFormBoundary' + String(Math.random()).slice(2);
const requestHeaders = {
'content-type': 'multipart/form-data; boundary=' + boundary
};

const body = '--'
+ boundary
+ EOL
+ 'Content-Disposition: form-data; name="file"; filename="fileName.ext"'
+ EOL
+ 'Content-Type: text/html'
+ EOL
+ EOL
+ file
+ EOL
+ '--'
+ boundary
+ '--';

const { success, data, headers } = await this.request('POST', 'https://file.io', {}, {
headers: requestHeaders,
body
});