PythonとPillowでブログ画像をWebPに一括変換するCLIツールを作る

2026.03.06 09:00
2026.03.16 13:32
PythonとPillowでブログ画像をWebPに一括変換するCLIツールを作る

以前、npmのimageminを使ったWebP変換の記事を書きましたが、今回はPythonでやってみます。Pythonだとargparseを使えばCLIツールとしてかなり柔軟に仕上げられるので、日常的に使うツールとして便利です。

なぜWebPに変換するのか

WebPはGoogleが開発した画像フォーマットで、JPEGやPNGと比べてファイルサイズを大幅に削減できます。具体的にはこんな感じです。

  • JPEGと比べて25〜35%ファイルサイズが小さい(同等画質で)
  • PNGと比べて26%以上小さい(ロスレスの場合)
  • 写真をPNGで保存してる場合は、WebPにするだけで劇的に軽くなる

たとえば1920×1080の写真だと、JPEGの品質85で約250KBのところ、WebPだと約180KBになります。ブログに大量の画像を貼っている場合、これはかなり効いてきます。

ブラウザの対応状況も2026年現在では97%以上をカバーしていて、Chrome・Firefox・Safari(14以降)・Edge・Operaすべて対応済みです。IE以外は問題なく表示できるので、もう実用上は心配いりません。

Pillowのインストール

PythonでWebP変換をするにはPillowというライブラリを使います。Python標準のPIL(Python Imaging Library)の後継で、画像処理の定番ですね。

pip install Pillow

インストールはこれだけです。Python 3.8以上なら問題なく動きます。

まずは1枚だけ変換してみる

いきなり一括変換に行く前に、まずは1枚だけWebPに変換するコードを見てみます。

from PIL import Image

# 画像を開く
img = Image.open("photo.jpg")

# WebPとして保存(品質80)
img.save("photo.webp", "WEBP", quality=80)

print("変換完了!")

たったこれだけです。Image.open()で画像を読み込んで、save()でWebP形式を指定して保存するだけ。Pillowが内部的にフォーマットの変換をやってくれます。

qualityパラメータは0〜100で指定でき、数値が大きいほど高画質(=ファイルサイズも大きい)になります。ブログ用途なら75〜85あたりがバランスが良いです。

ディレクトリ内の画像を一括変換する

1枚ずつ変換するのは面倒なので、指定したディレクトリ内のJPGとPNGをまとめてWebPに変換するスクリプトを作ります。

from PIL import Image
from pathlib import Path

def convert_to_webp(input_dir, output_dir, quality=80):
    """指定ディレクトリ内のJPG/PNG画像をWebPに一括変換する"""
    input_path = Path(input_dir)
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    # 対象の拡張子
    extensions = {".jpg", ".jpeg", ".png"}

    converted = 0
    for file in input_path.iterdir():
        if file.suffix.lower() in extensions:
            img = Image.open(file)

            # PNGのRGBAモードはそのまま、JPEGはRGBで処理
            if img.mode == "RGBA":
                pass  # 透過を保持
            else:
                img = img.convert("RGB")

            # 出力ファイルパス(拡張子を.webpに変更)
            output_file = output_path / f"{file.stem}.webp"
            img.save(output_file, "WEBP", quality=quality)

            # ファイルサイズの比較
            original_size = file.stat().st_size
            new_size = output_file.stat().st_size
            ratio = (1 - new_size / original_size) * 100

            print(f"✓ {file.name} → {output_file.name}")
            print(f"  {original_size:,} bytes → {new_size:,} bytes ({ratio:.1f}% 削減)")

            converted += 1

    print(f"\n合計 {converted} ファイルを変換しました")

# 使い方
convert_to_webp("./images", "./images_webp", quality=80)

ポイントはimg.modeのチェックです。PNGには透過(アルファチャンネル)を持つRGBAモードのものがあって、これをそのままWebPに変換すれば透過も保持されます。JPEGはRGBモードなのでそのまま変換すればOKです。

変換のたびにbefore/afterのファイルサイズと削減率を表示するようにしているので、どれくらい効果があったか一目でわかります。

オプション機能を追加する

基本の変換ができたら、もう少し便利な機能を追加していきます。

リサイズ機能

ブログ用の画像は横幅1200px程度あれば十分なことが多いです。大きすぎる画像はリサイズしてから変換するとさらにファイルサイズが小さくなります。

def resize_image(img, max_width):
    """最大幅を指定してリサイズ(アスペクト比を維持)"""
    if img.width > max_width:
        ratio = max_width / img.width
        new_height = int(img.height * ratio)
        img = img.resize((max_width, new_height), Image.LANCZOS)
    return img

Image.LANCZOSは高品質なリサイズアルゴリズムで、縮小時にきれいな結果が得られます。

EXIF情報(メタデータ)の保持

写真にはEXIF情報(撮影日時やカメラ情報)が含まれていることがあります。必要に応じてWebPにも引き継ぐことができます。

# EXIF情報を保持して保存
exif_data = img.info.get("exif", None)
if exif_data:
    img.save(output_file, "WEBP", quality=quality, exif=exif_data)
else:
    img.save(output_file, "WEBP", quality=quality)

ブログ用途ではEXIF情報は不要なことが多いですが、位置情報など個人情報を含む場合もあるので、むしろ削除したほうが安全なケースもあります。用途に応じて使い分ける感じですね。

元ファイルの扱い

変換後に元のJPG/PNGファイルをどうするかも重要です。削除するか残すかをオプションで選べるようにしておくと安心です。

import os

def remove_original(file_path, delete=False):
    """元ファイルを削除する(オプション)"""
    if delete:
        os.remove(file_path)
        print(f"  元ファイルを削除しました: {file_path.name}")

個人的には、まず出力先を別ディレクトリにして確認してから、問題なければ元ファイルを消す、というフローがおすすめです。いきなり削除は怖いですからね。

CLIツールとして仕上げる(argparse対応)

ここまでの機能をすべてまとめて、コマンドラインから使えるCLIツールに仕上げます。argparseを使うと、引数でオプションを柔軟に指定できるようになります。

#!/usr/bin/env python3
"""
画像をWebPに一括変換するCLIツール
対応形式: JPG, JPEG, PNG → WebP
"""

import argparse
import os
import sys
from pathlib import Path
from PIL import Image


def resize_image(img, max_width):
    """最大幅を指定してリサイズ(アスペクト比を維持)"""
    if img.width > max_width:
        ratio = max_width / img.width
        new_height = int(img.height * ratio)
        img = img.resize((max_width, new_height), Image.LANCZOS)
    return img


def convert_single(file_path, output_dir, quality, max_width, keep_exif, delete_original):
    """1ファイルをWebPに変換する"""
    file_path = Path(file_path)
    output_path = Path(output_dir) if output_dir else file_path.parent

    img = Image.open(file_path)

    # リサイズ
    if max_width:
        img = resize_image(img, max_width)

    # カラーモード処理
    if img.mode == "RGBA":
        pass  # 透過を保持
    elif img.mode == "P":
        img = img.convert("RGBA")  # パレットモードはRGBAに変換
    else:
        img = img.convert("RGB")

    # 出力先ディレクトリを作成
    output_path.mkdir(parents=True, exist_ok=True)
    output_file = output_path / f"{file_path.stem}.webp"

    # 保存オプション
    save_kwargs = {"quality": quality, "method": 4}

    # EXIF情報の処理
    if keep_exif:
        exif_data = img.info.get("exif", None)
        if exif_data:
            save_kwargs["exif"] = exif_data

    img.save(output_file, "WEBP", **save_kwargs)

    # ファイルサイズ比較
    original_size = file_path.stat().st_size
    new_size = output_file.stat().st_size
    ratio = (1 - new_size / original_size) * 100

    print(f"✓ {file_path.name} → {output_file.name}")
    print(f"  {original_size:,} bytes → {new_size:,} bytes ({ratio:.1f}% 削減)")

    # 元ファイル削除
    if delete_original:
        os.remove(file_path)
        print(f"  元ファイルを削除しました")

    return original_size, new_size


def main():
    parser = argparse.ArgumentParser(
        description="画像をWebPに一括変換するツール",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用例:
  python webp_converter.py ./images
  python webp_converter.py ./images -o ./output -q 85
  python webp_converter.py ./images --max-width 1200
  python webp_converter.py ./images --delete-original
  python webp_converter.py photo.jpg -o ./output
        """
    )
    parser.add_argument("input", help="変換する画像ファイルまたはディレクトリのパス")
    parser.add_argument("-o", "--output", help="出力先ディレクトリ(省略時は入力と同じ場所)")
    parser.add_argument("-q", "--quality", type=int, default=80,
                        help="WebPの品質 0-100(デフォルト: 80)")
    parser.add_argument("--max-width", type=int, default=None,
                        help="最大横幅(px)。超える場合はリサイズ")
    parser.add_argument("--keep-exif", action="store_true",
                        help="EXIF情報を保持する")
    parser.add_argument("--delete-original", action="store_true",
                        help="変換後に元ファイルを削除する")

    args = parser.parse_args()

    input_path = Path(args.input)

    if not input_path.exists():
        print(f"エラー: {args.input} が見つかりません", file=sys.stderr)
        sys.exit(1)

    # 対象の拡張子
    extensions = {".jpg", ".jpeg", ".png"}

    # ファイルリストを作成
    if input_path.is_file():
        if input_path.suffix.lower() not in extensions:
            print(f"エラー: {input_path.name} は対象外の形式です(JPG/PNG のみ対応)", file=sys.stderr)
            sys.exit(1)
        files = [input_path]
    else:
        files = sorted([f for f in input_path.iterdir() if f.suffix.lower() in extensions])

    if not files:
        print("変換対象のファイルが見つかりませんでした")
        sys.exit(0)

    print(f"変換対象: {len(files)} ファイル")
    print(f"品質: {args.quality}")
    if args.max_width:
        print(f"最大横幅: {args.max_width}px")
    print("---")

    total_original = 0
    total_new = 0

    for file in files:
        try:
            orig, new = convert_single(
                file,
                args.output,
                args.quality,
                args.max_width,
                args.keep_exif,
                args.delete_original
            )
            total_original += orig
            total_new += new
        except Exception as e:
            print(f"✗ {file.name}: エラー - {e}", file=sys.stderr)

    # サマリー
    if total_original > 0:
        total_ratio = (1 - total_new / total_original) * 100
        print("---")
        print(f"合計: {total_original:,} bytes → {total_new:,} bytes ({total_ratio:.1f}% 削減)")


if __name__ == "__main__":
    main()

実行例とbefore/afterの比較

実際に使ってみます。まずは基本的な使い方から。

# imagesディレクトリ内のJPG/PNGをWebPに変換(出力先: outputディレクトリ)
python webp_converter.py ./images -o ./output

実行すると、こんな感じで結果が表示されます。

変換対象: 3 ファイル
品質: 80
---
✓ photo1.jpg → photo1.webp
  1,245,680 bytes → 384,210 bytes (69.2% 削減)
✓ photo2.jpg → photo2.webp
  892,340 bytes → 276,890 bytes (69.0% 削減)
✓ screenshot.png → screenshot.webp
  2,456,780 bytes → 198,450 bytes (91.9% 削減)
---
合計: 4,594,800 bytes → 859,550 bytes (81.3% 削減)

JPEGからの変換で約70%削減、PNGからだと90%以上削減されることもあります。特にスクリーンショットなどのPNG画像はWebPとの相性が良くて、劇的にファイルサイズが小さくなりますね。

オプションを組み合わせるとこんな使い方もできます。

# 品質85、横幅1200pxにリサイズして変換
python webp_converter.py ./images -o ./output -q 85 --max-width 1200

# 単体ファイルの変換
python webp_converter.py photo.jpg -o ./output

# EXIF情報を保持して変換
python webp_converter.py ./images -o ./output --keep-exif

# 変換後に元ファイルを削除(注意して使う)
python webp_converter.py ./images -o ./output --delete-original

WordPressでのWebP運用の注意点

Pythonスクリプトで変換したWebP画像をWordPressにアップロードして使う場合、いくつか知っておきたいことがあります。

WordPress 5.8以降ならWebPは標準対応

WordPress 5.8からWebP形式のアップロードが標準でサポートされています。特別な設定なしにメディアライブラリにアップロードできます。

プラグインとの使い分け

WordPressにはEWWW Image OptimizerやConverter for Mediaなど、アップロード時に自動でWebP変換してくれるプラグインがあります。これらとPythonスクリプトの使い分けはこんな感じです。

  • プラグインが向いているケース: WordPressの管理画面から直接画像をアップロードする運用。既存画像の一括変換機能もある
  • Pythonスクリプトが向いているケース: アップロード前にローカルで画像を整理・加工したい場合。リサイズや品質の細かい調整を自分でコントロールしたい場合。WordPress以外のサイトでも使いたい場合

自分の場合は、ブログ用の画像はアップロード前にローカルで一括変換してからWordPressに上げています。プラグインに頼ると、サーバー側の処理負荷が気になるのと、変換の品質設定を自分で管理したいからです。

OGP画像には注意

SNSでシェアされたときに表示されるOGP画像(アイキャッチ画像)は、一部のSNSクローラーがWebPに対応していない場合があります。アイキャッチ画像だけはJPEGで用意しておくのが無難ですね。

まとめ

PythonとPillowを使えば、数十行のコードでWebP一括変換ツールが作れます。argparseでCLI対応すれば、日常的に使える便利なツールになります。

  • pip install Pillow だけで導入できる
  • JPG/PNGからWebPへの変換でファイルサイズを大幅に削減
  • リサイズ、品質指定、EXIF保持などのオプションも簡単に追加できる
  • argparseでコマンドライン引数に対応させればCLIツールとして完成

以前書いたnpmのimageminを使う方法と比べると、Pythonのほうがコード量は多いですが、そのぶん細かい制御ができます。自分好みにカスタマイズしていけるのがPythonの良いところですね。

今回は以上です!