spyck

framework

@brunomacabeusbr

#pylestras

quem?

Bruno

Macabeus

Mendes

de Aquino

João

Paolo

Cavalcante

Martins

Oliveira

de onde?

coletar informações

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

jargão

jargão

crawler

o coletor de informações.

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.

Motivação

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

Implementação

Crawler

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

  • requests + regexp
  • selenium + phantomjs

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...

Implementação

primitive

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:

  • escrever uma nova
  • editar uma já existente
  • se referenciar a outra
<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)
    )

Implementação

Banco de dados

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

Implementação

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

Futuro

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

obrigado

@brunomacabeusbr

#pylestras

Copy of spyck

By Matheus Brasil

Copy of spyck

  • 1,025