Contenidos

De WikiEducator
Saltar a: navegación, buscar



Introducción

Una de las tareas más frecuentes que tenemos es el tratamiento de información. Muchas veces esa información está bien estructurada y almacenada en herramientas estándar (bases de datos relacionales), pero otras veces los datos están en hojas de cálculo, páginas web y formatos menos estructurados. En este bloque vamos a ver cómo podemos usar python para manipular:

  • Hojas de cálculo
  • Bases de datos
  • Documentación web

Además de las formas de acceso y manipulación de los datos introduciremos mecanismos de presentación de los datos (templates, gráficos, pdf, ...)

Hojas de cálculo

Mucha información está almacenada en | hojas de cálculo.

Ficheros CSV

La información de una hoja de cálculo se puede exportar/importar desde un fichero csv. Python tiene soporte para tratar ficheros csv. Un fichero csv es un fichero de texto.

Lectura

import csv
with open('zaragoza_2010_10.csv') as fin:
    reader = csv.reader(fin, delimiter=";")
    for fila in reader:
        print fila
['Países', 'HOMBRES', 'MUJERES', 'TOTAL']
['Rumania', '16704', '14487', '31191']
['Ecuador', '5612', '5713', '11325']
['Marruecos', '5042', '3007', '8049']

Escritura

with open('codigo_ascii.txt', 'w') as fout:
    writer = csv.writer(fout)
    ascii_a = ord('a')
    for n in range(26):
        writer.writerow((chr(ascii_a+n), ascii_a + n))

Quoting

writer = csv.writer(f, quoting=csv.QUOTE_NONNUMERIC)

Dialectos

>>> csv.list_dialects()
['excel-tab', 'excel']

Se pueden crear dialectos:

csv.register_dialect(nombre, delimiter="|")

Usando nombres de campos

with open('zaragoza_2010_10.csv') as fin:
    # cuidado, toma la primera fila como títulos
    # usad parámetro fieldnames si no
    reader = csv.DictReader(fin, delimiter=";")

Hojas de cálculo Excel

Algunas librerías interesantes:

Sin embargo es necesario usar COM para :

  • manipular gráficos
  • celdas con texto enriquecido
  • leer fórmulas
  • trabajar con macros y nombres

xlrd

# ejemplo lectura
import xlrd
datos = {} # Dicc. vacío
book = xlrd.open_workbook('sampledata.xls')
hoja = book.sheet_by_index(0)
for i in range(1,sh.nrows): #salta la primera línea
    datos[hoja.cell_value(rowx=i, colx=1)] = hoja.cell_value(rowx=i, colx=2)

xlwt

# ejemplo escritura
import xlwt
lista1 = [1,2,3,4,5]
lista2 = [234,267,281,301,331]
wb = xlwt.Workbook()
ws = wb.add_sheet('Primera hoja')
ws.write(0,0,'Column A')
ws.write(0,1,'Column B')
i = 1
for x,y in zip(lista1,lista2): # Recorre las dos listas a la vez
    ws.write(i,0,x) # Fila, Columna, Datos.
    ws.write(i,1,y)
    i += 1
wb.save('hoja.xls')

Bases de Datos Relacionales

Plantilla:Preconocimiento

Usar bases de datos relacionales con Python es muy fácil. Python proporciona un estándar para acceder a bases de datos. La DB API 2.0 es la versión vigente (PEP 249) Módulos compatibles:

  • MySQLdb (MySQL)
  • psycopg2 (PostgreSQL)
  • cx_Oracle (Oracle)
  • mxODBC (SQL Server, DB2, Sybase, Oracle, etc.)

Estructura

La DB API usa dos conceptos para realizar los procesos:

  • Objeto Conexión
    • conexión con la base de datos
    • Transacciones
  • Objeto Cursor
    • Ejecuta las sentencias
    • Accede a los resultados

Conexión

  • El objeto conexión se encarga de conectar con la base de datos
  • Proporciona acceso (red/RPC) a la base de datos.
  • Este objeto no permite lanzar sentencias.
  • Gestiona las transacciones (grupos lógicos de sentencias)

Cursor

  • Creado a partir de una conexión
  • Sentencias de manipulación y de consulta en la bbdd.
  • Método execute(), que acepta una secuencia de parámetros.
  • Almacena los datos del result set depués de lanzar la consulta.
  • Método fetch*() que lee los datos del result set

Transacciones

  • DB API 2.0 soporta transacciones (si el motor las soporta) desde el objeto conexión.
  • conexión: commit / rollback

Introspección del esquema

  • Busca el tipo de las columnas de una tabla:
    • Método sencillo:
cursor.execute(select * from testtable where 1=0)
# mira el atributo cursor.description
    • Método avanzado:
cursor.columns(table='testtable')
rows = cursor.fetchall()

Muy importante: Paso de parámetros

  • No hay ue hacer nunca sustitución de cadenas de caracteres para evitar inyección de código.
  • Hay una variable paramstyle que define cómo se pasan los parámetros.
  • Todos los módulos admiten al menos uno de:
    • 'qmark': Signo de interrogación, ej. '...WHERE name=?'
    • 'numeric': Numerico, posicional, ej. '...WHERE name=:1'
    • 'named': por Nombre, ej. '...WHERE name=:name'
    • 'format': Formato ANSI C, ej. '...WHERE name=%s'
    • 'pyformat': Formato Python, ej. '...WHERE name=%(name)s'

Ejemplo con sqlite

# Fuente: http://mundogeek.net/archivos/2008/06/25/bases-de-datos-en-python
import sqlite3 as dbapi
 
# 1. Creamos objeto conexión
bbdd = dbapi.connect("bbdd.dat")
 
# 2. Creamos un cursor
cursor = bbdd.cursor()
 
# 3. Usamos cursor para acceder a la  base de datos
# 3.1. create
cursor.execute("""create table empleados (dni text,
                  nombre text,
                  departamento text)""")
 
# 3.2. insert
cursor.execute("""insert into empleados
                  values ('12345678-A', 'Manuel Gil', 'Contabilidad')""")
 
bbdd.commit()
 
# 3.3 select
cursor.execute("""select * from empleados
                  where departamento='Contabilidad'""")
 
# extraer resultados de select --> están almacenados en cursor
for tupla in cursor.fetchall():
    print tupla

Ejemplo mysql

import MySQLdb
 
dbusername = "user" 
dbname = 'user_private' 
dbpassword = 'some_password'
 
# connect to the database 
db = MySQLdb.Connect(db = dbname, user = dbusername, passwd = dbpassword)
 
#To perform a query, you first need a cursor, and then you can execute queries on it. 
cursor = db.cursor()
 
# create the query 
query = "SELECT * FROM foo"
 
# execute the query 
cursor.execute(query)
 
# retrieve the result 
results = cursor.fetchall()
 
for firstname, age, city in results: 
    print firstname, age, city

Ampliación MySQL

Acceso a MySQL con Python

Persistencia de objetos

pickle

Pickle: convierte un objeto python en secuencia de bytes Funciones de pickle:

  • dumps(objeto, proto): serializa a una string
  • dump(objeto, archivo, proto): guarda en archivo
  • loads(cadena, proto): des-serializa una string
  • load(archivo, proto): carga desde archivo
>>> import pickle
>>> s = pickle.dumps({1:'a',2:'b'},0) >>> s
"(dp0\nI1\nS'a'\np1\nsI2\nS'b'\np2\ns."
>>> print pickle.loads(s) {1: 'a', 2: 'b'}

Shelve

Shelve: objeto similar a un diccionario persistente

  • open(filename, flag='c', protocol=None, writeback=False) #crea un diccionario persistente
  • flag= 'r': solo lectura, 'w': lectura/escritura, 'c':creación y lectura/escritura, 'n': nuevo
  • shelve.sync(): sincronizar (writeback=True)
  • shelve.close(): grabar y cerrar diccionario
>>>import shelve
>>> d = shelve.open('alumnos.dat') # abrir archivo 
>>> d['12cd'] = {'nombre': 'Luis', 'apellido': 'Pérez'}
>>> data = d['12cd'] # leer una COPIA de los datos 
>>> del d['12cd'] # borra los datos almacenados

ORM



Icon inter.gif

ORM

El mapeo objeto-relacional (Object-Relational mapping, o sus siglas) es una técnica de programación para convertir datos entre el sistema de tipos utilizado en un lenguaje de programación orientado a objetos y el utilizado en una base de datos relacional, utilizando un motor de persistencia (http://es.wikipedia.org/wiki/Mapeo_objeto-relacional)


ORMs en Python

Nosotros vamos a usar elixir.

Elixir

(Tutorial basado en http://elixir.ematia.de/trac/wiki/TutorialDivingIn)

  • Capa declarativa sobre SQLAlchemy
  • SQLAlchemy usa el patrón "Data Mapper" (no hay relación uno a uno de tablas y clases: las clases se pueden mapear a selects arbitrarias)
  • Elixir usa el patrón "Active Record": relación uno a uno de clases y tablas.
  • Soporta claves primarias compuestas.

Instalar

$ sudo easy_install elixir  # pip install elixir

Un modelo sencillo

La clase Entity define clases, tablas y mapper en un sólo paso.

modelo.py

# -*- coding: utf-8 -*-
from elixir import *
 
metadata.bind = "sqlite:///peliculas.sqlite"
metadata.bind.echo = True
 
class Pelicula(Entity):
    titulo = Field(Unicode(30))
    year = Field(Integer)
    descripcion = Field(UnicodeText)
 
    def __repr__(self):
        return u'<Peli: "%s" (%d)>' % (self.titulo, self.year)

Creación de la bases de datos

>>> from modelo import *
>>> setup_all()   # crea la el objeto Tabla SQLAlchemy y el objeto Mapper para la clase Película.
>>> create_all()  # crea las tablas SQL correspondientes a la Tabla SQLAlchemy


CREATE TABLE modelo_pelicula (
    id INTEGER NOT NULL, 
    titulo VARCHAR(30), 
    YEAR INTEGER, 
    descripcion TEXT, 
    PRIMARY KEY (id)
)

Por defecto la tabla se llama <nombre_del_modulo>_<nombre_de_la_clase>. Si ningún campo tiene el parámetro primary_key=True, crea un atributo id.

>>> Pelicula(titulo=u"Blade Runner", year=1982)
<Peli: "Blade Runner" (1982)>
>>> session.commit()
>>> Pelicula.query.all()
[<Peli: "Blade Runner" (1982)>]
>>> peli = Pelicula.query.first() # .first() equivalente a .all()[0]
>>> peli.year = 1983
>>> session.commit()
>>> Pelicula.query.all()
[<Peli: "Blade Runner" (1983)>]

Eliminar

>>> movie.delete()
>>> session.commit()
>>> Pelicula.query.all()
[]

Relaciones

class Pelicula(Entity):
    titulo = Field(Unicode(30))
    year = Field(Integer)
    descripcion = Field(UnicodeText)
    director = ManyToOne('Director')    # <-- Añade esta línea
 
    def __repr__(self):
        return '<Peli: "%s" (%d)>' % (self.titulo, self.year)
 
class Director(Entity):
    nombre = Field(Unicode(60))
    peliculas = OneToMany('Pelicula')         # <-- Añade esta línea
 
    def __repr__(self):
        return '<Director "%s">' % self.nombre
>>> from model import *
>>> setup_all(True)
CREATE TABLE model_director (
    id INTEGER NOT NULL, 
    nombre VARCHAR(60), 
    PRIMARY KEY (id)
)
 
CREATE TABLE model_movie (
    id INTEGER NOT NULL, 
    titulo VARCHAR(30), 
    YEAR INTEGER, 
    descripcion TEXT, 
    director_id INTEGER, 
    PRIMARY KEY (id), 
    CONSTRAINT model_movie_director_id_fk FOREIGN KEY(director_id) REFERENCES model_director (id)
)
# directores
>>> rscott = Director(nombre=u"Ridley Scott")
>>> glucas = Director(nombre=u"George Lucas")
# películas
>>> alien = Pelicula(titulo=u"Alien", year=1979)
>>> swars = Pelicula(titulo=u"Star Wars", year=1977)
>>> brunner = Pelicula(titulo=u"Blade Runner", year=1982)
# Añadimos películas a los directores
>>> rscott.peliculas.append(brunner) 
>>> rscott.peliculas.append(alien)
>>> swars.director = glucas
# comprobación
>>> glucas.peliculas
[<Pelicula "Star Wars" (1977)>]
>>> session.commit()


Búsquedas

>>> Pelicula.query.filter_by(titulo=u"Alien").one()
<Pelicula "Alien" (1979)>
>>> Pelicula.query.filter(Pelicula.year > 1980).all()
[<Pelicula "Blade Runner" (1982)>]
>>> Pelicula.query.filter(Pelicula.director.has(nombre=u'Ridley Scott')).all()
[<Pelicula "Alien" (1979)>, <Pelicula "Blade Runner" (1982)>]
>>> Pelicula.query.filter(Pelicula.director.has(Director.nombre.endswith(u'Scott'))).all()
[<Pelicula "Alien" (1979)>, <Pelicula "Blade Runner" (1982)>]
 
# Búsquedas generativas (usando partes de otras búsquedas)
 
>>> d = Director.get_by(nombre=u'Ridley Scott') # Class.get_by(xxx) es un atajo para Class.query.filter_by(xxx).first()
>>> q = Pelicula.query.filter_by(director=d)
>>> q.filter_by(year=1979).all()
[<Pelicula "Alien" (1979)>]
>>> from sqlalchemy import desc
>>> q.order_by(desc(Pelicula.year)).all()
[<Pelicula "Blade Runner" (1982)>, <Pelicula "Alien" (1979)>]

Relaciones muchos a muchos

# modifica ...
 
class Genero(Entity):
    nombre = Field(Unicode(15), primary_key=True)
    peliculas = ManyToMany('Pelicula')
 
    def __repr__(self):
        return '<Genero "%s">' % self.nombre
 
class Pelicula(Entity):
    titulo = Field(Unicode(30), primary_key=True)   # <-- modifica esta línea
    year = Field(Integer, primary_key=True)        # <-- y ésta
    descripcion = Field(UnicodeText)
    director = ManyToOne('Director')
    generos = ManyToMany('Genero')                   # <-- añade esta línea
 
    def __repr__(self):
        return '<Pelicula "%s" (%d)>' % (self.titulo, self.year)
>>> from model import *
>>> setup_all(True)
>>> scifi = Genero(nombre=u"Science-Fiction")
>>> rscott = Director(nombre=u"Ridley Scott")
>>> glucas = Director(nombre=u"George Lucas")
>>> alien = Pelicula(titulo=u"Alien", year=1979, director=rscott, generos=[scifi, Genero(nombre=u"Horror")])
>>> brunner = Pelicula(titulo=u"Blade Runner", year=1982, director=rscott, generos=[scifi])
>>> swars = Pelicula(titulo=u"Star Wars", year=1977, director=glucas, generos=[scifi])
>>> session.commit()
>>> Pelicula.query.filter(Pelicula.generos.any(nombre=u"Horror")).all()
[<Pelicula "Alien" (1979)>]

Opciones

Nombre de la tabla

class Pelicula(Entity):
    using_options(tablenombre='peliculas')
 
    titulo = Field(Unicode(30))
    year = Field(Integer)
    descripcion = Field(UnicodeText)

Más opciones: ver API.

Sesiones

La sesión implementa el patrón unidad de trabajo.

Herencia

Si pensamos en introducir actores y directores:

class Persona(Entity):
    using_options(inheritance='multi')
    nombre = Field(Unicode(60))
    def __repr__(self):
        return '<Persona "%s">' % self.nombre
 
class Actor(Persona):
    using_options(inheritance='multi')
    peliculas = ManyToMany('Pelicula')
 
    def __repr__(self):
        return '<Actor "%s">' % self.nombre
 
class Director(Persona):
    using_options(inheritance='multi')
    peliculas = OneToMany('Pelicula')
 
    def __repr__(self):
        return '<Director "%s">' % self.nombre
>>> rscott = Director(nombre=u"Ridley Scott")
>>> glucas = Director(nombre=u"George Lucas")
>>> hford = Actor(nombre=u"Harrison Ford")
>>> mhamill = Actor(nombre=u"Mark Hamill")
>>> sweaver = Actor(nombre=u"Sigourney Weaver")
>>> session.commit()
 
>>> Persona.query.all()
[<Director "Ridley Scott">,  <Director "George Lucas">,
 <Actor "Harrison Ford">,  <Actor "Mark Hamill">,
 <Actor "Sigourney Weaver">]
>>> Actor.query.all()
[<Actor "Harrison Ford">, <Actor "Mark Hamill">, <Actor "Sigourney Weaver">]

Profundización

La web como fuente de información

La web se está transformando en una web de datos, pero muy poca información se sirve de forma estructurada y abierta:

Dificultades

  • Información poco o mal estructurada
  • Etiquetado no válido

Alternativas

  • Leer la información de la web y parsearla con herramientas de análisis textual (expresiones regulares, etc.)
from urllib import urlopen
URL = 'http://mipagina.com'
doc = urlopen(URL).read()
  • Usar parsers de html/xml. Estos parsers tiene que poder leer tagsoup porque se encontrarán con código no válido:
    • BeautifulSoup, lxml, amara.

Amara

  • Una muy rápida y con una sintaxis más cercana al xml
    import amara
    URL = '....'  # URL puede ser una url, una ruta de un fichero o una cadena de texto
    doc = amara.parse(URL)
  • Otra más amigable y fácil de utilizar, que convierte el xml en objetos python.

    from amara import bindery
    URL = '....'  
    doc = bindery.parse(URL)
    # si el html o xml puede no ser válido:
    from amara.bindery import html
    URL = '....'  
    doc = html.parse(URL)

Instalar

Para instalar la última versión:

$ sudo pip install http://files.akara.info/00-amara-latest.tar.bz2

Es necesario tener instalado un compilador de C y la cabeceras de python (en debian/ubuntu hay que instalar python-dev

Ejemplos habituales de uso

Búsqueda por expresiones XPATH

Icon present.gif
Tip: Hay herramientas como firebug que permite copiar el XPATH de un elemento.


Búsqueda de las imágenes de un artículo

>>> from amara.bindery import html
>>> URL = 'http://heraldo.es'
>>> doc = html.parse(URL)
>>> imagenes = doc.xml_select(u'//img') # las imágenes van en etiquetas img
>>> len(imagenes)
65
>>> primera_imagen = imagenes[0]
>>> print primera_imagen.xml_encode()
<img src="/MODULOS/global/publico/interfaces/img/logo-Heraldo.png" alt="Últimas noticias de Aragón, Zaragoza, Huesca y Teruel del periódico digital Heraldo.es"/>
>>> for im in imagenes:
        print im.src
/MODULOS/global/publico/interfaces/img/logo-Heraldo.png
/uploads/imagenes/iconos/titulos/jmj.jpg
/uploads/imagenes/rec70/_cuatrovientos6_011b2ad5.jpg
...

Búsqueda de las entradas de una revista Barrapunto publica sus entradas como

<div class="article">
<div class="generaltitle">
	<div class="title">
		<h3>
			<a href="//softlibre.barrapunto.com/">Software Libre</a>: Todo listo para la celebración de los 20 años de Linux
 
		</h3>
	</div>
</div>
<div class="details">
...		
</div>
...
</div>

Para extraer los nombres de los artículos de la primera página:

>>> from amara.bindery import html
>>> from amara.lib import U  # Extrae los nodos de texto de un fragmento
>>> articulos = doc.xml_select(u'//div[@class="article"]')
>>> len(articulos)
15
>>> for ar in articulos:
	print U(ar.div).strip()  # Navega por el nodo artículo.
                                 # Cuidado con los espacios en blanco y saltos
 
Software Libre: Todo listo para la celebración de los 20os de Linux
Publicado SmartOS, sistema operativo basado en Illumos
Un dispositivo permite a los invidentes ver a través de su lengua
El fin de la ley de Moore
...

Más ejemplos en http://wiki.xml3k.org/Amara/Recipes

Inyección de marcado

Se puede transformar un documento para añadirle o quitarle información. En este ejemplo añadimos unos links a los nombre de los autores, detectados por expresiones regulares: https://github.com/zepheira/amara/blob/master/demo/inject_markup.py