CSVの行数が合わない?Pythonでセル内改行と論理レコードを見分ける

プログラミング・Web開発カテゴリを表すパンダのイラスト プログラミング・Web開発

この記事について この記事は、生成AIを活用した自動生成フローで作成しています。Python標準ライブラリのcsvとioの公式仕様を参考に、セル内改行を含む架空データの数え方を説明します。

検証ステータス:📘 Python公式仕様確認・掲載コードの実機実行は未確認

CSVの行数が合わない?Pythonでセル内改行と論理レコードを見分ける

CSVをテキストエディターで開くと5行あるのに、Excelで開くと3件しかない。そんな違いは、CSVのセルに改行が入っていると起こります。ファイルの改行で区切った「物理行」と、CSVの引用符を解釈して得られる「論理レコード」は同じとは限りません。Pythonのcsv.readerで構造を読めば、セル内の改行を壊さずに数えられます。

対象環境と今回の成功条件

項目内容
対象Python 3の標準ライブラリcsv、io
データ架空の問い合わせ番号、担当、メモ
通信・権限外部通信なし。ファイルへの書込みなし
成功条件物理行数とCSVレコード数の違いを表示する
未確認実Excelでの表示、各種CSV製品との互換性

例えば、問い合わせ管理から出力したCSVに「折返し連絡」と「翌日対応」が同じメモ欄に記録されている場合を考えます。画面では一つのセルに二行表示されていても、テキストファイルには改行文字が一つ増えています。単純に改行の数だけ問い合わせ件数を数えると、報告書の件数が多くなり、問い合わせの重複や欠落があったように見えてしまいます。

安全な実験:引用符の内側に改行を入れる

まずはファイルを作らず、Pythonの文字列だけで試します。データは架空の問い合わせ二件です。二件目のメモだけを二行にしています。文字列の先頭行は見出しです。次のコードをPython 3で実行してください。

import csv
import io

sample = 'id,担当,メモ\r\nA01,佐藤,受付済み\r\nA02,鈴木,"折返し連絡\n翌日対応"\r\n'
physical_lines = sample.splitlines()
records = list(csv.reader(io.StringIO(sample, newline="")))

print("物理行:", len(physical_lines))
print("CSVレコード:", len(records))
for number, record in enumerate(records, start=1):
    print(number, repr(record))

このコードで期待する物理行は4、CSVレコードは3です。後者には見出しが含まれるため、実際の問い合わせ件数は2になります。表示される二件目のメモには、改行を表す\nが残ります。これは行が壊れたのではなく、CSVの一つのフィールドに改行が保持されたことを意味します。実行環境や表示方法によって文字の見え方は変わるため、件数とreprの表示を確認してください。

ここで重要なのは、splitlines()が間違った関数という話ではないことです。この関数はテキストを行に分けるためのものです。一方、問い合わせ件数を数えたいときに必要なのはCSVの構造を理解するパーサーです。目的が異なる二つの関数を、同じ「行数」という言葉で比較してしまうことが誤解の原因です。

なぜ引用符の中だけ改行を許せるのか

CSVには、フィールドをカンマで区切り、必要に応じて二重引用符で囲むという規則があります。引用符で囲まれたフィールド内では、カンマや改行もデータとして扱えます。Pythonのcsv.readerは、文字を順に読み、現在の位置が引用符の内側なのか外側なのかを区別します。外側でレコード区切りを見つけたら一件が終わり、内側の改行ならフィールドの内容として保持します。

たとえばA02,鈴木,"折返し連絡まで読んだ時点では、引用符が閉じていません。その直後に改行が現れても、次の問い合わせが始まったとは判断しません。翌日対応"を読み、引用符が閉じた後にレコード区切りへ到達して初めて、A02の一件が完成します。テキストエディター上で改行が見えることと、CSVのレコードが終わることは別の出来事です。

図で見る:どの改行でレコードが確定するか

次の図は、改行を見つけたときの判断だけに絞っています。引用符の内側ならフィールドへ追加し、外側ならレコードの終端として扱う、という違いが中心です。実際のパーサーには引用符のエスケープなど追加の規則があります。

flowchart TD
    A[文字を読み取る] --> B{改行か}
    B -- いいえ --> C[フィールドを解析]
    B -- はい --> D{引用符の内側か}
    D -- はい --> E[セルの内容に保持]
    D -- いいえ --> F[レコードを確定]
    C --> A
    E --> A

図の引用符の内側という状態は、CSVを途中で区切って処理すると失われることがあります。大きなファイルを扱う際も、改行だけで分割した断片を個別にcsv.readerへ渡すと、引用符が閉じる前に入力が終わり、正しいレコード境界を復元できません。CSVとしての状態を維持する読み方が必要です。

一条件だけ変える:セル内改行を消す

次に、メモの折返し連絡\n翌日対応を折返し連絡/翌日対応へ置き換えて実行してみてください。変更するのは二件目のメモの改行一文字だけです。物理行は3、CSVレコードも3になるはずです。問い合わせの件数は変わりません。改行を消しただけで物理行数が一つ減るという観察から、件数の差は追加の問い合わせではなく、一つのセルの表示形式に由来すると分かります。

ただし、元データから改行を取り除くことを本番の対策にしてはいけません。自由記述欄の改行は、作業手順や顧客とのやり取りを読みやすくする意味を持つことがあります。必要な情報を捨てるより、集計側でCSVを正しく解析する方が自然です。別システムの制約で一行化が必要な場合は、元データを保存したうえで変換規則を合意します。

Pythonのnewline指定が重要な理由

実ファイルを読む場合は、Python公式ドキュメントが案内するようにopen(..., newline="")を使います。newline=""はテキスト入出力層に改行の自動変換を任せず、CSVモジュールに改行を適切に扱わせるための指定です。今回のio.StringIOも同じ意図で指定しています。CSVファイルの文字コードまで自動判定する指定ではないため、実ファイルではencoding="utf-8"など対象の文字コードも別途決める必要があります。

import csv

with open("sample.csv", "r", encoding="utf-8", newline="") as stream:
    reader = csv.DictReader(stream)
    count = sum(1 for row in reader)
print("データ件数:", count)

この例は実ファイルが存在する場合の参考コードであり、今回の実験でファイルを作成したわけではありません。ヘッダーを持つCSVならDictReaderでデータ行だけを数えられます。ただし、空行、重複ID、ヘッダーの不備、コメント扱いしたい行などの業務ルールは別途検討が必要です。DictReaderが返す件数は、業務上の有効件数と必ずしも同じではありません。

失敗時は三つの境界を切り分ける

まず、csv.Errorが出る、あるいはレコードが意図せず結合される場合は、引用符が閉じているか、引用符そのものを二つ重ねてエスケープしているかを確認します。CSVの方言によって区切り文字や引用符の扱いが異なる場合もあります。タブ区切りのデータをカンマ区切りとして読めば、件数が合っても列の意味が壊れます。件数だけでなく、列数と代表的なフィールド値を照合してください。

次に、日本語が文字化けする場合は改行ではなく文字コードを疑います。UTF-8、UTF-8 BOM付き、Windows系の文字コードなどは別の論点です。読み込み時のencodingを確認し、文字化けした値をそのまま上書き保存しないでください。文字化けとレコード境界の問題は同時に発生することもありますが、原因を一つに決めつけず、少数の架空データで個別に再現すると切り分けやすくなります。

最後に、ExcelとPythonで件数が異なる場合は、空白行、フィルター、ヘッダーの有無、インポート時の型推定を調べます。Excelが見た目上の改行を一セル内に表示していても、内部で文字列として保持することと、業務システムが有効レコードとして数えることは同義ではありません。どの段階の何を数えたのかを記録して比較する必要があります。

実務では「件数」と「内容」の両方を照合する

問い合わせ一覧、受注データ、作業ログをCSVで受け渡すとき、ファイルの物理行数をレコード件数として報告しないことが第一歩です。取り込み前後でcsv.readerによるレコード件数を比較し、さらにIDの重複、必須列の欠落、代表的な長文メモの改行保持を確認します。件数だけ一致していても、カンマの解釈が誤って列がずれている可能性があるためです。

大量のCSVを処理するときはlist(csv.reader(...))で全件をメモリに展開する必要はありません。ストリームから一件ずつ取り出して件数を数えれば、メモリ使用量を抑えられます。ただし、業務上の照合に必要なIDやエラー情報は、機密性と保存期間を決めたうえで最小限だけ記録します。個人情報を含む元の自由記述欄を、調査用ログへ無制限に複製することは避けます。

受け取り側がCSVの仕様を変えられない場合は、送信元と区切り文字、引用符、改行、文字コード、空行の扱いを確認し、セル内改行を含むテストデータを受入試験に含めます。平常時に一行の短いメモだけで試験すると、実運用の自由記述が長くなったとき初めて件数不一致が発生します。障害になってから改行を削除するより、最初から境界条件を含む小さなテストを用意する方が安全です。

まとめ

CSVの物理行数とレコード件数が違うとき、まず引用符内の改行を疑います。Pythonのsplitlines()はテキストの行を数え、csv.readerはCSVの構造を解釈します。セル内改行を一つ消すだけで物理行数は変わってもレコード件数は変わらない、という比較が理解の鍵です。実務では件数だけでなく列の対応、ID、文字コードまで確認し、元データを壊さずに取り込む設計へつなげてください。

参考資料

文書情報

記事タイトル
CSVの行数が合わない?Pythonでセル内改行と論理レコードを見分ける
作成日
更新日
Source URL
https://papanda925.com/?p=18591

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

タイトルとURLをコピーしました