DadosAbertosDeFeira / maria-quiteria

Backend para coleta e disponibilização dos dados 📜
https://mq.dadosabertosdefeira.com.br/painel
MIT License
166 stars 69 forks source link

FileNotFoundError: [Errno 2] No such file or directory: '/code/data/tmp/5368pmfscp0012021.rar' #311

Open sentry-io[bot] opened 3 years ago

sentry-io[bot] commented 3 years ago

Essa exceção acontece porque o Tika não consegue extrair o rar 5 (formato proprietário). Temos que pegar a exceção e cancelar o retry nesse caso.

Sentry Issue: MARIA-QUITERIA-4V

FileNotFoundError: [Errno 2] No such file or directory: '/code/data/tmp/5368pmfscp0012021.rar'
  File "dramatiq/worker.py", line 476, in process_message
    res = actor(*message.args, **message.kwargs)
  File "dramatiq/actor.py", line 145, in __call__
    return self.fn(*args, **kwargs)
  File "web/datasets/tasks.py", line 99, in content_from_file
    Path(path).unlink()
  File "pathlib.py", line 1324, in unlink
    self._accessor.unlink(self)

Failed to process message <dramatiq.brokers.rabbitmq._RabbitmqMessage object at 0x7fa0cb6d4a90> with unhandled exception.
exageraldo commented 3 years ago

Eu pensei em verificar a extensão antes de tentar executar o unlink. Cê acha que faz sentido?

Algo mais ou menos assim (nesse arquivo):

    has_rar_extension = path.endswith('.rar')
    raw = parser.from_file(path)

    if not has_rar_extension and not keep_file:
        Path(path).unlink()
exageraldo commented 3 years ago
    has_rar_extension = path.endswith('.rar')

    if has_rar_extension:
        return

    raw = parser.from_file(path)

    if not keep_file:
        Path(path).unlink()

    if a_file:
        a_file.content = raw["content"]
        a_file.save()

    return raw["content"]

Ou algo assim, dependendo do que seja retornado pelo parser.from_file quando o arquivo é RAR.

anapaulagomes commented 3 years ago

Na verdade, o melhor seria evitar pular os arquivos .rar porque eles são maioria nos arquivos compactados - se não forem todos. Perdão pela descrição da issue porque ela não reflete nada disso.

Uma alternativa seria usar alguma outra biblioteca para descompactar os arquivos e aí sim salvá-los de maneira individual. Isso traria alguns problemas, como nós salvando arquivos de corel draw etc. Mas só testando pra saber hehe

Pesquisando sobre .rar e Apache Tika vi isso aqui:

Tika uses the Commons Compress library to support various compression and packaging formats. The CompressorParser class handles parsing of the top level compression formats, then PackageParser class and its subclasses parse the packaging formats and then pass the unpacked document streams to a second parsing stage using the parser instance specified in the parse context. Formats supported include Tar, AR, ARJ, CPIO, Dump, Zip, 7Zip, Gzip, BZip2, XZ, LZMA, Z and Pack200.

Additionally, the RarParser class supports the RAR archive format, which isn't supported by Commons Compress. https://tika.apache.org/1.17/formats.html

Talvez só tenhamos que modificar algo na configuração.

exageraldo commented 3 years ago

@anapaulagomes tenho algumas dúvidas, cê poderia me ajudar?

Procurei um pouco na documentação da tika-pythone não encontrei muita coisa para usar outro parser (o RarParser). Nem no google achei muitos resultados. Não sei se estou procurando no canto e/ou do jeito certo. Foquei mais na biblioteca, acha que devo voltar o olhar mais na API?

anapaulagomes commented 3 years ago

Claro! :)

exageraldo commented 3 years ago

Show! Daí, quando manipulamos arquivos compactados com o Tika, ele vai saber como manipular todos os tipos de arquivos lá dentro (PDF, corel draw, txt) e nos trazer uma string com todo o conteúdo, confere?

Eu procurei dentro da documentação to tika-python, procurei no código também, mas não consegui muita coisa ainda, infelizmente. Encontrei uma issue (link) sobre uma dúvidas parecida, porem a resposta apenas sugere a leitura da documentação (de forma bem geral). ): A função from_file tem um argumento chamado requestOptions, busquei se havia alguma forma de mudar algo no header, ou em outro canto, para configurar o parser, mas também sem muito sucesso até o momento.

Encontrei o RarParser na documentação, mas não faço ideia de como fazemos para usa-lo. Tambem achei uma resposta no stackoverflow relacionado a essa questão do RAR5 (link) que comenta sobre "não haver solução disponível para descompactar arquivos RAR5 (no java)".

Estava pensando que se esse for realmente o caso, podíamos tentar descompactar usando algum pacote python (como a rarfile que suporta o RAR5, por exemplo) e mudarmos a função de processamento de parser.from_file(path) para parser.from_buffer(blob_file). Faz sentido essa abordagem? Teríamos que ver qual seria o impacto dessa mudança (tempo e memória principalmente eu acredito).

anapaulagomes commented 3 years ago

Show! Daí, quando manipulamos arquivos compactados com o Tika, ele vai saber como manipular todos os tipos de arquivos lá dentro (PDF, corel draw, txt) e nos trazer uma string com todo o conteúdo, confere?

Isso mesmo.

Estava pensando que se esse for realmente o caso, podíamos tentar descompactar usando algum pacote python (como a rarfile que suporta o RAR5, por exemplo) e mudarmos a função de processamento de parser.from_file(path) para parser.from_buffer(blob_file). Faz sentido essa abordagem? Teríamos que ver qual seria o impacto dessa mudança (tempo e memória principalmente eu acredito).

Eu gosto dessa ideia! 🏆 Mas, sim, temos que ver o impacto. Pensando rápido aqui eu acredito que não seria um problema. Obrigada, Geraldo!

exageraldo commented 3 years ago

Feshow! Vou fazer uns testes/profilings e trago mais informações sobre a mudança.

exageraldo commented 3 years ago

Fazendo alguns experimentos com o tika e com bibliotecas que descompactam arquivos RAR no python (como patoolib e rarfile) pude perceber alguns pontos (interessantes talvez):

cenário

In [1]: from tika import parser
In [2]: rar_file = '5152pmfscp0272020.rar'
In [3]: pdf_file = '5152pmfscp0272020/teste.pdf'
In [4]: rar_parser = parser.from_file(rar_file)
In [5]: pdf_parser = parser.from_file(pdf_file)
In [6]: pdf_parser['metadata']['X-Parsed-By']
Out[6]: 
['org.apache.tika.parser.DefaultParser',
 'org.apache.tika.parser.pdf.PDFParser']
In [7]: rar_parser['metadata']['X-Parsed-By']
Out[7]: 
['org.apache.tika.parser.DefaultParser',
 'org.apache.tika.parser.pkg.RarParser']
In [8]: len(rar_parser['content'] or '')
Out[8]: 0
In [9]: len(pdf_parser['content'] or '')
Out[9]: 225739

Baixei um arquivo RAR para testar (link para o arquivo).

Uma outra ideia que tive foi: Descompactarmos todos os arquivos em um arquivo (temporário), criarmos um novo arquivo compactado (formato ZIP), enviamos o arquivo pra processar e depois de recebermos o retorno deletarmos tudo! Fiz alguns testes e notei que se enviarmos um arquivo ZIP, até o parser é diferente, mostrando que pegou o arquivo ZIP e os PDF dentro. Tentei criar o arquivo compactado apenas em memória (com a biblioteca zipfile) mas não é identificado como válido para processar... ):

A ideia de descompactar e compactar pra enviar é pra que todos os arquivos permaneçam juntos para retirarmos um conteúdo único.

In [10]: import zipfile
    ...: 
    ...: zip_file = zipfile.ZipFile('teste.zip', 'w', zipfile.ZIP_DEFLATED)
    ...: for file_name in [f'5152pmfscp0272020/teste{num}.pdf' for num in range(1, 6)]:
    ...:     zip_file.write(file_name, file_name)
    ...: 

In [11]: zip_parser = parser.from_file('teste.zip')

In [12]: zip_parser['metadata']['X-Parsed-By']
Out[12]: 
['org.apache.tika.parser.DefaultParser',
 'org.apache.tika.parser.pkg.PackageParser',
 ['org.apache.tika.parser.DefaultParser',
  'org.apache.tika.parser.pdf.PDFParser'],
 ['org.apache.tika.parser.DefaultParser',
  'org.apache.tika.parser.pdf.PDFParser'],
 ['org.apache.tika.parser.DefaultParser',
  'org.apache.tika.parser.pdf.PDFParser'],
 ['org.apache.tika.parser.DefaultParser',
  'org.apache.tika.parser.pdf.PDFParser'],
 ['org.apache.tika.parser.DefaultParser',
  'org.apache.tika.parser.pdf.PDFParser']]

In [13]: len(zip_parser['content'] or '')
Out[13]: 574902

Os aquivos de testes foram retirados do mesmo arquivo RAR.

exageraldo commented 3 years ago

Encontrei uma abordagem interessante no stackoverflow para converter arquivos RAR para ZIP (link).

Podemos usar a biblioteca tempfile para criar um diretorio temporario com os arquivos descompactados e depois compactarmos para ZIP e enviarmos para o tika. O que acham?

>>> import tempfile

# create a temporary directory using the context manager
>>> with tempfile.TemporaryDirectory() as tmpdirname:
...     print('created temporary directory', tmpdirname)
>>>
# directory and contents have been removed

Dessa forma, acredito que conseguimos até fazer uma lista de formatos permitidos, ou a serem ignorados, e criamos o arquivo ZIP apenas com aquilo que queremos que o tika processe.

anapaulagomes commented 3 years ago

Massa, @exageraldo! Por ora, podemos converter em zip e mandar tudo mesmo. Não são muitos os que tem arquivos corel draw e outros. hahaha Obrigada pela investigação! 🥇 Taca lhe pau!

anapaulagomes commented 3 years ago

Curiosidade: dos 260 mil arquivos que temos, 500 deles são .rar. Embora seja um número baixo, as licitações que tem mais arquivos tem mais itens ou são mais caras (pela minha experiência haha).