From 4b15fb72d919507efdfe817483593abc0c4a6ff0 Mon Sep 17 00:00:00 2001 From: Renne Rocha Date: Thu, 13 Jan 2022 18:06:21 -0300 Subject: [PATCH] Novo post sobre monitores do Spidermon --- config.yaml | 3 +- ...ando-seus-projetos-de-raspagem-de-dados.md | 266 ++++++++++++++++++ 2 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 content/posts/monitorando-seus-projetos-de-raspagem-de-dados.md diff --git a/config.yaml b/config.yaml index f672428..2cc910e 100644 --- a/config.yaml +++ b/config.yaml @@ -3,6 +3,7 @@ languageCode: "pt-br" title: "Renne Rocha" theme: "hugo-tania" paginate: 6 +googleAnalytics: "UA-22087729-1" params: author: "Renne Rocha" @@ -27,7 +28,7 @@ params: # Comments settings comments: - enabled: true + enabled: false provider: giscus giscus: diff --git a/content/posts/monitorando-seus-projetos-de-raspagem-de-dados.md b/content/posts/monitorando-seus-projetos-de-raspagem-de-dados.md new file mode 100644 index 0000000..1e12e5e --- /dev/null +++ b/content/posts/monitorando-seus-projetos-de-raspagem-de-dados.md @@ -0,0 +1,266 @@ +--- +title: "Monitorando seus projetos de raspagem de dados" +publishdate: 2022-01-13 +tags: ["monitoramento", "scrapy", "raspagem de dados", "python", "spidermon"] +--- + +Quando passamos a extrair dados de uma página com regularidade, com +uma quantidade grande de dados, é muito importante monitorar a saúde +dos seus raspadores e a qualidade das informações extraídas. + +As páginas podem mudar (as vezes sutilmente), medidas anti-bot podem +ser adicionadas e outros problemas temporários e inesperados podem +ocorrer e, caso estejamos tratando de uma quantidade grande de +*scripts* retornando milhares de itens, é praticamente impossível +monitorar manualmente todo o nosso projeto. + +Deste modo, devemos adicionar em nosso projeto, ferramentas que auxiliem +no monitoramento. + +## Spidermon + +O **[Scrapy](https://scrapy.org)** é um dos principais *frameworks* para +raspagem de dados em **[Python](https://www.python.org/)** possuindo +diversas bibliotecas e ferramentas que facilitam a criação de projetos +de raspagem de dados. + +O **[Spidermon](https://spidermon.readthedocs.io/)** +é uma extensão do **Scrapy** que podemos adicionar ao nosso projeto +para facilitar o monitoramento da execução dos seus *spiders* (como são +chamados os *scripts* que realizam a extração de dados). + +Com ela, é possível definir validar dados retornados, monitorar as +**estatísticas** da execução (*jobs*) de seu *spider* e, a partir do +resultadodessas validações, tomar **ações** como enviar notificações, +gerar relatórios, abrir chamados em sistemas externos quando algo não +esteja funcionando corretamente de uma maneira extensível. + +O **[Spidermon](https://spidermon.readthedocs.io/)** pode ser instalado +com o **[pip](https://pypi.org/project/pip/)**: + +{{}} +pip install spidermon +{{}} + +Uma vez instalado, precisamos habilitá-lo em nosso projeto +**[Scrapy](https://scrapy.org)**: + +{{}} +# myproject/settings.py +SPIDERMON_ENABLED = True +EXTENSIONS = { + "spidermon.contrib.scrapy.extensions.Spidermon": 500, +} +{{}} + +Feito isso, podemos começar a pensar no que será monitorado. + +## Monitores + +É no **[monitor](https://spidermon.readthedocs.io/en/latest/monitors.html#monitors)** +onde a maioria das validações são definidas. Nele temos acesso as +estatísticas e informações das execuções de nossos *spiders* e podemos +definir as nossas regras de monitoramento. + +Esse **monitor** precisa estar dentro de uma +**[suíte de monitores](https://spidermon.readthedocs.io/en/latest/monitors.html#monitor-suites)**, além de definir um conjunto de **[ações](https://spidermon.readthedocs.io/en/latest/actions/index.html)** que devem ser executadas após a execução deles. + +Como exemplo, queremos monitorar o número de mensagens de erro emitidas durante +os nossos *jobs*. Isso pode ser feito verificando se o valor dentro da estatística +`log_count/ERROR` é menor a um determinado valor que podemos, por exemplo, definir +no arquivo de configuração do nosso projeto. + +Um monitor que realiza essa tarefa pode ser definido como: + +{{}} +# myproject/monitors.py +from spidermon import Monitor + +class JobErrorsMonitor(Monitor): + def test_error_count(self): + num_errors = self.data.stats.get("log_count/ERROR") + num_errors_threshold = self.data.crawler.settings.getint( + "MAX_NUM_ERRORS", 0 + ) + if num_errors: + self.assertTrue( + num_errors <= num_errors_threshold, + msg=f"No more than {num_errors_threshold} allowed." + ) +{{}} + +Porém, só definir o **monitor** não é o suficiente para que esse +teste seja rodado ao final da execução do seu *spider*. Precisamos +adicioná-lo a uma **suíte de monitores**: + +{{}} +# myproject/monitors.py +from spidermon import Monitor, MonitorSuite + +class JobErrorsMonitor(Monitor): + # (...) O código do nosso monitor + +class SpiderCloseMonitorSuite(MonitorSuite): + monitors = [ + JobErrorsMonitor, + ] +{{}} + +E definir o momento em que momento a suíte será executada, além +do nosso valor de referência com o limite da quantidade de erros +que aceitamos. Nesse caso, quando o *spider* terminar sua execução +as verificações será feita. + +{{}} +# myproject/settings.py +SPIDERMON_ENABLED = True +EXTENSIONS = { + "spidermon.contrib.scrapy.extensions.Spidermon": 500, +} +SPIDERMON_SPIDER_CLOSE_MONITORS = ( + "myproject.monitors.SpiderCloseMonitorSuite", +) +MAX_NUM_ERRORS = 10 +{{}} + +Ao executar *spider* podemos ver os resultados desse monitor nos nossos +*logs*: + +{{}} +[scrapy.core.engine] INFO: Closing spider (finished) +[myproject] INFO: [Spidermon] ---------------------- MONITORS ---------------------- +[myproject] INFO: [Spidermon] JobErrorsMonitor/test_error_count... OK +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 1 monitor in 0.000s +[myproject] INFO: [Spidermon] OK +[myproject] INFO: [Spidermon] ------------------ FINISHED ACTIONS ------------------ +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 0 actions in 0.000s +[myproject] INFO: [Spidermon] OK +[myproject] INFO: [Spidermon] ------------------- PASSED ACTIONS ------------------- +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 0 actions in 0.000s +[myproject] INFO: [Spidermon] OK +[myproject] INFO: [Spidermon] ------------------- FAILED ACTIONS ------------------- +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 1 action in 0.000s +[myproject] INFO: [Spidermon] OK (skipped=1) +{{}} + +Ou caso aconteça um erro: + +{{}} +[scrapy.core.engine] INFO: Closing spider (finished) +[myproject] INFO: [Spidermon] ---------------------- MONITORS ---------------------- +[myproject] INFO: [Spidermon] JobErrorsMonitor/test_error_count... FAIL +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] ERROR: [Spidermon] +====================================================================== +FAIL: JobErrorsMonitor/test_error_count +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/renne/projects/rennerocha/spidermon/examples/tutorial/tutorial/monitors.py", line 44, in test_error_count + self.assertTrue( +AssertionError: False is not true : No more than 10 errors allowed. + +[myproject] INFO: [Spidermon] 1 monitor in 0.001s +[myproject] INFO: [Spidermon] FAILED (failures=1) +[myproject] INFO: [Spidermon] ------------------ FINISHED ACTIONS ------------------ +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 0 actions in 0.000s +[myproject] INFO: [Spidermon] OK +[myproject] INFO: [Spidermon] ------------------- PASSED ACTIONS ------------------- +[myproject] INFO: [Spidermon] ------------------------------------------------------ +[myproject] INFO: [Spidermon] 0 actions in 0.000s +[myproject] INFO: [Spidermon] OK +[myproject] INFO: [Spidermon] ------------------- FAILED ACTIONS ------------------- +{{}} + +## Simplificando o nosso monitor + +Grande parte dos monitores que precisamos, verifica +o valor de uma estatística do seu *job* com um valor +pré-determinado. Por isso, podemos usar a classe +*BaseStatMonitor* com um conjunto definido de atributos +para simplificar o nosso código. + +O mesmo monitor anterior, verificando se o valor +de `log_count/ERROR` é menor ou igual ao valor definido +em `MAX_NUM_ERRORS` nas configurações do projeto +pode ser definido como: + +{{}} +# myproject/monitors.py +from spidermon.contrib.scrapy.monitors import BaseStatMonitor + +class JobErrorsMonitor(BaseStatMonitor): + stat_name = "log_count/ERROR" + threshold_setting = "MAX_NUM_ERRORS" + assert_type = "<=" +{{}} + +O resultado da execução desse monitor é o mesmo do anterior, porém +com um código muito mais simples. + +## Tornando o monitor mais dinâmico + +Não precisamos nos restringir a testes de um valor nas estatísticas +em relação a um valor estático definido nas configurações do projeto. + +Para isso, ao invés de definir um valor para o atributo +`threshold_setting`, podemos definir o método `get_threshold` que +deverá retornar o valor que será usado como referência para o nosso +monitor. + +Isso é muito útil por exemplo, quando queremos verificar mais de +uma estatística para definir o nosso valor de referência. No +exemplo anterior, definimos uma valor fixo aceitável de mensagens +de erro durante a execução do nosso spider. Porém termos 10 erros +em uma execução que retorna 100k itens (0.001%) pode ser mais aceitável +por exemplo do que 10 erros em uma execução que retornou 1k itens (1%). + +Então podemos melhoramos o nosso monitor de erros, considerando além +da quantidade de erros, a quantidade de itens retornados. +Desse modo só haverá falha se o número de erros for menor do que 0.001% +da quantidade de itens retornados. + +{{}} +# myproject/monitors.py +from spidermon.contrib.scrapy.monitors import BaseStatMonitor + +class JobErrorsMonitor(BaseStatMonitor): + stat_name = "log_count/ERROR" + assert_type = "<=" + + def get_threshold(self): + item_scraped_count = self.data.stats.get( + "item_scraped_count" + ) + return item_scraped_count * 0.0001 +{{}} + +Podemos também buscar dados de fontes externas, comparar com dados de execuções +passadas que estejam armazenados em um banco de dados ou fazer cálculos mais +complexos para definir o nosso valor de referência. + +O importante é que sempre o retorno do método `get_threshold` seja um valor que +possa ser usado na comparação com a estatística que queremos. + +Na [documentação](https://spidermon.readthedocs.io/en/latest/monitors.html#base-stat-monitor) +é possível encontrar mais exemplo e opções para os nossos monitores, além de um conjunto +de monitores pré-definidos para as validações mais comuns nos projetos diminuindo a quantidade +de código que precisamos escrever para começar a monitorar nossos projetos. + +## Próximos passos + +Ver o resultado dos nossos monitores apenas nos *logs* não é muito prático. O +que precisamos fazer em seguida é configurar nosso projeto para que alguma **ação** +seja tomada em caso de falha (ou de sucesso). + +Para isso, precisamos definir *actions*. Na extensão temos integrações +prontas para receber notificações por e-mail, Slack, Telegram ou Sentry, +além de ser possível criar suas próprias ações de acordo com as +necessidades do projeto. + +Na [documentação](https://spidermon.readthedocs.io/en/latest/actions/index.html) +é possível encontrar mais informações sobre como defini-las e configurá-las. \ No newline at end of file