Matheus Brasil
Web alchemist who loves evangelizing cool technologies such as Javascript, functional programming and NoSQL.
Há uma abundância de informações coletáveis na Internet e no mundo real
Uma informação coletada possibilita conseguir coletar uma nova informação numa outra fonte
Informações dispersas não são úteis, portando, devemos agrega-las em um contexto em comum, assim organizando-as
Após organiza-las, devemos carrega-las para fazermos análises
O coletor de informação é chamado de crawler. A execução é chamada de harvest. Para colhermos, pode ser que precisemos de dados prévios, chamados de dependencies. Cada crawler tem seu crop possível de se conseguir após a colheita. Cada crawler trabalha em um ou mais primitives diferentes, para onde contextualizará e armazenará as informações coletadas.
Então... podemos criar um framework...
que facilitará a criação e integração de diversos crawlers!
unem-se através do crop e dependencies
podemos automatizar o harvest
Cada fonte é funciona independentemente
Cada crawler é independente um do outro, porém, todos fazem algumas tarefas parecidas, tais como atualizar suas tabelas no banco de dados e recolher as dependências do banco
Então, criei uma classe abstrata para servir de base para todos os crawlers, chamada de Crawler
Cada crawler é uma subclasse de Crawler
Todos os crawlers ficam na pasta /crawler e são herdeiros de Crawler
import os
import importlib
my_path = os.path.dirname(__file__)
for i in os.listdir(my_path):
if not os.path.isfile(os.path.join(my_path, i)):
continue
py_name = os.path.splitext(i)[0]
importlib.import_module('crawler.' + py_name)
Então, podemos carrega-los automaticamente
Carregar crawlers
e torna-lo acessível
for cls in Crawler.__subclasses__(): # lista os crawlers
setattr(self, 'crawler_' + cls.name(), cls())
# criará métodos com nomes tais como "crawler_foo"
# que se referenciará para o objeto da classe CrawlerFoo
# Retorna uma string com o nome do crawler
def name(): ...
# Retorna uma tupla contendo os nomes das dependências
def dependencies(): ...
# Retorna uma tupla contendo os nomes dos dados colhíveis
def crop(): ...
# Retorna uma tupla contendo as primitives requeridas
def primitive_required(): ...
# Executa a colheita
def harvest(cls): ...
Alguns métodos abstratos de Crawler
Os parâmetros do método harvest varia de crawler para crawler, conforme o funcionamento dele
Exemplos
# Não requer primitive, portando, não tem o parâmetro dependencie
def harvest(cls): ...
# Requer uma primitive do tipo person
# desse modo, editará a primitive passada e recolherá as dependências dela
def harvest(cls, primitive_person=None, dependencies=None): ...
# Requer uma primitive do tipo person ou então do tipo firm
# ou seja, o crawler pode tanto editar uma primitive firm como person
def harvest(cls, primitive_person=None, primitive_firm=None, dependencies=None): ...
# Requer uma primitive do person, ou então
# podemos fornecer os parâmetros specific_name
def harvest(cls, primitive_person=None, dependencies=None, specific_name=None): ...
Parâmetros de harvest
A criação de novos crawlers (infelizmente) ficou complexa, e desejo facilitar a criação
Felizmente, todos os crawlers seguem um padrão específico nas implementações dos métodos (exceto o harvest)
Então, pode-se usar um XML que gerará o esqueleto de um crawler
Criação de novos crawlers com XML
import ...
class CrawlerFazendaReceita(Crawler):
def create_my_table(self):
self.db.execute(
'CREATE TABLE IF NOT EXISTS %s('
'primitive_person_id INTEGER,'
'death_year INTEGER'
');' % self.name())
@staticmethod
def name():
return 'fazenda_receita'
@staticmethod
def dependencies():
return 'cpf', 'birthday_year',
@staticmethod
def crop():
return 'name', 'death_year',
@staticmethod
def primitive_required():
return 'primitive_person',
@classmethod
def harvest(cls, primitive_person=None,
dependencies=None,
specific_siteid=None):
...
<crawler>
<primitive_required>
<primitive type_requirement="harvest">
person
</primitive>
</primitive_required>
<database>
<table_main>
<column>
<name>death_year</name>
<type>INTEGER</type>
</column>
</table_main>
</database>
<dependencies>
<route>
<dependence>cpf</dependence>
<dependence>birthday_year</dependence>
</route>
</dependencies>
<crop>
<info>name</info>
<info>death_year</info>
</crop>
<harvest>
<param_additional>
specific_siteid
</param_additional>
</harvest>
</crawler>
No caso de um crawler para uma página web, há duas formas principais de acessa-la e capturar os dados relevantes
Formas de implementar o harvest
Ele pode ser implementado para colher dados de qualquer fonte, como por meio da Internet, no disco rígido, no mundo real a partir de sensores...
Primitives são usadas para agregar informações em um contexto em comum
Novas primitives podem ser criadas a medida que for necessário e, tal como os crawlers, são através de XML
Um crawler pode precisar de uma primitive por três razões:
<primitive>
<column>
<name>cnpj</name>
<type>TEXT</type>
</column>
<column>
<name>razao_social</name>
<type>TEXT</type>
</column>
<column>
<name>nome_fantasia</name>
<type>TEXT</type>
</column>
<column>
<name>porte_empresa</name>
<type>TEXT</type>
</column>
<column>
<name>administration</name>
<type>TEXT</type>
</column>
</primitive>
XML da primitive firm
Códigos
Códigos
Traduzir o XML para tabelas
import xml.etree.ElementTree as ET
for current_xml in os.listdir(path_pykyourinrin + '/primitives/'):
xml_root = ET.parse('primitives/' + current_xml).getroot() # carregar xml
columns = [ # criar array de tuplas com (<nome da coluna>, <tipo da coluna>)
(current_xml.find('name').text, current_xml.find('type').text)
for current_xml in xml_root.findall('column')
]
primitive_name = current_xml[:-4] # nome da primitive será o mesmo nome do xml
self.execute( # criar sql e executa-la, para gerar a tabela da primitive
'CREATE TABLE IF NOT EXISTS {}('
'id INTEGER PRIMARY KEY AUTOINCREMENT,'
'{}'
');'.format('primitive_' + primitive_name,
','.join([i[0] + ' ' + i[1] for i in columns]))
)
self.execute( # criar sql e executa-la, para gerar a tabela de crawlers executados
'CREATE TABLE IF NOT EXISTS {}('
'id INTEGER,'
'FOREIGN KEY(id) REFERENCES {}(id)'
');'.format('primitive_' + primitive_name + '_crawler',
'primitive_' + primitive_name)
)
Novas primitives e crawlers vão sendo criados, e eles salvam dados no banco
Logo, o banco de dados precisa ser dinâmico - novas tabelas vão sendo criadas quando necessário
Cada elemento de uma primitive tem um id. Esse id é usado como chave estrangeira para ligar às tabelas dos crawlers
Abaixo temos um exemplo de banco de dados gerado
fieldnames = self.select_column_and_value( # esse método executa uma query
# e retorna dicionário sendo a chave
# o nome da coluna e o valor o conteúdo dela
'SELECT * FROM primitive_{} '.format(primitive_name) +
' '.join([
'INNER JOIN {} ON {}.primitive_{}_id == {}'.format(
i.name(), i.name(), primitive_name, primitive_id
)
for i in crawler_list_success_cls
]) +
' WHERE primitive_{}.id == {}'.format(primitive_name, primitive_id)
)
Código para recolher do banco os dados de uma primitive e nas tabelas principais do crawler
Dependencies
Um decorator é adicionado de forma implícita ao método harvest
Se for fornecido um id de primitive, automaticamente puxará as dependências do banco e as colocarão no parâmetro dependencies
class GetDependencies:
def __init__(self, f):
self.f = f
...
def __call__(self, *args, **kwargs):
... # Recolhe as dependências e salvar em dict_dependencies
# Colher
self.harvest(*args, dependencies=dict_dependencies, **kwargs)
...
for i in Crawler.__subclasses__():
if i.have_dependencies():
i.harvest = GetDependencies(i) # adicionar o decorator
Decorator
A primeira ideia em mente para mim foi fazer um grafo de dependências... e assim foi feito
Resultou em 200 linhas, muito pesado e péssimo de se manter
Depois, decidi simplificar tudo usando um dicionário. Fico bem mais leve e precisando só de 8 linhas
from collections import defaultdict
dict_info_to_crawlers = defaultdict(list)
[
dict_info_to_crawlers[current_crop].append(cls)
for cls in Crawler.__subclasses__()
if cls.have_dependencies() is True
for current_crop in cls.crop()
]
Recolher dependências
Simplificar o código e facilitar o desenvolvimento do Spyck
Criar interface gráfica para ser acessível para leigos, tanto para escrever novos crawlers como para usar
Implementar análises e inferências a respeito dos dados colhidos
By Matheus Brasil
Web alchemist who loves evangelizing cool technologies such as Javascript, functional programming and NoSQL.