De la web al model


Escrit per Aaloy a 29 de July , 2011 a les 6:04 p.m.

L'scrapping

Ahir ja vàrem veure quin era el model de dades i el bé que va sorl per a manipular imatges, així com la utilització de la llibreria requests ens quedava veure una altra part important: com agafar el contingut de la web, parsejar-lo i obtenir-ne la informació que necessitam.

Per fer això hi ha diverses utilitats, algunes molt especialitzades com scrappy, i amb més solera és BeautifulSoup. Aquesta llibreria té la qualitat de ser molt permisiva amb l'HTML i hi ha poques planes que no pugui tractar d'una manera o altra.

La plana de Meneame té las notícies de portada dins un div anomenat news-summari, així que el primer que farem serà carregar la plana dins una instància de BeautifulSoup i cercar aquestes notícies.

page = requests.get('http://www.meneame.net')
if page.status_code != 200:
    print "Ups! pareix que hi ha un petit problema"
    return page.status_code
soup = BeautifulSoup(page.content)
noticies = soup.findAll('div', 'news-summary')

amb això BS ens haurà donat tots els divs que tenen la classes 'news-summary' amb la qual cosa ja és sols cosa d'aplicar un tractament semblant per a obtenir la informació de cada notícia

for noticia in noticies:
    titular = noticia.find('h1').text
    texto = noticia.find('p').text
    img = noticia.find('img', 'thumbnail')
    if img:
        src = img['src']
        match_obj = compile_obj.search(src)
        id = match_obj.group('id')
        try:
            NoticiasPortada.objects.get(identificador=id)
        except NoticiasPortada.DoesNotExist:
            print u"Tractant la noticia: %s" % id
            noticia = NoticiasPortada(
                identificador = id,
                texto = texto,
                titular = titular,
                thumbnail = download_image(src,           
                                     'thumb-%s.jpg' % id)
            )
            noticia.save()

El mètode find ens permet a accedir al primer tag que compleix la condició i text ens en dona el contingut. Si com a segon paràmetre hi possam una classe ens retornarà el primer element d'aquell tipus que tengui la classe que li hem donat.

El problema ve quan volem identificar d'alguna manera les notícies. Volem guardar sols les que tenen una imatge. Podem veure que Meneame genera el thumbnail de la notícia amb l'identificador de la mateixa, així que podem fer us d'una expressió regular:

rawstr = r"""(?P<id>\d+).jpg$"""
compile_obj = re.compile(rawstr)

així

src = img['src']
match_obj = compile_obj.search(src)
id = match_obj.group('id')

ens donarà l'identificar de la notícia a partir de la informació de la url de la imatge. Hi ha una utilitat fantàstica per a la depuració i testeig d'expressions regulars anomenada Kodos, no us la podeu perdre.

El toc final

En una aplicació com aquesta la CPU està molt de temps sense fer res, esperant que li arribi la informació. És un bon candidat per a que l'aplicació faci ús dels Threads o del multiprocés. Una de le maneres més senzilles de fer-ho és fent servir la llibreria Queue, d'aquesta manera sols hem de definir quants Threads farem servir en el processament i que aquests consumeixin el contingut (cada notícia) de la cua.

Així el codi final quedaría si fa no fa:

import re
import cStringIO
import requests
from Queue import Queue
from threading import Thread
from BeautifulSoup import BeautifulSoup

from django.core.management.base import BaseCommand
from django.core.files.uploadedfile import SimpleUploadedFile

from t1.models import NoticiasPortada

rawstr = r"""(?P<id>\d+).jpg$"""
compile_obj = re.compile(rawstr)

class Command(BaseCommand):
    """Divertimento. Permet posar les notícies amb foto
    de meneame dins una BD.
    """

    def handle(self, *args, **options):
        page = requests.get('http://www.meneame.net')
        if page.status_code != 200:
            print "Ups! Pareix que tenim un problema"
            return page.status_code

        #cream les coes
        self.q = Queue()
        for i in range(5):
            t = Thread(target = self._importar_portada)
            t.daemon = True
            t.start()

        soup = BeautifulSoup(page.content)
        noticies = soup.findAll('div', 'news-summary')
        for noticia in noticies:
            self.q.put(noticia)
        self.q.join()

    def download_image(self, img_url, filename):
        r = requests.get(img_url)
        if r.status_code == 200:
            return SimpleUploadedFile(content=r.content,
                name=filename,
                content_type=r.headers.get('content-type'))
        else:
            return None

    def _importar_portada(self):
        while True:
            noticia = self.q.get()
            titular = noticia.find('h1').text
            texto = noticia.find('p').text
            img = noticia.find('img', 'thumbnail')
            if img:
                src = img['src']
                match_obj = compile_obj.search(src)
                id = match_obj.group('id')
                try:
                    NoticiasPortada.objects.get(identificador=id)
                except NoticiasPortada.DoesNotExist:
                    print u"Processant la notícia: %s" % id
                    noticia = NoticiasPortada(
                        identificador = id,
                        texto = texto,
                        titular = titular,
                        thumbnail = self.download_image(src, 
                          'thumb-%s.jpg' % id)
                    )
                    noticia.save()
            self.q.task_done()

I això és tot, com sempre amb Python l'explicació sol ser molt més llarga que el codi a executar, fins i tot amb els fils.

Esperant que us hagi agradat el divertimento.


Traducciones/Translations by apertium

0 comentaris, 0 trackbacks (URL) , Tags: Python Django


Imatges de la web al model Django


Escrit per Aaloy a 28 de July , 2011 a les 5:42 p.m.

L'altra dia estava fent una aplicació web part de la qual consisteix en aprofitar els continguts de la web anterior, continguts als que no tenim accés directe.

La part de text va ser senzilla, però obtenir una imatge de la web i associar-la a un model Django va resultar una mica més interessant del que suposava. Tant que vaig pensar que potser convenia posar-ho en forma d'apunt.

Per posar-ho en context vaig començar a fer una aplicació Django, la qual tenia que obtenir la imatge i guardar-la, però ja que hi era, vaig voler fer quelcom més educatiu, així que l'aplicació es va anant transformant amb codi per a descarregar-se les fotografies associades a la plana principal de meneame. Si provau el codi mirau de canviar la url que ja que amablement Ricardo em va dir que no hi havia problema en fer algunes proves, tampoc és cosa que anem putejant la web.

Així doncs, aquest apunt serà un poc mescladissa d'utilitats, d' screen scrapping, de thread, processos i altres herbes. Miraré d'anar a poc a poc, però ja us aviso que hi ha de tot i molt!

Definim el model

El més habitual quan hom tracta amb imatges és tenir-ne que fer algun tipus de conversió, si més no per presentar-les a l'administrador de Django. És tan habitual que quan he de fer servir un camp ImageField de Django ja ho converteixo directament a un camp ImageField d'una llibreria que va ideal per a la manipulació d'imagtes sorl.thumbnail.

Així doncs, el nostre model serà molt senzill, tindrà un identificador per poder distingir les notícies, el titular, el texte de la notícia i com no la imatge.

M'ha queda talment així:

!/usr/bin/env python
# -*- coding: UTF-8 -*-
from django.db import models
from sorl.thumbnail import ImageField
from sorl.thumbnail import get_thumbnail

class NoticiasPortada(models.Model):
    """Base de datos de noticias"""

    def get_upload_path(instance, filename):
        return "fotos/%s" % filename

    identificador = models.IntegerField()
    titular = models.CharField(max_length=200)
    texto = models.TextField()
    thumbnail = ImageField(upload_to=get_upload_path, 
              blank=True, null=True)

    def img(self):
        if self.thumbnail:
            try:
                im = get_thumbnail(self.thumbnail, 
                                     '50x50', crop='center', quality=80)
                return '<img src="%s">' % im.url
            except IOError:
                return "error"
        else:
            return "%s" % self.identificador
    img.allow_tags = True
    img.short_description = 'img'

    def __unicode__(self):
        return self.titular

Com es pot veure Sorl incorpora una sèrie d'utilitats per a la conversió d'imatges que ens van molt bé. Així podem definir el mètode img dins el model per tal de poder-ne fer referència a l'administrador de d'Django.

Fixem-nos també com li podem passar un mètode per tal de poder calcular a quin lloc es deixarà la imatge. Aquest mètode ha de tenir la signatura nomfuncio(instància, nom) on instància és la instància de l'objecte al qual s'associarà la imatge i el nom és el nom de la imatge en sí. Això és molt útil per poder deixar les imatges a diferents carpetes segons convingui.

A l'admin.py podem fer

from django.contrib import admin
from models import NoticiasPortada

class NoticiasPortadaAdmin(admin.ModelAdmin):
    list_display = ('img', 'identificador', 'titular')
    search_fields = ('titular', 'texto')

admin.site.register(NoticiasPortada, NoticiasPortadaAdmin)

Sorl té opcions per a que al formulari se'ns presenti també la imatge, però per ara ho deixarem així.

La càrrega de les imatges

La càrrega de les imatges la podríem haver fet de moltes maners, però com que ja vaig posar un article de com fer comandes de Django, doncs ho aprofitaré. Crearem una comanda anomenada importar que es podrà cridar com

python manage.py importar

Per això s'ha de crear un paquet Python dins l'aplicació amb l'estructura

app
    models.py
    management
        __init__.py
        commands
            __init__.py
            importar.py

Si feis servir django-extensions recordau que hi ha una comanda anomenada create_command que crea directament aquesta estructura.

Per a importar les imatges utilitzarem la classe InMemoryUpladedFile que es troba a django.core.files.uploadedfile. Podem passar una instància d'aquesta classe al nostre model dins l'atribut thumbnail i Django se n'encarregarà de la resta. La complexitat addicional està en que ens hem de davallar la imatge de la web.

def download_photo(self, img_url, filename, field_name):
    img = cStringIO.StringIO()
    image_on_web = urllib.urlopen(img_url)
    while True:
        buf = image_on_web.read(65536)
        if len(buf) == 0:
            break
        img.write(buf)
    image_on_web.close()
    return InMemoryUploadedFile(file=img,
            field_name= field_name, name=filename,
            content_type="image/jpeg", size=img.tell(), charset=None)

Per davallar-nos la imatge farem servir la llibreria urllib. Passant-li una url es capaç de llegir-la i donar-nos el contingut. Com que la memòria de moment no és un requeriment, el que farem serà deixar el contingut dins un buffer que es comporta a tots els efectes com un fitxer. Feis una ullada al duck typing per saber com és això possible.

InMemoryUploadedFile espera un fitxer (d'aquí el truc de fer servir StringIO), el nom del fitxer, el content_type, el tamany i el charset, així com el nom del camp.

De fet, però ho podem fer una mica més senzill, ja que no necessitam el nom del camp. Django té una altra classe que fa el cid més senzill, és el SimpleUploadedFile de manera que el codi queda un poc més net:

def download_photo_simple(self, img_url, filename):
    img = cStringIO.StringIO()
    image_on_web = urllib.urlopen(img_url)
    while True:
        buf = image_on_web.read(65536)
        if len(buf) == 0:
            break
        img.write(buf)
    image_on_web.close()
    return SimpleUploadedFile(content=img.getvalue(),
            name=filename,
            content_type="image/jpeg")

Però encara així és molta línea per a tan poca cosa. Em feia ganes provar una llibreria que promet simplificar el procés d'accés als recursos web. La llibreria requests. Teniu en compte que el codi amb urllib encara es complicaria més en una aplicació real, ja que convé controlar les excepcions que hi pot haver. El tema està força ben explicat a l'apunt urllib2 - The Missing Manual, així que no m'estendré més.

L'avantatge de requests és que ens permet abstreure'ns d'aquests excepcions i tractar únicament amb codis d'estat web. Així el que ens interessarà és obtenir la imatge si el codi és 200 (tot bé) o retornar un None si hi ha problemes. Així el codi es simplifica encara més.

def download_more_simple(self, img_url, filename):
    r = requests.get(img_url)
    if r.status_code == 200:
        return SimpleUploadedFile(content=r.content,
            name=filename,
            content_type=r.headers.get('content-type'))
    else:
        return None

Amb això, crear una instància de NoticiasPortada pot quedar com

noticia = NoticiasPortada(
    identificador = id,
    texto = texto,
    titular = titular,
    thumbnail = self.download_more_simple(src, 'thumb-%s.jpg' % id)
                )
 noticia.save()

on id és l'identificador de la notícia i src representa la url de la imatge.

Com ho treim a això? Doncs és una bona pregunta. A la segona part de l'article us ho explico. Fins demà!


Traducciones/Translations by apertium

0 comentaris, 0 trackbacks (URL) , Tags: Python Django


Slicing en Python


Escrit per Paurullan a 17 de July , 2011 a les 1:04 p.m.

Slicing en Python

O com l[::-1] gira una llista.

Resum

En Toni va comentar al darrer creantbits que es pot girar un string fent cadena[::-1]. Vos puc adelantar que no sols és la forma més clara pythonica d'escriurer-ho sinó que aquesta és la més ràpida. En aquest petit apunt exposarem els usos i perquè dels slices.

Què són els slices

Un slicing és el procés de seleccionar un rang d'elements d'un objecte de tipus seqüència. Aquest subconjunt pot ser creat de vàries maneres però la gràcia és que un cop generat es converteix en un objecte més i podem manipular-lo de manera tradicional. Cal recordar que els iteradors en python s'indexen des de zero i que podem usar un signe negatiu per començar a contar des del darrera. També és important fixar-se que els límits inferiors són tancats i els superiors oberts.

La sintaxi és:

iterable[inici:final:passa]   # la gramàtica
iterable[0]                   # el primer element
iterable[-1]                  # el darrer element
iterable[-3:]                 # els darrers tres elements
iterable[:-3]                 # fins als darrers tres
iterable[::2]                 # de dos en dos
iterable[::-4]                # de quatre en quatre des del darrera

Exemples

D'aquesta manera podem mostrar que:

# range per defecte fa de 0 fins al l'entorn obert
In [1]: l = range(10)
In [2]: l
Out[2]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Des del tercer fins al quart no inclòs (sols el tercer)
In [3]: l[2:3]
Out[3]: [2]

# Des del tercer fins al cinquè
In [4]: l[2:5]
Out[4]: [2, 3, 4]

# Des del segon fins a l'antepenúltim
In [5]: l[1:-2]
Out[5]: [1, 2, 3, 4, 5, 6, 7]

# De dos en dos
In [6]: l[::2]
Out[6]: [0, 2, 4, 6, 8]

# Des dos en dos començant pel darrera
In [7]: l[::-2]
Out[7]: [9, 7, 5, 3, 1]

# Des del segon fins a l'octau de tres en tres
In [8]: l[1:7:3]
Out[8]: [1, 4]

I no cal olvidar que les cadenes de text són al cap i a la fi llistes de caràcters, per la qual cosa podem fer:

In [1]: s = "Cadena de text"

# Inverteix la cadena
In [2]: s[::-1]
Out[2]: 'txet ed anedaC'

Conclusions

Hem vist que els slices són una ràpida i còmode manera de manipular objectes iterables, podent fer operacions comunes com invertir llistes o extreure elements pegant bots de tres en tres de manera molt simple. A més de ser la forma més pythonica és especialment lleugera i cal tenir-los en compte pel dia a dia.

Referències

http://docs.python.org/whatsnew/2.3.html#extended-slices

http://docs.python.org/reference/expressions.html#slicings

http://stackoverflow.com/questions/509211/good-primer-for-python-slice-notation


Traducciones/Translations by apertium

0 comentaris, 0 trackbacks (URL) , Tags: Python


Creantbits un 15 de juliol


Escrit per Aaloy a 16 de July , 2011 a les 11 a.m.

Ahir divendres 15 hi hagué una nova edició del creantbits. Aquesta vegada enlloc de que la inscripció es fes en un comentari al blog, ho ferem amb una aplicació creada ad-hoc i que serví per experimentar amb un hosting de Python. El hosting va caure un pic en el procés d'inscripció (després de tot encara està en beta), però la gent d'Eldarion va respondre i en poques hores estava una altra vegada operatiu.

L'aplicació en si crec que ha respost bastant bé, tot i estar feta en quatre potades. Ha permés a la gent que s'havia inscrit prest i després no ha pogut venir, fer-ho saber ràpidament i comunicar-ho al següent de la llista d'espera. Tot d'una que tengui una estona més miraré de documentar l'aplicació (que el codi ja hi està) i posar-ho a bitbucket per tal que si hi ha més gent que s'animi entre tots poguem fer un bon programa de gestió d'events.

De l'event en sí poca cosa a dir, la sala plena d'amics, gent que ja coneixia personalment i gent que he pogut desvirtualitzar per primera vegada. Però la part important és amics, això fa que parlar en públic sigui molt menys estressant i també molt més divertit. Va venir molta gent que fa coses interessants en programari lliure, Python o no, però que té pànic escènic. Encara que ja sabeu no tenc cap problema en parlar de Python hores i hores, també m'agradaria que el Creantbits fos un punt de trobada on els amics s'expliquen tecnologies interessants. Pensau que no xerrau davant un auditori estrany, sinó a un grupet de gent, d'iguals, i allò que per vosaltres pot ser el dia a dia potser sigui una revelació per altra gent. Així que animau-vos, que llevar-se la por escènica és important i res millor que fer-ho entre gent de confiança.

La valoració de les xerrades no seré jo qui les faci, esper que us resultessin interessants. Era la primera xerrada de @morenosan, l'home passava pena per si els nirvis el traïen, però ja vàreu veure que se'n va desfer prou bé. En Juan té moltes coses a dir en el món de la programació i noves tecnologies i esper que ara que ja sap que no passa res, s'animi a preparar-nos altres matèries.

En Bernat també tenia els seus dubtes, al matí va començar a dir que igual si no hi havia temps la conferència de Varnish no era tan important, que a lo millor no calia, ... Però no em vaig deixar convèncer i crec sincerament que va ser una de les exposicions més interessants que hem fet al creantbits.

Ja veis, el que costa més d'un event d'aquest és que la gent perdi la por, però és important fer-ho, perquè personalment estic convençut que en aquesta Illa nostra hi ha molta gent que fa coses molt interessants i que no les valora prou. Contava l'anècdota d'un conegut empresari madrileny que es dedica a fer webs per hotels, anava dient a tort i dret que havien fet un cms que admetia llenguatges no llatins, i això ho deia al 2011, nosaltres mateixos ja havíem fet webs en xinès al 2006, i ja no us cont Juan que estigué 5 anys al Japó. És, però un bon exemple de que potser no li donam la importància que es mereix a fer aquestes coses.

I una altra anècdota de l'event. Ens vàrem deixar els curetes per remenar el sucre. Però potser la millor anècdota de totes va der la de @SebaSj , que ens va dir que no podia venir perquè la filla estava en camí. Hem estat molt contents de saber que n'Helena ha fet gairebé tres kilos i que han passat bona nit. L'enhorabona!

El proper creantbis no sé quan serà. Depèn de la feina i de les ganes de la gent, però al manco ja sabem que n'Antònia està disposta a parlar-nos de Python i càlcul numèric, que potser en Xesc també s'animarà a fer alguna coseta i que en Pau quan aprovi la pràctica, té moltes ganes de parlar de Haskell.

Una abraçada a tots el que vinguéreu i disculpes al que quedàreu fora. L'aforament de la sala és el que és i per ara no tenim un lloc millor. La sala gran de l'auditòrium estareu d'acord amb mi que imposaria massa a l'hora de parlar. La por escènica és molt menor quan tens la gent propera i els oients estan agafant gominolas!


Traducciones/Translations by apertium

2 comentaris, 0 trackbacks (URL) , Tags: Informàtica Python Django APSL


Presuposts petits


Escrit per Aaloy a 06 de July , 2011 a les 8:16 p.m.

Avui m'ha telefonat un client d'un projecte relativament petit que tenim des de fa temps. Volia un pressupost d'una modificació que ha de fer de la web, molt poca feina però encara així vol un pressupost, supòs que més d'un s'haurà trobat en la mateixa situació.

El problema de fer un pressupost per una feina petita és que el cost de fer el pressupost pot superar amb escreix la feina en sí, però a més com que no hi ha pràcticament marge d'error el pressupost si està ben fet implica que t'has d'haver mirat ben bé totes les implicacions i considerar tota la feina que hi pot haver. És a dir, no es sols modificar una funcionalitat, és verificar que funciona, passar-la a pre, validar-la amb l'usuari i passar-la a producció.

Per aquests casos sóc partidari de la borsa d'hores. Tot és més fluid, no és necessari pressupost, i a més crec que és més just, ja que no has de carregar al client la feina de fer el pressupost.

El tema està en on traçar la ratlla, és a dir, quan convé fer un pressupost i quan tirar de borsa d'hores. Quan el client et demana un pressupost normalment és per saber si el cost li compensarà.

El compte és prou senzill, si contam que en durà una mitja d'una hora fer el pressupost (entre parlar amb el client, confirmar que és el que vol, fer la redacció i mirar-ho) i que el treball "petit" típic duu entre 3 i 5 hores, estam parlant que convé tirar de pressupost a partir de feines que pareix que et duran un dia o més. En cas contrari trob que el més just per tothom és tirar de borsa d'hores o "anar a jornal", però fixant un límits, m'explicaré:

En projectes petits a l'hora o dues de fer-hi feina ja veus ben bé l'abast del projecte i el que et durà. Llavors si la feina veus que s'allarga se li ha de dir al client, de manera que sols arrisca aquesta hora o dues i pot decidir amb coneixement.

D'altra manera crec que és menys just, ja que força a endevinar en tasques tan petites on l'estimació es fa pràcticament impossible i podem tenir desviacions de més del 100%.


Traducciones/Translations by apertium

4 comentaris, 0 trackbacks (URL) , Tags: Informàtica General