Proyecto de ejemplo =================== Instalación ----------- Asumiendo pip3 como instalador de paquetes para python 3: :: $ pip3 install pyscrap3 Si estamos usando pyenv tendremos que ejecutar :: $ pyenv rehash Creación de un nuevo proyecto ----------------------------- 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. .. code-block:: bash ~/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. Estructura del proyecto ----------------------------- Cada proyecto nuevo se compone de los siguientes archivos: * **LICENSE, NOTICE** : Archivos de la licencia del proyecto, apache por defecto. * **MANIFEST.in, setup.py** : Configuración del proyecto, nombre, ficheros a incluir, dependencias, etc... * **README.md, README.rst** : Descripción del proyecto usados por github y pip. * **demoSpider.py** : Script de extracción de ejemplo. * **__init__.py** : Fichero usado por python para indicar que el directorio actual es un paquete. * **items.py** : Fichero donde se definen la estructura de los objetos que procesará el proyecto. * **pipeline.py** : Fichero donde se definen las funciones para procesar los objetos definidos en items.py * **__pycache__** : Carpeta generada por python3. Se puede borrar sin problemas. .. code-block:: bash 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 items.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: .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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. .. code-block:: python 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' pipeline.py ^^^^^^^^^^^ 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: .. code-block:: python 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: .. code-block:: python 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*. .. code-block:: python 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: .. code-block:: python 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. demoSpider.py ^^^^^^^^^^^^^ 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*. .. code-block:: python 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: 1. Se crea una instancia de la clase *webCrawler* .. code-block:: python 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) .. code-block:: python 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. .. code-block:: python a.start(url) Envía url al primer parámetro de la función *parse* .. code-block:: python def parse(self, url, category=None): 4. La función *parse* se ejecuta normalmente hasta el primer *yield*, donde retorna un *DemoItem* .. code-block:: python :emphasize-lines: 11 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: .. code-block:: python :emphasize-lines: 7 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* .. code-block:: python :emphasize-lines: 5 lItems = DemoListItems() lItems["author"] = "Jhon Doe" lItems.append("comment 1") lItems.append("comment 2") yield lItems Ejecutando el proyecto ---------------------- Nos situamos en la ruta de *demoSpider.py* y ejecutamos: .. code-block:: bash ~/proyectos/example1/example1 $ python3 demoSpider.py Loading urls from getUrls url: 'http://www.some_news_site.cl' category: None saving item Saving list items comment 1 comment 2 url: 'http://www.another_news_site.cl' category: None saving item Saving list items comment 1 comment 2 Loading data from getSearchData url: 'http://www.some_cats_site.cl' category: cats saving item Saving list items comment 1 comment 2 url: 'http://www.some_dogs_site.cl' category: dogs saving item Saving list items comment 1 comment 2