fileinput — Marco de filtro de línea de comandos

Propósito:Crea programas de filtro de línea de comandos para procesar líneas desde flujos de entrada.

El módulo fileinput es un marco para crear programas de línea de comandos para procesar archivos de texto como filtro.

Conversión de archivos M3U a RSS

Un ejemplo de filtro es m3utorss, un programa para convertir un conjunto de archivos MP3 en una fuente RSS que se puede compartir como un podcast. Las entradas al programa son uno o más archivos m3u que enumeran los archivos MP3 que se distribuirán. La salida es una fuente RSS impresa en la consola. Para procesar la entrada, el programa necesita iterar sobre la lista de nombres de archivo y

  • Abrir cada archivo.
  • Leer cada línea del archivo.
  • Averiguar si la línea se refiere a un archivo mp3.
  • Si lo hace, agregar un nuevo elemento a la fuente RSS.
  • Imprimir la salida.

Todo este manejo de archivos podría haber sido codificado a mano. No es tan complicado y, con algunas pruebas, incluso el manejo de errores sería correcto. Pero fileinput maneja todos los detalles, por lo que el programa se simplifica.

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 función input() toma como argumento una lista de nombres de archivos para examinar. Si la lista está vacía, el módulo lee datos de la entrada estándar. La función devuelve un iterador que produce líneas individuales a partir de los archivos de texto que se procesan. La persona que llama solo necesita recorrer cada línea, omitiendo espacios en blanco y comentarios, para encontrar las referencias a los archivos MP3.

Aquí está el programa completo.

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

# Establish the RSS and channel nodes
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 = '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="  "))

Este archivo de entrada de muestra contiene los nombres de varios archivos MP3.

sample_data.m3u
# This is a sample m3u file
episode-one.mp3
episode-two.mp3

Ejecutar fileinput_example.py con la entrada de muestra produce datos XML utilizando el formato RSS.

$ python3 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>Sun Mar 18 16:20:44 2018</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>

Metadatos de progreso

En el ejemplo anterior, el nombre de archivo y el número de línea que se procesaba no eran importantes. Otras herramientas, como la búsqueda tipo grep, pueden necesitar esa información. fileinput incluye funciones para acceder a todos los metadatos sobre la línea actual (filename(), filelineno() y lineno()).

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()))

Se puede usar un bucle básico de coincidencia de patrones para encontrar las ocurrencias de la cadena "fileinput" en la fuente de estos ejemplos.

$ python3 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:21:            fileinput.filena
me()))
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.f
ilelineno(),

El texto también se puede leer desde la entrada estándar.

$ 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():
40:            fileinput.filename()))
54:"""Example for fileinput module.
58:import fileinput
78:for line in fileinput.input(sys.argv[1:]):
101:import fileinput
107:for line in fileinput.input(sys.argv[2:]):
109:        if fileinput.isstdin():
113:        print(fmt.format(filename=fileinput.filename(),
114:                         lineno=fileinput.filelineno(),

Filtrado in situ

Otra operación común de procesamiento de archivos es modificar el contenido de un archivo donde está, en lugar de crear un nuevo archivo. Por ejemplo, un archivo de hosts Unix podría necesitar actualizarse si cambia un rango de subred.

etc_hosts.txt before modifications
##
# 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.16.177.128  hubert hubert.hellfly.net
10.16.177.132  cubert cubert.hellfly.net
10.16.177.136  zoidberg zoidberg.hellfly.net

La forma segura de realizar el cambio automáticamente es crear un nuevo archivo basado en la entrada y luego reemplazar el original con la copia editada. fileinput admite esto automáticamente usando la opción 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)

Aunque la secuencia de comandos usa print(), no se produce ninguna salida porque fileinput redirige la salida estándar al archivo que se sobrescribe.

$ python3 fileinput_change_subnet.py 10.16 10.17 etc_hosts.txt

El archivo actualizado tiene las direcciones IP modificadas de todos los servidores en la red 10.16.0.0/16.

etc_hosts.txt after modifications
##
# 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

Antes de comenzar el procesamiento, se crea un archivo de copia de seguridad con el nombre original más .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('Started processing {}\n'.format(
            fileinput.filename()))
        sys.stderr.write('Directory contains: {}\n'.format(
            glob.glob('etc_hosts.txt*')))
    line = line.rstrip().replace(from_base, to_base)
    print(line)

sys.stderr.write('Finished processing\n')
sys.stderr.write('Directory contains: {}\n'.format(
    glob.glob('etc_hosts.txt*')))

El archivo de copia de seguridad se elimina cuando se cierra la entrada.

$ python3 fileinput_change_subnet_noisy.py 10.16. 10.17. etc_h\
osts.txt

Started processing etc_hosts.txt
Directory contains: ['etc_hosts.txt.bak', 'etc_hosts.txt']
Finished processing
Directory contains: ['etc_hosts.txt']

Ver también