0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python初学者】楽天価格チェッカーを作ってみた ⑤ CSV保存(csv_saver.py)

0
Last updated at Posted at 2026-04-18

【Python初学者】楽天価格チェッカーを作ってみた ⑤ ― CSV保存(csv_saver.py)

はじめに🐰

Python初学者のわたしが覚えるために、学んだことを整理し、理解を深めるために記事を書いています。
私が実際にやってみて、悩んだ部分や過程などを残していきます🐥

検索した商品データをCSVファイルに保存する処理です😊
「ファイルの書き込み」「日付からファイル名を作る」「重複ファイル名の連番処理」など、実用的なファイル操作について学んでいきます✨️


本日のゴール⚽️

  • csv モジュールでCSVファイルを書き出す方法を理解する
  • datetime で今日の日付を文字列にする方法を覚える
  • os.path.exists() でファイルの存在確認ができるようになる
  • with open() によるファイル操作の基本を理解する

完成したコード🐣

import csv
import os
import sys
from datetime import datetime

sys.path.append(os.path.join(os.path.dirname(__file__), "../"))

from utils.logger import SimpleLogger
from utils.path_helper import get_root_path
from config import (
    CSV_REPORT_TITLE,
    CSV_SUMMARY_HEADERS,
    CSV_LIST_TITLE,
    CSV_PRODUCT_HEADERS
)


class CsvSaver:
    def __init__(self):
        self.logger_setup = SimpleLogger()
        self.logger = self.logger_setup.get_logger()

    # -----------------------
    # 1つ目のフロー
    # ファイル名を生成する(同名ファイルがあれば連番を付ける)
    # -----------------------
    def build_filename(self, keyword: str, data_dir: str) -> str:
        # 今日の日付を yyyymmdd 形式で取得
        today = datetime.now().strftime("%Y%m%d")
        # スペースをアンダースコアに置換してファイル名を安全にする
        safe_keyword = keyword.replace(" ", "_").replace(" ", "_")

        # まず連番なしで試し、存在すれば _2, _3 と増やす
        base     = f"{today}_{safe_keyword}"
        filename = f"{base}.csv"
        count    = 2
        while os.path.exists(os.path.join(data_dir, filename)):
            filename = f"{base}_{count}.csv"
            count += 1

        return filename

    # -----------------------
    # 2つ目のフロー
    # 保存先のパスを組み立てる(フォルダがなければ作成する)
    # -----------------------
    def build_filepath(self, filename: str) -> str:
        data_dir = os.path.join(get_root_path(), "data")
        os.makedirs(data_dir, exist_ok=True)
        return os.path.join(data_dir, filename)

    # -----------------------
    # 3つ目のフロー
    # CSVファイルに書き出す
    # -----------------------
    def save(self, keyword: str, summary: dict, product_list: list[dict], count: int) -> str:
        data_dir  = os.path.join(get_root_path(), "data")
        os.makedirs(data_dir, exist_ok=True)

        filename = self.build_filename(keyword, data_dir)
        filepath = self.build_filepath(filename)

        self.logger.info(f"CSVの保存を開始します: {filepath}")

        with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.writer(f)

            # ── サマリーセクション ──
            writer.writerow([CSV_REPORT_TITLE])
            writer.writerow(list(CSV_SUMMARY_HEADERS.values()))
            writer.writerow([
                keyword,
                summary.get("min_price", ""),
                summary.get("avr_price", ""),
                summary.get("max_price", ""),
                count,
            ])

            writer.writerow([])  # 空行(区切り)

            # ── 商品一覧セクション ──
            writer.writerow([CSV_LIST_TITLE])
            writer.writerow(CSV_PRODUCT_HEADERS)
            for product in product_list:
                writer.writerow([
                    product.get("price",        ""),
                    product.get("name",         ""),
                    product.get("shop",         ""),
                    product.get("url",          ""),
                    product.get("review_avg",   ""),
                    product.get("review_count", ""),
                ])

        self.logger.info(f"CSV保存完了: {filename}")
        return filepath

出力されるCSVのイメージ🐣

■ 検索統計レポート
検索キーワード,最安価格,平均価格,最高価格,有効ヒット件数
青汁,980,1203,2980,42

■ 商品詳細(価格の安い順)
価格,商品名,ショップ名,商品URL,平均レビュー,レビュー件数
980,青汁 30包,健康ショップA,https://...,4.2,156
1080,有機青汁 1か月分,自然食品B,https://...,4.5,89
...

コードの詳細解説

① datetime で今日の日付を文字列にする

from datetime import datetime

today = datetime.now().strftime("%Y%m%d")
# → "20260405"

datetime.now() は「今この瞬間の日時」を取得します。.strftime() はその日時を好きな形式の文字列に変換するメソッドです。

書式 意味
%Y 4桁の西暦 2026
%m 2桁の月(ゼロ埋め) 04
%d 2桁の日(ゼロ埋め) 05
%H 2桁の時(24時間) 14
%M 2桁の分 30
# 他の使用例
datetime.now().strftime("%Y年%m月%d日")   # → "2026年04月05日"
datetime.now().strftime("%Y%m%d_%H%M")   # → "20260405_1430"

② str.replace() でファイル名を安全にする

この処理は、検索キーワードを「ファイル名として使える形」に整えるためのものです🐣

safe_keyword = keyword.replace(" ", "_").replace(" ", "_")

ファイル名にスペース(半角・全角)が入っていると、OSによっては扱いにくくなることがあります。
replace() で全部アンダースコアに置き換えて安全なファイル名にしています。

つまり、キーワードがそのままファイル名の一部になります😊

# replace() の基本
"青汁 雑穀米".replace(" ", "_")   # → "青汁_雑穀米"
"青汁 雑穀米".replace(" ", "_") # → "青汁_雑穀米"

💡 半角スペースと全角スペースをそれぞれ置き換える
👉 「半角スペース」と「全角スペース」をそれぞれ置き換えているだけ

  • .replace(" ", "_") は「半角スペースだけ」を置き換える
  • .replace(" ", "_") は「全角スペースだけ」を置き換える

⚠️ 注意(よくあるミス)

  • .replace("", "_")
    "たまごたまご".replace("", "_") → _た_ま_ご__た_ま_ご_

.replace("", "_") は 全く別物(全部に入るのでNG)です😱
まんまと私は引っかかりました〜💦


③ os.path.exists() でファイルの存在確認

⚠️この処理は、同じ名前のファイルがすでに存在しないかを確認するためのものです😊

while os.path.exists(os.path.join(data_dir, filename)):
    filename = f"{base}_{count}.csv"
    count += 1

同じキーワードで同じ日に2回実行すると、同名ファイルが作られて上書きされてしまいます。それを防ぐために、ファイルが存在すれば _2, _3 と連番を付けるようにしています。

🔍 os.path.exists() とは?

os.path.exists("パス")  # ファイルまたはフォルダが存在すれば True

指定したパスに「ファイルやフォルダが存在するか」を確認する関数

  • 存在する → True
  • 存在しない → False

🔸 何をチェックしているの?

os.path.exists(os.path.join(data_dir, filename))

「dataフォルダの中に、そのファイル名があるか?」を確認している

あとはwhile ループで「存在しなくなるまでカウントを増やす」という流れです🐣

while os.path.exists(...):
#存在する間はずっと繰り返す

【実際の流れ】
🔸1回目
20260405_青汁.csv → 存在しない
→そのまま使う

🔸2回目
20260405_青汁.csv → 存在する
→じゃあ名前を変える!
20260405_青汁_2.csv

🔸さらにチェック
20260405_青汁_2.csv → 存在しない

🔸これを採用!
という感じの流れになります😊


④ os.makedirs() でフォルダを作成する

この処理は、CSVファイルを保存するフォルダ(dataフォルダ)を準備するためのものです🐣

🤔 なぜ必要?

ファイルを保存するとき👇
保存先のフォルダが存在していないとエラーになる!!

os.makedirs(data_dir, exist_ok=True)

data フォルダが存在しない場合に自動で作成します。
exist_ok=True をつけると、フォルダがすでに存在してもエラーにならず、何もしないで続けてくれます。

os.makedirs("data")              # フォルダがすでにあればエラー
os.makedirs("data", exist_ok=True)  # フォルダがあってもOK

⑤ with open() でファイルを開く

このコードはCSVファイルを開いて、データを書き込む処理です!🐣

with open(filepath, "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow([...])

with open() はファイルを開いて処理する定番の書き方です😊
with ブロックを抜けると自動でファイルが閉じられます(閉じ忘れを防げる)。

🔰この一連の流れをまとめて書いているのが with open(...) as f:
ファイルを開く

データを書く

ファイルを閉じる(自動)

🔍 引数の意味(ここがつまずきやすい)

open(filepath, "w", newline="", encoding="utf-8-sig")
引数 意味
filepath 開くファイルのパス
"w" 書き込みモード(ファイルがなければ新規作成、あれば上書き)
newline="" CSVで改行が二重にならないようにする設定
encoding="utf-8-sig" Excelで文字化けしないUTF-8(BOM付き)

encoding="utf-8-sig" について: 通常の utf-8 でCSVを保存するとExcelで開いたときに日本語が文字化けすることがあります。utf-8-sig はBOM(バイトオーダーマーク)付きで保存するため、Excelが「これはUTF-8だ」と認識してくれます。


⑥ csv.writer で行を書く

CSV形式で書き込むための「書き込み用オブジェクト」を作るために、
writer = csv.writer(f)が必要になります🐣

  • csv.writer() → CSV形式で書くための機能
  • (f) → 「このファイルに書くよ」と指定
writer = csv.writer(f)
writer.writerow(["列1", "列2", "列3"])  # 1行書く

csv.writer を使うと、リストを渡すだけで自動的にカンマ区切りの形式に変換して書き込んでくれます!
文字にカンマが含まれていても、自動でダブルクォートで囲んでくれます🐣

🔰ポイント
文字の中にカンマ(,)が含まれている場合、そのままだと列の区切りと勘違いされてしまいます😱

そのため、csv.writer は自動でダブルクォート("")で囲み、「これは1つのデータです」と分かるようにしてくれます。

writer.writerow(["たまご,いちご", "みかん"])
# → "たまご,いちご",みかん

これは知っておこう🐰
writerow() は1行を書き込むメソッド、writerows() は複数行をまとめて書き込むメソッドです!


⑦ dict.values() でヘッダーを取り出す

この処理は、CSVのヘッダー(見出し行)を作るためのものです!

writer.writerow(list(CSV_SUMMARY_HEADERS.values()))

config.py では辞書でヘッダーを管理しています🐣
🔍 もとになっているデータ

# config.py
CSV_SUMMARY_HEADERS = {
    "keyword": "検索キーワード",
    "min":     "最安価格",
    "avg":     "平均価格",
    "max":     "最高価格",
    "count":   "有効ヒット件数"
}

👉 「キー」と「値」がセットになった辞書です!

🤔 やりたいこと
CSVにはこんな1行を書きたい👇

検索キーワード,最安価格,平均価格,最高価格,有効ヒット件数

🔸 .values() の役割
CSV_SUMMARY_HEADERS.values()
👉 値(表示用の文字)だけを取り出す

🔸 list() にする理由
.values() は辞書の値を取り出しますが、そのままだと dict_values という専用の型になります!

CSV_SUMMARY_HEADERS.values()
# → dict_values([...])

writer.writerow() には「リスト形式」で渡す必要があるため、list() で変換しています。
list() で変換してから writerow() に渡しています🐣

d = {"a": "名前", "b": "価格"}
list(d.values())  # → ["名前", "価格"]

まとめ

学んだこと ポイント
datetime.now().strftime() 日付を好きな形式の文字列にする
str.replace() 文字列の一部を別の文字に置き換える
os.path.exists() ファイルが存在するか確認する
while + 連番 重複ファイル名を避ける実用パターン
os.makedirs(exist_ok=True) フォルダを安全に作成する
with open() ファイルを自動でクローズする書き方
encoding="utf-8-sig" ExcelでCSVを開いても文字化けしない設定
csv.writer リストをCSV形式で書き込む

次回は utils/popup.py(tkinterによるポップアップUI)を解説します!


🐣シリーズ一覧🐣

  • ① プロジェクト全体像とフォルダ構成
  • ② 楽天APIリクエスト(rakuten_api.py)
  • ③ フィルタリング・並び替え(price_list_builder.py)
  • ④ 統計計算(price_stats.py)
  • ⑤ CSV保存(csv_saver.py) ← 今回
  • ⑥ ポップアップUI(popup.py)
  • ⑦ パス管理・設定(path_helper.py & config.py)
  • ⑧ 全体統括(main_flow.py)
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?