Web Scraping: Serverless Node.JS e Puppeteer
Tags: node, aws, javascript, serverless
Published: 2021-10-04 03:08:53

Existem situações onde precisamos coletar informações de uma página web e essa prática é chamada de _web scraping_. Geralmente esse processo é simples e, provavelmente, não se faz necessário executá-lo o tempo todo. 

Atualmente temos à nossa disposição a arquitetura serverless, onde conseguimos hospedar funções sem preocupação com infraestrutura. Esse modelo de arquitetura torna bastante agradável o desenvolvimento de funcionalidades que serão executadas sob demanda, que é o caso padrão de um _web scraping_.

No ecossistema Node.JS, o Puppeteer é uma biblioteca bastante utilizada para a implementação de web scrapings. Para ser mais honesto, essa biblioteca é bem mais poderosa e possui muito mais funcionalidades do que simplesmente coletar informações de web sites. O Puppeteer é capaz de gerar screenshots e PDFs de páginas web, automatizar o envio de formulários e fazer testes de UI. E o que o torna tão poderoso é a sua API de alto nível capaz de controlar instâncias do Chrome ou Chromium sobre o protocolo DevTools.

Bom... agora que já mencionei sobre a arquitetura serverless e o Puppeteer, podemos meter a mão no código e escrever uma função lambda que irá acessar https://stackoverflow.com/jobs e capturar as informações das vagas de emprego e retorná-las em formato _json_. 

Para esse exemplo iremos utilizar Serverless Framework, provider   AWS Lambda com runtime Node.JS utilizando Typescript e, obviamente, o Puppeteer. Estou assumindo que você já tem o serverless instalado, bem como as credenciais da AWS configuradas na sua máquina.

Eu não gosto muito de utilizar templates, pois geralmente trazem um monte de coisas não tão necessárias assim, principalmente para esse projeto em questão. Então, eu começo criando toda lambda function com os seguintes passos:

* mkdir __puppeteer-lambda__ 
* cd __puppeteer-lambda__ 
* yarn init -y (ou npm init -y)
* criar arquivo __serverless.yml__ 
* criar arquivo __app.ts__ 

## serverless.yml

Dentro do arquivo __serverless.yml__ precisamos fornecer as informações do nosso serverless. 

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iiy1m9w9j47vyxxlwthg.png)

O _service_ recebe o nome que queremos dar para a nossa lambda function. 

O _provider_ é basicamente a infra onde a lambda irá executar, nesse caso é a AWS, como podemos ver na propriedade _name_. A propriedade _region_ recebe o valor da região da AWS que você irá executar seu código, em _layers_ temos um _arn_ para o pacote __chrome-aws-lambda__, as outras propriedades armazenam informações sobre o ambiente de execução, tamanho de memória RAM e timeout.

Os _plugins_ são ferramentas facilitadoras e nesse caso temos dois: _serverless-plugin-typescript_ que configura todo TS no nosso projeto e _serverless-offline_ que irá facilitar a execução local do projeto. 

Na parte de _functions_ temos a nossa função, que pode receber qualquer nome, mas no handler deve apontar para o path do arquivo e função que irá executar. É importante notar a parte de _events_, que nada mais é do que o evento que irá disparar a chamada para a a nossa função, e nesse caso é um evento http com uma requisição GET para o endpoint _/scraping_.

## app.ts

No nosso _app.ts_ iremos escrever o código que irá executar quando a nossa função for chamada. 

Nesse momento, o caminho natural ao desenvolver um projeto com Node.JS seria instalar o puppeteer no projeto, utilizando o __yarn add puppeteer__ (ou npm install puppeteer). Porém quando estamos falando de funções lambda temos algumas limitações, e uma delas é o tamanho da aplicação, que deve ser no máximo 50MB. 

Quando instalamos o Puppeteer na aplicação, ele instala também uma versão do Chrome que é utilizado para manipular o conteúdo das páginas web, sendo assim o limite de 50MB é facilmente ultrapassado, já que a versão do chrome que é instalada com o Puppeteer chega a um pouco mais de 200MB de tamanho. Mas para resolver esse problema podemos utilizar o pacote __chrome-aws-lambda__, que irá fornecer todo o ferramental para podermos trabalhar com o puppeteer sem que o nosso projeto fique inflado a ponto de ultrapassar o limite de 50MB. Nesse momento iremos instalar também um pacote chamado _puppeteer-core_ que contém o puppeteer mas sem a instância do chrome embutida.

Depois de instalar os pacotes _chrome-aws-lambda_ e _puppeteer-core_, podemos importar no código da nossa função. Vou explicar detalhadamente cada trecho de código da função abaixo:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wd2okbffwszs68clyw05.png)  

1. Na linha 12 uma instância do Chrome é inicializada passando alguns parâmetros, e temos que destacar dois deles:
  * __executablePath__: o caminho da instância do chrome instalada no projeto
  * __headless__: que recebe __true__ para que o navegador não seja inicializado com recursos visuais, somente a execução do processo. 

2. Em seguida inicializamos uma nova página. Mas aqui, tenho uma dica legal pra passar. A maioria dos códigos de exemplos que encontramos na internet exibe o código __await browser.newPage()__, fazendo com que uma nova aba seja aberta nonavegador. Mas se pararmos para pensar quando o browser foi iniciado ele já abriu uma página, então nós só precisamos pegar ela utilizando __(await browser.pages())[0]__. De todo modo, precisamos acessar essa __page__ para navegar até uma URL, que nesse caso está declarada em uma constante na linha 10.

3. O objeto __page__ nos fornece o acesso à função _.evaludate()_, onde conseguimos utilizar javascript para acessar os elementos da página e extrair as informações. Essa função retorna uma promessa de um tipo genérico, então você pode estruturar as informações de retorno como quiser. No nosso caso, estamos retornando um array do tipo _Job_.

Após retornar o nosso conteúdo podemos então fechar a nossa instância do Chrome, ou caso você queira fazer ainda mais procedimentos, pode utilizar o __await page.close()__ para fechar uma página que não irá utilizar mais.

Agora que entendemos o que está no serverless.yml e app.ts, podemos executar a nossa função. E agora tenho outra dica: quando estamos trabalhando com o _chrome-aws-lambda_ localmente ele não tem acesso à uma instância do chrome para trabalhar, então precisamos instalar o _puppeteer_ como dependência de desenvolvimento utilizando o comando _yarn add puppeteer -D_ (ou npm install puppeteer -D). Internamente o _chrome-aws-lambda_ se resolve e consegue encontrar a instância de acordo com o environment.

Então, para que não fiquem dúvidas em relação aos pacotes instalados, temos o seguinte _packge.json_:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8nm8y4r3tps3ftf3aspq.png)

__OBS: lembrando que todos plugin declarado no serverless.yml deve ser instalado na aplicação também, e nesse caso estamos utilizando como dependências de desenvolvimento.__

Para executar a aplicação basta utilizar o comando _serverless offline_ e para fazer o deploy basta executar _serverless deploy_ e ele irá subir o código para a nuvem do provedor e na região declarada.

Ao executar o comando _serverless offline_ o que esperamos de retorno é algo parecido com essa imagem:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q2hd8yo2wuz8l0pnu49w.png)

Podemos perceber uma URL GET exatamente com o endpoint que configuramos no serverless.yml, basta fazer uma solicitação utilizando postman, insomnia ou até mesmo no próprio browser e poderemos ver o retorno em formato _JSON_.

Bom... acho que é isso! :) 

Na próxima publicação estou querendo trazer algo mais elaborado mostrando um pouco sobre a configuração de um schedule que irá disparar a execução da função, detalhando um pouco mais sobre os recursos da AWS.