Contenidos
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:
- xlrd
- http://pypi.python.org/pypi/xlrd
- Lee datos y formato de ficheros .xls
- Documentación de la API: https://secure.simplistix.co.uk/svn/xlrd/trunk/xlrd/doc/xlrd.html
- xlwt
- http://pypi.python.org/pypi/xlwt
- Escribe datos y formatos a ficheros .xls
- Doc: https://secure.simplistix.co.uk/svn/xlwt/trunk/xlwt/doc/xlwt.html
- Ejemplos: https://secure.simplistix.co.uk/svn/xlwt/trunk/xlwt/examples/
- xlutils
- http://pypi.python.org/pypi/xlutils
- Colección de utilidades que usan xlrd y xlwt:
- Doc y ejemplos: https://secure.simplistix.co.uk/svn/xlutils/trunk/xlutils/docs/
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
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
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
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
- http://wiki.python.org/moin/HigherLevelDatabaseProgramming
- SQLAlchemy: http://www.sqlalchemy.org/
- Elixir: http://elixir.ematia.de/
- SQLObject: http://sqlobject.org
- Storm: https://storm.canonical.com/
- Autumn: http://autumn-orm.org/
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
- Documentación de elixir: http://elixir.ematia.de/trac/wiki
- Documentación de SQLAlchemy: http://www.sqlalchemy.org/
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
- http://wiki.xml3k.org/Amara (Tutorial)
- Tiene dos interfaces:
-
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
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 20 años 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