অধ্যায় 4 · Python ও ডেটা হ্যান্ডলিং
ইমপোর্ট ও এক্সপোর্ট: CSV, JSON, Excel আর Parquet
- পৃষ্ঠা 16 / 22
- 17 মিনিট পড়া
ডেটা খুব কমই আপনার ডেটাবেসে শুরু বা শেষ হয়। সেটা আসে পার্টনারের পাঠানো CSV হয়ে, ফিন্যান্সের Excel শিট হয়ে, API-র JSON হয়ে, ডেটা লেকের Parquet ফাইল হয়ে; আর বেরিয়েও যায় একই পথে: ম্যানেজারের জন্য রিপোর্ট, LLM ফাইন-টিউনিংয়ের উদাহরণের একটা JSON Lines ফাইল, ট্রেনিং ডেটার একটা Parquet স্ন্যাপশট। এই প্রতিটা পারাপারে ডেটা চুপচাপ নষ্ট হতে পারে: ভাঙা এনকোডিং বাংলা নামকে হিজিবিজি বানায়, ঠিকানার ভেতরের একটা কমা প্রতিটা কলাম সরিয়ে দেয়, একটা খারাপ সারি দশ লাখ সারির লোড বাতিল করে দেয়।
ডেটা হ্যান্ডলিংয়ের মূল কথা: ফরম্যাট, মান আর উৎস পাতায় ফরম্যাটগুলোর তুলনা দেখেছেন; এই পাতা তার হাতে-কলমে অংশ: Python থেকে প্রতিটা ফরম্যাট কীভাবে ঠিকভাবে পড়বেন আর লিখবেন, ফাইল কীভাবে বাল্কে ডেটাবেসে লোড করবেন, আর খারাপ সারি না হারিয়ে কীভাবে বাইরে রাখবেন।
যা শিখবেন
- ঠিকঠাক CSV: কোটিং, ডিলিমিটার, এনকোডিং, আর বাংলা লেখার জন্য UTF-8।
- ফাইল থেকে ডেটাবেসে লোড (csv মডিউল, pandas, স্টেজিং টেবিল, PostgreSQL COPY, MySQL LOAD DATA), যাচাইসহ, আর ব্যর্থ সারির জন্য কোয়ারেন্টাইন।
- কোয়েরির ফলাফল স্বয়ংক্রিয়ভাবে CSV-তে এক্সপোর্ট, একটা ম্যানিফেস্টসহ।
- pandas আর openpyxl দিয়ে Excel; JSON, নেস্টেড JSON (
json_normalize) আর JSON Lines; XML-এর মূল কথা। - Parquet আর কলামভিত্তিক স্টোরেজ, ফাইলের আকারের তুলনা, আর বড় ফাইল টুকরো টুকরো করে প্রসেস করা।
CSV: কোটিং, ডিলিমিটার আর এনকোডিং
CSV দেখতে সহজ, আসলে নয়। Python-এর csv মডিউলকে দিয়ে এমন একটা ফাইল লেখান, যাতে দুটো চিরচেনা ফাঁদ আছে, একটা মানের ভেতরে কমা আর একটা মানের ভেতরে উদ্ধৃতিচিহ্ন, সাথে একটা বাংলা নাম:
import csv
new_customers = [
{"customer_id": 9, "name": "Rumana Begum", "email": "rumana@example.com", "city": "Barishal", "joined_on": "2026-06-15", "address": "House 12, Road 5, Dhanmondi"},
{"customer_id": 10, "name": "তানিয়া সুলতানা", "email": "tania@example.com", "city": "Dhaka", "joined_on": "2026-06-18", "address": "Shop \"Tech Corner\", Level 3"},
{"customer_id": 11, "name": "Arif Hasan", "email": "nadia@example.com", "city": "Sylhet", "joined_on": "2026-06-20", "address": "Zindabazar"},
{"customer_id": 12, "name": "Jui Akter", "email": "jui@example.com", "city": "Khulna", "joined_on": "2026-13-01", "address": "Sonadanga"},
{"customer_id": 13, "name": "", "email": "blank@example.com", "city": "Dhaka", "joined_on": "2026-06-22", "address": "Mirpur 10"},
{"customer_id": 14, "name": "Sabbir Khan", "email": "sabbir@example.com", "city": "", "joined_on": "2026-06-25", "address": "Agrabad"},
]
with open("new_customers.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(new_customers[0]))
writer.writeheader()
writer.writerows(new_customers)
with open("new_customers.csv", encoding="utf-8") as f:
print(f.read())customer_id,name,email,city,joined_on,address
9,Rumana Begum,rumana@example.com,Barishal,2026-06-15,"House 12, Road 5, Dhanmondi"
10,তানিয়া সুলতানা,tania@example.com,Dhaka,2026-06-18,"Shop ""Tech Corner"", Level 3"
11,Arif Hasan,nadia@example.com,Sylhet,2026-06-20,Zindabazar
12,Jui Akter,jui@example.com,Khulna,2026-13-01,Sonadanga
13,,blank@example.com,Dhaka,2026-06-22,Mirpur 10
14,Sabbir Khan,sabbir@example.com,,2026-06-25,Agrabadকমাওয়ালা ঠিকানার চারপাশে writer উদ্ধৃতিচিহ্ন বসিয়েছে, আর "Tech Corner"-এর ভেতরের উদ্ধৃতিচিহ্ন দ্বিগুণ করেছে। এটাই CSV-র স্ট্যান্ডার্ড (RFC 4180), আর এজন্যই CSV-র লাইন কখনো নিজে ভাগ করবেন না:
with open("new_customers.csv", encoding="utf-8") as f:
line = f.readlines()[1]
print(len(line.split(",")), "pieces with split(',')")
print(len(next(csv.reader([line]))), "fields with csv.reader")8 pieces with split(',')
6 fields with csv.readerঅন্য ডিলিমিটারেও একই নিয়ম: ইউরোপীয় এক্সপোর্টের জন্য (যেখানে কমা দশমিক চিহ্ন) csv.reader(f, delimiter=";"), আর TSV-র জন্য delimiter="\t"; pandas-এ pd.read_csv(path, sep=";")। csv মডিউলের জন্য CSV খোলার সময় সবসময় newline="" দিন, নইলে লাইন-ব্রেকওয়ালা উদ্ধৃত মান এলোমেলো হয়ে যায়।
এনকোডিং: বাংলা কেন হিজিবিজি হয়ে যায়
ফাইল মানে বাইট; এনকোডিং (encoding) বলে অক্ষর কীভাবে বাইট হয়। UTF-8 ইংরেজি অক্ষর রাখে ১ বাইটে, বাংলা অক্ষর ৩ বাইটে। UTF-8-এর বাইট ভুল এনকোডিংয়ে পড়লে পাবেন mojibake (হিজিবিজি অক্ষর), নয়তো একটা এরর:
city = "ঢাকা"
data = city.encode("utf-8")
print(len(city), "characters,", len(data), "bytes")
print(data.decode("cp1252", errors="replace")) # ভুল এনকোডিংয়ে পড়া
try:
with open("new_customers.csv", encoding="ascii") as f:
f.read()
except UnicodeDecodeError as exc:
print("UnicodeDecodeError:", exc)4 characters, 12 bytes
ঢাকা
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe0 in position 135: ordinal not in range(128)যে নিয়মগুলো প্রায় সব এনকোডিং বাগ আটকায়:
- টেক্সট ফাইল খোলার সময় সবসময়
encoding="utf-8"লিখুন। ডিফল্টটা অপারেটিং সিস্টেমের ওপর নির্ভর করে (Windows প্রায়ই cp1252 ব্যবহার করে), তাই আপনার ল্যাপটপে চলা কোড সহকর্মীর মেশিনে ভেঙে পড়ে। Python 3.15 থেকে UTF-8-ই ডিফল্ট (PEP 686), কিন্তু যে কোড পুরোনো সংস্করণেও চলবে, তাতে এটা লিখে দিতেই হবে। - Windows-এর Excel CSV-র এনকোডিং আন্দাজ করে, আর প্রায়ই ভুল আন্দাজ করে। Excel-এর জন্য ফাইল লিখুন
encoding="utf-8-sig"দিয়ে: এটা শুরুতে ৩ বাইটের একটা চিহ্ন (BOM) বসায়, যা Excel-কে বলে "এটা UTF-8"। - পার্টনার অজানা এনকোডিংয়ে ফাইল পাঠালে জিজ্ঞেস করুন।
charset-normalizer-এর মতো লাইব্রেরি আন্দাজ করতে পারে, কিন্তু আন্দাজ তো আন্দাজই।
CSV থেকে ডেটাবেসে নিরাপদে ইমপোর্ট
ওপরের ছয়টা সারির মাত্র তিনটা ঠিক আছে। সারি ১১ নাদিয়ার ইমেইল আবার ব্যবহার করেছে (UNIQUE কনস্ট্রেইন্ট এটা ফিরিয়ে দেবে), সারি ১২-তে মাস ১৩, আর সারি ১৩-তে কোনো নাম নেই। সতর্ক একটা ইমপোর্ট প্রতিটা সারি যাচাই করে, ভালোগুলো একটা ট্রানজ্যাকশনে লোড করে, আর বাতিলগুলো কারণসহ একটা কোয়ারেন্টাইন ফাইলে লেখে; পুরো লোড ব্যর্থ করে না, আবার চুপচাপ ফেলেও দেয় না:
import sqlite3
from datetime import date
con = sqlite3.connect("shop.db")
con.execute("PRAGMA foreign_keys = ON")
def problem_with(row):
"""সারিটা কেন অবৈধ তা ফেরত দেয়, ঠিক থাকলে None।"""
if not row["name"].strip():
return "name is empty"
if "@" not in row["email"]:
return "email is invalid"
try:
date.fromisoformat(row["joined_on"])
except ValueError:
return "joined_on is not a valid date"
return None
loaded, quarantined = 0, []
with open("new_customers.csv", newline="", encoding="utf-8") as f, con:
for row in csv.DictReader(f):
reason = problem_with(row)
if reason is None:
try:
con.execute(
"INSERT INTO customers (customer_id, name, email, city, joined_on) VALUES (?, ?, ?, ?, ?)",
(int(row["customer_id"]), row["name"], row["email"], row["city"] or None, row["joined_on"]),
)
loaded += 1
continue
except sqlite3.IntegrityError as exc:
reason = f"database refused: {exc}"
quarantined.append({**row, "reason": reason})
with open("customers_quarantine.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(quarantined[0]))
writer.writeheader()
writer.writerows(quarantined)
print(loaded, "loaded,", len(quarantined), "quarantined")
for q in quarantined:
print(q["customer_id"], "-", q["reason"])3 loaded, 3 quarantined
11 - database refused: UNIQUE constraint failed: customers.email
12 - joined_on is not a valid date
13 - name is emptyযা পারেন তা Python-এ যাচাই করুন (ফরম্যাট, আবশ্যিক ঘর), আর যা শুধু ডেটাবেস জানে, তার পাহারার ভার ডেটাবেসকেই দিন (স্বতন্ত্রতা, ফরেন কি)। দুটো খুঁটিনাটি: খালি শহর খালি স্ট্রিং না হয়ে হয় None (NULL), আর SQLite-এ একটা INSERT ব্যর্থ হলে শুধু সেই স্টেটমেন্টটা বাতিল হয়, ট্রানজ্যাকশন চলতে থাকে। PostgreSQL-এ একটা স্টেটমেন্ট ব্যর্থ হলে পুরো ট্রানজ্যাকশনই বাতিল হয়ে যায়, যেমন Python ও ডেটাবেস: কানেকশন, প্যারামিটার আর ট্রানজ্যাকশন পাতায় দেখেছেন: সেখানে প্রতিটা সারি একটা সেভপয়েন্টে (savepoint) মুড়ে দিন, অথবা লোডের আগে পুরোপুরি যাচাই করুন।
স্টেজিং টেবিলের পদ্ধতি
বড় ফাইলের বেলায় আগে সবকিছু কনস্ট্রেইন্টহীন একটা স্টেজিং টেবিলে লোড করুন, তারপর SQL-কে দিয়ে বৈধ সারিগুলো আসল টেবিলে সরান। ভারী কাজটা করে ডেটাবেস, আর স্টেজিং টেবিল পরে দেখার জন্য কাঁচা সারিগুলো রেখে দেয়:
import pandas as pd
staging = pd.read_csv("new_customers.csv", dtype=str, keep_default_na=False)
staging.to_sql("staging_customers", con, if_exists="replace", index=False)
with con:
cur = con.execute("""
INSERT INTO customers (customer_id, name, email, city, joined_on)
SELECT CAST(customer_id AS INTEGER), name, email, NULLIF(city, ''), joined_on
FROM staging_customers s
WHERE name <> ''
AND date(joined_on) = joined_on -- a real date
AND NOT EXISTS (SELECT 1 FROM customers c
WHERE c.email = s.email OR c.customer_id = CAST(s.customer_id AS INTEGER))
""")
print(cur.rowcount, "new rows (the earlier import already loaded the valid ones)")
print(pd.read_sql("SELECT customer_id, name, city FROM customers WHERE customer_id >= 9 ORDER BY customer_id", con))0 new rows (the earlier import already loaded the valid ones)
customer_id name city
0 9 Rumana Begum Barishal
1 10 তানিয়া সুলতানা Dhaka
2 14 Sabbir Khan NaNশূন্য সারি: বৈধ গ্রাহকেরা আগেই ঢুকে গেছেন, আর NOT EXISTS শর্তের কারণে লোডটা বারবার চালানো নিরাপদ। দুবার চালালেও কিছু না বদলানোর এই গুণকে বলে আইডেমপোটেন্সি (idempotency), আর পরের পাতার মূল কথা এটাই। খেয়াল করুন, বাংলা নামটা ঠিকঠাক সেভ হয়ে ঠিকঠাক ফেরত এসেছে।
সার্ভারে বাল্ক লোডিং
কয়েক হাজার সারির জন্য এক-এক করে INSERT, বা প্রতি ব্যাচে একটা executemany (দেখুন Python ও ডেটাবেস: কানেকশন, প্যারামিটার আর ট্রানজ্যাকশন), যথেষ্ট। লাখ-কোটি সারির জন্য ডেটাবেসের বাল্ক লোডার ব্যবহার করুন, যা এক কমান্ডে পুরো একটা ফাইল পড়ে আর প্রায়ই ১০ থেকে ১০০ গুণ দ্রুত। PostgreSQL-এ সেটা COPY। COPY table FROM '/path/file.csv' ফাইলটা পড়ে ডেটাবেস সার্ভারের ওপর থেকে, আর এর জন্য superuser বা pg_read_server_files রোল লাগে; ফাইলটা আপনার নিজের মেশিনে থাকলে COPY … FROM STDIN ব্যবহার করে কানেকশনের মধ্য দিয়ে ফাইলটা পাঠান। psql-এর \copy কমান্ড ঠিক এটাই করে, আর এখানে psycopg2-র copy_expert-ও তাই করছে (psycopg 3-এ এটা cur.copy(...)):
import os
import psycopg2
pg = psycopg2.connect(host=os.environ["PGHOST"], port=os.environ["PGPORT"], user=os.environ["PGUSER"],
password=os.environ["PGPASSWORD"], dbname=os.environ["PGDATABASE"])
with pg, pg.cursor() as cur:
cur.execute("CREATE TABLE staging_customers (customer_id text, name text, email text, "
"city text, joined_on text, address text)")
with open("new_customers.csv", encoding="utf-8") as f:
cur.copy_expert("COPY staging_customers FROM STDIN WITH (FORMAT csv, HEADER true)", f)
cur.execute("SELECT customer_id, name, address FROM staging_customers ORDER BY customer_id::int LIMIT 2")
for row in cur.fetchall():
print(row)
pg.close()('9', 'Rumana Begum', 'House 12, Road 5, Dhanmondi')
('10', 'তানিয়া সুলতানা', 'Shop "Tech Corner", Level 3')COPY নিজেই কোটিং আর UTF-8 বুঝে নিয়েছে। MySQL-এ এর সমতুল্য LOAD DATA। LOCAL ছাড়া এটা সার্ভারের ফাইল পড়ে, তাও শুধু secure_file_priv-এ বলা ফোল্ডার থেকে। LOCAL দিলে পড়ে ক্লায়েন্ট মেশিনের ফাইল, আর তার জন্য সার্ভার আর ক্লায়েন্ট দুই দিকেই local_infile চালু থাকতে হয় (যেমন pymysql.connect-এ local_infile=True); MySQL 8-এ নিরাপত্তার জন্য এটা ডিফল্টভাবে বন্ধ, তাই এখানে কমান্ডটা দেখানো হলো, চালানো হলো না:
-- MySQL
LOAD DATA LOCAL INFILE 'new_customers.csv'
INTO TABLE staging_customers
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 LINES;
csv.writerডিফল্টভাবে লাইন শেষ করে\r\nদিয়ে (CSV-র স্ট্যান্ডার্ড অনুযায়ী), এজন্যই MySQL-এর উদাহরণে সেটা বলা আছে। যে লোডার\nআশা করে, সে প্রতিটা সারির শেষ কলামের শেষে একটা বাড়তি\rরেখে দেবে।
এক্সপোর্ট: SQL থেকে CSV, স্বয়ংক্রিয়ভাবে
যে এক্সপোর্ট প্রতি রাতে চলে, তাতে কোনো চমক থাকা উচিত নয়, আর সেটা যাচাই করা যাওয়া উচিত। কার্সর থেকে সারিগুলো সরাসরি ফাইলে স্ট্রিম করুন (সব একসাথে মেমরিতে রাখার দরকার নেই), আর পাশে সারির সংখ্যা আর একটা চেকসামসহ ছোট একটা ম্যানিফেস্ট লিখুন, যাতে যিনি ফাইলটা পাবেন, তিনি যাচাই করতে পারেন যে কিছু হারায়নি বা বদলায়নি:
import hashlib
import json
def export_query(con, sql, params, path):
"""কোয়েরির সারিগুলো একটা UTF-8 CSV আর একটা JSON ম্যানিফেস্টে লেখে; ম্যানিফেস্টটা ফেরত দেয়।"""
cur = con.execute(sql, params)
rows = 0
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([d[0] for d in cur.description])
for row in cur:
writer.writerow(row)
rows += 1
with open(path, "rb") as f:
digest = hashlib.sha256(f.read()).hexdigest()
manifest = {"file": path, "rows": rows, "sha256": digest[:16]}
with open(path + ".manifest.json", "w", encoding="utf-8") as f:
json.dump(manifest, f)
return manifest
print(export_query(
con,
"SELECT order_id, customer_id, order_date, status FROM orders WHERE order_date >= ? ORDER BY order_id",
("2026-05-01",),
"orders_2026-05.csv",
)){'file': 'orders_2026-05.csv', 'rows': 4, 'sha256': '5993311f728321ff'}ফাইলের নামে সময়কালটা রাখুন (orders_2026-05.csv), গতকালের ফাইল কখনো আজকেরটা দিয়ে ওভাররাইট করবেন না, আর স্ক্রিপ্টটা cron বা একটা অর্কেস্ট্রেটর দিয়ে নির্দিষ্ট সময়ে চালান (পরের পাতা)। pandas-এ এক্সপোর্ট এক লাইনের, pd.read_sql(sql, con).to_csv(path, index=False), তবে বিনিময়ে পুরো ফলাফল মেমরিতে রাখতে হয়।
Excel
pandas .xlsx পড়ে আর লেখে openpyxl-এর মাধ্যমে। একটা ওয়ার্কবুকে কয়েকটা শিট থাকতে পারে, আর sheet_name=None সবগুলো একটা ডিকশনারিতে পড়ে আনে:
orders = pd.read_sql("SELECT order_id, customer_id, order_date, status FROM orders ORDER BY order_id", con)
products = pd.read_sql("SELECT product_id, name, price FROM products ORDER BY product_id", con)
with pd.ExcelWriter("shop_report.xlsx") as xl:
orders.to_excel(xl, sheet_name="orders", index=False)
products.to_excel(xl, sheet_name="products", index=False)
sheets = pd.read_excel("shop_report.xlsx", sheet_name=None)
print({name: df.shape for name, df in sheets.items()}){'orders': (14, 4), 'products': (11, 3)}ফরম্যাটিংয়ের জন্য, বা যে শিট পরিষ্কার টেবিল নয় (শিরোনামের সারি, মার্জ করা সেল, নিচে মোট) তা পড়তে, সরাসরি openpyxl ব্যবহার করুন:
from openpyxl import load_workbook
from openpyxl.styles import Font
wb = load_workbook("shop_report.xlsx")
ws = wb["products"]
for cell in ws[1]:
cell.font = Font(bold=True) # হেডারের সারি বোল্ড
ws.column_dimensions["B"].width = 30
ws.freeze_panes = "A2" # স্ক্রল করলেও হেডার দেখা যাবে
wb.save("shop_report.xlsx")
for row in ws.iter_rows(min_row=1, max_row=3, values_only=True):
print(row)('product_id', 'name', 'price')
(1, 'Python Crash Course', 1200)
(2, 'Hands-On Machine Learning', 2500)Excel ফাইলের নিজস্ব ফাঁদও আছে: সংখ্যা হিসেবে রাখা তারিখ, লেখা হিসেবে রাখা সংখ্যা, ফোন নম্বরের শুরুর শূন্য উধাও, আর ১০,৪৮,৫৭৬ সারির সীমা। Excel-কে মানুষের জন্য ফরম্যাট হিসেবে দেখুন, পাইপলাইনের স্টোরেজ হিসেবে নয়।
JSON, নেস্টেড JSON আর JSON Lines
API JSON ফেরত দেয়, আর সেটা সাধারণত নেস্টেড: একটা অর্ডারের ভেতরে একটা customer অবজেক্ট আর আইটেমের একটা লিস্ট। টেবিলে চাই প্রতি আইটেমে এক সারি। pd.json_normalize এটাকে সমতল করে: record_path বলে কোন লিস্ট সারি হবে, meta বলে প্যারেন্টের কোন ঘরগুলো প্রতিটা সারিতে কপি হবে। এখানে API-র উত্তরের জায়গায় একটা লোকাল ফাইল:
api_response = {
"orders": [
{"order_id": 6, "customer": {"id": 2, "name": "Tanvir Ahmed"},
"items": [{"sku": "AI-BOOT", "qty": 1, "price": 15000}]},
{"order_id": 9, "customer": {"id": 6, "name": "Imran Hossain"},
"items": [{"sku": "KEYB-MECH", "qty": 1, "price": 4050},
{"sku": "MOUSE-WL", "qty": 1, "price": 900}]},
]
}
with open("orders_api.json", "w", encoding="utf-8") as f:
json.dump(api_response, f)
with open("orders_api.json", encoding="utf-8") as f:
payload = json.load(f)
items = pd.json_normalize(payload["orders"], record_path="items",
meta=["order_id", ["customer", "name"]])
print(items) sku qty price order_id customer.name
0 AI-BOOT 1 15000 6 Tanvir Ahmed
1 KEYB-MECH 1 4050 9 Imran Hossain
2 MOUSE-WL 1 900 9 Imran Hossainউল্টো দিকে, JSON Lines (প্রতি লাইনে একটা JSON অবজেক্ট, .jsonl) হলো LLM ফাইন-টিউনিং ডেটা, ইভ্যালুয়েশন সেট আর লগের প্রচলিত ফরম্যাট, কারণ এতে শেষে নতুন লাইন যোগ করা যায় আর লাইন ধরে ধরে স্ট্রিম করা যায়। বাংলা লেখার জন্য দুটো খুঁটিনাটি জরুরি:
examples = pd.DataFrame({
"prompt": ["Where does customer 10 live?", "What is the name of customer 10?"],
"answer": ["Dhaka", "তানিয়া সুলতানা"],
})
print(json.dumps({"answer": "তানিয়া"})) # ডিফল্ট: এস্কেপ করা
print(json.dumps({"answer": "তানিয়া"}, ensure_ascii=False)) # পড়া যায় এমন UTF-8
examples.to_json("examples.jsonl", orient="records", lines=True, force_ascii=False)
with open("examples.jsonl", encoding="utf-8") as f:
print(f.read().strip())
print(pd.read_json("examples.jsonl", lines=True).shape){"answer": "\u09a4\u09be\u09a8\u09bf\u09af\u09bc\u09be"}
{"answer": "তানিয়া"}
{"prompt":"Where does customer 10 live?","answer":"Dhaka"}
{"prompt":"What is the name of customer 10?","answer":"তানিয়া সুলতানা"}
(2, 2)এস্কেপ করা JSON-ও সঠিক (যেকোনো পার্সার \u09a4-কে আবার ত বানায়), কিন্তু মানুষের পক্ষে পড়া অসম্ভব, আর ডিস্কে প্রায় দ্বিগুণ জায়গা নেয়। ensure_ascii=False (json) বা force_ascii=False (pandas) ব্যবহার করুন, আর ফাইলটা UTF-8-এ লিখুন।
XML-এর মূল কথা
ব্যাংক, সরকারি সিস্টেম আর পুরোনো এন্টারপ্রাইজ সফটওয়্যার থেকে এখনো XML আসে। সরল, পুনরাবৃত্ত রেকর্ডের জন্য pd.read_xml প্রতিটা এলিমেন্টকে একটা সারি বানায় (অ্যাট্রিবিউট আর চাইল্ড এলিমেন্ট হয় কলাম); এর চেয়ে অনিয়মিত যেকোনো কিছু সামলায় স্ট্যান্ডার্ড লাইব্রেরির xml.etree.ElementTree:
import xml.etree.ElementTree as ET
xml_text = """<?xml version="1.0" encoding="UTF-8"?>
<price_feed supplier="Gadget House">
<item sku="KEYB-MECH"><name>Mechanical Keyboard</name><price>4300</price></item>
<item sku="MOUSE-WL"><name>Wireless Mouse</name><price>850</price></item>
</price_feed>"""
with open("price_feed.xml", "w", encoding="utf-8") as f:
f.write(xml_text)
root = ET.parse("price_feed.xml").getroot()
print(root.get("supplier"))
for item in root.iter("item"):
print(item.get("sku"), item.findtext("name"), int(item.findtext("price")))
print(pd.read_xml("price_feed.xml", xpath=".//item", parser="etree"))Gadget House
KEYB-MECH Mechanical Keyboard 4300
MOUSE-WL Wireless Mouse 850
sku name price
0 KEYB-MECH Mechanical Keyboard 4300
1 MOUSE-WL Wireless Mouse 850parser="etree" স্ট্যান্ডার্ড লাইব্রেরি ব্যবহার করে; ডিফল্ট "lxml" দ্রুততর, কিন্তু আলাদা ইনস্টল লাগে। অবিশ্বস্ত উৎসের XML এগুলো দিয়ে অসাবধানে পার্স করবেন না: কারসাজি করা XML মেমরি শেষ করে দিতে পারে। নিরাপদ বিকল্প হলো defusedxml প্যাকেজ।
Parquet: কলামভিত্তিক স্টোরেজ
CSV আর JSON ডেটা রাখে সারি ধরে ধরে, লেখা হিসেবে। Parquet রাখে কলাম ধরে ধরে, বাইনারিতে; টাইপগুলো ফাইলের ভেতরেই লেখা থাকে, আর প্রতিটা কলাম আলাদাভাবে কম্প্রেস করা হয়:
সারিভিত্তিক ফরম্যাট (CSV) কলামভিত্তিক ফরম্যাট (Parquet)
1,2026-01-12,chat,312 event_id: 1, 2, 3, ... (int64)
2,2026-01-12,view,45 day: 2026-01-12, ... (date)
3,2026-01-13,chat,280 kind: chat, view, ... (আলাদা মান কম: ভালো কম্প্রেস হয়)
... seconds: 312, 45, 280, ... (int64)এতে তিনটা লাভ: ছোট ফাইল (একই রকম মান পাশাপাশি থাকে আর ভালো কম্প্রেস হয়), অল্প কয়েকটা কলাম দ্রুত পড়া (seconds দরকার এমন কোয়েরি কখনো kind পড়েই না), আর লেখা-পড়ার পরও টাইপ অটুট থাকা। pandas Parquet পড়ে আর লেখে pyarrow দিয়ে, যেটা আলাদা ইনস্টল করতে হয় (pip install pyarrow)। বানানো ২,০০,০০০টা ইভেন্ট দিয়ে তুলনা করুন:
import numpy as np
rng = np.random.default_rng(0)
n = 200_000
events = pd.DataFrame({
"event_id": np.arange(1, n + 1),
"day": pd.to_datetime("2026-01-01") + pd.to_timedelta(rng.integers(0, 180, n), unit="D"),
"kind": rng.choice(["view", "search", "add_to_cart", "chat"], n),
"seconds": rng.integers(1, 600, n),
})
events.to_csv("events.csv", index=False)
events.to_parquet("events.parquet", index=False)
for path in ["events.csv", "events.parquet"]:
print(f"{path:15} {os.path.getsize(path) / 1_000_000:5.1f} MB")
back_csv = pd.read_csv("events.csv")
back_parquet = pd.read_parquet("events.parquet", columns=["day", "seconds"]) # শুধু দুটো কলাম পড়া
print("day from CSV:", back_csv["day"].dtype, "| day from Parquet:", back_parquet["day"].dtype)events.csv 5.7 MB
events.parquet 1.6 MB
day from CSV: str | day from Parquet: datetime64[us]CSV তারিখের টাইপ হারিয়েছে (লেখা হয়ে ফেরত এসেছে), Parquet রেখে দিয়েছে। Parquet হলো ডেটা লেক, Spark, DuckDB আর ওয়্যারহাউস এক্সপোর্টের প্রচলিত ফাইল ফরম্যাট, আর ট্রেনিং ডেটাসেট সেভ করার সঠিক পছন্দ। মানুষ খুলে দেখবে এমন ছোট ফাইল, আর শুধু CSV বোঝে এমন সিস্টেমের জন্য CSV-ই ঠিক।
বড় ফাইল: টুকরো আর স্ট্রিমিং
ফাইল যখন মেমরির চেয়ে বড়, কখনো পুরোটা লোড করবেন না। read_csv(chunksize=…) নির্দিষ্ট আকারের DataFrame দিতে থাকে; প্রতিটার অ্যাগ্রিগেট করে জোড়া দিন:
totals = {}
for chunk in pd.read_csv("events.csv", usecols=["kind", "seconds"], chunksize=50_000):
for kind, s in chunk.groupby("kind")["seconds"].sum().items():
totals[kind] = totals.get(kind, 0) + int(s)
print(totals)
def stream_rows(path):
"""প্রতিটা CSV সারির জন্য একটা dict দেয়: ফাইল যত বড়ই হোক, মেমরি একই।"""
with open(path, newline="", encoding="utf-8") as f:
yield from csv.DictReader(f)
long_chats = sum(1 for row in stream_rows("events.csv") if row["kind"] == "chat" and int(row["seconds"]) > 500)
print(long_chats, "chats longer than 500 seconds"){'add_to_cart': 14949553, 'chat': 15001659, 'search': 15148999, 'view': 14898307}
8295 chats longer than 500 secondsটুকরো করলে pandas-এর গতি থাকে, মেমরিও সীমিত থাকে; জেনারেটর প্রায় কোনো মেমরিই নেয় না, আর যাচাই বা লোডের মতো এক-এক সারির কাজে মানানসই। বড় CSV বা Parquet ফাইলের ওপর অ্যানালিটিক্যাল কোয়েরির জন্য DuckDB সেগুলো আগে ডেটাবেসে লোড না করেই সরাসরি SQL চালাতে পারে।
সাধারণ ভুল
encoding="utf-8"ছাড়া ফাইল খোলা। Linux-এ চলে, Windows-এ ভাঙে। সবসময় এনকোডিংয়ের নাম দিন, আর মানুষ Excel-এ খুলবে এমন CSV-র জন্যutf-8-sig।split(",")দিয়ে CSV-র লাইন ভাগ করা। কমাওয়ালা প্রথম ঠিকানাটাই তার পরের প্রতিটা কলাম ভেঙে দেয়।csvবাpandasব্যবহার করুন।- একটা খারাপ সারির কারণে পুরো লোড ব্যর্থ হতে (বা চুপচাপ ছোট হয়ে যেতে) দেওয়া। যাচাই করুন, ভালো সারিগুলো লোড করুন, বাকিগুলো কারণসহ কোয়ারেন্টাইনে রাখুন, আর দুটো সংখ্যাই রিপোর্ট করুন।
- ইমপোর্টের সময় pandas-কে টাইপ আন্দাজ করতে দেওয়া।
"0172…"-এর মতো id শুরুর শূন্য হারায়,"NA"-এর মতো কোড অনুপস্থিত মান হয়ে যায়। id বা কোডের মতো যেকোনো আইডেন্টিফায়ারdtype=str(আরkeep_default_na=False) দিয়ে পড়ুন, তারপর বুঝেশুনে রূপান্তর করুন। - পাইপলাইনের ধাপগুলোর মাঝে ট্রেনিং ডেটা CSV-তে রাখা। টাইপ হারিয়ে যায়, প্রতিবার পড়ার সময় আবার পার্স করতে হয়। Parquet ব্যবহার করুন।
নিজে চেষ্টা করুন
- সহজ: pandas দিয়ে products টেবিলটা
products_for_excel.csv-এ এমনভাবে এক্সপোর্ট করুন, যাতে Excel ঠিকভাবে খোলে; তারপর যাচাই করুন যে ফাইলটা UTF-8 BOM-এর বাইটb'\xef\xbb\xbf'দিয়ে শুরু হয়েছে। - মাঝারি: ডেলিভারড অর্ডারগুলো তাদের আইটেমসহ নেস্টেড JSON হিসেবে (প্রতি অর্ডারে একটা অবজেক্ট, ভেতরে
itemsলিস্ট)delivered.json-এ লিখুন, তারপরjson_normalizeদিয়ে আবার সমতল করে যাচাই করুন যে সারির সংখ্যা ডেলিভারড অর্ডারের order_items সারির সংখ্যার সমান। - কঠিন:
load_csv(con, path, table, required)লিখুন, যাcsvমডিউল দিয়ে যেকোনো CSV আগে থেকে থাকা একটা টেবিলে লোড করে, আবশ্যিক ঘর খালি বা ডেটাবেস এরর হলে সারিটা<path>.rejected.csv-এ কোয়ারেন্টাইন করে, আর(loaded, rejected)ফেরত দেয়।price_changes.csvলোড করে টেস্ট করুন (নিজে লিখবেন: নতুন টেবিলprice_changes-এর জন্য তিনটা সারি, যারproduct_idproducts-এর ফরেন কি আরnew_price INTEGER CHECK (new_price > 0), একটায় প্রোডাক্ট ৯৯, একটায় দাম ০)।
উত্তর
# ১. সহজ
pd.read_sql("SELECT * FROM products ORDER BY product_id", con).to_csv(
"products_for_excel.csv", index=False, encoding="utf-8-sig")
with open("products_for_excel.csv", "rb") as f:
print(f.read(3))
# ২. মাঝারি
lines = pd.read_sql("""
SELECT o.order_id, o.order_date, oi.product_id, oi.quantity, oi.unit_price
FROM orders o JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'delivered' ORDER BY o.order_id, oi.product_id""", con)
nested = [
{"order_id": int(oid), "order_date": g["order_date"].iloc[0],
"items": g[["product_id", "quantity", "unit_price"]].to_dict("records")}
for oid, g in lines.groupby("order_id")
]
with open("delivered.json", "w", encoding="utf-8") as f:
json.dump(nested, f, default=int)
with open("delivered.json", encoding="utf-8") as f:
flat = pd.json_normalize(json.load(f), record_path="items", meta=["order_id", "order_date"])
print(len(flat), len(lines), len(flat) == len(lines))
# ৩. কঠিন
def load_csv(con, path, table, required):
loaded, rejected = 0, []
with open(path, newline="", encoding="utf-8") as f, con:
reader = csv.DictReader(f)
cols = reader.fieldnames
sql = f"INSERT INTO {table} ({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))})"
for row in reader:
empty = [c for c in required if not row[c].strip()]
if empty:
rejected.append({**row, "reason": f"empty: {', '.join(empty)}"})
continue
try:
con.execute(sql, [row[c] or None for c in cols])
loaded += 1
except sqlite3.Error as exc:
rejected.append({**row, "reason": str(exc)})
if rejected:
with open(path + ".rejected.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(rejected[0]))
w.writeheader()
w.writerows(rejected)
return loaded, len(rejected)
con.execute("CREATE TABLE price_changes (product_id INTEGER, new_price INTEGER CHECK (new_price > 0), "
"FOREIGN KEY (product_id) REFERENCES products (product_id))")
with open("price_changes.csv", "w", newline="", encoding="utf-8") as f:
f.write("product_id,new_price\n3,4300\n99,500\n4,0\n")
print(load_csv(con, "price_changes.csv", "price_changes", required=["product_id", "new_price"]))
with open("price_changes.csv.rejected.csv", encoding="utf-8") as f:
print(f.read().strip())সারসংক্ষেপ
csvমডিউল বা pandas ব্যবহার করুন, কখনোsplit(",")নয়; টেক্সট ফাইল সবসময়encoding="utf-8"দিয়ে খুলুন (Excel-এর জন্যutf-8-sig)। এনকোডিং স্পষ্ট থাকলে বাংলা লেখা প্রতিটা ধাপ পেরিয়েও অটুট থাকে।- যাচাইসহ ইমপোর্ট করুন: ভালো সারিগুলোর জন্য একটা ট্রানজ্যাকশন, বাকিগুলোর জন্য কারণসহ কোয়ারেন্টাইন ফাইল; স্টেজিং টেবিল যাচাইয়ের কাজ SQL-কে দেয়; বিশাল পরিমাণের জন্য COPY আর LOAD DATA।
- স্বয়ংক্রিয় এক্সপোর্ট কার্সর থেকে স্ট্রিম করে আর একটা ম্যানিফেস্ট (সারির সংখ্যা, চেকসাম) লেখে।
- Excel মানুষের জন্য; নেস্টেড JSON-এ
json_normalizeলাগে; JSON Lines হলো LLM ডেটাসেটের ফরম্যাট; XML পার্স হয়read_xmlবা ElementTree দিয়ে। - Parquet কলামভিত্তিক, টাইপসহ আর কম্প্রেসড: ডেটাসেটের জন্য সঠিক ফরম্যাট। বড় ফাইল টুকরো করে বা স্ট্রিম হিসেবে প্রসেস করুন।
এরপর: ETL ও ELT: নির্ভরযোগ্য ডেটা পাইপলাইন বানানো পাতা এই অধ্যায়ের সবকিছু (কানেকশন, pandas, পরিষ্কার, ফাইল) জুড়ে একটা পাইপলাইন বানায়, যা একটা ডেটাবেস, একটা API আর একটা ফাইল থেকে ডেটা তোলে, রূপান্তর করে, লোড করে, আর বারবার চালালেও কিছুই বদলায় না।