Category: python

  • Python e il metodo setdefault

    Note to self (pun intended): questo post rappresenta una sorta di appunto mentale per ricordarmi il funzionamento di setdefault in Python, visto che puntualmente mi trovo ad utilizzarlo.

    setdefault è utilizzato sui dizionari (dict) e permette di impostare una chiave di default durante una set. Mi spiego meglio con un esempio:

    >>> D
    {1: 'leonard', 2: 'sheldon', 3: 'wolowitz', 4: 'rajesh'}
    

    Vediamo come funziona il metodo setdefault:

    >>> D.setdefault(1, 'penny')
    'leonard'
    >>> D
    {1: 'leonard', 2: 'sheldon', 3: 'wolowitz', 4: 'rajesh'}
    >>> D.setdefault(5, 'penny')
    'penny'
    >>> D
    {1: 'leonard', 2: 'sheldon', 3: 'wolowitz', 4: 'rajesh', 5: 'penny'}
    

    Ovvero, setdefault valuta la chiave passata come primo argomento:

    • se è già presente nel dizionario, viene ritornato il valore della chiave presente nel dizionario;
    • altrimenti viene aggiunta al dizionario con il valore specificato come argomento.

    in termini pythonici:

    def setdefault(d, key, val):
        if key in d:
            return d[key]
        else:
            d[key] = val
    
  • Python: scriviamo un quine

    Una sfida che intriga molti programmatori è quella di scrivere un quine, ovvero quello di scrivere un programma che stampi il proprio sorgente.

    Partiamo con la definizione più semplice, e man mano aggiungiamo i vincoli necessari. Un programmatore furbo potrebbe stampare il contenuto del sorgente semplicemente aprendolo e stampando il contenuto:

    import sys
    print open(sys.argv[0],'r').read()
    

    Per agevolare il compito di confronto, usiamo diff (in modalità unified), e quindi inseriamo il Python hashbang e modifichiamo il print in modo che non vada a capo:

    #! /usr/bin/env python
    import sys
    print ''.join(open(sys.argv[0],'r').readlines()),
    

    Il confronto è presto fatto:

    $ ./quine.py | diff  -u - quine.py
    $
    

    (D’ora in poi, avendo inserito un alias nel .bashrc, assumerò di usare sempre diff unified).

    Abbiamo creato il nostro primo quine, ma abbiamo imbrogliato: leggere il file di input è considerato come barare; dobbiamo quindi ideare un metodo alternativo.

    Consideriamo quindi un programma che, data una stringa hardcoded, la stampi. Il problema non è banale, infatti ci potremmo addentrare in una ricorsione infinita se non studiamo un modo “furbo” per stampare il sorgente del programma:

    #! /usr/bin/env python
    import sys
    source = [
    "import sys" ,
    source = [ ... ] , # ricorsione infinita!
    "if __name__ == ‘__main__’:" ,
    "for i in source:" ,
    "print i"
    ]
    
    if __name__ == ‘__main__’:
    for i in source:
    print i
    

    Studiamo un metodo alternativo per stampare il sorgente: nella stringa che rappresenta il sorgente (source); il segreto è che usare un carattere speciale (es. $$) per dire al programma di stampare la stringa stessa!

    #! /usr/bin/env python
    import sys
    source = [
    "import sys" ,
    source = $$
    "if __name__ == '__main__':" ,
    "for i in source:" ,
    "print i"
    ]
    
    if __name__ == ‘__main__’:
    for i in source:
    if ‘$$’ in i:
    # stampiamo la rappresentazione della stringa che rappresenta il codice sorgente
    else:
    print i
    

    Per terminare il quine, dobbiamo espandere la sezione che manca; è importante prestare attenzione agli string literal (lettura consigliatissima!) per effettuare una stampa corretta delle stringhe nel programma (con i caratteri di escape corretti).
    Dopo aver ottenuto un primo esempio, armati di diff e di pazienza, dovremo correggere eventuali piccoli problemi (ad esempio: le “,” a fine riga per gli elementi della lista sourcecode — la virgola c’è per tutti gli elementi, tranne che per l’ultimo!).
    Volete la soluzione?

    #!/usr/bin/env python
    import string
    source = [
    '#!/usr/bin/env python' ,
    'import string' ,
    'source = $$' ,
    'if __name__ == "__main__":' ,
    'for i in source:' ,
    'if i == "source = $$":' ,
    'print i[0:i.find("$")] + "["' ,
    'for i in source:' ,
    '''if string.count(i,"'") != 0:''' ,
    '''print "'"*3 + i + "'"*3,''' ,
    'else:' ,
    '''print "'" + i + "'",''' ,
    'if source.index(i) < len(source)-1:' ,
    'print ","' ,
    'else:' ,
    'print ""' ,
    'print "]"’ ,
    ‘else:’ ,
    ‘print i’
    ]
    if __name__ == "__main__":
    for i in source:
    if i == "source = $$":
    print i[0:i.find("$")] + "["
    for i in source:
    if string.count(i,"'") != 0:
    print "'"*3 + i + "'"*3,
    else:
    print "'" + i + "'",
    if source.index(i) < len(source)-1:
    print ","
    else:
    print ""
    print "]"
    else:
    print i
    

    [notate come ho evitato alcuni problemi degli string literals tramite triple quoting (”’) e il costrutto di ripetizione (“”*3)].

    Da un rapido confronto tra l’output del programma e il sorgente, a meno di indentazioni del codice sorgente (richieste da Python), possiamo dire che dopo tanta fatica, abbiamo creato il nostro primo quine!

    Il prossimo passo sarebbe quello di ottimizzare il codice: short is better. Ma io non mi dilungherò oltre: aprite un editor e datevi da fare!

  • Trenitalia e Viaggiatreno: come implementare un servizio “fai da te” per avere le informazioni dello stato di un treno via SMS

    Da quando sono diventato un pendolare (per lavoro), ho iniziato a (ri)frequentare assiduamente le stazioni ferroviarie e i treni di Trenitalia.

    station.

    Dopo un paio di settimane passate sui treni, mi sono accorto di un servizio interessante: il sito Viaggiatreno: il sito contiene le informazioni sui treni (informazioni in tempo reale sullo stato di un treno come ritardo o anticipo, ora prevista di arrivo e fermate già effettuate).

    Esigenza

    Dopo aver provato il disagio di un treno soppresso (e quindi essermi svegliato un’ora prima per stare un’ora ad aspettare in stazione), mi sono reso conto di poter, perlomeno, arginare il problema se si fosse presentato in futuro; conoscere lo stato del treno direttamente sul telefonino mi permetterebbe, la mattina, di poter scegliere un mezzo alternativo senza recarmi in stazione nel caso in cui il treno sia in ritardo o soppresso. Oppure, se il treno è in anticipo, di accelerare i tempi.

    Soluzioni esistenti

    • Potrei controllare il sito Viaggiatreno accedendo il computer, connettendomi a Internet e andando sul sito Viaggiatreno. Contro: il tempo richiesto è troppo (~ 5 minuti. E la mattina anche 5 minuti sono preziosissimi).
    • Potrei installare su iPhone l’applicazione ProntoTreno, un’applicazione che fornisce lo stato di tutti i treni in tempo reale. Contro: il mio access point WIFI non è sempre attivo; non ho a disposizione la connessione 3G. Inoltre, se dovessi cambiare telefono per passare ad un telefono senza WIFI/3G, questa soluzione non è più applicabile.

    L’idea

    Il mezzo da utilizzare è sicuramente il mobile. L’incognita era semplicemente trovare il modo per raggiungere i telefoni, ed è stato individuato negli SMS, supportati da tutti i telefoni.

    L’altro fattore da tenere in considerazione è: come ottenere i dati da Viaggiatreno? Il sito utilizza Flash, ed estrarre informazioni testuali e puntuali da Flash penso sia infattibile. La soluzione è presto trovata: fortunatamente, Viaggiatreno offre anche una versione mobile, realizzata in (X)HTML, per la gioia dei parser.

    La realizzazione

    Per la realizzazione del servizio abbiamo bisogno di:

    • Un server Linux attivo e connesso a Internet durante il lasso di tempo in cui vogliamo essere avvisati via SMS. Inoltre, il server Linux deve avere installato crontab, l’interprete python e alcuni pacchetti python che vedremo più avanti.
    • Un servizio per mandare SMS gratuitamente: io mi appoggio a virgilio.it, che mi permette di ricevere via sms tutte le email ricevute all’indirizzo indirizzo@sms.tin.it [per i più impazienti: in questo modo per ricevere SMS è sufficiente mandare una mail]. Se invece non potete usare virgilio.it, vi raccomando di utilizzare MoioSMS per l’invio gratuito di SMS.
    • Un account GMail da cui mandare la mail del punto sopra (se siete degli smanettoni, non avete bisogno di questo e potrete usare il vostro SMTP, a patto di sostituirlo nel codice).
    • Un telefono mobile abilitato alla ricezione di SMS e attivo (ovviamente!)
    • Il numero del treno di nostro interesse: per conoscere il numero del treno, è sufficiente andare su TreniItalia, immettere partenza e arrivo e annotarsi il numero del treno. Il numero del treno è unico e non cambia.
    • Lo script python seguente, che ho scritto in Python utilizzando le librerie python-mechanize e python-beautifulsoup (installatele seguendo la guida per la vostra distribuzione. Per Ubuntu/Debian ho usato apt-get install python-mechanize python-beautifulsoup). Il codice è ancora immaturo, ma funziona (vedi screenshot):
      “`
      #! /usr/bin/python

      import BeautifulSoup
      import mechanize
      import smtplib
      import time
      import datetime
      from email.MIMEMultipart import MIMEMultipart
      from email.MIMEBase import MIMEBase
      from email.MIMEText import MIMEText
      from email import Encoders
      import os
      import sys

      gmail_user = "gmail username"
      gmail_pwd = "gmail pass"

      def mail(to, subject, text):
      msg = MIMEMultipart()

      msg['From'] = gmail_user
      msg['To'] = to
      msg['Subject'] = subject

      msg.attach(MIMEText(text))

      mailServer = smtplib.SMTP("smtp.gmail.com", 587)
      mailServer.ehlo()
      mailServer.starttls()
      mailServer.ehlo()
      mailServer.login(gmail_user, gmail_pwd)
      mailServer.sendmail(gmail_user, to, msg.as_string())
      mailServer.close()

      numtreno = sys.argv[1]
      stazione = sys.argv[2]

      tobeparsed = mechanize.urlopen(str(str('https://mobile.viaggiatreno.it/viaggiatreno/mobile/scheda?numeroTreno=') + str(numtreno) + str('&tipoRicerca=numero&lang=IT')))
      f = BeautifulSoup.BeautifulSoup(tobeparsed)
      f = f.prettify()
      f = f[f.find('<!– SITUAZIONE –>'):]
      f = f[:f.find('</div>')]
      s1 = f[f.find('<strong>')+len('<strong>'):f.find('<br />')]
      s2 = f[f.rfind('–>')+len('–>'):f.find('</strong>')]
      s1 = s1.replace(''',"'")
      s1 = s1.split()
      s2 = s2.split()
      tobeparsed = mechanize.urlopen(str(str('https://mobile.viaggiatreno.it/viaggiatreno/mobile/scheda?dettaglio=visualizza&numeroTreno=') + str(numtreno) + str('&tipoRicerca=numero&lang=IT')))
      f = BeautifulSoup.BeautifulSoup(tobeparsed)
      f = f.prettify()
      f = f[f.find(stazione):]
      f = f[:f.find("</div>")]
      arr1 = f[f.find('')+len('<p>'):f.find('<br />')]
      or1 = f[f.find('<strong>')+len('<strong>'):f.find('</strong>')]
      arr = f[f.rfind('<p>'):f.rfind('</p>')]
      arr2 = arr[arr.find('<p>')+len('<p>'):arr.find('<br />')]
      or2 = arr[arr.find('<strong>')+len('<strong>'):arr.find('</strong>')]
      arr1 = arr1.split()
      or1 = or1.split()
      arr2 = arr2.split()
      or2 = or2.split()
      arr1.extend(or1)
      arr2.extend(or2)

      L = [s1, s2, arr2]
      for i in xrange(len(L)):
      L[i] = ' '.join(L[i])
      message = '\n'.join(L)

      mail("indirizzo@sms.tin.it",
      sys.argv[1],
      message,
      )
      “`

      Il codice estrae le informazioni di interesse da Viaggiatreno mobile (come detto, per fare il parsing occore usare il sito in versione mobile) come lo stato corrente del treno (posizione ed eventuali minuti di ritardo o anticipo) e l’orario di arrivo previsto e manda queste informazioni all’indirizzo mail “speciale” che gira il messaggio al mio numero di cellulare via SMS [per l’invio della mail il servizio usa GMail]. Come detto, potreste usare MoioSMS (via pipe o python import) semplicemente sostituiendo la parte di invio della mail.

    Come si utilizza?

    • Lo script accetta due parametri da command-line: il numero di treno a cui siete interessati e la stazione di cui volete conoscere l’orario di arrivo stimato (es. “LECCO MAGGIANICO”).
    • Modificate, nello script le variabili gmail_user e gmail_password con le credenziali del vostro account GMail che useremo per spedire la mail verso l’indirizzo speciale che girerà le mail in SMS. Sempre nel codice, sostituite appunto indirizzo@sms.tin.it con il vostro indirizzo “speciale” che tramuta il messaggio e-mail in SMS.
    • Usando crontab, inserite una entry del tipo:
      08 07 * * 1-5 python /home/mbologna/parser.py 5033 “LECCO MAGGIANICO”
      Per avere un aggiornamento via SMS alle 07:08 di tutti i giorni feriali del treno 5033 specificando che deve fornirci l’orario di arrivo previsto alla stazione di “LECCO MAGGIANICO”.

    Risultati

    La mattina, prima di recarmi in stazione, controllo sul mio telefono e automaticamente ricevo 3 SMS gratuiti diversi contenenti lo stato del treno man mano che si avvicina alla stazione in cui io salgo sul treno. In questo modo, ancora prima di uscire di casa, posso sapere se il treno è in ritardo o se è stato soppresso.

    Qui accanto, la testimonianza di 2 degli SMS che ricevo quotidianamente.

    Sviluppi futuri

    • Miglioramento del codice di download/parsing
    • Integrazione con MoioSMS

    Curiosità

  • Python: come ordinare un dizionario [dict] per chiave o per valore

    Python: come ordinare un dizionario [dict] per chiave o per valore

    In Python, i dizionari (dict) sono una struttura dati associativa che associa una chiave ad un valore (k, v):

    d = {}
    d['bart'] = 'marge'
    d['maggie'] = 'homer'
    d['milhouse'] = 'nelson'
    d['lisa'] = 'skinner'
    >>> d
    {'maggie': 'homer', 'lisa': 'skinner', 'bart': 'marge', 'milhouse': 'nelson'}
    

    Esistono due modi di ordinare un dizionario:

      • La modalità “classica” e più conosciuta: ovvero ordinare il dizionario sulla base delle chiavi che lo compongono:
    for k in sorted(d.keys()):
    print k + ' : ' + d[k]
    
    bart : marge
    lisa : skinner
    maggie : homer
    milhouse : nelson
    
      • La modalità meno conosciuta e che ho dovuto ricercare [e che quindi condivido con voi] ovvero quella di ordinare il dizionario sulla base dei valori che sono associati alle chiavi:
    sorted(d.items(), key=lambda(k,v):(v,k))
    >>> [('maggie', 'homer'), ('bart', 'marge'), ('milhouse', 'nelson'), ('lisa', 'skinner')]
    

    Come vedete questa volta abbiamo ottenuto una lista degli elementi ordinati secondo il valore associato ad ogni chiave. Può tornare utile!

    Esistono molteplici versioni di quest’ultima istruzione, ma questa ritengo che sia la più compatta e veloce.

  • Python: uno script per eliminare i file Thumbs.db e le cartelle .DS_Store

    Windows XP crea automaticamente un file, chiamato Thumbs.db, in tutte le cartelle che visita per salvarne la visualizzazione (dettagli, anteprima, etc.). La stessa cosa fa OSX, creando addirittura (!) una cartella dal nome .DS_Store, contenente gli stessi attributi.

    Stanco di avere il mio hard disk pieno di questi file (in Vista la gestione di questi file è intelligentemente centralizzata), ho creato uno script Python che analizza ricorsivamente una directory (specificata dall’utente) e stampa tutte le posizioni di questi file e directory indesiderati.image

    Lo script richiede esattamente due argomenti: il path da cui si vuole iniziare ad analizzare ricorsivamente la presenza di file e directory indesiderate, e una stringa che conferma l’azione: nel caso in cui l’opzione sia delete, lo script cancella i file desiderati, altrimenti stamperà soltanto le loro posizioni.

    Esempio:
    python recursiveDeleteUnwantedFilesAndDir.py C:\ print

    stampa la posizione dei i files/directory indesiderati

    python recursiveDeleteUnwantedFilesAndDir.py C:\ delete

    stampa la posizione dei i files/directory indesiderati e li elimina dal disco

    Veniamo al codice sorgente:

    import os
    import sys
    
    def recursiveDeleteUnwantedFilesAndDir(path,action):
        for root, dirs, files in os.walk(path):
            for name in files:
                if name == 'Thumbs.db':
                    print os.path.join(str(root),str(name))
                    if action == 'delete':
                        os.remove(os.path.join(str(root),str(name)))
            for name in dirs:
                if name == '.DS_Store':
                    print os.path.join(str(root),str(name))
                    if action == 'delete':
                        os.removedirs(os.path.join(str(root),str(name)))
    
    recursiveDeleteUnwantedFilesAndDir(sys.argv[1],sys.argv[2])
    

    Per scaricare lo script: recursiveDeleteUnwantedFilesAndDir.zip

  • Python: come mandare un e-mail con GMail TLS/SSL

    Mi è capitato di dover mandare un e-mail da uno script Python: quale provider utilizzare? Ovviamente GMail!

    Per questioni di spam, alcuni server SMTP non permettono di inviare e-mail senza essersi prima autenticati; nel caso di GMail, il protocollo per l’invio di mail non è il semplice SMTP in chiaro, bensì SMTP con autenticazione via TLS/SSL.

    Ecco lo script Python che vi permetterà di mandare un’e-mail con GMail tramite il vostro account GMail:

    #!/usr/bin/python
    
    import smtplib
    import time
    import datetime
    from email.MIMEMultipart import MIMEMultipart
    from email.MIMEBase import MIMEBase
    from email.MIMEText import MIMEText
    from email import Encoders
    import os
    
    gmail_user = "username@gmail.com"
    gmail_pwd = "password"
    
    def mail(to, subject, text):
    msg = MIMEMultipart()
    
    msg['From'] = gmail_user
    msg['To'] = to
    msg['Subject'] = subject
    
    msg.attach(MIMEText(text))
    
    mailServer = smtplib.SMTP("smtp.gmail.com", 587)
    mailServer.ehlo()
    mailServer.starttls()
    mailServer.ehlo()
    mailServer.login(gmail_user, gmail_pwd)
    mailServer.sendmail(gmail_user, to, msg.as_string())
    mailServer.close()
    
    file = open('body.txt','r')
    document = file.read()
    mail("dst_addr@gmail.com",
    "Subject",
    document,
    )