spyck

framework

@brunomacabeusbr

#pylestras

autores

Macabeus

Aquino

Paolo

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 organizá-las, devemos carregá-las para fazermos análises

jargão

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 entity diferentes, para onde contextualizará e armazenará as informações coletadas.

em síntese...

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

exemplos de uso

Antes de contratarmos um funcionário, poderemos fazer uma busca no histórico dele

 

Podemos fazer uma análises de preços e produtos comprados por um funcionário de uma empresa, para sabermos se ele está favorecendo familiares

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 com métodos abstratos para servir de base para todos os crawlers, chamada 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 entity_required(): ...

# Executa a colheita
def harvest(cls): ...

Alguns métodos abstratos de Crawler

Os parâmetros do método harvest variam 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 entity do tipo person
# desse modo, recolherá as dependências dela e a editará
def harvest(cls, entity_person=None, dependencies=None): ...

# Requer uma entity do tipo person ou então do tipo firm
# ou seja, o crawler pode tanto editar uma entity firm como person
def harvest(cls, entity_person=None, primitive_firm=None, dependencies=None): ...

# Requer uma entity do person, ou então
# podemos fornecer os parâmetros specific_name
def harvest(cls, entity_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, podemos 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('
                'entity_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 entity_required():
        return 'entity_person',

    @classmethod
    def harvest(cls, entity_person=None,
                dependencies=None,
                specific_siteid=None):
        ...
<crawler>
  <primitive_required>
    <entity type_requirement="harvest">
      person
    </entity>
  </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>

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

 

Exemplo para colher dados numa página da Internet

# usando selenium + phantomjs
from selenium import webdriver
phantom = webdriver.PhantomJS()
phantom.get('http://pudim.com.br/')
info = phantom.find_element_by_tag_name('a').text

# o mesmo que o acima, mas usando requests + regexp
import requests
import re
r = requests.get('http://pudim.com.br/')
regexp = re.compile('<a.*?>(.*)</a>')
info = regexp.search(r.text).group(1)

# salvar info no banco
cls.db.update_entity_row({'info': info})

implementação

entity

Entities são usadas para agregar informações sobre um contexto em comum

 

Um crawler pode requerer uma entity por três razões:

  • escrever um novo elemento de entity
  • editar um já existente
  • se referenciar a algum
<entity>
    <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>
</entity>

Novas entities podem ser criadas a medida que for necessário e, tal como os crawlers, são através de XML

Códigos

Traduzir o XML para tabelas

import xml.etree.ElementTree as ET

for current_xml in os.listdir(path_spyck + '/entities/'):
    xml_root = ET.parse('entities/' + 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')
    ]

    entity_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('entity_' + entity_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('entity_' + primitive_name + '_crawler',
                     'entity_' + primitive_name)
    )

Códigos

implementação

banco de dados

Novas entity e crawlers vão sendo criadas dias após dia, e eles precisam salvar dados no banco

 

Logo, o banco de dados precisa ser dinâmico - novas tabelas vão sendo criadas

 

Cada elemento de uma entity tem um id. Esse id é usado como chave estrangeira para ligar às tabelas dos crawlers

<database>
  <table_main>
    <column>
      <name>cia</name>
      <type>INTEGER</type>
    </column>
  </table_main>

  <table_secondary>
    <name>records_school</name>
    <column>
      <name>timestamp</name>
      <type>TEXT</type>
    </column>
    <column>
      <name>school</name>
      <type primitive="">firm</type>
    </column>
    <column>
      <name>course</name>
      <type>TEXT</type>
    </column>
    <column>
      <name>turn</name>
      <type>TEXT</type>
    </column>
  </table_secondary>
</database>
class CrawlerEtufor(Crawler):
  def create_my_table(self):
      self.db.execute(
        'CREATE TABLE IF NOT EXISTS %s('
          'entity_person_id INTEGER,'
          'cia INTEGER'
        ');' % self.name()
      )

      self.db.execute(
        'CREATE TABLE IF NOT EXISTS %s('
          'entity_person_id INTEGER,'
          'timestamp TEXT,'
          'entity_firm_id_school INTEGER,'
          'course TEXT,'
          'turn TEXT,'
          'FOREIGN KEY(entity_firm_id_school)'
          '  REFERENCES entity_firm(id)'
        ');' % (self.name() + '_records_school')
      )

Nos crawlers definimos que tabelas eles criarão

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 entity_{} '.format(entity_name) +
    ' '.join([
        'INNER JOIN {} ON {}.entity_{}_id == {}'.format(
            i.name(), i.name(), entity_name, entity_id
         )
        for i in crawler_list_success_cls
    ]) +
    ' WHERE entity_{}.id == {}'.format(entity_name, entity_id)
)

Código para recolher do banco os dados de uma entity e nas tabelas principais do crawler

implementação

dependências

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)

# a chave de dict_info_to_crawlers será o nome da info e
# o value uma lista com os crawlers em que pode-se consegui-la
[
    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

Um decorator é adicionado de forma implícita ao método harvest

Se for fornecido um id de entity, 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

class CrawlerEtufor(Crawler):
  ...

  @staticmethod
  def dependencies():
    return 'name', 'birthday_month', 'birthday_year',

  ...

  @classmethod
  def harvest(cls, entity_person=None, dependencies=None):
    phantom = webdriver.PhantomJS()

    phantom.get('http://www.etufor.ce.gov.br/index_novo.asp?pagina=sit_carteira2007.asp')

    form_consultation = phantom.find_element_by_name('Nome')
    form_consultation.send_keys(
      dependencies['name'] + Keys.TAB + 
      '{:02}'.format(dependencies['birthday_month']) +
      '{:02}'.format(dependencies['birthday_year'])
    )
    phantom.find_element_by_name('btnpesq').click()

    ...

Nos crawlers, precisamos apenas especificar os dados dependentes

 

O parâmetro dependencies receberá o dicionário com os valores providos do decorator

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

Made with Slides.com