↓ メインコンテンツへスキップ
  1. 第05講 再帰関数(2/2; 探索)/

第05講 週次課題

課題05-1 特定の名前のファイルを見つける
#

難易度 ⭐

コマンドライン引数でファイル名と、ディレクトリ名もしくはファイル名が与えられます。 コマンドライン引数で最初に与えられた名前を持つファイルを再帰的に探索し、そのファイルが見つかったらそのパスを表示するプログラムを作成してください。

ファイル名は find_file.py というファイル名で保存してください。

import sys
import os

# name という名前のファイルを base 以下から探す関数。
# 複数見つかる場合もありうるため、見つかったものをリストで返す。
def find_file(name, base, result = None):
    if result is None:
        result = []
    # base がディレクトリの場合
        # base の一覧を取得し、それぞれを処理する。
            # それぞれのアイテムに対して、base と item を join する。
            # find_file を再帰的に呼び出す。
    # そうでない場合
        filename = os.path.basename(base) # base から ファイル名部分を取り出す。
        # filename が name と一致する場合
            # result に base を追加する。
    return result

if os.path.isdir("root"): # dirs.zip を展開した root がある場合だけテストする。
    assert sorted(find_file("file4", "root")) == ["root/dir4/file4", "root/dir5/file4"], "file4 は 2 つ見つかるはず"
    assert find_file("nothing", "root") == [], "存在しない名前なら空のリスト"
    assert find_file("file1", "root/dir1/file1") == ["root/dir1/file1"], "base がファイルでも探せるはず"

for base in sys.argv[2:]:
    paths = find_file(sys.argv[1], base)
    if paths:
        print(f"{sys.argv[1]}: {paths}")

📥 dirs.zipのダウンロード

実行例
#

$ python3 find_file.py file4 root
file4: ['root/dir5/file4', 'root/dir4/file4']

課題05-2: 年齢を当てる
#

難易度 ⭐⭐

コマンドライン引数で年齢を与えます。 その年齢を当てるまでにかかるステップ数を求めるプログラムを作成してください。 コマンドラインで与える年齢は 0以上100以下とします。

guess_age.py というファイル名で保存してください。

import sys

def guess_age(age, low, high, step = 1):
    mid = (low + high) // 2
    pass # この pass を削除して、関数を実装してください。

assert guess_age(50, 0, 100) == 1, "50 は 1 回目で当たる"
assert guess_age(75, 0, 100) == 2, "75 は 2 回目で当たる"
assert guess_age(25, 0, 100) == 6
assert guess_age(0, 0, 100) == 6
assert guess_age(100, 0, 100) == 7

for age_string in sys.argv[1:]:
    age = int(age_string)
    step = guess_age(age, 0, 100)
    print(f'age = {age}, step = {step}')

実行例
#

$ python3 guess_age.py 50 25 75 12 37 62 87
age = 50, step = 1
age = 25, step = 6
age = 75, step = 2
age = 12, step = 6
age = 37, step = 3
age = 62, step = 3
age = 87, step = 7
発展課題(任意) 誕生日を当てる

発展課題(任意) 誕生日を当てる
#

この課題は任意です。提出は必須ではなく、取り組まなくても不利にはなりません。余裕があれば挑戦してください。

課題05-2 と同様に、誕生日を当てることにも挑戦してみましょう。 コマンドライン引数で誕生日(月、日)を与えます。 その与えられた誕生日を当てるまでにかかるステップ数を求めるプログラムを作成してください。 コマンドラインで与える誕生日は 1月1日以上12月31日以下とします。

guess_birthday.py というファイル名で作成してください。

import sys

days_of_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

# 月と日が与えられたとき、1月1日からの日数を返す関数
def month_day_to_int(month, day):
    days = 0
    for i in range(month - 1):
        days += days_of_month[i]
    return days + day

# 1〜365 が与えられたとき、月と日を返す関数
def int_to_month_day(n):
    for i, d in enumerate(days_of_month):
        if n <= d:
            return i + 1, n
        n = n - d

def guess_birthday(target_day, low, high, step = 1):
    mid = (low + high) // 2
    pass # この pass を修正して、関数を実装してください。

month = int(sys.argv[1])
day = int(sys.argv[2])
target_day = month_day_to_int(month, day)
step = guess_birthday(target_day, 1, 365)
print(f'birthday = {month}/{day}, step = {step}')

# 以下、テストコード
assert month_day_to_int(1, 1) == 1
assert month_day_to_int(2, 1) == 32
assert month_day_to_int(12, 31) == 365

assert int_to_month_day(1) == (1, 1)
assert int_to_month_day(60) == (3, 1)
assert int_to_month_day(365) == (12, 31)

プログラムの単純化のため、うるう年には対応していません。更なる挑戦問題として、うるう年にも対応するプログラムを作成してみましょう。

実行例
#

$ python3 guess_birthday.py 7 1
birthday = 7/1, step = 9
$ python3 guess_birthday.py 7 2
birthday = 7/2, step = 1

課題05-3 特定の内容を含むファイルを検索する
#

難易度 ⭐⭐⭐

課題05-1 の応用です。 コマンドライン引数でキーワードと、ディレクトリ名もしくはファイル名が与えられます。 ディレクトリ、ファイルを再帰的に探索し、キーワードを含むファイルがあればそのパスを表示するプログラムを作成してください。

ファイル名は find_grep.py というファイル名で保存してください。 grep とは Linux 系OSでのコマンドで、ファイル内の特定の文字列を検索するコマンドです。 global regular expression print の頭文字だそうです。🔗

import sys
import os

# file_path というファイルに keyword が含まれているかを判定する関数
# 含まれていれば True を返し、そうでなければ False を返す。
def contains_keyword(file_path, keyword):
    pass # この pass を削除して、関数を実装してください。
    # file_path を読み込みモードで開く。
        # ファイルを一行ずつ読み込む。
            # line に keyword が含まれているかを判定する。
                # 含まれていれば True を返す。
    # 含まれていなければ False を返す。

# name という名前のファイルを base 以下から探す関数。
# 複数見つかる場合もありうるため、見つかったものをリストで返す。
def find_grep(keyword, base, result = None):
    if result is None:
        result = []
    # base がディレクトリの場合
        # base の一覧を取得し、それぞれを処理する。
        for child in os.listdir(base):
            if not child.startswith("."): # 隠しファイルは対象外とする。
                # それぞれのアイテムに対して、base と child を join する。
                # find_grep を再帰的に呼び出す。
    # そうでない場合
        if contains_keyword(base, keyword): # base に keyword が含まれているかを判定する。
            # result に base を追加する。
    return result

if os.path.isdir("root"): # dirs.zip を展開した root がある場合だけテストする。
    assert contains_keyword("root/dir1/file2", "file2"), "root/dir1/file2 には file2 が書かれている"
    assert not contains_keyword("root/dir1/file2", "zzz")
    assert sorted(find_grep("file2", "root")) == ["root/dir1/file2", "root/dir2/file2"]
    assert find_grep("zzz", "root") == [], "どのファイルにもなければ空のリスト"

for base in sys.argv[2:]:
    paths = find_grep(sys.argv[1], base)
    if paths:
        print(f"{sys.argv[1]}: {paths}")

実行例
#

この実行例は以下の dirs.zip をダウンロードしてできたディレクトリ(root)での実行例です。

📥 dirs.zipのダウンロード

$ python3 find_grep.py file2 root             # root ディレクトリの中にある file2 を含むファイルを探す。
file2: ['root/dir2/file2', 'root/dir1/file2'] # root/dir2/file2 と root/dir1/file2 が見つかる。
バイナリファイルの扱い

この課題は、バイナリファイルが含まれている場合、エラーが発生します。 バイナリファイルを読む場合は、open 関数の第二引数に rb を指定してください。 ただし、バイナリファイルは、1行ずつ読み込めないため、read で1バイトずつ読み込む必要があります。

mimetypes モジュールを用いて、ファイルの種類を判定しテキストファイルのみを対象にする場合は、contains_keywordの最初に以下のようなコードを追加してください。 なお、mimetypes の import が必要です(プログラムコードの最初に import mimetypesを入力する)。

注意点として、mimetypes モジュールはファイルの拡張子を元にファイルの種類を判別しています。 そのため、拡張子が偽装されているものは見抜けません。 ファイルの先頭の数バイトを読み、どのようなファイルであるかを判定する python-magic というライブラリもあります。 こちらを利用するのも良いですが、libmagic というライブラリを別途インストールする必要があります。

def contains_keyword(file_path, keyword):
    ret = mimetypes.guess_type(file_path, strict=True)
    if (ret[0] == None) or (not('text' in ret[0])): 
        return False   # テキストファイルでない場合は False を返す。
    # file_path を読み込みモードで開く。
        # ファイルを一行ずつ読み込む。
            # line に keyword が含まれているかを判定する。
                # 含まれていれば True を返す。
    # 含まれていなければ False を返す。