Python 逆引き集 | 文字列操作:特定文字列で終わるか判定する

Python Python
スポンサーリンク
スポンサーリンク

Pythonで特定の文字列で終わるかどうかを判定する方法

ファイル名が「.csv」で終わるか確認したい、メールアドレスが「@example.com」で終わるか検証したい、URLが「.pdf」で終わるリンクだけを抽出したい――プログラミングの現場では、「この文字列は特定の文字列で終わっているか?」を調べたい場面が非常に多くあります。この記事では、Pythonが標準で用意しているendswith()メソッドを中心に、基本的な使い方から複数条件への対応、大文字・小文字を区別しない判定、拡張子チェックの落とし穴、そして実務でそのまま使えるテンプレートまで、プログラミング初心者の方でも確実に使いこなせるように順を追って解説します。

はじめに:文字列の末尾を調べる代表的な場面

文字列の「末尾」を判定したい場面は、実務のさまざまな場所で登場します。主な例を挙げます。

  • ファイル形式の判定:「.csv」で終わるファイルだけ処理対象にする
  • アップロードファイルの検証:許可する拡張子だけを受け付ける(セキュリティ上重要)
  • メールアドレスの確認:社内ドメイン「@example.co.jp」で終わるかチェックする
  • データの分類:「円」で終わる金額データだけを抽出する
  • ログの解析:「.exe」で終わるパスを検出して不正実行を防ぐ

これらの判定も、文字列を1文字ずつ調べるのではなく、Pythonの標準メソッドを使えば1行で書けます。まずは基本構文から見ていきましょう。

基本編:endswith()メソッドの使い方

endswith()の基本構文と挙動

endswith()は、文字列が指定した文字列で終わっていればTrue、そうでなければFalseを返すメソッドです。startswith()(先頭判定)と対をなすメソッドで、使い方はほぼ同じです。

# endswith()の基本例
filename = "report_2026.csv"

print(filename.endswith(".csv"))   # True(.csvで終わる)
print(filename.endswith(".txt"))   # False(.txtでは終わらない)
print(filename.endswith("2026.csv"))  # True(長い文字列でもOK)

# 大文字・小文字は区別される(重要な落とし穴)
print(filename.endswith(".CSV"))   # False(大文字のCSVは一致しない)
print(filename.endswith(""))       # True(空文字列は「すべての文字列で終わる」と判定)
Python

ポイントを整理します。

  • 判定したい文字列を引数に渡すだけで、True/Falseが返る
  • 大文字・小文字は厳密に区別される(「.CSV」と「.csv」は別物)
  • 空文字列""を渡すと常にTrueになる(後述の注意点)
  • startswith()と同じ構文なので、覚えやすい

in演算子との違いに注意

「文字列に特定の文字列が含まれるか」を判定するin演算子は、末尾に限らずどこにでもマッチします。endswith()は「末尾で終わるか」の判定のみ行うため、目的に応じて使い分ける必要があります。

# endswith()とin演算子の違い
text = "backup_report.csv"

# endswith():末尾で終わるかを判定
print(text.endswith("report"))   # False(末尾は"report"ではない)

# in演算子:どこかに含まれるかを判定
print("report" in text)          # True(中盤に"report"が含まれる)
Python

「末尾だけ」を厳密に判定したい場合にinを使うと、文字列の途中にマッチしてもTrueになってしまい、意図しない判定結果になります。「〜で終わる」ならendswith()、「〜を含む」ならinと、目的に合わせて選択してください。

否定(〜で終わらない)の判定

「〜で終わらない」場合の処理を書きたい場合は、not演算子を使います。

# 「〜で終わらない」の判定
filename = "photo.jpg"

if not filename.endswith(".png"):
    print("PNGファイルではありません")  # この例では実行される
Python

発展編:複数条件と大文字・小文字への対応

複数の文字列のいずれかで終わるかを判定する

「.jpg」または「.png」で終わるか、のように複数の条件のいずれかを満たすかを判定したい場面もよくあります。endswith()もstartswith()と同様、タプル(複数の値をまとめたもの)を渡すことで、より簡潔に書けます。

# 方法1:orで繋ぐ(従来の方法。条件が増えると冗長になる)
filename = "photo.png"
print(filename.endswith(".jpg") or filename.endswith(".png"))  # True

# 方法2:タプルを渡す(推奨。条件をカンマ区切りで並べるだけ)
print(filename.endswith((".jpg", ".png")))  # True(どちらかで終わればTrue)

# 否定と組み合わせる例(画像ファイル以外を弾く)
if not filename.endswith((".jpg", ".png", ".gif")):
    print("対応していないファイル形式です")
Python

タプルを使う方法は、条件が増えてもorを繰り返さずに済み、コードの見通しがよくなります。複数条件の判定ではタプルを使うのがPythonの慣習と覚えておいてください。

大文字・小文字を区別せずに判定する

「.JPG」でも「.jpg」でも同じ扱いにしたい場合、判定前に文字列を小文字(または大文字)に揃えます。lower()やcasefold()を使うのが定番です。

# 大文字・小文字を区別せずに判定する例
filename = "PHOTO.JPG"

# 小文字に揃えてから判定
print(filename.lower().endswith(".jpg"))  # True

# 判定する文字列側を大文字にしても同じ結果
print(filename.endswith((".JPG", ".jpg", ".Jpg")))  # True(タプル版)
Python

タプルで全パターンを列挙する方法も有効ですが、パターンが増えるほど冗長になります。一般には、比較対象の文字列を正規化(小文字化)してから判定する方がシンプルで保守性が高いです。厳密な照合が必要な場合は、以前の記事で解説したcasefold()を使うと、特殊な文字にも対応できます。

# casefold()を使った厳密な正規化の例
def ends_with_ignore_case(text, suffix):
    if not isinstance(text, str) or not isinstance(suffix, str):
        return False
    # casefold()で大小文字に加えて特殊文字も正規化してから判定
    return text.casefold().endswith(suffix.casefold())

print(ends_with_ignore_case("Report.PDF", ".pdf"))  # True
Python

判定位置を指定する(start/end引数)

endswith()にも、検索範囲を指定するstart引数、end引数があります。ただし、実務ではあまり頻繁には使われないため、覚えておく程度で構いません。

# end引数で検索終了位置を指定する例
text = "report_2026.csv"

# 0〜10文字目(インデックス0〜9)の範囲で判定
print(text.endswith("2026", 0, 10))  # True("report_202"の範囲ではないためFalse)

# 正確な例:0〜11文字目までなら "report_2026" となる
print(text.endswith("2026", 0, 11))  # True
Python

実務編:具体的な活用例とテンプレート

例題1:アップロードファイルの拡張子検証

ファイルアップロード機能では、許可する拡張子だけを受け付けることがセキュリティ上必須です。endswith()はその第一関門として有効ですが、後述の通り単独では完全ではありません。

# アップロードファイルの拡張子を検証するテンプレート
def validate_upload(filename):
    # Noneや非文字列のガード
    if not isinstance(filename, str):
        return False, "ファイル名が不正です"

    # 小文字に揃えてから判定(大文字小文字の表記ゆれを吸収)
    allowed_extensions = (".jpg", ".jpeg", ".png", ".gif")

    if not filename.lower().endswith(allowed_extensions):
        return False, "許可されていないファイル形式です"

    return True, "OK"

# 使用例
tests = ["photo.jpg", "document.pdf", "image.PNG", "script.exe"]
for t in tests:
    ok, msg = validate_upload(t)
    print(f"{t:>15} → {ok} ({msg})")
# photo.jpg      → True (OK)
# document.pdf   → False (許可されていないファイル形式です)
# image.PNG      → True (OK)(大文字も小文字化して判定)
# script.exe     → False (許可されていないファイル形式です)
Python

例題2:メールアドレスのドメイン確認

社内メールアドレスかどうかを判定する例です。正規化とタプルの組み合わせで、堅牢な判定が実現できます。

# メールアドレスが社内ドメインかを判定する例
def is_company_email(email):
    if not isinstance(email, str):
        return False
    cleaned = email.strip().lower()  # 空白除去+小文字化
    company_domains = ("@example.co.jp", "@example.com")
    return cleaned.endswith(company_domains)

# 使用例
tests = ["[email protected]", "[email protected]", "[email protected]"]
for e in tests:
    print(f"{e:>22} → {is_company_email(e)}")
# [email protected] → True
# [email protected]   → True(正規化のおかげで判定できる)
# [email protected]     → False
Python

例題3:特定の拡張子を持つファイルだけ処理する

ディレクトリ内のファイルを走査して、CSVファイルだけを処理対象にする例です。os.listdir()やglobと組み合わせるのが定番です。

# ディレクトリ内のCSVファイルだけを処理する例(概念コード)
import os

def process_csv_files(directory):
    for filename in os.listdir(directory):
        # .csvで終わるファイルだけを処理対象にする
        if filename.endswith(".csv"):
            filepath = os.path.join(directory, filename)
            print(f"処理中: {filepath}")
            # 実際の処理(CSVの読み込みなど)をここに書く

# 使用例(カレントディレクトリの場合)
# process_csv_files(".")
Python

注意すべき重要なポイント

拡張子チェックの落とし穴:ドットを含むファイル名

これは実務で非常に重要な注意点です。endswith(".csv")のように判定すると、ファイル名にドットが複数含まれる場合、意図しない判定結果になることがあります。

# ドットを含むファイル名の落とし穴
filename = "archive.tar.csv"  # 実際はtarで圧縮されたCSV

print(filename.endswith(".csv"))  # True(ただし中身はtarファイルの可能性)
print(filename.endswith(".tar"))  # False

# より正確に拡張子を取得したい場合はos.path.splitext()を使う
import os
name, ext = os.path.splitext(filename)
print(ext)  # .csv(最後のドット以降を取得)
Python

さらに深刻なのが、「.jpg」や「.png」などの拡張子を複数持つファイル名です。攻撃者が「malware.exe.jpg」のようなファイル名を使うと、endswith(".jpg")はTrueを返しますが、実際には悪意のある実行ファイルである可能性があります。

# 多重拡張子攻撃の例(概念的な説明)
malicious = "virus.exe.jpg"  # 見た目はjpgだが、中身はexe

print(malicious.endswith(".jpg"))  # True(危険!)
# このファイルをそのまま許可すると、悪意のあるコードが実行される可能性がある
Python

対策としては、「最後の1つの拡張子だけ」を判定対象にする、ファイルの中身(MIMEタイプやマジックナンバー)を検証する、アップロード後のファイル名をランダムに変更するなど、多層防御を講じる必要があります。

# 最後の拡張子だけを厳密に判定する例
import os

def get_extension(filename):
    """最後のドット以降の拡張子を小文字で返す"""
    if not isinstance(filename, str):
        return ""
    # splitext()は最後のドットで分割する
    _, ext = os.path.splitext(filename)
    return ext.lower()

def is_safe_image(filename):
    """画像ファイルかを厳密に判定(多重拡張子は弾く)"""
    ext = get_extension(filename)
    # 画像ファイルの拡張子だけを許可
    return ext in (".jpg", ".jpeg", ".png", ".gif")

# 使用例
tests = ["photo.jpg", "virus.exe.jpg", "archive.tar.csv", "document.pdf"]
for t in tests:
    print(f"{t:>18} → {is_safe_image(t)}")
# photo.jpg         → True
# virus.exe.jpg     → False(最後の拡張子が.jpgだが、exeを含むため厳密にはNG)
# archive.tar.csv   → False
# document.pdf      → False
Python

なお、上記のis_safe_image()は「最後の拡張子だけ」を見るため、"virus.exe.jpg"は最後が.jpgなのでTrueになります。より厳密にするには、ファイル名全体に実行可能な拡張子が含まれないかをチェックする必要があります。

# より厳密なチェック(実行可能な拡張子を含むかどうかも判定)
def is_strictly_safe_image(filename):
    if not isinstance(filename, str):
        return False
    # ファイル名全体に実行可能な拡張子が含まれていないかチェック
    dangerous = (".exe", ".bat", ".cmd", ".sh", ".php", ".js")
    lower_name = filename.lower()
    if any(d in lower_name for d in dangerous):
        return False  # 危険な拡張子を含む

    # 最後の拡張子が画像かどうか
    ext = get_extension(filename)
    return ext in (".jpg", ".jpeg", ".png", ".gif")

print(is_strictly_safe_image("virus.exe.jpg"))  # False(.exeを含むため弾く)
print(is_strictly_safe_image("photo.jpg"))      # True
Python

Noneや非文字列型への対策

外部データや辞書の値など、Noneや整数が混入する可能性がある場合、そのままendswith()を呼ぶとエラーになります。型チェックのガードを入れるのが安全です。

# 安全に判定するためのガード付きテンプレート
def safe_endswith(value, suffix):
    # 文字列型でなければFalse(Noneや数値を誤って通さない)
    if not isinstance(value, str):
        return False
    return value.endswith(suffix)

print(safe_endswith("hello.txt", ".txt"))  # True
print(safe_endswith(None, ".txt"))         # False(エラーにならない)
print(safe_endswith(123, ".txt"))          # False(数値型も安全に処理)
Python

ユーザー入力や外部APIのレスポンスを扱う場合は、必ず型チェックを行ってから判定する習慣を付けてください。こうした防御的プログラミングが、予期しないクラッシュやセキュリティインシデントを防ぎます。

空文字列を判定対象に含めるべきか

先述の通り、endswith("")は常にTrueになります。これは「空文字列は任意の文字列の末尾部分とみなせる」という論理によるものですが、実務では意図しない判定を招くことがあります。

# 空文字列は常にTrueになる例
print("hello".endswith(""))  # True(空文字列はどの文字列でも「末尾に含まれる」)
print("".endswith(""))       # True
Python

もし「空文字列は弾きたい」という要件がある場合は、あらかじめ空文字列チェックを行ってください。

# 空文字列を弾く例
def strict_endswith(text, suffix):
    if not isinstance(text, str) or not text:
        return False  # 空文字列はFalse
    return text.endswith(suffix)

print(strict_endswith("hello.txt", ".txt"))  # True
print(strict_endswith("", ".txt"))            # False(空文字列を弾く)
Python

endswith()は「完全一致」ではない

endswith()は「末尾部分が一致するか」の判定であり、「文字列全体が一致するか(完全一致)」の判定ではありません。完全一致が必要な場合は==を使い分けてください。

# endswith()と完全一致(==)の違い
text = "report.txt"

print(text.endswith("report"))  # False(末尾は"report"ではない)
print(text == "report.txt")     # True(全体が一致)
Python

セキュリティ上の考慮:拡張子判定は「入口の検査」に過ぎない

情報セキュリティの観点では、endswith()による検証は多層防御の一環です。特にファイルアップロードでは、以下の対策を組み合わせる必要があります。

  • 拡張子のホワイトリスト:許可する拡張子を限定する(endswith()で判定)
  • MIMEタイプの検証:ファイルの中身を確認する(Pythonのmimetypesモジュールやmagicライブラリ)
  • ファイル名のサニタイズ:パストラバーサル対策、危険な文字の除去
  • 保存先の制限:Webサーバーから直接実行できない場所に保存する
  • リネーム:アップロード後にランダムなファイル名に変更する
# 多層防御の概念コード(簡略化)
import os
import mimetypes

def secure_file_upload(filename, file_content):
    # 第1層:拡張子の検証
    if not filename.lower().endswith((".jpg", ".png")):
        return False, "許可されていない拡張子です"

    # 第2層:MIMEタイプの検証(実際は file_content から検出する)
    mime_type, _ = mimetypes.guess_type(filename)
    if mime_type not in ("image/jpeg", "image/png"):
        return False, "ファイルの内容が不正です"

    # 第3層:保存先の制限(Webルート外など)
    # 実際の実装では、セキュアなディレクトリに保存する

    return True, "OK"
Python

つまり、endswith()は「拡張子の確認」として有効ですが、ファイルの正当性の完全な保証にはなりません。後続の処理で、ファイルの中身検証や保存場所の制限など、より厳密な対策を行うよう設計してください。

まとめ

特定の文字列で終わるかどうかを判定するには、endswith()メソッドを使います。基本構文は文字列.endswith(判定文字列)で、タプルを渡すことで複数条件の「いずれか」も簡潔に表現できます。大文字・小文字を区別しない判定が必要な場合は、lower()やcasefold()で正規化してから判定するのが定番です。

注意すべきは、in演算子との使い分け(末尾限定かどうか)、Noneや非文字列型へのガード、空文字列が常にTrueになる挙動、そしてセキュリティ上の検証は多層防御の一環に過ぎない点です。特にファイルアップロードの拡張子判定では、多重拡張子攻撃や中身の偽装に対する防御を怠らないよう、os.path.splitext()やMIMEタイプ検証などを併用してください。紹介したテンプレートを実際に動かし、挙動を確かめながら、堅牢で安全な文字列判定の実装に役立ててください。

タイトルとURLをコピーしました