fileinput - Struttura di Filtri da Riga di Comando
Scopo: Crea programmi di filtro da riga di comando per elaborare righe da flussi di input
Il modulo fileinput è una struttura per creare programmi da riga di comando in grado di agire come filtro per file di testo.
Convertire file M3U in RSS
Un esempio di un filtro è m3utorss - scritta dall'autore dell'articolo originale Doug Hellmann (n.d.t.) - una applicazione per convertire un insieme di file MP3 in un flusso RSS che possa essere condiviso come podcast. Gli input per il programma sono uno o più file m3u contenenti l'elenco dei file MP3 da distribuire. Il risultato è un flusso RSS stampato alla console. Per elaborare l'input, il programma deve iterare attraverso una lista 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, aggiungere un nuovo elemento al flusso RSS.
- Stampare il risultato.
Tutto questo si sarebbe potuto codificare manualmente. Non è così complicato, e con qualche test si sarebbero potuti gestire correttamente anche gli errori. Ma fileinput è in grado di gestire tutti i dettagli, quindi la stesura del programma è semplificata.
# fileinput_example.py
import fileinput
import sys
import time
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom
# Impostazione dei nodi RSS e canale
rss = Element('rss',
{'xmlns:dc': "http://purl.org/dc/elements/1.1/",
'versione': '2.0'})
channel = SubElement(rss, 'channel')
title = SubElement(channel, 'title')
title.text = 'Flusso podcast di esempio'
desc = SubElement(channel, 'description')
desc.text = 'Generato per PyMOTW'
pubdate = SubElement(channel, 'pubDate')
pubdate.text = time.asctime()
gen = SubElement(channel, 'generator')
gen.text = 'https://pymotw.com/'
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=" "))
La funzione input()
riceve come argomento 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 di testo che sta elaborando. Il chiamante deve eseguire un ciclo per ogni riga, saltando quelle vuote ed i commenti, per trovare i riferimenti ai file MP3.
Il file di input di esempio contiene i nomi di parecchi file MP3.
# sample_data.m3u
# Questo è un file campione m3u
episode-one.mp3
episode-two.mp3
L'esecuzione di fileinput_example.py
con l'input di esempio produrre dati XML usando il formato RSS.
$ python3 fileinput_example.py sample_data.m3u <?xml version="1.0" ?> <rss xmlns:dc="http://purl.org/dc/elements/1.1/" versione="2.0"> <channel> <title>Flusso podcast di esempio</title> <description>Generato per PyMOTW</description> <pubDate>Thu Jun 3 09:14:36 2021</pubDate> <generator>https://pymotw.com/</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>
Metadati in Progressione
Nell'esempio precedente, non interessa quale file o numero di riga si sta elaborando in fase di input. Per altri strumenti (per ricerche sul tipo di grep, ad esempio) queste informazioni potrebbero essere necessarie. Il modulo fileinput include funzioni per accedere a tutti i metadati rispetto alla riga corrente (filename()
, filelineno()
e lineno()
restituiscono nell'ordine il nome del file, il numero di riga nel file corrente ed il numero totale di righe lette.
# fileinput_grep.py
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}:{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.
$ python3 fileinput_grep.py fileinput *.py fileinput_change_subnet_noisy.py:1:# fileinput_change_subnet_noisy.py fileinput_change_subnet_noisy.py:3:import fileinput fileinput_change_subnet_noisy.py:11:for line in fileinput.input(files, inplace=True): fileinput_change_subnet_noisy.py:12: if fileinput.isfirstline(): fileinput_change_subnet_noisy.py:14: fileinput.filename())) fileinput_change_subnet.py:1:# fileinput_change_subnet.py fileinput_change_subnet.py:3:import fileinput fileinput_change_subnet.py:10:for line in fileinput.input(files, inplace=True): fileinput_example.py:1:# fileinput_example.py fileinput_example.py:3:import fileinput fileinput_example.py:23:for line in fileinput.input(sys.argv[1:]): fileinput_grep.py:1:# fileinput_grep.py fileinput_grep.py:3:import fileinput fileinput_grep.py:9:for line in fileinput.input(sys.argv[2:]): fileinput_grep.py:11: if fileinput.isstdin(): fileinput_grep.py:15: print(fmt.format(filename=fileinput.filename(), fileinput_grep.py:16: lineno=fileinput.filelineno(),
Il testo può anche essere letto dallo standard input.
$ cat *.py | python3 fileinput_grep.py fileinput 1:# fileinput_change_subnet_noisy.py 3:import fileinput 11:for line in fileinput.input(files, inplace=True): 12: if fileinput.isfirstline(): 14: fileinput.filename())) 23:# fileinput_change_subnet.py 25:import fileinput 32:for line in fileinput.input(files, inplace=True): 35:# fileinput_example.py 37:import fileinput 57:for line in fileinput.input(sys.argv[1:]): 72:# fileinput_grep.py 74:import fileinput 80:for line in fileinput.input(sys.argv[2:]): 82: if fileinput.isstdin(): 86: print(fmt.format(filename=fileinput.filename(), 87: lineno=fileinput.filelineno(),
Filtrare sul Posto
Un'altra comune operazione di elaborazione file è la modifica contestuale del contenuto, senza la creazione di un nuovo file. Ad esempio un file hosts Unix potrebbe avere bisogno di essere aggiornato se viene modificato l'intervallo di una sottorete. Di seguito il file di esempio prima delle modifiche.
##
# 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
10.17.177.128 hubert hubert.hellfly.net
10.17.177.132 cubert cubert.hellfly.net
10.17.177.136 zoidberg zoidberg.hellfly.net
Il 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
.
# fileinput_change_subnet.py
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)
Nonostante lo script usi print()
, non viene prodotto alcun output perchè fileinput redirige lo standard output al file che si sta sovrascrivendo.
$ python3 fileinput_change_subnet.py 10.16 10.17 etc_hosts.txt
Il file aggiornato ha gli indirizzi IP modificati per tutti i server sulla rete 10.16.0.0/16. Ecco il file dopo la modifica.
##
# 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
10.17.177.128 hubert hubert.hellfly.net
10.17.177.132 cubert cubert.hellfly.net
10.17.177.136 zoidberg zoidberg.hellfly.net
Prima che inizi l'elaborazione, una copia del file viene creata usando il nome originale più .bak
.
# fileinput_change_subnet_noisy.py
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 {}\n'.format(
fileinput.filename()))
sys.stderr.write('La directory contiene: {}\n'.format(
glob.glob('etc_hosts.txt*')))
line = line.rstrip().replace(from_base, to_base)
print(line)
sys.stderr.write('Terminata elaborazione\n')
sys.stderr.write('La directory contiene: {}\n'.format(
glob.glob('etc_hosts.txt*')))
La copia del file viene rimossa quando viene chiuso l'input.
$ python3 fileinput_change_subnet_noisy.py 10.16 10.17 etc_hosts.txt Iniziata elaborazione etc_hosts.txt La directory contiene: ['etc_hosts.txt', 'etc_hosts.txt.bak'] Terminata elaborazione La directory contiene: ['etc_hosts.txt']
Vedere anche:
- fileinput
- La documentazione della libreria standard per questo modulo.
- m3utorss
- Script per convertire file m3u con elenchi di MP3 in un file RSS adatto all'uso come flusso di podcast.
- xml.etree.ElementTree
- Maggiori dettagli sull'utilizzo di
ElementTree
per produrre XML