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