Contenidos
(En formato presentación)
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) |
En un modelo tradicional de bases de datos (ver charla de pycamp.orm:
MySQL <------------> dialecto_sql_1 <---> MySQLdb <----> Python PostgreSQL <-------> dialecto_sql_2 <---> psycopg2 <---> python Oracle <-----------> dialecto_sql_3 <---> cx_Oracle <--> Python
Esto dificulta las migraciones y la escalabilidad de las aplicaciones:
- Cada base de datos usa un dialecto.
- Al usar el driver de Python tenemos que usar ese dialecto. Una migración supone cambiar muchas sentencias al nuevo dialecto.
Cuando trabajamos con un ORM sólo usamos objetos Python. El ORM se encarga de traducir las sentencias al dialecto de la base de datos:
BASE_DE_DATOS <-----> ORM (SQLAlchemy, Elixir ...) <------> PYTHON
El mismo código Python funcionará en PostgreSQL, Oracle o MySQL.
Un ejemplo
Incrementar la edad de las personas que tienen 20 años: Sin ORM
cursor.execute(“SELECT * FROM personas WHERE edad=20") for row in cursor.fetchall(): id = row[0] edad = row[1] cursor.execute(“UPDATE personas WHERE id=%s SET edad=%d” % (id, edad+1))
Con ORM
for p in Personas.listado(edad=20): # ejecuta SELECT p.edad = p.edad + 1 p.update() # ejecuta UPDATE
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/
Web scraping: 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.)
Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems. – Jamie Zawinski
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
Expresiones XPATH útiles
# Nodo que contenga una cadena de texto: expresion = u'.//text()[contains(., "python")]' expresion = u'.//text()[contains(., "%s")]' % cadena.decode('utf-8') # Nodos o atributos que contengan una cadena: expresion = expr = u'.//@*[contains(., "%s")]'
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
- En este proyecto hacemos un uso intensivo de inyección de marcado en páginas web: https://bitbucket.org/lmorillas/iaaa/