Pythonでファイルから特定の文字列を含む行を抽出する

あるファイルから、特定の文字列を含む行を抽出する必要があった。

これをPythonで実装したところ、いくつか問題にぶつかったのでまとめておく。

 

[公開後追記]

grep -e ‘^■’じゃダメだったのかな

との声をいただきました。確かに…!(;ω;)

な、泣いてなんかないんだからねっ。

[追記ここまで]

 

 

[やりたいこと]

問題のファイル(memo.txt)は、以下のような内容になっている。

■ 1. XXXXX

1.1. XXXXX

YYYYYYYYYY

1.2. XXXXX

YYYYYYYYYY

■ 2. XXXXX

2.1. XXXXX

YYYYYYYYYY

2.2. XXXXX

YYYYYYYYYY

 

ここからを含む行を抽出して、以下のような出力結果を得たい。

■ 1. XXXXX

■ 2. XXXXX

 

なお、利用している環境は以下の通り。

macOS Catalina

バージョン 10.15.7

 

[エンコードの問題]

何も考えずにPythonでこれを実装した場合、以下のようになった。

path = 'memo.txt'



with open(path) as f:

    for line in f:

        if line.startswith('■'):

            print(line)

 

上記をextract.pyというファイル名で保存しpythonコマンドで実行すると、以下のようにエンコードの指定が不足しているとエラーが表示された。

$ python extract.py 

  File "extract.py", line 5

SyntaxError: Non-ASCII character '\xe2' in file extract.py on line 5,but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

 

指定されたURLを確認すると、ファイルの先頭でエンコードを指定する必要があるようだ。

www.python.org

 

ファイルの内容を以下のように修正する。

# coding=utf8

path = 'memo.txt'



with open(path) as f:

    for line in f:

        if line.startswith('■'):

            print(line)

 

すると、実行結果が表示されるようになった。

$ python extract1.py 

■ 1. XXXXX



■ 2. XXXXX



 

しかし、意図した結果と異なり、余分な改行が加えられてしまっている。が、この問題は一旦おいておく。

 

エンコードの問題に戻る。Python 3系のドキュメントをみると、以下のような記述がある。

Python ソースコードのデフォルトエンコーディングUTF-8 なので、文字列リテラルの中に Unicode 文字をそのまま含めることができます

Unicode HOWTO — Python 3.9.0 ドキュメント

 

もしや、と思いPythonのバージョンを表示してみると、なんと2系であった。不覚……。

$ python -V

Python 2.7.16

 

Python 3系を利用するには、python3 extract.pyのようにして実行する必要があった。Python 3系を利用する場合、extract.pyの先頭にエンコード指定がなくとも、エラーが発生しなかった。

$ python3 -V

Python 3.8.5

$ python3 extract.py

■ 1. XXXXX



■ 2. XXXXX



 

[改行コードが重複する問題]

エンコードの問題は解決したので、改行コードが多い問題を考える。

 

とりあえず、何も見ずに考えてみると、以下のあたりに原因がありそうだった。

  1. 変数lineには元々のファイルの改行コード込みの文字列が格納されている
  2. print()関数がJavaSystem.out.println()のように改行コードを自動的に付加する

 

2のprint()関数をいじる方法がパッと思いつかなかったので、以下のように実装することで1の問題点を解決した。

path = 'memo.txt'



with open(path) as f:

    for line in f:

        if line.startswith('■'):

            print(line[0:len(line)-1])

 

これを実行すると、以下のように不要な改行が削除されている。

$ python3 extract.py

■ 1. XXXXX

■ 2. XXXXX

 

これで一応やりたいことはできたのだが、print()関数をどうにかできないか調べてみた。

公式ドキュメントのprint()関数の説明を読んでみる。

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

objects を sep で区切りながらテキストストリーム file に表示し、最後に end を表示します。sep 、 end 、 file 、 flush を与える場合、キーワード引数として与える必要があります。

組み込み関数 — Python 3.9.0 ドキュメント

 

 

endというキーワード引数で、自動的に末尾に改行コードが付与されるということがわかったので、以下のように実装を修正した。

path = 'memo.txt'



with open(path) as f:

    for line in f:

        if line.startswith('■'):

            print(line, end='')

 

これを実行すると、同じように、不要な改行が削除されている。

$ python3 extract.py

■ 1. XXXXX

■ 2. XXXXX

 

ちなみに、Python 2系ではprint()関数はデフォルトでは使用できず、print文として扱われてしまう。

注釈 この関数は print という名前が print ステートメン トとして解釈されるため、通常は使用できません。ステートメントを無効化して、 print() 関数を使うためには、以下の future ステートメントをモジュールの最初に書いて下さい。

: from __future__ import print_function

バージョン 2.6 で追加.

2. 組み込み関数 — Python 2.7.18 ドキュメント

6. 単純文 (simple statement) — Python 2.7.18 ドキュメント

そのため、以下のように実装することで、Python 2系でもprint()関数を利用することができる。

# coding=utf8

from __future__ import print_function

path = 'memo.txt'



with open(path) as f:

    for line in f:

        if line.startswith('■'):

            print(line, end='')

 

[bashで実装した場合]

ファイルを読み込んで処理するだけでPythonなんか使わなくても、という声が聞こえてきたので、bashでも実装してみた。

#!/bin/bash

while read line

do

  if  $line == '■'* ; then

    echo $line

  fi

done < ./memo.txt

 

以下の記事を参考に実装した。

stackoverflow.com

 

bashで実装すればPythonのことであれこれ悩まずに済んだのに……とは思うが、Pythonの理解が深まったので良しとする。

 

[まとめ]

  • macOS CatalinaのデフォルトのPythonは2系なので注意
  • Python 2系でUnicodeソースコードに利用する場合はファイルの先頭でエンコード指定が必要
  • Pythonのprint()関数はキーワード引数endに空文字を指定することで末尾の改行コードをなくすことができる
  • サンプルコードは以下のリポジトリにまとめた

github.com