Pythonからテキストファイルの内容を置換する方法について紹介します。状況によって使い分けることが出来るようになるとGood!です。
プログラムは短ければ短いほど可読性が高くなってバグが混入しにくくなるので、基本的に短い記述で済み可読性が高いものを選択します。ファイルサイズが大きすぎるなど、状況によりその手段では不都合が生じるような場合には別の方法を検討する感じでいきましょう。
置換方法と今回のお題
置換方法の確認
Pythonで文字列の置換をする場合は、次のようにstr.replace()を使います。
>>> s = "今日はいい天気ですね。"
>>> s.replace("天気", "気分")
今日はいい気分ですね。
正規表現を使う方法もありますが、少し話が長くなるので今回は触れないでいきます。
今回のお題
今回は、こんな内容のhtmlファイルがあって、<div>タグを全て<p>タグに置き換えたい、というような状況を想定しましょう。
<html>
<body>
<div>習うより慣れろ?</div>
<div>違う。習いながら慣れる!これが最強!!</div>
</body>
</html>
これを置換するプログラムは次のような感じになります。
s = """
<html>
<body>
<div>習うより慣れろ?</div>
<div>違う。習いながら慣れる!これが最強!!</div>
</body>
</html>
"""
s = s.replace('div>', 'p>')
print(s)
# 出力:
# <html>
# <body>
# <p>習うより慣れろ?</p>
# <p>違う。習いながら慣れる!これが最強!!</p>
# </body>
# </html>
次の章からは、この知識を前提に、ファイルの内容を置換する方法について紹介していきます。
ファイル全体に対していっきに処理
読み込みも置換も書き出しも、全体に対していっきに処理するやり方です。
- ファイル全体をいっきにに読み込む
- 全体に対していっきにに置換をかける
- 全体をファイルにいっきに書き出す
全体に対していっきに処理するのでファイルサイズに比例して消費メモリが多くなります。20年ほど前だとこれはお行儀が悪いと言われていましたが、今ではほとんど気にしなくても大丈夫になりました。
かなりポピュラーな方法
open()関数でファイルをオープンして読み込み、置換して、書き出し先ファイルをオープンして書き出します。
# 読み込み
with open('sample.html') as reader:
content = reader.read()
# 置換
content = content.replace('div>', 'p>')
# 書き出し
with open('sample_fixed.html', 'w') as writer:
writer.write(content)
最も一般的に解説されている方法です。Pythonにおけるファイルの基本的な操作方法というのもあって知っている人が多いのと、そのためか質問サイトで寄せられる回答もこの形式のものがほとんどとなっています。(初心者にも分かりやすいように配慮して回答しているから?)
pathlibモジュールを使う方法
ファイル全体に対して読み込んだり書き出したりする場合は、pathlib.Path.read_text()メソッドやpathlib.Path.write_text()メソッドを使うとwith構文を使う必要がなくなり可読性が向上します。
from pathlib import Path
read_path = Path('sample.html')
write_path = Path('sample_fixed.html')
# 読み込み
content = read_path.read_text()
# 置換
content = content.replace('div>', 'p>')
# 書き出し
write_path.write_text(content)
この方法が利用可能になったのはPythonバージョン3.5からという事もありあまり知られていませんが、知らないと本当にもったいない!
外部コマンドを呼び出す方法
Linuxのようなsedコマンドが使える環境なら、Pythonからsedコマンドを呼び出してコマンドを呼び出し置換させる、という手もあるようです。この例では、sample.htmlの内容がそのまま置換されます。
import subprocess
subprocess.run('sed -i -e s/div>/p>/g sample_fixed.html', shell=True)
ファイルサイズが大きすぎてもsedコマンド側でメモリ消費量を抑えてくれたりする、といった利点もあったりしますが、プログラムからコマンドを呼び出すというのはあまり乱用したくない方法ですよね。
1行ずつ処理
読み込みも置換も書き出しも、ファイルの内容に対して1行ずつ処理するやり方です。
- 読み込み元ファイルを開く
- 書き出し先ファイルを開く
- 1行読み込む
- 置換する
- 1行書き出す(3〜5を繰り返す)
- (ファイルクローズなどはwith構文を使う)
ファイルサイズがあまりに大きいなどの理由で、内容全体をいっきに読み込みたくない場合に使う方法です。
また、これは少し上級編のお話ですが、ファイルだけでなくストリームのような出力が小出しにされるような状況にも適しています。
ポピュラーな方法
読み込み元と書き出し先の両方のファイルをオープンして1行ずつイテレーションで回しながら処理する方法です。
# 読み込み元と書き出し先を同時にオープン
with open('sample.html') as reader, open('sample_fixed.html', 'w') as writer:
# 読み込み元の行ごとにイテレーション
for line in reader:
# 置換
line = line.replace('div>', 'p>')
# 書き出し
writer.write(line)
with構文は2つ同時に使用することができます。こうすると、インデントが深くなるのを抑えることができます。
fileinputモジュールを使う方法
fileinputモジュールを活用する手もあります。この例では、sample.htmlの内容がそのまま置換されます。
import fileinput
# ファイルを置換モードで開いてイテレーション開始
for line in fileinput.input(files=('sample.html'), inplace=True):
# 置換
line = line.replace('div>', 'p>')
# 書き出し
print(line, end='')
fileinputモジュールの本来の目的は、複数のファイルからシームレスに情報を読み込むというものなのですが、inplaceパラメータにTrueを指定すると、fileinput.input()でファイルを開いている間だけ標準出力がファイルの内容に反映されるようになります。
標準出力の切り替えが全体に影響するので、シングルスレッドのプログラムでしか使えない方法です。
1行のプログラムで完結させる
textfileパッケージを使うと、ファイルの内容を読み込んで、置換して、書き出すまでを、たった1行のプログラムで行うことができます。(import文は行数としてはノーカウントだー!…と言い張ります☺️)
パッケージのインストール:
> pip install textfile
プログラム:
import textfile
textfile.replace('sample.html', 'div>', 'p>')
何行もあったプログラムが1行になるとすごくスッキリしますね。