fetch_images.rb - Markdown画像取得・更新ツール by #Claude

結城浩: 以下は、Claude Codeが作成した文書とコードです。

概要

fetch_images.rbは、Markdownファイル内の外部画像URLを検出し、ローカルにダウンロードして、Markdownファイル内のURLをローカルパスに自動更新するRubyスクリプトです。

主な機能

  • Markdownファイル内の画像URL(![alt](url)形式とHTMLの<img>タグ)を検出
  • 外部画像をローカルディレクトリにダウンロード
  • Markdownファイル内の画像URLをローカルパスに自動更新
  • 元のMarkdownファイルのバックアップを作成
  • ソースファイルごとに整理された画像ディレクトリ構造

使用方法

基本的な使い方

ruby fetch_images.rb [ディレクトリ] [出力ディレクトリ] [オプション]

パラメータ

  • ディレクトリ(省略可能): Markdownファイルを検索するディレクトリ。デフォルトは現在のディレクトリ(.
  • 出力ディレクトリ(省略可能): 画像を保存するディレクトリ。デフォルトはfetched_images
  • --no-update(オプション): このフラグを指定すると、画像のダウンロードのみ行い、Markdownファイルは更新しません

使用例

# カレントディレクトリのMarkdownファイルを処理
ruby fetch_images.rb

# 特定のディレクトリを指定
ruby fetch_images.rb ./documents

# 出力ディレクトリを指定
ruby fetch_images.rb . ./images

# Markdownファイルを更新せずに画像のみダウンロード
ruby fetch_images.rb . ./images --no-update

動作の詳細

1. ディレクトリ構造

画像は以下のような構造で保存されます:

fetched_images/
├── ドキュメント名1/
│   ├── image_1.jpg
│   ├── image_2.png
│   └── ...
├── ドキュメント名2/
│   ├── image_1.jpg
│   └── ...
└── ...

2. 画像の命名規則

  • ダウンロードされた画像はimage_1.jpgimage_2.pngのように連番で命名されます
  • 拡張子は元のファイルから取得し、不明な場合は.jpgがデフォルトで使用されます

3. 対応する画像形式

  • Markdown形式: ![代替テキスト](画像URL)
  • HTML形式: <img src="画像URL" ...>
  • HTTP/HTTPSのURLのみ対応(データURIはスキップ)

4. バックアップ

  • Markdownファイルを更新する前に、.backup拡張子を付けたバックアップファイルが作成されます
  • 例: document.mddocument.md.backup

必要な環境

  • Ruby(標準ライブラリのみ使用)
  • インターネット接続(外部画像のダウンロード用)

エラー処理

  • ダウンロードに失敗した画像URLは元のまま保持されます
  • エラーメッセージが表示されますが、処理は続行されます
  • データURIや非HTTP/HTTPSのURLは自動的にスキップされます

注意事項

  • 大量の画像を含むMarkdownファイルを処理する場合、ダウンロードに時間がかかることがあります
  • 同じURLの画像が複数回出現する場合、一度だけダウンロードされ、すべての参照が同じローカルパスに更新されます
  • バックアップファイルは自動的に削除されないため、必要に応じて手動で削除してください

ライセンス

このスクリプトは個人使用を目的として作成されています。使用は自己責任でお願いします。

コード

#!/usr/bin/env ruby

require 'open-uri'
require 'fileutils'
require 'uri'
require 'net/http'

class MarkdownImageFetcher
  def initialize(directory = '.', output_dir = 'fetched_images', update_markdown = true)
    @directory = directory
    @output_dir = output_dir
    @update_markdown = update_markdown
    @image_mappings = {}
  end

  def run
    create_output_directory
    scan_and_process_markdown_files
    puts "\nCompleted!"
  end

  private

  def create_output_directory
    FileUtils.mkdir_p(@output_dir)
    puts "Created output directory: #{@output_dir}"
  end

  def scan_and_process_markdown_files
    markdown_files = Dir.glob(File.join(@directory, '*.md'))

    if markdown_files.empty?
      puts "No Markdown files found in #{@directory}"
      return
    end

    puts "Found #{markdown_files.count} Markdown files"

    markdown_files.each do |file|
      process_markdown_file(file)
    end
  end

  def process_markdown_file(file)
    content = File.read(file)
    filename = File.basename(file)
    base_name = filename.gsub('.md', '')

    puts "\nProcessing: #{filename}"

    # Track image mappings for this file
    file_mappings = {}
    image_counter = 1

    # Process Markdown image syntax: ![alt text](url)
    modified_content = content.gsub(/!\[([^\]]*)\]\(([^)]+)\)/) do |match|
      alt_text = $1
      url = $2

      new_path = download_and_get_new_path(url, base_name, image_counter)
      if new_path
        file_mappings[url] = new_path
        image_counter += 1
        "![#{alt_text}](#{new_path})"
      else
        match  # Keep original if download failed
      end
    end

    # Process HTML img tags: <img src="url" ...>
    modified_content = modified_content.gsub(/<img([^>]+)src=["']([^"']+)["']([^>]*)>/i) do |match|
      prefix = $1
      url = $2
      suffix = $3

      # Check if we already processed this URL
      if file_mappings[url]
        new_path = file_mappings[url]
      else
        new_path = download_and_get_new_path(url, base_name, image_counter)
        if new_path
          file_mappings[url] = new_path
          image_counter += 1
        end
      end

      if new_path
        "<img#{prefix}src=\"#{new_path}\"#{suffix}>"
      else
        match  # Keep original if download failed
      end
    end

    # Save the modified content if requested
    if @update_markdown && file_mappings.any?
      backup_file = file + '.backup'
      File.write(backup_file, content)
      File.write(file, modified_content)
      puts "  Updated #{filename} (backup saved as #{File.basename(backup_file)})"
      puts "  Replaced #{file_mappings.count} image URLs"
    end
  end

  def download_and_get_new_path(url, base_name, index)
    # Skip if URL is a data URI
    if url.start_with?('data:')
      puts "  Skipping data URI"
      return nil
    end

    begin
      # Parse URL
      uri = URI.parse(url)

      # Determine filename and extension
      original_filename = File.basename(uri.path)
      extension = File.extname(original_filename)
      extension = '.jpg' if extension.empty?  # Default extension

      # Create new filename
      new_filename = "image_#{index}#{extension}"

      # Create subdirectory for this markdown file
      source_dir = File.join(@output_dir, base_name)
      FileUtils.mkdir_p(source_dir)

      # Full output path
      output_path = File.join(source_dir, new_filename)

      # Download the image
      print "  Downloading #{url}..."

      if uri.scheme =~ /^https?$/
        URI.open(url) do |remote_file|
          File.open(output_path, 'wb') do |local_file|
            local_file.write(remote_file.read)
          end
        end
        puts " Done!"

        # Return the relative path that will be used in the markdown
        return "#{base_name}/#{new_filename}"
      else
        puts " Skipped (not HTTP/HTTPS)"
        return nil
      end
    rescue => e
      puts " Error: #{e.message}"
      return nil
    end
  end
end

# Main execution
if __FILE__ == $0
  # Parse command line arguments
  directory = ARGV[0] || '.'
  output_dir = ARGV[1] || 'fetched_images'
  update_markdown = !ARGV.include?('--no-update')

  puts "Markdown Image Fetcher and Updater"
  puts "=================================="
  puts "Scanning directory: #{directory}"
  puts "Output directory: #{output_dir}"
  puts "Update Markdown files: #{update_markdown}"
  puts "(Use --no-update flag to only download without modifying markdown files)"
  puts

  fetcher = MarkdownImageFetcher.new(directory, output_dir, update_markdown)
  fetcher.run
end

(2025年6月4日)