Asumiendo pip3 como instalador de paquetes para python 3:
$ pip3 install pyscrap3
Si estamos usando pyenv tendremos que ejecutar
$ pyenv rehash
Si tenemos más de un python 3 instalado podemos intentar ocupar wscrap3.3 para crear un proyecto nuevo usando
$ wscrap3.3
Que empezará a preguntar algunos datos para el nuevo proyecto.
~/proyectos $ wscrap3.3
Welcome to mr.bob interactive mode. Before we generate directory structure, some questions need to be answered.
Answer with a question mark to display help.
Values in square brackets at the end of the questions show the default value if there is no answer.
--> Name of the package referenced by installers? [example1]:
nombre: example1
path proyectos/example1
no existe, ok
--> Description of the package (visible on PyPI and similar) [Just a pyscrap3 project.]:
--> Version? [0.0.1]:
--> URL for more information about the package [http://localhost/example1]:
--> Author name? [Author]:
--> Author email address? [author_email@email.com]:
--> What year was the project started (for license notice)? [2014]:
Generated file structure at proyectos
Se recomienda escoger un nombre significativo distinto a “test” o “example” para no estar modificando posteriormente.
Cada proyecto nuevo se compone de los siguientes archivos:
example1/
├── example1
│ ├── demoSpider.py
│ ├── __init__.py
│ ├── items.py
│ ├── pipeline.py
│ └── __pycache__
│ ├── demoSpider.cpython-33.pyc
│ ├── __init__.cpython-33.pyc
│ ├── items.cpython-33.pyc
│ └── pipeline.cpython-33.pyc
├── LICENSE
├── MANIFEST.in
├── NOTICE
├── README.md
├── README.rst
└── setup.py
En el fichero items.py se pueden definir 2 tipos de items:
Item: Heredan de pyscrap3.Item y están pensados para guardar contenido único como el título de una página o el texto de una noticia.
Ejemplo:
class DemoItem(Item):
"""Los Item son ideales para guardar contenido único como el
título de una página o el cuerpo de una noticia."""
def __init__(self):
super().__init__()
self.newfield("title")
self.newfield("body")
ItemList: Heredan de pyscrap3.ItemList y están pensados para guardar contenido que se repite múltiples veces en como un mismo autor para varios post o parámetros arbitrarios que sirvan para agrupar items.
Ejemplo:
class DemoListItems(ItemList):
"""Las ItemList son ideales para guardar multiples contenidos
agrupados, como todos los comentarios de un solo autor."""
def __init__(self):
super().__init__()
self.newfield("author")
En ambos casos, los campos se definen mediante la directiva self.newfield. Posteriormente se pueden usar como diccionarios normales inicializados con ‘None’ (que solo aceptarán los campos definidos), ejemplo:
In [1]: from items import DemoItem
In [2]: unItem = DemoItem()
In [3]: unItem["title"] = "test title"
In [4]: str(unItem["body"])
Out[4]: 'None'
Las ItemList funcionan como un diccionario y una lista unidos, se pueden agregar objetos con append además de accesar a los campos definidos mediante self.newfield. Los objetos añadidos mediante append y como diccionario no se mezclan.
In [1]: from items import DemoListItems
In [2]: litems = DemoListItems()
In [3]: litems.append("agregado como lista")
In [4]: litems["author"] = "agregado como diccionario"
In [5]: litems["author"]
Out[5]: 'agregado como diccionario'
In [6]: litems[0]
Out[6]: 'agregado como lista'
En el fichero pipeline.py se definen varias funciones de uso interno:
getUrls: El propósito original de esta función era obtener desde algún lado (base de datos) una serie de urls que serían usadas posteriormente por el script de extracción. Este generador puede ser llamado posteriormente por la clase que herede de pyscrap3.Spider al crear el script de extracción sin necesidad de importarlo directamente en dicho script. Nótese que debe ser un generador, no una función (debe usar “yield” no “return”).
Ejemplo:
def getUrls():
"""Generador, acá se pueden obtener las url a procesar desde la base de datos"""
yield "http://www.some_news_site.cl"
yield "http://www.another_news_site.cl"
getSearchData: El propósito original de esta función era obtener desde algún lado (base de datos) una serie de datos extra que serían usados por el script de extracción con propósitos varios, a saber, por ejemplo para búsquedas mediante apis por palabras claves. La lógica interna es la misma que la de getUrls.
ejemplo:
def getSearchData():
"""Generador, acá se puede obtener la data a procesar desde la base de datos"""
yield {"url": "http://www.some_api_url.cl", "palabras": ("cats", "dogs")}
yield {"url": "http://www.some_other_api_site.cl", "palabras": ("yellow", "green")}
getPipes: El propósito de esta función es conectar otras funciones definidas en pipeline.py con items de en items.py y clases derivadas de pyscrap3.Spider del script de extracción. Dichas funciones serán ejecutadas a medida que la clase de extracción procese items con su función parse.
def getPipes():
"""Asocia una función con un item o un ListItem respectivo.
Al momento en que el generador 'parse' retorne un item o ItemList,
dicha función ejecutará."""
pipes = {"items":
{
"DemoItem": saveItem
},
"itemLists":
{
"DemoListItems": saveListItems
}
}
return pipes
En el caso anterior, el item DemoItem tiene una función asociada llamada saveItem que puede estar definida como:
def saveItem(item):
print("saving item " + str(item))
Según lo anterior por tanto, cada vez que el generador ‘parse’ de un clase Spider retorne un item DemoItem se ejecutará la función saveItem que impimirá el texto “saving item” más un str del DemoItem que en condiciones normales podría guardar dicho item en una base de datos o procesarlo con algún otro objetivo cualquiera.
En el fichero demoSpider.py -el nombre en este caso es solo un ejemplo y puede ser cualquiera a diferencia de los otros archivos- se definen las clases que efectivamente realizarán la extracción de contenido desde la web, desde alguna api o alguna otra fuente y procesarán ese contenido para ajustarlo al formato de items definido en items.py.
class webCrawler(Spider):
def __init__(self):
super().__init__()
def parse(self, url, category=None):
"""Esta función/generador será llamada cuando se ejecute webCrawler().start() con
los mismos parámetros.
Cada Item o itemList tiene una función asociada en getPipes, en pipeline.py;
dicha función será ejecutada al momento de retornar (yield) el item o
itemList durante la ejecución de 'start()'."""
print("url: '" + url + "' category: " + str(category))
dItem = DemoItem()
dItem["title"] = "some title"
dItem["body"] = "a bunch of text"
yield dItem
lItems = DemoListItems()
lItems["author"] = "Jhon Doe"
lItems.append("comment 1")
lItems.append("comment 2")
yield lItems
a = webCrawler()
print("Loading urls from getUrls")
for url in a.getUrls():
a.start(url)
Acá la clase webCrawler hereda de pyscra3.Spider y usa la función parse para crear un DemoItem, retornarlo con yield y luego hace lo mismo con un DemoListItems.
Internamente lo que sucede es:
a = webCrawler()
2. Se llama a la función getUrls definida internamente en pipeline.py como si fuera una función de la clase webCrawler (no se importa nada de pipeline.py directamente)
for url in a.getUrls():
3. Usando la función especial start se envía una url a la función parse definida en la clase de forma tradicional.
a.start(url)
Envía url al primer parámetro de la función parse
def parse(self, url, category=None):
def parse(self, url, category=None):
"""Esta función/generador será llamada cuando se ejecute webCrawler().start() con
los mismos parámetros.
Cada Item o itemList tiene una función asociada en getPipes, en pipeline.py;
dicha función será ejecutada al momento de retornar (yield) el item o
itemList durante la ejecución de 'start()'."""
print("url: '" + url + "' category: " + str(category))
dItem = DemoItem()
dItem["title"] = "some title"
dItem["body"] = "a bunch of text"
yield dItem
5. pyscrap3 revisa si el item DemoItem tiene definida una función asociada en getPipes dentro de pipeline.py. En este caso, DemoItem tiene asociada una función saveItem En pineline.py:
def getPipes():
"""Asocia una función con un item o un ListItem respectivo.
Al momento en que la función 'parse' retorne un item o ItemList,
dicha función ejecutará."""
pipes = {"items":
{
"DemoItem": saveItem
},
6. Se ejecuta la función saveItem sobre el objeto DemoItem. Luego la función parse continua hasta el segundo yield donde repite el proceso esta vez con un DemoListItems
lItems = DemoListItems()
lItems["author"] = "Jhon Doe"
lItems.append("comment 1")
lItems.append("comment 2")
yield lItems
Nos situamos en la ruta de demoSpider.py y ejecutamos:
~/proyectos/example1/example1 $ python3 demoSpider.py
Loading urls from getUrls
url: 'http://www.some_news_site.cl' category: None
saving item <items.DemoItem object at 0x7f9c9434ff90>
Saving list items
comment 1
comment 2
url: 'http://www.another_news_site.cl' category: None
saving item <items.DemoItem object at 0x7f9c9434ff90>
Saving list items
comment 1
comment 2
Loading data from getSearchData
url: 'http://www.some_cats_site.cl' category: cats
saving item <items.DemoItem object at 0x7f9c9434ff90>
Saving list items
comment 1
comment 2
url: 'http://www.some_dogs_site.cl' category: dogs
saving item <items.DemoItem object at 0x7f9c9434ff90>
Saving list items
comment 1
comment 2