課題17: ファイル処理ユーティリティ

課題概要

この課題では、Goの標準ライブラリを活用して、実用的なファイル処理ユーティリティを作成します。io、os、filepath、strings、time、encoding/json などのパッケージを組み合わせて使います。

マンダトリー要件(80点)

要件1: ファイル統計ツール(30点)

ディレクトリ内のファイル情報を収集し、統計を表示するツールを作成してください。

実装ファイル: filestats/filestats.go

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

type FileStats struct {
    TotalFiles      int
    TotalDirs       int
    TotalSize       int64
    FilesByExt      map[string]int
    LargestFile     string
    LargestFileSize int64
}

// AnalyzeDirectory はディレクトリを再帰的に解析します
func AnalyzeDirectory(rootPath string) (*FileStats, error) {
    stats := &FileStats{
        FilesByExt: make(map[string]int),
    }

    // TODO: filepath.Walk を使ってディレクトリを走査
    // - ファイル数とディレクトリ数をカウント
    // - 合計サイズを計算
    // - 拡張子ごとのファイル数を記録
    // - 最大ファイルを追跡

    return stats, nil
}

// PrintStats は統計情報を表示します
func (s *FileStats) PrintStats() {
    // TODO: 統計情報を整形して表示
    fmt.Printf("Total Files: %d\n", s.TotalFiles)
    fmt.Printf("Total Directories: %d\n", s.TotalDirs)
    fmt.Printf("Total Size: %d bytes (%.2f MB)\n", s.TotalSize, float64(s.TotalSize)/(1024*1024))

    fmt.Println("\nFiles by extension:")
    for ext, count := range s.FilesByExt {
        if ext == "" {
            ext = "(no extension)"
        }
        fmt.Printf("  %s: %d\n", ext, count)
    }

    if s.LargestFile != "" {
        fmt.Printf("\nLargest file: %s (%d bytes)\n", s.LargestFile, s.LargestFileSize)
    }
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: filestats <directory>")
        os.Exit(1)
    }

    dir := os.Args[1]

    stats, err := AnalyzeDirectory(dir)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }

    stats.PrintStats()
}

要件2: CSV変換ツール(25点)

CSVファイルをJSON形式に変換するツールを作成してください。

実装ファイル: csvtojson/converter.go

package main

import (
    "encoding/csv"
    "encoding/json"
    "fmt"
    "io"
    "os"
)

// ConvertCSVToJSON はCSVファイルをJSONに変換します
func ConvertCSVToJSON(csvFile, jsonFile string) error {
    // TODO: 実装
    // 1. CSVファイルを開く
    // 2. ヘッダー行を読み込む
    // 3. データ行を読み込み、map[string]stringのスライスに変換
    // 4. JSONファイルに書き込む

    // CSVファイルを開く
    file, err := os.Open(csvFile)
    if err != nil {
        return err
    }
    defer file.Close()

    reader := csv.NewReader(file)

    // ヘッダーを読み込み
    headers, err := reader.Read()
    if err != nil {
        return err
    }

    // データを読み込み
    var records []map[string]string
    for {
        row, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }

        // TODO: 行をマップに変換
        // record := make(map[string]string)
        // for i, header := range headers {
        //     record[header] = row[i]
        // }
        // records = append(records, record)
    }

    // TODO: JSONファイルに書き込み

    return nil
}

func main() {
    if len(os.Args) < 3 {
        fmt.Println("Usage: csvtojson <input.csv> <output.json>")
        os.Exit(1)
    }

    csvFile := os.Args[1]
    jsonFile := os.Args[2]

    err := ConvertCSVToJSON(csvFile, jsonFile)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Converted %s to %s\n", csvFile, jsonFile)
}

テスト用CSVファイル: test.csv

name,age,email
Alice,30,alice@example.com
Bob,25,bob@example.com
Charlie,35,charlie@example.com

要件3: テキスト処理ツール(25点)

テキストファイルに対して様々な操作を行うツールを作成してください。

実装ファイル: textutil/textutil.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

type TextStats struct {
    Lines      int
    Words      int
    Characters int
    UniqueWords map[string]int
}

// AnalyzeText はテキストファイルを解析します
func AnalyzeText(filename string) (*TextStats, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    stats := &TextStats{
        UniqueWords: make(map[string]int),
    }

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        stats.Lines++

        // TODO: 文字数をカウント
        // TODO: 単語を分割してカウント
        // TODO: ユニークな単語を記録

        words := strings.Fields(line)
        stats.Words += len(words)
        stats.Characters += len(line)

        for _, word := range words {
            word = strings.ToLower(strings.Trim(word, ".,!?;:"))
            stats.UniqueWords[word]++
        }
    }

    return stats, scanner.Err()
}

// PrintStats は統計情報を表示します
func (s *TextStats) PrintStats() {
    fmt.Printf("Lines: %d\n", s.Lines)
    fmt.Printf("Words: %d\n", s.Words)
    fmt.Printf("Characters: %d\n", s.Characters)
    fmt.Printf("Unique words: %d\n", len(s.UniqueWords))

    // 上位10単語を表示
    // TODO: 頻度順にソートして表示
}

// SearchAndReplace はテキスト内の文字列を置換します
func SearchAndReplace(inputFile, outputFile, search, replace string) error {
    // TODO: 実装
    // 1. 入力ファイルを読み込み
    // 2. 文字列を置換
    // 3. 出力ファイルに書き込み

    data, err := os.ReadFile(inputFile)
    if err != nil {
        return err
    }

    // TODO: 置換処理
    // content := string(data)
    // newContent := strings.ReplaceAll(content, search, replace)

    // TODO: 書き込み

    return nil
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage:")
        fmt.Println("  textutil analyze <file>")
        fmt.Println("  textutil replace <input> <output> <search> <replace>")
        os.Exit(1)
    }

    command := os.Args[1]

    switch command {
    case "analyze":
        if len(os.Args) < 3 {
            fmt.Println("Usage: textutil analyze <file>")
            os.Exit(1)
        }
        stats, err := AnalyzeText(os.Args[2])
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            os.Exit(1)
        }
        stats.PrintStats()

    case "replace":
        if len(os.Args) < 6 {
            fmt.Println("Usage: textutil replace <input> <output> <search> <replace>")
            os.Exit(1)
        }
        err := SearchAndReplace(os.Args[2], os.Args[3], os.Args[4], os.Args[5])
        if err != nil {
            fmt.Fprintf(os.Stderr, "Error: %v\n", err)
            os.Exit(1)
        }
        fmt.Println("Replacement complete")

    default:
        fmt.Println("Unknown command:", command)
        os.Exit(1)
    }
}

期待される出力

filestats の実行例

$ go run filestats.go /path/to/directory

Total Files: 42
Total Directories: 8
Total Size: 1048576 bytes (1.00 MB)

Files by extension:
  .go: 15
  .txt: 10
  .json: 5
  (no extension): 12

Largest file: /path/to/directory/large.txt (524288 bytes)

csvtojson の実行例

$ go run converter.go test.csv output.json
Converted test.csv to output.json

$ cat output.json
[
  {
    "age": "30",
    "email": "alice@example.com",
    "name": "Alice"
  },
  {
    "age": "25",
    "email": "bob@example.com",
    "name": "Bob"
  }
]

textutil の実行例

$ go run textutil.go analyze test.txt

Lines: 100
Words: 500
Characters: 3000
Unique words: 200

Top 10 words:
  the: 45
  and: 30
  to: 25
  ...

ボーナス課題(20点)

ボーナス1: ログ解析ツール(10点)

構造化されたログファイルを解析し、統計とフィルタリング機能を提供するツールを作成してください。

実装ファイル: loganalyzer/analyzer.go

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "time"
)

type LogEntry struct {
    Timestamp time.Time `json:"timestamp"`
    Level     string    `json:"level"`
    Message   string    `json:"message"`
}

// ParseLogFile はログファイルを解析します
func ParseLogFile(filename string) ([]LogEntry, error) {
    // TODO: 実装
    // フォーマット: "2024-01-15 10:30:00 [INFO] Message"
}

// FilterByLevel は指定されたレベルのログをフィルタします
func FilterByLevel(entries []LogEntry, level string) []LogEntry {
    // TODO: 実装
}

// FilterByTimeRange は時間範囲でフィルタします
func FilterByTimeRange(entries []LogEntry, start, end time.Time) []LogEntry {
    // TODO: 実装
}

// ExportToJSON はログをJSON形式でエクスポートします
func ExportToJSON(entries []LogEntry, filename string) error {
    // TODO: 実装
}

// PrintStatistics は統計情報を表示します
func PrintStatistics(entries []LogEntry) {
    stats := make(map[string]int)
    for _, entry := range entries {
        stats[entry.Level]++
    }

    fmt.Println("Log Statistics:")
    for level, count := range stats {
        fmt.Printf("  %s: %d\n", level, count)
    }
}

func main() {
    // TODO: コマンドライン引数処理
    // - parse <logfile>: ログファイルを解析
    // - filter <logfile> <level>: レベルでフィルタ
    // - export <logfile> <output.json>: JSON形式でエクスポート
}

ボーナス2: ファイル同期ツール(5点)

2つのディレクトリを比較し、差分を表示するツールを作成してください。

実装ファイル: filesync/sync.go

package main

import (
    "crypto/md5"
    "fmt"
    "io"
    "os"
    "path/filepath"
)

type FileDiff struct {
    OnlyInSource []string
    OnlyInDest   []string
    Different    []string
}

// CompareDirectories は2つのディレクトリを比較します
func CompareDirectories(source, dest string) (*FileDiff, error) {
    // TODO: 実装
    // 1. 両方のディレクトリのファイルリストを取得
    // 2. 存在チェック
    // 3. MD5ハッシュで内容を比較
}

// CalculateMD5 はファイルのMD5ハッシュを計算します
func CalculateMD5(filename string) (string, error) {
    // TODO: 実装
}

func main() {
    // TODO: 実装
}

ボーナス3: 設定ファイル管理ツール(5点)

JSON設定ファイルを読み書きし、環境変数で上書きできるツールを作成してください。

実装ファイル: config/manager.go

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

type Config struct {
    Database struct {
        Host     string `json:"host"`
        Port     int    `json:"port"`
        Username string `json:"username"`
        Password string `json:"password"`
    } `json:"database"`
    Server struct {
        Port    int    `json:"port"`
        Host    string `json:"host"`
        Timeout int    `json:"timeout"`
    } `json:"server"`
}

// LoadConfig は設定ファイルを読み込みます
func LoadConfig(filename string) (*Config, error) {
    // TODO: 実装
    // 1. JSONファイルを読み込み
    // 2. 環境変数で上書き(例: DB_HOST -> Database.Host)
}

// SaveConfig は設定をファイルに保存します
func SaveConfig(config *Config, filename string) error {
    // TODO: 実装
}

// PrintConfig は設定を表示します
func (c *Config) PrintConfig() {
    // TODO: 実装
}

func main() {
    // TODO: 実装
    // - load <file>: 設定を読み込んで表示
    // - set <file> <key> <value>: 値を設定
}

評価基準

項目 配点 詳細
ファイル統計ツール 30点 ディレクトリ走査と統計計算が正確
CSV変換ツール 25点 CSV解析とJSON出力が正しい
テキスト処理ツール 25点 テキスト解析と置換が動作する
**ボーナス1: ログ解析** 10点 ログのパース、フィルタリング、エクスポートが実装されている
**ボーナス2: ファイル同期** 5点 ディレクトリ比較とMD5ハッシュが正しく実装されている
**ボーナス3: 設定管理** 5点 JSON設定の読み書きと環境変数の統合が動作する

提出方法

以下のディレクトリ構造で提出してください:

submission/
├── filestats/
│   └── filestats.go
├── csvtojson/
│   ├── converter.go
│   └── test.csv
├── textutil/
│   └── textutil.go
├── bonus/              # ボーナス課題(オプション)
│   ├── loganalyzer/
│   ├── filesync/
│   └── config/
└── README.md           # 実行方法と使用例

ヒント