fileinput - Elabora righe da flussi di input

Scopo Crea programmi filtro da riga di comando per elaborare righe da flussi di input
Versione Python 1.5.2 e superiore

Il modulo fileinput è una struttura per creare programmi da riga di comando per elaborare dei file di testo in modo "filtrato".

Convertire file M3U in RSS

Ad esempio l'applicazione m3utorss - scritta dall'autore dell'articolo originale Doug Hellmann, creata per il suo amico Patrick (n.d.t.) - per convertire alcune incisioni demo in un formato che possa essere trasmesso in podcast.

Gli input per il programma sono uno o più file m3u contenenti l'elenco dei file mp3 da distribuire. L'output è un singolo file XML, che assomiglia ad un feed RSS (l'ouput viene scritto allo stdout, per semplicità). Per elaborare l'input, occorre iterare attraverso l' elenco di nomi di file e:

  • Aprire ciascun file.
  • Leggere ogni riga del file.
  • Identificare se la riga fa riferimento ad un file mp3.
  • In caso positivo, estrarre le informazioni dal file mp3 necessarie al feed RSS.
  • Stampare il risultato.

Avrei potuto (Doug Hellmann - n.d.t.) scrivere la parte che gestisce i file manualmente. Non è così complicato, e con qualche test sono sicuro che avrei potuto anche gestire correttamente gli errori. Ma con il modulo fileinput non devo preoccuparmi di questo. Basta semplicemente scrivere qualcosa del tipo:

for line in fileinput.input(sys.argv[1:]):
    mp3filename = line.strip()
    if not mp3filename or mp3filename.startswith('#'):
        continue
    item = SubElement(rss, 'item')
    title = SubElement(item, 'title')
    title.text = mp3filename
    encl = SubElement(item, 'enclosure', {'type':'audio/mpeg', 'url':mp3filename})

La funzione fileinput.input() riceve come parametro una lista di nomi di file da esaminare. Se la lista è vuota, il modulo legge i dati dallo standard input. La funzione ritorna un iteratore che a sua volta restituisce le singole righe dal file che sta elaborando. Quindi, tutto quello che si deve fare è eseguire un ciclo per ogni riga, saltando quelle vuote ed i commenti, per trovare i riferimenti ai file mp3.

Ecco il listato completo del programma:

import fileinput
import sys
import time
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom

# Impostazione dei nodi RSS e channel
rss = Element('rss', {'xmlns:dc':"http://purl.org/dc/elements/1.1/",
                      'version':'2.0',
                      })
channel = SubElement(rss, 'channel')
title = SubElement(channel, 'title')
title.text = 'Sample podcast feed'
desc = SubElement(channel, 'description')
desc.text = 'Generated for PyMOTW'
pubdate = SubElement(channel, 'pubDate')
pubdate.text = time.asctime()
gen = SubElement(channel, 'generator')
gen.text = 'http://www.doughellmann.com/PyMOTW/'

for line in fileinput.input(sys.argv[1:]):
    mp3filename = line.strip()
    if not mp3filename or mp3filename.startswith('#'):
        continue
    item = SubElement(rss, 'item')
    title = SubElement(item, 'title')
    title.text = mp3filename
    encl = SubElement(item, 'enclosure', {'type':'audio/mpeg', 'url':mp3filename})

rough_string = tostring(rss)
reparsed = minidom.parseString(rough_string)
print reparsed.toprettyxml(indent="  ")

ed il suo risultato:

$ python fileinput_example.py sample_data.m3u
<?xml version="1.0" ?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>
      Sample podcast feed
    </title>
    <description>
      Generated for PyMOTW
    </description>
    <pubDate>
      Thu Mar 25 10:49:28 2010
    </pubDate>
    <generator>
      http://www.doughellmann.com/PyMOTW/
    </generator>
  </channel>
  <item>
    <title>
      episode-one.mp3
    </title>
    <enclosure type="audio/mpeg" url="episode-one.mp3"/>
  </item>
  <item>
    <title>
      episode-two.mp3
    </title>
    <enclosure type="audio/mpeg" url="episode-two.mp3"/>
  </item>
</rss>

Progressione dei Meta-dati

Nell'esempio precedente, non interessa quale file o numero di riga si sta processando in fase di input. Per altri strumenti (ricerche sul tipo di grep, ad esempio) queste informazioni potrebbero interessare. Il modulo fileinput include le funzioni per accedere a quelle informazioni (filename() restituisce il nome del file attualmente in lettura, filelineno() restituisce il numero di righe lette nel file corrente, lineno() restituisce il numero totale di righe lette, ecc.).

import fileinput
import re
import sys

pattern = re.compile(sys.argv[1])

for line in fileinput.input(sys.argv[2:]):
    if pattern.search(line):
        if fileinput.isstdin():
            fmt = '{lineno}:{line}'
        else:
            fmt = '{filename:<20}:{lineno}:{line}'
        print fmt.format(filename=fileinput.filename(),
                         lineno=fileinput.filelineno(),
                         line=line.rstrip())

Si può usare questo basico ciclo per la corrispondenza di stringhe per trovare le occorrenze di "fileinput" nel sorgente di questi esempi.

$ python fileinput_grep.py fileinput *.py
fileinput_change_subnet.py:10:import fileinput
fileinput_change_subnet.py:17:for line in fileinput.input(files, inplace=True):
fileinput_change_subnet_noisy.py:10:import fileinput
fileinput_change_subnet_noisy.py:18:for line in fileinput.input(files, inplace=True):
fileinput_change_subnet_noisy.py:19:    if fileinput.isfirstline():
fileinput_change_subnet_noisy.py:20:        sys.stderr.write('Started processing %s\n' % fileinput.filename())
fileinput_example.py:6:"""Example for fileinput module.
fileinput_example.py:10:import fileinput
fileinput_example.py:30:for line in fileinput.input(sys.argv[1:]):
fileinput_grep.py   :10:import fileinput
fileinput_grep.py   :16:for line in fileinput.input(sys.argv[2:]):
fileinput_grep.py   :18:        if fileinput.isstdin():
fileinput_grep.py   :22:        print fmt.format(filename=fileinput.filename(),
fileinput_grep.py   :23:                         lineno=fileinput.filelineno(),

Si può anche passare l'input attraverso stdin

$ cat *.py | python fileinput_grep.py fileinput
10:import fileinput
17:for line in fileinput.input(files, inplace=True):
29:import fileinput
37:for line in fileinput.input(files, inplace=True):
38:    if fileinput.isfirstline():
39:        sys.stderr.write('Started processing %s\n' % fileinput.filename())
51:"""Example for fileinput module.
55:import fileinput
75:for line in fileinput.input(sys.argv[1:]):
96:import fileinput
102:for line in fileinput.input(sys.argv[2:]):
104:        if fileinput.isstdin():
108:        print fmt.format(filename=fileinput.filename(),
109:                         lineno=fileinput.filelineno(),

Filtrare sul Posto

Un'altra comune operazione di elaborazione file è la modifica del contenuto. Ad esempio un file Unix hosts potrebbe avere bisogno di essere aggiornato se viene modificato l'intervallo di una subnet.

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1       localhost
255.255.255.255 broadcasthost
::1             localhost
fe80::1%lo0     localhost
172.16.177.128  hubert hubert.hellfly.net
172.16.177.132  cubert cubert.hellfly.net
172.16.177.136  zoidberg zoidberg.hellfly.net

Un modo sicuro per eseguire le modifiche automaticamente è creare un nuovo file basato sull'input, quindi rimpiazzare l'originale con la copia modificata. fileinput supporta questo automaticamente usando l'opzione inplace.

import fileinput
import sys

from_base = sys.argv[1]
to_base = sys.argv[2]
files = sys.argv[3:]

for line in fileinput.input(files, inplace=True):
    line = line.rstrip().replace(from_base, to_base)
    print line
$ python fileinput_change_subnet.py 172.16.177 172.16.178 etc_hosts.txt

Sebbene lo script usi print, non viene prodotto alcun output verso stdout perchè fileinput mappa stdout al file che si sta sovrascrivendo.

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1       localhost
255.255.255.255 broadcasthost
::1             localhost
fe80::1%lo0     localhost
172.16.178.128  hubert hubert.hellfly.net
172.16.178.132  cubert cubert.hellfly.net
172.16.178.136  zoidberg zoidberg.hellfly.net

Prima che inizi l'elaborazione, un file di backup viene creato usando il nome originale più .bak. Il file di backup viene rimosso quando viene chiuso l'input.

import fileinput
import glob
import sys

from_base = sys.argv[1]
to_base = sys.argv[2]
files = sys.argv[3:]

for line in fileinput.input(files, inplace=True):
    if fileinput.isfirstline():
        sys.stderr.write('Iniziata elaborazione %s\n' % fileinput.filename())
        sys.stderr.write('Contenuto della directory: %s\n' % glob.glob('etc_hosts.txt*'))
    line = line.rstrip().replace(from_base, to_base)
    print line

sys.stderr.write('Finita elaborazione\n')
sys.stderr.write('Contenuto della directory: %s\n' % glob.glob('etc_hosts.txt*'))
$ python fileinput_change_subnet_noisy.py 172.16.177 172.16.178 etc_hosts.txt
Iniziata elaborazione etc_hosts.txt
Contenuto della directory: ['etc_hosts.txt', 'etc_hosts.txt.bak']
Finita elaborazione
Contenuto della directory: ['etc_hosts.txt']

Vedere anche:

fileinput
La documentazione della libreria standard per questo modulo.
Patrick Bryant
Cantautore con base in Atlanta.
m3utorss
Script per convertire file m3u con elenchi di MP3 in un file RSS adatto all'uso come feed di podcast.
Creare documenti XML
Maggiori dettagli sull'uso di ElementTree per produrre XML
File Access
Altri moduli per lavorare con i file
Text Processing Tools
Altri moduli per lavorare con il testo