Pular para o conteúdo principal

Requisições HTTP (+trabalho com cookies, proxy, sessões)

Métodos da classe base

Para coletar dados de uma página web, é necessário realizar uma requisição HTTP. Na JavaScript API v2 do A-Parser, foi implementado um método de fácil utilização para execução de requisições HTTP, que retorna um objeto JSON dependendo dos argumentos do método especificados. A seguir, você aprenderá: como a requisição HTTP é feita, quais argumentos e opções o método possui, os resultados das opções especificadas, como definir a condição de sucesso de uma requisição HTTP e muito mais.

Também são descritos métodos que permitem manipular facilmente cookies, proxies e sessões no scraper que está sendo criado. Após a execução bem-sucedida de uma requisição HTTP, ou antes da execução, você pode definir/alterar os dados de proxy/cookies/sessão para a execução de requisições HTTP ou salvar para execução por outra thread usando o Gerenciador de sessões.

Estes métodos são herdados de BaseParser e servem como base para a criação de scrapers personalizados

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

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

Obtenção de resposta HTTP por requisição, os seguintes argumentos são especificados:

  • method - método da requisição (GET, POST...)
  • url - link para a requisição
  • queryParams - hash com parâmetros get ou hash com o corpo da requisição post
  • opts - hash com opções da requisição

opts.check_content

check_content: [ condição1, condição2, ...] - array de condições para verificar o conteúdo recebido; se a verificação não passar, a requisição será repetida com outro proxy.

Possibilidades:

  • uso de strings como condições (busca por ocorrência de string)
  • uso de expressões regulares como condições
  • uso de funções de verificação personalizadas, nas quais são passados os dados e os headers da resposta
  • é possível definir vários tipos diferentes de condições simultaneamente
  • para negação lógica, coloque a condição em um array, ou seja, check_content: ['xxxx', [/yyyy/]] significa que a requisição será considerada bem-sucedida se os dados recebidos contiverem a substring xxxx e, ao mesmo tempo, a expressão regular /yyyy/ não encontrar correspondências na página

Para uma requisição bem-sucedida, todas as verificações especificadas no array devem passar

Exemplo (os comentários indicam o que é necessário para que a requisição seja considerada bem-sucedida):

let response = await this.request('GET', set.query, {}, {
check_content: [
/<\/html>|<\/body>/, // esta expressão regular deve ser acionada na página recebida
['XXXX'], // esta substring não deve estar presente na página recebida
'</html>', // esta substring deve estar presente na página recebida
(data, hdr) => {
return hdr.Status == 200 && data.length > 100;
} // esta função deve retornar true
]
});

opts.decode

decode: 'auto-html' - detecção automática de codificação e conversão para utf8

Valores possíveis:

  • auto-html - baseado em cabeçalhos, tags meta e no conteúdo da página (opção recomendada ideal)
  • utf8 - indica que o documento está na codificação utf8
  • <encoding> - qualquer outra codificação

opts.headers

headers: { ... } - hash com cabeçalhos, o nome do cabeçalho é definido em letras minúsculas, pode-se incluir inclusive cookie.

Exemplo:

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 a ordem de classificação dos cabeçalhos

opts.onlyheaders

onlyheaders: 0 - define a leitura de data; se ativado (1), recebe apenas os cabeçalhos

opts.recurse

recurse: N - número máximo de redirecionamentos; o padrão é 7, use 0 para desativar o seguimento de redirecionamentos

opts.proxyretries

proxyretries: N - número de tentativas de execução da requisição; por padrão, é obtido das configurações do scraper

opts.parsecodes

parsecodes: { ... } - lista de códigos de resposta HTTP que o scraper considerará bem-sucedidos; por padrão, é obtido das configurações do scraper. Se especificar '*': 1, todas as respostas serão consideradas bem-sucedidas.

Exemplo:

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

opts.timeout

timeout: N - tempo limite de resposta em segundos; por padrão, é obtido das configurações do scraper

opts.do_gzip

do_gzip: 1 - define se deve usar compressão (gzip/deflate/br); por padrão está ativado (1), para desativar é necessário definir o valor como 0

opts.max_size

max_size: N - tamanho máximo da resposta em bytes; por padrão, é obtido das configurações do scraper

opts.cookie_jar

cookie_jar: { ... } - hash com cookies. Exemplo 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 o número da tentativa atual; ao usar este parâmetro, o manipulador de tentativas integrado para esta requisição é ignorado

opts.browser

browser: 1 - emulação automática de cabeçalhos de navegador (1 - ativado, 0 - desativado)

opts.use_proxy

use_proxy: 1 - substitui o uso de proxy para uma requisição individual dentro do scraper JS sobre o parâmetro global Use proxy (1 - ativado, 0 - desativado)

opts.noextraquery

noextraquery: 0 - desativa a adição de Extra query string à URL da requisição (1 - ativado, 0 - desativado)

opts.save_to_file

save_to_file: file - permite baixar um arquivo diretamente para o disco, ignorando a gravação na memória. Em vez de file, especifica-se o nome e o caminho sob o qual salvar o arquivo. Ao usar esta opção, tudo relacionado a data é ignorado (a verificação de conteúdo em opts.check_content não será executada, response.data estará vazio, etc.)

opts.bypass_cloudflare

bypass_cloudflare: 0 - contorno automático da proteção JavaScript do CloudFlare usando o navegador Chrome (1 - ativado, 0 - desativado)

O controle do Chrome Headless neste caso é realizado pelas configurações do scraper bypassCloudFlareChromeMaxPages e bypassCloudFlareChromeHeadless, que devem ser especificadas em static defaultConf e 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 redirecionamentos declarados via meta tag HTML:

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

opts.redirect_filter

redirect_filter: (hdr) => 1 | 0 - permite definir uma função de filtragem para seguir redirecionamentos; se a função retornar 1, o scraper seguirá o redirecionamento (considerando o parâmetro opts.recurse); ao retornar 0, o seguimento de redirecionamentos será interrompido:

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

opts.follow_common_rediects

opts.follow_common_rediects: 0 - define se deve seguir redirecionamentos padrão (por exemplo http -> https e/ou www.domain.com -> domain.com); se especificar 1, o scraper seguirá os redirecionamentos padrão sem considerar o parâmetro opts.recurse

opts.http2

opts.http2: 0 - define se deve usar o protocolo HTTP/2 ao realizar requisições; por padrão é usado HTTP/1.1

opts.randomize_tls_fingerprint

opts.randomize_tls_fingerprint: 0 - esta opção permite contornar o banimento de sites por impressão digital TLS (1 - ativado, 0 - desativado)

opts.tlsOpts

tlsOpts: { ... } – permite passar configurações para conexões https ​

await this.cookies.*

Trabalho com cookies para a requisição atual

.getAll()

Obtenção de um array de cookies

await this.cookies.getAll();
Exemplo de resultado da obtenção de um array de cookies

.setAll(cookie_jar)

Definição de cookies; um hash com cookies deve ser passado como argumento

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;
}
Exemplo de resultado da definição de um array de cookies

.set(host, path, name, value)

await this.cookies.set(host, path, name, value) - definição de um único cookie.

O escopo de visibilidade do cookie depende diretamente do formato do domínio especificado, portanto em host a presença de um ponto antes do host é levada em conta:

  • se o ponto for especificado (this.cookies.set('.domain.com', ...)), o cookie será usado para todos os subdomínios (por exemplo a.domain.com, b.a.domain.com)
  • se o host for especificado sem o ponto à frente (this.cookies.set('site.com', ...)), o cookie será usado estritamente para o host especificado (host-only cookie) e não será passado para subdomínios
informação

Esta diferença é criticamente importante, pois a existência simultânea de cookies com e sem ponto pode levar à sua duplicação e ao comportamento imprevisível do site. Para uma emulação correta, verifique sempre como exatamente o site de destino define os cookies (com o atributo Domain ou sem) e use o formato correspondente.

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;
}
Exemplo de resultado da definição de um único cookie

await this.proxy.*

Trabalho com proxy

.next()

Mudar o proxy para o próximo; o proxy antigo não será mais usado para a requisição atual

.ban()

Mudar e banir o proxy (necessário usar quando o serviço bloqueia o trabalho por IP); o proxy será banido pelo tempo especificado nas configurações do scraper (proxybannedcleanup)

.get()

Obter o proxy atual (o último proxy com o qual a requisição foi feita)

.set(proxy, noChange?)

await this.proxy.set('http://127.0.0.1:8080', true) - definir o proxy para a próxima requisição. O parâmetro noChange é opcional; se definido como true, o proxy não mudará entre as tentativas. Por padrão noChange = false

await this.sessionManager.*

Métodos para trabalhar com sessões. Cada sessão armazena obrigatoriamente o proxy e os cookies utilizados. Também é possível salvar adicionalmente dados arbitrários. Para usar sessões no scraper JS, primeiro é obrigatório inicializar o Gerenciador de sessões. Isso é feito usando o método await this.sessionManagerinit() em init()

.init(opts?)

Inicialização do Gerenciador de sessões. Como argumento, pode-se passar um objeto (opts) com parâmetros adicionais (todos os parâmetros são opcionais):

  • name - permite redefinir o nome do scraper ao qual as sessões pertencem; por padrão é igual ao nome do scraper no qual a inicialização ocorre
  • waitForSession - instrui o scraper a esperar por uma sessão até que ela apareça (isso é relevante apenas quando várias tarefas estão em execução, por exemplo, uma gera sessões e a segunda as utiliza), ou seja, .get() e .reset() sempre esperarão por uma sessão
  • domain - instrui a procurar sessões entre todas as salvas para este scraper (se o valor não for definido), ou apenas para um domínio específico (é necessário especificar o domínio com um ponto à frente, por exemplo .site.com)
  • sessionsKey - permite definir manualmente o nome do armazenamento de sessões; se não for definido, o nome é formado automaticamente com base em name (ou no nome do scraper, se name não for definido), domínio e proxychecker
  • expire - define o tempo de vida da sessão em minutos; por padrão é ilimitado

Exemplo de uso:

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

.get(opts?)

Obtenção de uma nova sessão; deve ser chamado antes de realizar a requisição (antes da primeira tentativa). Retorna um objeto com dados arbitrários salvos na sessão. Como argumento, pode-se passar um objeto (opts) com parâmetros adicionais (todos os parâmetros são opcionais):

  • waitTimeout - possibilidade de especificar quantos minutos esperar pelo aparecimento da sessão; funciona independentemente do parâmetro waitForSession em .init() (ignora-o); após o tempo limite, uma sessão vazia será usada
  • tag - obtenção de uma sessão com uma tag específica; pode-se usar, por exemplo, o nome do domínio para vincular sessões aos domínios dos quais foram obtidas

Exemplo de uso:

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

.reset(opts?)

Limpeza de cookies e obtenção de uma nova sessão. Deve ser usado se a requisição com a sessão atual não foi bem-sucedida. Retorna um objeto com dados arbitrários salvos na sessão. Como argumento, pode-se passar um objeto (opts) com parâmetros adicionais (todos os parâmetros são opcionais):

  • waitTimeout - possibilidade de especificar quantos minutos esperar pelo aparecimento da sessão; funciona independentemente do parâmetro waitForSession em .init() (ignora-o); após o tempo limite, uma sessão vazia será usada
  • tag - obtenção de uma sessão com uma tag específica; pode-se usar, por exemplo, o nome do domínio para vincular sessões aos domínios dos quais foram obtidas

Exemplo de uso:

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

.save(sessionOpts?, saveOpts?)

Salvamento de uma sessão bem-sucedida com a possibilidade de salvar dados arbitrários na sessão. Suporta 2 argumentos opcionais:

  • sessionOpts - dados arbitrários para armazenamento na sessão; pode ser um número, string, array ou objeto
  • saveOpts - objeto com parâmetros de salvamento da sessão:
    • multiply - parâmetro opcional; permite multiplicar a sessão; deve-se especificar um número como valor
    • tag - parâmetro opcional; define uma tag para a sessão salva; pode-se usar, por exemplo, o nome do domínio para vincular sessões aos domínios dos quais foram obtidas

Exemplo de uso:

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

.count()

Retorna a quantidade de sessões para o Gerenciador de sessões atual

Exemplo de uso:

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

.removeById(sessionId)

Remove todas as sessões com um determinado id. Retorna a quantidade de sessões removidas. O Id da sessão atual está contido na variável this.sessionId Exemplo de uso:

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

Exemplo complexo de uso do Gerenciador de sessões

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;
}
Exemplo de salvamento de dados arbitrários e sua posterior obtenção

Métodos de requisição await this.request

Método GET

Passar os parâmetros da requisição diretamente na string da requisição https://a-parser.com/users/?type=staff:

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

Ou como um objeto em queryParams, onde key: value é igual a param=value:

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

Método POST

Se o método POST for usado, o corpo da requisição pode ser passado de duas maneiras:

  • Listar os nomes das variáveis e seus valores em queryParams, por exemplo:

    {
    "key": set.query,
    "id": 1234,
    "type": "text"
    }
  • Listá-los em opts.body, por exemplo:

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

Se o corpo da requisição for passado como um objeto, ele é automaticamente convertido para o formato form-urlencoded; da mesma forma, se body for especificado e o cabeçalho content-type não for especificado, será atribuído automaticamente 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
});

Se o corpo da requisição POST for uma string ou buffer, ele será passado como tal:

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

// requisição com 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
});

Upload de arquivos

Envio de arquivo por requisição POST usando o 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()
});

Exemplo de envio de arquivo em uma requisição POST com o tipo de conteúdo 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
});