অধ্যায় 25

JSON আর CSV

csv মডিউল দিয়ে উদ্ধৃতিসহ টেবিল পড়া ও লেখা, newline="" কেন লাগে, json দিয়ে গঠনযুক্ত তথ্য রাখা, আর কোন ধরনগুলো যাত্রাপথে বদলে যায়।

34 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

তেইশতম অধ্যায় থেকে আমরা ফাইল ভাঙছি .split(",") দিয়ে, আর সেটা কাজও করছিল। এবার একটা ফাইল, যেখানে একটা ক্ষেত্রের ভিতরে কমা আছে:

text
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0
python
with open("items.csv", "r", encoding="utf-8") as fh:
    for line in fh:
        print(line.strip().split(","))
text
['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', '"large', ' sturdy"', '850.0']

শেষ সারিটায় তিনটে ক্ষেত্রের বদলে চারটে এসেছে, আর উদ্ধৃতি চিহ্নগুলো তথ্যের অংশ হয়ে গেছে।

উদ্ধৃতিটা ভুল নয় — ওটাই CSV-র নিয়ম, ঠিক এই পরিস্থিতির জন্যই আছে। ভুলটা আমাদের পাঠকের। আর নিয়মটা নিজে লিখতে গেলে দেখা যাবে উদ্ধৃতির ভিতরে উদ্ধৃতি, ক্ষেত্রের ভিতরে নতুন লাইন — একটা ছোট কাজ ধীরে ধীরে একটা বড় কাজ হয়ে উঠছে।

এই কাজটা আগেই করা আছে।

python
import csv

with open("items.csv", "r", encoding="utf-8", newline="") as fh:
    for row in csv.reader(fh):
        print(row)
text
['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', 'large, sturdy', '850.0']

এই অধ্যায়টা দুটো ফরম্যাট নিয়ে — টেবিলের জন্য CSV, আর গঠনযুক্ত তথ্যের জন্য JSON — আর দুটোরই মডিউল পাইথনের সাথেই আসে।

এই অধ্যায় শেষে আপনি পারবেন

  • csv.reader আর csv.DictReader দিয়ে পড়তে, আর csv.writer দিয়ে লিখতে
  • বলতে newline="" কেন লেখা হয়
  • json.dumps/loads আর json.dump/load-এর পার্থক্য বলতে
  • বলতে JSON থেকে কোন পাইথন ধরনগুলো ফিরে আসে আর কোনগুলো আসে না
  • কখন CSV আর কখন JSON — সিদ্ধান্ত নিতে
  • JSONDecodeError পড়ে সমস্যাটা খুঁজে বের করতে

পূর্বশর্ত: এক্সেপশন।


CSV পড়া

csv.reader প্রতিটা সারিকে একটা লিস্ট হিসেবে দেয়। কিন্তু অবস্থান মনে রাখতে হয় — row[2] কোনটা ছিল যেন?

csv.DictReader প্রথম সারিটাকে শিরোনাম ধরে নেয় আর প্রতিটা সারিকে ডিকশনারি বানায়:

python
import csv

with open("items.csv", "r", encoding="utf-8", newline="") as fh:
    for row in csv.DictReader(fh):
        print(row["name"], "-", row["price"], type(row["price"]))
text
pen - 15.0 <class 'str'>
bag - 850.0 <class 'str'>

দুটো জিনিস।

নাম দিয়ে পৌঁছানো যায়, তাই কলামের ক্রম বদলালেও কোড ভাঙে না। প্রায় সবসময় DictReader-ই ভালো পছন্দ।

কিন্তু price এখনো একটা লেখা। csv মডিউল ক্ষেত্র আলাদা করে, রূপান্তর করে না — CSV ফাইলে কোথাও লেখা থাকে না কোনটা সংখ্যা আর কোনটা নয়। float() আর int() আপনাকেই ডাকতে হবে, ঠিক তেইশতম অধ্যায়ের মতো।

CSV লেখা

python
import csv

rows = [["pen", "blue ink", 15.0], ["bag", "large, sturdy", 850.0]]

with open("out.csv", "w", encoding="utf-8", newline="") as fh:
    writer = csv.writer(fh)
    writer.writerow(["name", "note", "price"])
    writer.writerows(rows)
text
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0

লক্ষ্য করুন large, sturdy নিজে থেকেই উদ্ধৃতির ভিতরে চলে গেছে — আর blue ink যায়নি, কারণ তার দরকার ছিল না। যেটা পড়তে আমরা নিজেরা পারিনি, সেটা লিখতেও আমাদের লাগেনি।

ডিকশনারির লিস্ট হলে DictWriter:

python
import csv

rows = [
    {"name": "pen", "price": 15.0},
    {"name": "bag", "price": 850.0},
]

with open("out.csv", "w", encoding="utf-8", newline="") as fh:
    writer = csv.DictWriter(fh, fieldnames=["name", "price"])
    writer.writeheader()
    writer.writerows(rows)
text
name,price
pen,15.0
bag,850.0

fieldnames কলামের ক্রমও ঠিক করে দেয়, আর writeheader() ছাড়া শিরোনামের সারিটা আসবে না।

newline="" কেন? csv মডিউল নিজেই ঠিক করে সারির শেষে কী বসবে। ফাইলটা স্বাভাবিকভাবে খুললে কিছু সিস্টেমে পাইথন আরেকবার লাইন-শেষ অনুবাদ করে, ফলে প্রতিটা সারির পর একটা করে বাড়তি খালি লাইন পড়ে। newline="" সেই দ্বিতীয় অনুবাদটা বন্ধ করে। পড়া আর লেখা — দুই ক্ষেত্রেই লিখুন।

JSON — গঠন যেখানে টেবিলে ধরে না

CSV একটা টেবিল: সারি আর কলাম, সব সমতল। কিন্তু একটা অর্ডারের ভিতরে যদি লাইনের লিস্ট থাকে, আর প্রতিটা লাইনের ভিতরে ট্যাগের লিস্ট? টেবিলে সেটা ধরে না।

python
import json

order = {"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": True, "note": None}

print(json.dumps(order))
text
{"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": true, "note": null}

json.dumps একটা পাইথন বস্তুকে লেখায় রূপান্তর করে। আকারটা পাইথনের মতোই দেখতে, দুটো পার্থক্য ছাড়া: True হয়েছে true, আর None হয়েছে null। JSON আলাদা একটা ভাষা, যা বহু প্রোগ্রামিং ভাষার মধ্যে ভাগ করা — তাই তার নিজের বানান।

মানুষের পড়ার মতো করে চাইলে:

python
import json

order = {"name": "pen", "tags": ["ink", "blue"]}

print(json.dumps(order, indent=2))
text
{
  "name": "pen",
  "tags": [
    "ink",
    "blue"
  ]
}

আর ফিরিয়ে আনা:

python
import json

text = '{"name": "pen", "price": 15.0, "stocked": true, "note": null}'
order = json.loads(text)

print(order)
print(type(order["price"]), type(order["stocked"]), order["note"] is None)
text
{'name': 'pen', 'price': 15.0, 'stocked': True, 'note': None}
<class 'float'> <class 'bool'> True

এটাই CSV-র সাথে সবচেয়ে বড় পার্থক্য। price ফিরে এসেছে সংখ্যা হয়ে, stocked ফিরে এসেছে True হয়ে — কোনো রূপান্তর ডাকতে হয়নি। JSON ধরনগুলোও বহন করে; CSV করে না।

নাম চারটে মনে রাখা সহজ: dumps/loads লেখা নিয়ে কাজ করে, dump/load ফাইল নিয়ে। শেষের s-টা string।

python
import json
from pathlib import Path

order = {"name": "pen", "price": 15.0}

Path("order.json").write_text(json.dumps(order, indent=2) + "\n", encoding="utf-8")

with open("order.json", "r", encoding="utf-8") as fh:
    back = json.load(fh)

print(back)
text
{'name': 'pen', 'price': 15.0}

JSON যা ফেরত দেয় না

সব পাইথন বস্তু JSON-এ যায় না, আর যেগুলো যায় তাদের সবাই একই রকম হয়ে ফেরে না।

python
import json

data = {"pair": (1, 2), "numbers": {3, 4}}

print(json.dumps({"pair": data["pair"]}))
print(json.dumps(data))
text
{"pair": [1, 2]}
TypeError: Object of type set is not JSON serializable

টাপল নীরবে লিস্ট হয়ে যায়। JSON-এ টাপল বলে কিছু নেই, তাই লেখার সময় কোনো অভিযোগ আসে না — কিন্তু ফিরিয়ে আনলে আপনি একটা লিস্ট পাবেন। চতুর্দশ অধ্যায়ের সেই «বদলাবে না» প্রতিশ্রুতিটা যাত্রাপথে হারিয়ে যায়।

সেট একেবারেই যায় না, আর সেটা ভালো — নীরবে ভুল হওয়ার চেয়ে থেমে যাওয়া ভালো। দরকার হলে sorted(...) করে লিস্ট বানিয়ে পাঠান।

আরেকটা, যেটা বেশি কামড়ায়:

python
import json

data = {1: "pen", 2: "bag"}
text = json.dumps(data)

print(text)
print(json.loads(text))
text
{"1": "pen", "2": "bag"}
{'1': 'pen', '2': 'bag'}

JSON-এ কী সবসময় লেখা। সংখ্যার কী দেওয়া হলে সে নীরবে লেখা হয়ে যায়, আর ফিরে আসে লেখা হয়েই। তাই data[1] কাজ করত, আর ফেরত আসার পর data["1"] লাগে।

মনে রাখার তালিকাটা ছোট: ডিকশনারি, লিস্ট, লেখা, সংখ্যা, True/False, None — এগুলো নিরাপদ। টাপল বদলে যায়। সেট, তারিখ আর নিজের বানানো বস্তু যায় না।

JSON কড়া

python
import json

print(json.loads("{'name': 'pen'}"))
text
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

পাইথনে একক আর দ্বৈত উদ্ধৃতি সমান। JSON-এ নয় — সেখানে কেবল দ্বৈত উদ্ধৃতি চলে, আর শেষ জিনিসটার পর কমা রাখা যায় না।

বার্তাটা লাইন ও কলাম বলে দেয়, তাই বড় ফাইলেও জায়গাটা খুঁজে পাওয়া যায়। আর JSONDecodeError আসলে একটা ValueError — তাই except ValueError দিয়েও ধরা যায়, যেমনটা গত অধ্যায়ের পরিবারের নিয়ম বলে।

কোনটা কখন

CSV নিন যখন তথ্যটা সমতল টেবিল আর মানুষ সেটা স্প্রেডশিটে খুলবে। সারি বেশি হলে ফাইলও ছোট থাকে।

JSON নিন যখন গঠন আছে — ভিতরে লিস্ট, ভিতরে ডিকশনারি — বা যখন ধরনগুলো অক্ষত ফিরে আসা দরকার। সেটিংস ফাইল আর ওয়েব API প্রায় সবসময়ই JSON।


একটা সম্পূর্ণ উদাহরণ

items.csv:

text
name,note,price,quantity
pen,blue ink,15.0,3
bag,"large, sturdy",850.0,1
ink,,120.0,2
clip,broken,,4

main.py:

python
"""Read a CSV of items, total it, and write the result as JSON."""

import csv
import json
from pathlib import Path

TAX_RATE = 0.15


def read_items(path):
    """Returns the usable rows and the complaints. csv handles the quoting."""
    items = []
    problems = []

    with open(path, "r", encoding="utf-8", newline="") as fh:
        for number, row in enumerate(csv.DictReader(fh), start=2):
            try:
                items.append({
                    "name": row["name"],
                    "note": row["note"],
                    "amount": round(
                        float(row["price"]) * int(row["quantity"]) * (1 + TAX_RATE), 2),
                })
            except ValueError as err:
                problems.append(f"line {number}: {err}")

    return items, problems


def main():
    items, problems = read_items(Path("items.csv"))

    summary = {
        "items": items,
        "total": round(sum(item["amount"] for item in items), 2),
        "skipped": problems,
    }

    Path("summary.json").write_text(
        json.dumps(summary, indent=2) + "\n", encoding="utf-8")

    print(Path("summary.json").read_text(encoding="utf-8"), end="")


if __name__ == "__main__":
    main()
text
{
  "items": [
    {
      "name": "pen",
      "note": "blue ink",
      "amount": 51.75
    },
    {
      "name": "bag",
      "note": "large, sturdy",
      "amount": 977.5
    },
    {
      "name": "ink",
      "note": "",
      "amount": 276.0
    }
  ],
  "total": 1305.25,
  "skipped": [
    "line 5: could not convert string to float: ''"
  ]
}

চারটে জিনিস দেখার মতো।

ভিতরে আসে CSV, বাইরে যায় JSON, আর সেটা দুই ফরম্যাটের চরিত্র থেকেই আসে। ইনপুটটা একটা সমতল টেবিল, যা স্প্রেডশিটেও খোলা যায়। আউটপুটটা সমতল নয় — ভিতরে জিনিসের লিস্ট, তার ভিতরে ডিকশনারি, আর পাশে অভিযোগের আরেকটা লিস্ট। এটা CSV-তে লেখা যেত না।

large, sturdy সারাটা পথ অক্ষত। csv সেটাকে ঠিকভাবে পড়েছে, আর json সেটাকে ঠিকভাবে লিখেছে। এই অধ্যায়ের শুরুর সমস্যাটা এখানে কোথাও নেই।

খালি note আর খালি price — দুটোই খালি লেখা, কিন্তু ফল আলাদা। CSV-তে খালি ক্ষেত্র মানে "", None নয়। note লেখা হিসেবেই থেকে যায় আর কোনো সমস্যা করে না; কিন্তু float("") একটা ValueError তোলে, আর তাই clip সারিটা বাদ পড়ে অভিযোগসহ। CSV-তে «নেই» বলে কিছু নেই — কেবল «খালি» আছে, আর তার মানে কী তা আপনাকেই ঠিক করতে হয়।

start=2 দৈব নয়। DictReader প্রথম সারিটা শিরোনাম হিসেবে খেয়ে ফেলে, তাই তার দেওয়া প্রথম তথ্য-সারিটা ফাইলের দ্বিতীয় লাইন। যে বার্তা মানুষ ফাইল খুলে মেলাবে, তার সংখ্যাটা ফাইলের সাথে মিলতে হবে।


কিছু ভাঙা অবস্থা ও তার সমাধান

CSV ফাইলের প্রতিটা সারির পর একটা করে খালি লাইন newline="" দেওয়া হয়নি। লেখার সময় দিন।

KeyError: 'price' — অথচ কলামটা আছে শিরোনামের বানান বা ফাঁকা স্পেস মিলছে না। print(reader.fieldnames) দিয়ে দেখুন পাইথন আসলে কী পড়েছে।

সংখ্যার সাথে হিসাব করতে গিয়ে TypeError csv সব লেখা হিসেবে দেয়। float() বা int() ডাকতে হবে।

প্রথম সারিটাও তথ্য হিসেবে ধরা পড়ছে csv.reader ব্যবহার করছেন, যে শিরোনাম চেনে না। DictReader নিন, নয়তো প্রথম সারিটা আলাদা করে সরান।

json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes একক উদ্ধৃতি, নয়তো শেষে বাড়তি কমা। JSON পাইথন নয়।

TypeError: Object of type set is not JSON serializable সেট, তারিখ বা নিজের বানানো কোনো বস্তু পাঠানো হয়েছে। আগে সেটাকে লিস্ট বা লেখায় বদলান।

ফেরত আনার পর আমার টাপলগুলো লিস্ট হয়ে গেছে সেটাই হওয়ার কথা — JSON-এ টাপল নেই। দরকার হলে ফেরত আনার পর নিজে tuple(...) করুন।

data[1] কাজ করছিল, ফেরত আনার পর KeyError JSON-এ কী সবসময় লেখা। এখন data["1"]।