fileinput – 入力ストリームを行単位で処理する

目的:入力ストリームを行単位で処理するコマンドラインフィルタプログラムを作成する
利用できるバージョン:1.5.2 以上

fileinput モジュールは、フィルタっぽいルールでテキストファイルを処理するためのコマンドラインプログラムを作成するフレームワークです。

M3U を RSS に変換する

例えば、私は、デモテープを podcast で配布できるフォーマットに変換する m3utorss というアプリケーションを友だちの Patrick のために作りました。

このプログラムへの入力は、配布する mp3 ファイルを再生する1つ以上の m3u ファイルです。その出力は、RSS フィードのように見える XML のブロブです(シンプルに使用するために標準出力に出力される)。その入力を処理するには、ファイル名のリストを繰り返し処理する必要があります。

  • それぞれのファイルをオープンする
  • ファイルから行を読み込む
  • その行が mp3 ファイルを参照しているなら解析する
  • mp3 ファイルだったら、RSS フィードに必要な情報を展開する
  • その結果を表示する

私は手作業で全てのファイルを扱うことができました。その作業は複雑ではなく、いくつかテストしてエラーも適切に扱えることを確認しました。しかし fileinput モジュールを使用すると、エラーに関して心配する必要はありません。私はただ次のように書きました。

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

fileinput.input() 関数は、調べるファイル名のリストを引数として取ります。そのファイル名のリストが空っぽなら、 fileinput モジュールは標準入力からデータを読み込みます。その関数は処理対象のテキストファイルから行を取得するイテレータを返します。つまり、行わなければならないことは、空行やコメントを読み飛ばして mp3 ファイルの参照先を見つけるために各行のループ処理です。

完全なプログラムは次になります。

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

# RSS とチャンネルノードを設定する
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="  ")

その実行結果は次になります。

$ 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>
      Sun Feb 17 11:32:26 2013
    </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>

進捗情報のメタデータ

前節のサンプルでは、入力から処理するものがファイルなのか、行番号なのかは扱いませんでした。その他のツール(例えば grep のような検索)でも扱えます。 fileinput モジュールは、そういった情報にアクセスする関数を提供します(filename(), filelineno(), lineno() 等)。

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

このサンプルのソースファイルに “fileinput” という文字列があるかどうかを見つける基本的なパターンマッチングを処理します。

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

さらに標準入力からそのソースファイルを渡すこともできます。

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

インプレースフィルタリング

その他の一般的なファイル処理は、そのファイルのコンテンツを変更することです。例えば、Unix の hosts ファイルは、サブネットの範囲が変更されると更新する必要があります。

##
# 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

自動的に変更する確実な方法は、その入力に基づいて新しいファイルを作成してから、そのコピーして編集したファイルとオリジナルのファイルを置き換えます。 fileinput モジュールは、 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

このスクリプトは print を使用していますが、標準出力には何も出力されません。これは fileinput モジュールが標準出力を上書きされるファイルへマッピングするからです。

##
# 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

処理を開始する前に、オリジナルのファイル名に .bak を付けてバックアップファイルが作成されます。このバックアップファイルはその入力がクローズされるときに削除されます。

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 %s\n' % fileinput.filename())
        sys.stderr.write('Directory contains: %s\n' % 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: %s\n' % glob.glob('etc_hosts.txt*'))
$ python fileinput_change_subnet_noisy.py 172.16.177 172.16.178 etc_hosts.txt
Started processing etc_hosts.txt
Directory contains: ['etc_hosts.txt', 'etc_hosts.txt.bak']
Finished processing
Directory contains: ['etc_hosts.txt']

See also

fileinput
本モジュールの標準ライブラリドキュメント
Patrick Bryant
アトランタを中心に活動するシンガーソングライター
m3utorss
MP3 を再生する m3u ファイルを podcast フィードに適した RSS フィードに変換するスクリプト
XML ドキュメントを作成する
XML を生成する ElementTree の使用方法の詳細
ファイルアクセス
ファイルを扱うその他のモジュール
テキスト処理ツール
テキストを扱うその他のモジュール
Bookmark and Share