お悩み女子取得したデータをCSVに保存できたのに、Excelで開くと列がずれたり、商品コードのゼロが消えたりします。
スクレイピング結果の保存では取得した値・列の順番・開くソフトでの扱いを分けて確かめると、原因を追いやすくなります。文字コードを変えるだけでは、先頭ゼロや数式としての解釈は直りません。
この記事では手元の練習HTMLから3件取得し、列名付きCSVへ保存して読み直します。Excel形式への保存例も紹介します。Windows 11とPython 3.12.10で、ローカルのHTTPサーバーと架空データを使って確認しています。外部サイトやExcelアプリの画面では試していません。
- CSVの列名・文字コード・改行の決め方
- 取得結果を原文JSONと表計算用CSVに保存する方法
- カンマ・引用符・先頭ゼロの確認
- openpyxlで文字列を保ったExcelファイルを作る例
CSVの列設計と取得原文の保持
取得処理と保存処理の境目で、出力する項目を固定します。後から取得項目が増えても、意図しない列が混ざらない形にします。
固定した列名と順番
例ではid、title、urlの3列にします。idは商品を区別する文字列、titleは表示名、urlは取得元をたどるためのURLです。取得した辞書の順番に任せず、DictWriterのfieldnamesで列順を指定します。
IDを整数に変えると001が1になります。計算しない識別子は文字列として保ちます。金額を追加する場合も、通貨記号付きの表示と、集計用の数値を別にするほうが扱いやすくなります。
原文と閲覧用の使い分け
外部から取った文字列には=で始まる値もあり得ます。表計算ソフトで数式として扱われないよう、ここでは閲覧用CSVだけ先頭にアポストロフィを足します。ただし値が変わるため、元の値はraw.jsonに別保存します。
CSVに保存できたことと、Excelで意図どおりに開けたことは別です。この例の対策もすべての読み込み方法を保証するものではありません。利用するソフトで文字列として取り込む確認まで行ってください。
文字コードと改行
PythonでBOM付きUTF-8はutf-8-sigと指定します。BOMは先頭に付く文字コードの目印です。CSVの読み書きにはnewline=””も指定し、引用符や行末の扱いをcsvモジュールに任せます。
タイトルにカンマが含まれるとき、文字列を単純にカンマでつなぐと列がずれます。csv.writerやDictWriterで書くと、必要な引用符が付きます。行内の改行があるCSVでは、画面上の行数とデータの件数が違う場合もあります。
取得とCSV保存に使う練習用HTML
まず実際の業務サイトを使わず、保存時の問題だけを確認します。以下は実在の商品ではない練習データです。
新しい練習フォルダへ、次のHTMLをproducts.htmlとしてUTF-8で保存します。商品コード、カンマ、引用符、数式に見える文字を意図的に含めています。
<!doctype html>
<meta charset="utf-8">
<a data-item="product" data-id="001" href="/items/001">ノート,青</a>
<a data-item="product" data-id="002" href="/items/002">付箋"大"</a>
<a data-item="product" data-id="003" href="/items/003">=1+1</a>
そのフォルダで次のコマンドを実行し、自分のPCだけから見られるHTTPサーバーを起動します。別のターミナルで後の保存コードを実行します。終わったらサーバー側でCtrl+Cを押して停止します。
py -3.12 -m http.server 8765 --bind 127.0.0.1
データの取得・検査とCSVへの書き出し
次をsave_products.pyとして、HTMLと同じ練習フォルダへ保存します。取得した3項目を検査してから、新しいファイルに書きます。
import csv
import json
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin, urlsplit
import requests
BASE = "http://127.0.0.1:8765/products.html"
FIELDS = ["id", "title", "url"]
class Products(HTMLParser):
def __init__(self):
super().__init__()
self.rows, self.current, self.parts = [], None, []
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "a" and a.get("data-item") == "product":
self.current = {"id": a.get("data-id", ""),
"url": urljoin(BASE, a.get("href", ""))}
self.parts = []
def handle_data(self, data):
if self.current is not None:
self.parts.append(data)
def handle_endtag(self, tag):
if tag == "a" and self.current is not None:
self.current["title"] = "".join(self.parts).strip()
self.rows.append(self.current)
self.current = None
response = requests.get(BASE, timeout=(3, 10))
response.raise_for_status()
response.encoding = "utf-8" # 練習HTMLの文字コード
parser = Products()
parser.feed(response.text)
rows = parser.rows
if not rows or any(not r["id"] or not r["title"] for r in rows):
raise ValueError("件数・ID・タイトルを確認してください")
if any(urlsplit(r["url"]).scheme not in {"http", "https"} for r in rows):
raise ValueError("URLの形式を確認してください")
def for_spreadsheet(value):
# 表計算用CSVでは、数式になり得る値の前に ' を付ける
if value.lstrip().startswith(("=", "+", "-", "@")):
return "'" + value
return value
with Path("raw.json").open("x", encoding="utf-8") as f:
json.dump(rows, f, ensure_ascii=False, indent=2)
with Path("products.csv").open("x", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
writer.writerows({k: for_spreadsheet(r[k]) for k in FIELDS} for r in rows)
print(f"CSV: {len(rows)}件 / 原文JSON: {len(rows)}件")
with Path("products.csv").open(encoding="utf-8-sig", newline="") as f:
saved = list(csv.DictReader(f))
print("ID:", [r["id"] for r in saved])
print("タイトル:", [r["title"] for r in saved])
raise_for_status()はHTTPのエラーを検出します。timeout=(3, 10)は接続と読み取りの待ち時間を指定するもので、処理全体が13秒で終わる保証ではありません。HTMLの文字コードはこの練習ファイルに合わせてUTF-8にしています。
このHTMLParserは練習用のdata-item属性を目印にしています。実際のサイトに同じ属性があるとは限りません。0件や必須値の欠損で停止するのは、保存成功を取得成功と取り違えないためです。
保存モードのxは、同名ファイルがあれば停止する指定です。毎回別の練習フォルダや出力名を使います。JSONの保存後にCSVで止まることもあるため、2ファイルを1組で確認し、片方だけを完成品としないでください。
CSVの読み戻しとExcel形式への保存
出力したファイルをプログラムで読み直し、文字と件数を確かめます。Excelで配る場合は、文字列の型を指定したxlsxも選べます。
CSVの読み戻し結果
py -3.12 save_products.py
CSV: 3件 / 原文JSON: 3件
ID: ['001', '002', '003']
タイトル: ['ノート,青', '付箋"大"', "'=1+1"]
先頭のゼロ、タイトル中のカンマと引用符を読み戻せています。数式風の文字列はCSVだけアポストロフィ付きです。raw.jsonには元の=1+1が残ることも確認しました。文字コードは先頭のBOMも含めて確認しています。
Excel形式への書き出し
次の例はopenpyxlを使います。既存のExcelブックを編集せず、新しいproducts.xlsxを作ります。CSVを経由せず原文JSONから読み、取得した各値を文字列として保存します。
import json
from pathlib import Path
from openpyxl import Workbook, load_workbook
rows = json.loads(Path("raw.json").read_text(encoding="utf-8"))
book = Workbook()
sheet = book.active
sheet.title = "取得結果"
sheet.append(["id", "title", "url"])
for row in rows:
sheet.append([row["id"], row["title"], row["url"]])
for cells in sheet.iter_rows(min_row=2):
for cell in cells:
cell.data_type = "s" # 取得値を文字列として保存
with Path("products.xlsx").open("xb") as f:
book.save(f)
check = load_workbook("products.xlsx").active
print("Excelの先頭ID:", check["A2"].value)
print("数式風文字列:", check["B4"].value, "/ 型:", check["B4"].data_type)
Excelの先頭ID: 001
数式風文字列: =1+1 / 型: s
読み直したIDは001、=1+1のセルの型はs(文字列)でした。これはExcelファイルの内部を読み直した結果です。Excelアプリを開いて画面を確認した結果とは区別しています。金額を数値セルにする場合は、その列だけ検査して数値として渡します。
配布前の確認
渡す相手がCSVをダブルクリックするのか、インポート機能から読み込むのかを確認します。CSV自体には列の型がありません。Excelで商品コードを保つ場合は、テキストまたはCSVからの取り込みで列を文字列にするか、型を持つxlsxを使います。
なべくんCSVの中身が正しくても、開くときに数字へ変わる場合があります。保存した値と、使うソフトで見える値を分けて確かめましょう。
スクレイピング結果のCSV保存|よくある質問
文字化けと、Excelで先頭のゼロが消える場合の疑問を確認します。
- 日本語が文字化けしたらどうしますか?
-
取得したHTMLと、保存するCSVのどちらで文字が崩れたかを確認します。HTMLの段階で崩れていれば取得時の文字コードを、CSVだけなら保存・読み込み時の文字コードを見直します。errors=”ignore”で消して進めないようにします。
- CSVのIDがExcelで1になるのは保存失敗ですか?
-
CSVを読み直して001が残っていれば、Excel側の型推測が原因の可能性があります。インポート時に列を文字列にするか、文字列セルで保存したxlsxを使います。元データを先に整数へ変換していないかも確認します。
CSV保存後に読み直す項目
取得と保存が通った後に、値と型をもう一度見ます。
列:列名と順番を固定し、識別子は文字列で保つ
形式:csvモジュールと明示した文字コードで書く
原文:閲覧用の加工前データを別に保持する
確認:読み戻した件数・特殊文字・先頭ゼロを照合する
まず練習HTMLの3件が同じ値で戻ることを確かめてから、必要な取得項目へ広げてください。
次に進める作業に合わせて、こちらの記事も参考にしてください。
参考資料(2026年10月確認)
csv | Python
html.parser | Python
Quickstart | Requests
Tutorial | openpyxl
