Foundationপ্রথম নীতি থেকে
LEVEL 1লেসন ১০/১৪মাঝারি৪৫ মিনিট

ASCII ও Character Encoding — সংখ্যা কীভাবে অক্ষর হয়

Character Encoding and ASCII

একটা byte শুধু একটা সংখ্যা 0-255 — কোন সংখ্যা কোন অক্ষর বোঝায় সেটা কেউ ঠিক করে দেয়, আর ASCII ছিল প্রথম ব্যাপকভাবে গৃহীত সেই চুক্তি, যার সীমাবদ্ধতা থেকেই code page বিশৃঙ্খলা আর mojibake জন্ম নেয়।

এই লেসন শেষে আপনি পারবেন

  • কেন একটা byte নিজে থেকে 'অক্ষর' বোঝে না, আর encoding-এর কাজ ঠিক কী সেটা ব্যাখ্যা করতে পারবেন
  • ASCII-র 7-bit নকশা, control character-এর উৎস, আর case bit-এর trick বুঝে প্রয়োগ করতে পারবেন
  • 8th bit-এর ইতিহাস আর code page বিস্ফোরণ কেন ঘটেছিল সেটা ব্যাখ্যা করতে পারবেন
  • একটা নির্দিষ্ট byte sequence-কে ভুল code page দিয়ে decode করলে কী mojibake তৈরি হয় সেটা হাতে-কলমে দেখাতে পারবেন
  • CR বনাম LF-এর পার্থক্য আর তার বাস্তব প্রভাব (Windows vs Unix, git) চিনতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনে দেখেছি একটা byte আসলে একটা সংখ্যা — 0 থেকে 255। এখন প্রশ্নটা সহজ কিন্তু গভীর:

আপনি একটা text file খুলে “A” দেখেন। কম্পিউটার ডিস্কে কী আছে? একটা byte, মান 65। কিন্তু 65 সংখ্যাটা নিজে থেকে “A” বলে না — 65 মানে 65, একটা integer। “A” হিসেবে দেখানোটা একটা সিদ্ধান্ত, একটা চুক্তি

byte value:  01000001  (৬৫)
আপনার স্ক্রিনে দেখায়:  A

কে ঠিক করল যে ৬৫ মানে “A”? কেউ না কেউ বসে একটা টেবিল বানিয়েছিল — “এই সংখ্যাটা এই অক্ষর বোঝাবে।” সেই টেবিলটাই character encoding

এই লেসনে আমরা প্রথম ব্যাপকভাবে গৃহীত এমন টেবিল — ASCII — এবং এর ভাঙনের গল্প দেখব। কারণ ASCII একা যথেষ্ট ছিল না, আর সেই অভাব থেকেই জন্ম নেয় দশকের পর দশকের বিশৃঙ্খলা, যা শেষমেশ Unicode (পরের লেসন) সমাধান করতে বাধ্য হয়।

মূল ধারণা

ASCII — 7 বিট, ১২৮টা সিদ্ধান্ত

ASCII (American Standard Code for Information Interchange) প্রথম প্রকাশিত হয় ১৯৬৩ সালে, বড় সংশোধন ১৯৬৭-তে (lowercase letter যোগ হয়)। এটা প্রতিটা অক্ষরকে ৭ bit-এ কোড করে — অর্থাৎ 2^7 = 128 টা সম্ভাব্য code point।

কেন ৭ বিট, ৮ নয়? এটা দুর্ঘটনা নয় — নিচে দেখব এর একটা স্পষ্ট প্রকৌশলগত কারণ ছিল।

১২৮টা code point চারটা ভাগে বিভক্ত:

পরিসরসংখ্যাকী
0–31৩২টাControl character
32১টাSpace
33–126৯৪টাPrintable — সংখ্যা, অক্ষর, চিহ্ন
127১টাDEL

মোট: ৩৩টা control character + ৯৫টা printable (space সহ) = ১২৮। ✓

পরিসর (dec)পরিসর (hex)কীসংখ্যা
48–570x30–0x3909১০
65–900x41–0x5AAZ২৬
97–1220x61–0x7Aaz২৬
বাকিগুলোযতিচিহ্ন ও প্রতীক (পূর্ণ তালিকা নিচের টেবিলে)৩২
ASCII printable range-এর কাঠামো — লক্ষ্য করুন প্রতিটা ব্লক ১৬-এর গুণিতকে শুরু, আর uppercase/lowercase-এর মধ্যে ঠিক একটা bit-এর পার্থক্য।
dechexchdechexchdechexchdechexch
3220(space)563888050P10468h
3321!573998151Q10569i
3422583A:8252R1066Aj
3523#593B;8353S1076Bk
3624$603C<8454T1086Cl
3725%613D=8555U1096Dm
3826&623E>8656V1106En
3927633F?8757W1116Fo
4028(6440@8858X11270p
4129)6541A8959Y11371q
422A*6642B905AZ11472r
432B+6743C915B[11573s
442C,6844D925C\11674t
452D-6945E935D]11775u
462E.7046F945E^11876v
472F/7147G955F_11977w
483007248H9660`12078x
493117349I9761a12179y
50322744AJ9862b1227Az
51333754BK9963c1237B{
52344764CL10064d1247C|
53355774DM10165e1257D}
54366784EN10266f1267E~
55377794FO10367g
সম্পূর্ণ ASCII টেবিল — printable range, ৩২ থেকে ১২৬। প্রতিটা সারি ১৬টা কোড, hex-এর শেষ digit column হিসেবে পড়ুন।

লক্ষ্য করুন প্যাটার্নটা: প্রতিটা column-এ hex-এর শেষ digit স্থির — 0x_1 সবসময় !, 1, A, Q, a, q-এর সাথে (21, 31, 41, 51, 61, 71)। এটা কোনো কাকতালীয় নয় — ASCII কমিটি ইচ্ছাকৃতভাবে digit, uppercase, আর lowercase-কে ৩২-এর গুণিতকে সারিবদ্ধ রেখেছিল যাতে mask/shift অপারেশন দিয়ে category চেনা সহজ হয় (ch >> 5 দিয়ে মোটামুটি বলা যায় কোন block-এ পড়ছে)।

Case bit — একটা সুন্দর trick

'A' = 65 = 0100_0001, 'a' = 97 = 0110_0001। পার্থক্য ঠিক একটা bit — bit position 5 (মান 0x20)।

>>> ord('A') ^ 0x20
97          # 'a'
>>> ord('a') ^ 0x20
65          # 'A'

এই নকশাটা ইচ্ছাকৃত। ASCII কমিটি uppercase আর lowercase-কে ঠিক 0x20 দূরত্বে রেখেছিল যাতে case toggle একটা single XOR হয় — সেই সময়ের হার্ডওয়্যারে (bit-shift আর logic gate সস্তা, arithmetic ব্যয়বহুল) এটা একটা গুরুত্বপূর্ণ পারফরম্যান্স সিদ্ধান্ত ছিল।

Control character — কেন এত বেশি

Code 0–31 আর 127 — মোট ৩৩টা — কোনো visible glyph নয়, বরং নির্দেশ। এগুলো ডিজাইন হয়েছিল টেলিটাইপ (mechanical printer + keyboard) নিয়ন্ত্রণের জন্য, ঠিক এমন এক যুগে যখন “কম্পিউটার” মানেই একটা টাইপরাইটারের সাথে তার দিয়ে জোড়া থাকা একটা যন্ত্র।

কোডনামআজও যা মানে
0NULC string-এর terminator ('\0')
7BELটার্মিনাল beep/flash
8BS (Backspace)এক ঘর পেছনে
9TAB (HT)পরের tab stop-এ
10LF (Line Feed)নতুন লাইন — Unix-এর \n
13CR (Carriage Return)লাইনের শুরুতে ফেরত — \r
27ESCTerminal escape sequence-এর শুরু (রং, cursor move)
127DELমুছে ফেলা

আজও প্রায় প্রতিটা প্রোগ্রামিং ভাষায় \t, \n, \r, \0 — এই কোডগুলোই। ৬০ বছর পুরনো একটা সিদ্ধান্ত এখনো প্রতিটা কীবোর্ডে জীবিত।

CR বনাম LF — একটা বাস্তব দৈনন্দিন সমস্যার শিকড়

টেলিটাইপে “নতুন লাইনে যাওয়া” আসলে দুইটা আলাদা শারীরিক কাজ:

  1. Carriage Return (CR) — print head-কে লাইনের শুরুতে ফেরত আনা
  2. Line Feed (LF) — কাগজ এক লাইন উপরে/নিচে ঘোরানো

মেকানিক্যাল টাইপরাইটারে আপনি এই দুইটা আলাদা লিভার হিসেবে কল্পনা করতে পারেন — carriage স্লাইড করে ফেরত আসে, রোলার আলাদাভাবে কাগজ ঘোরায়। দুটো না করলে নতুন লাইনে লেখা পুরনো লাইনের উপর ওভাররাইট হয়ে যেত।

কম্পিউটার অপারেটিং সিস্টেমগুলো এই ঐতিহ্য ভিন্নভাবে ধরে রেখেছে:

OSNewline conventionকেন
Unix / Linux / macOS (আধুনিক)LF শুধু (\n)সরলীকরণ — Multics/Unix-এর নকশা সিদ্ধান্ত
Windows / MS-DOSCRLF (\r\n)সরাসরি টেলিটাইপ থেকে উত্তরাধিকার (CP/M হয়ে)
Classic Mac OS (৯-এর আগে)CR শুধু (\r)ভিন্ন ঐতিহাসিক সিদ্ধান্ত, macOS X-এ বাতিল

বাস্তব পরিণতি:

  • Windows-এ তৈরি একটা text file Unix-এ খুললে প্রতিটা লাইনের শেষে একটা দৃশ্যমান ^M (CR) দেখা যেতে পারে
  • Git-এর core.autocrlf সেটিং ঠিক এই সমস্যা সামলাতে — checkout-এ LF → CRLF (Windows) আর commit-এ উল্টো
  • Shebang লাইন (#!/usr/bin/env python) যদি ভুলবশত CRLF দিয়ে শেষ হয়, Linux-এ env: python\r: No such file or directory — কারণ \r interpreter path-এর অংশ হিসেবে পড়া হয়
  • HTTP header আবশ্যিকভাবে CRLF দিয়ে শেষ হয় (RFC অনুযায়ী), Unix সিস্টেমে বানানো হলেও — network protocol এখনো টেলিটাইপ যুগের কনভেনশন ধরে রেখেছে
একটা 'নতুন লাইন' আসলে কতগুলো bit
  1. Enter কী চাপাব্যবহারকারীর অভিপ্রায়
  2. OS/editor সিদ্ধান্ত নেয়LF, CRLF, নাকি CR — platform অনুযায়ী
  3. Byte(s) লেখা হয়LF = ০x0A (১ byte), CRLF = ০x0D ০x0A (২ byte)
  4. ফাইলে সংরক্ষিতদুই platform-এর একই "লাইন" আলাদা byte sequence
  5. অন্য OS-এ পড়াযদি convention না মেলে — দৃশ্যমান আবর্জনা বা ভাঙা script

ভেতরে কী ঘটছে

যেখানে ASCII-র নকশা ফাঁস করে সমস্যা

১. ৭ বিট কেন, ৮ নয় — উত্তরটাই পরের বিপর্যয়ের বীজ

ASCII ডিজাইন হয়েছিল serial transmission-এর যুগে — ডেটা একটা তার দিয়ে, বিট বাই বিট, প্রায়ই শব্দ-লাইনের মাধ্যমে (মডেম, ফোন লাইন) পাঠানো হতো। ফোন লাইন শোরগোলপূর্ণ — bit উল্টে যাওয়া স্বাভাবিক ঘটনা।

সমাধান: parity bit। ৭টা data bit + ১টা parity bit = ৮ bit মোট। Receiver গণনা করে দেখত ১-এর সংখ্যা even না odd — না মিললে বুঝত ট্রান্সমিশনে ভুল হয়েছে।

তাই ASCII ইচ্ছাকৃতভাবে ৭ বিট রাখল, আর ৮ম বিটটা এই error-checking কাজের জন্য সংরক্ষিত রইল — অক্ষরের জন্য নয়।

কিন্তু সময়ের সাথে যোগাযোগ লাইন নির্ভরযোগ্য হয়ে উঠল, higher-level protocol (যেমন TCP-র checksum) ভুল ধরার কাজ নিজে নিয়ে নিল, আর parity bit ধীরে ধীরে অপ্রয়োজনীয় হয়ে পড়ল।

তখন প্রশ্ন উঠল: সেই খালি পড়ে থাকা ৮ম বিট দিয়ে কী করা যায়?

উত্তর — আরো ১২৮টা code point! 128–255 রেঞ্জ খুলে গেল। আর ঠিক এখান থেকেই বিশৃঙ্খলা শুরু।

২. Extended ASCII — একই সংখ্যা, ভিন্ন অর্থ, শত শত রকম

সমস্যা হলো: কেউই একসাথে বসে ঠিক করল না ওই ১২৮টা নতুন slot-এ কী থাকবে। প্রতিটা দেশ, প্রতিটা কোম্পানি, প্রতিটা operating system নিজের মতো করে সেই জায়গাটা পূর্ণ করল। ফলে “8-bit ASCII” নামে একটা জিনিস নেই — শত শত ভিন্ন, পরস্পর-অসামঞ্জস্যপূর্ণ code page আছে।

Code pageঅঞ্চল/উদ্দেশ্য128–255-এ কী
ISO-8859-1 (Latin-1)পশ্চিম ইউরোপà, é, ñ, ü — accented Latin
Windows-1252 (CP1252)Windows-এর “Latin-1”Latin-1-এর মতোই, কিন্তু 0x80–0x9F-এ curly quote, em-dash, ™
IBM437 (CP437)মূল IBM PC (1981)box-drawing character, গ্রিক অক্ষর, গাণিতিক চিহ্ন
IBM850DOS পশ্চিম ইউরোপLatin-1-এর কাছাকাছি কিন্তু ভিন্ন বিন্যাস
CP866DOS রাশিয়ানCyrillic
KOI8-RUnix/ইন্টারনেট রাশিয়ানCyrillic, কিন্তু ভিন্ন বিন্যাস CP866 থেকে

একই byte value 0xE0, ভিন্ন code page-এ:

Code page0xE0 মানে
ASCIIসংজ্ঞায়িত নয় (7-bit-এর বাইরে)
ISO-8859-1à
Windows-1252à (এখানে Latin-1-এর সাথে মেলে)
IBM437α (গ্রিক আলফা!)
CP866р (Cyrillic এর)
KOI8-Rю (Cyrillic ю)

একটাই সংখ্যা, ছয়টা সম্পূর্ণ ভিন্ন অক্ষর — নির্ভর করে কোন code page দিয়ে আপনি সেটা পড়ছেন তার উপর।

একটা byte sequence-এর ফাইলের ভেতরে কোথাও লেখা নেই “আমি কোন code page” — সেই তথ্যটা ফাইলের বাইরে রাখতে হয় (email header-এর charset=, HTML-এর \<meta charset>, বা নিছক অনুমান)।

৩. বাংলার জন্য কোনো official code page ছিল না

ASCII-র মূল ১২৮টা slot সম্পূর্ণ ইংরেজি-কেন্দ্রিক — বাংলা অক্ষরের কোনো জায়গাই নেই। যখন 8-bit extension-এর যুগ এলো, ইউরোপীয় ভাষা আর রুশ ভাষার জন্য মোটামুটি প্রমিত code page তৈরি হলো (উপরের টেবিল), কিন্তু বাংলার জন্য এমন কোনো ব্যাপকভাবে গৃহীত standard ছিল না।

ভারত সরকার ১৯৯১ সালে ISCII (Indian Script Code for Information Interchange, IS 13194:1991) প্রকাশ করে — এটাই একমাত্র প্রকৃত সরকারি pre-Unicode Indian script encoding। কিন্তু এটা ব্যাপকভাবে ব্যবহারিক সফটওয়্যারে গৃহীত হয়নি।

বাস্তবে ঘটল আরো বিশৃঙ্খল কিছু — সফটওয়্যার ভেন্ডররা (Bijoy, SutonnyMJ, Likhun-এর মতো Bangla word processor আর font) 128–255 রেঞ্জ নিজেদের ইচ্ছামতো পুনর্ব্যবহার করল — কোনো আনুষ্ঠানিক মান-নির্ধারণী সংস্থা ছাড়াই। এগুলো আসলে “code page” ও নয়, নেহাত font hack: ওই বাইনারি byte value-গুলোকে বাংলা glyph দেখানো হতো একটা কাস্টম font ব্যবহার করে, ইংরেজি অক্ষরের কোনো মিলই নেই।

ফলাফল: একই ফাইল ভিন্ন font ছাড়া খুললে সম্পূর্ণ পাঠযোগ্যহীন, কপি-পেস্ট করলে গার্বেজ, সার্চ করা অসম্ভব, আর আজও বহু পুরনো সরকারি ও ব্যক্তিগত বাংলা document এই non-Unicode বিন্যাসে আটকে আছে — রূপান্তরের জন্য বিশেষ কনভার্টার প্রয়োজন। পরের লেসনে যখন আমরা Unicode-এর Bengali block দেখব, তখন বুঝবেন কেন সেই নকশাটা ISCII-র কাঠামো থেকে সরাসরি প্রভাবিত।

উদাহরণ

একটা byte, একাধিক গল্প — সম্পূর্ণ worked example

ধরুন আপনার কাছে এই hex byte sequence আছে:

63 61 66 c3 a9

এটা আসলে UTF-8-এ এনকোড করা ফরাসি শব্দ “café”c, a, f সাধারণ ASCII (প্রতিটা ১ byte), আর é UTF-8-এ ২ byte (c3 a9, পরের লেসনে এই এনকোডিং-এর নিয়ম বিস্তারিত দেখব)।

এখন এই একই ৫টা byte যদি Latin-1 (ISO-8859-1) দিয়ে পড়া হয় — যেখানে প্রতিটা byte সরাসরি একটা single code point:

ByteLatin-1-এ অর্থ
63c
61a
66f
c3Ã
a9©

ফলাফল: “café” হয়ে যায় “café” — একটা extra, ভুল অক্ষর ঢুকে গেছে, যদিও byte-গুলো একদম অক্ষত।

>>> original = "café"
>>> b = original.encode('utf-8')
>>> b
b'caf\xc3\xa9'
>>> b.decode('latin-1')
'café'          # mojibake — ঠিক এটাই

এটাই ইন্টারনেটের সবচেয়ে পরিচিত mojibake pattern — যেকোনো UTF-8 accented character যদি ভুল করে Latin-1/Windows-1252 হিসেবে পড়া হয়, Ã দিয়ে শুরু হওয়া একজোড়া অক্ষরে পরিণত হয়। এই pattern চিনে রাখলে ভবিষ্যতে সমস্যাটা এক নজরে ধরতে পারবেন।

Byte:        63    61    66    c3    a9
              │     │     │     └──┬──┘
UTF-8 পড়া:    c     a     f    é (২ byte একসাথে)
                                  = "café"  ✓

Latin-1 পড়া:  c     a     f     Ã     ©
                                  = "café"  ✗ mojibake
একই byte sequence, দুইটা ভিন্ন lens — অক্ষত ডেটা, ভুল ব্যাখ্যা।

নিজে চালিয়ে দেখুন

EXPERIMENT

নিজে মোজিবেক তৈরি ও উদ্ধার করুন

Python 3· ১৫ মিনিট
# ধাপ ১: বাংলা আর ফরাসি টেক্সট UTF-8-এ এনকোড করুন
words = ["café", "বাংলা", "naïve", "Zürich"]

for w in words:
    utf8_bytes = w.encode('utf-8')
    print(f"{w!r:12} → UTF-8 bytes: {utf8_bytes}")

print()

# ধাপ ২: সেই একই byte-গুলো ভুল করে Latin-1 দিয়ে decode করুন
for w in words:
    utf8_bytes = w.encode('utf-8')
    try:
        mojibake = utf8_bytes.decode('latin-1')
        print(f"{w!r:12} → ভুল decode (latin-1): {mojibake!r}")
    except UnicodeDecodeError as e:
        print(f"{w!r:12} → decode ব্যর্থ: {e}")

প্রত্যাশিত আউটপুট:

'café'       → UTF-8 bytes: b'caf\xc3\xa9'
'বাংলা'       → UTF-8 bytes: b'\xe0\xa6\xac\xe0\xa6\xbe\xe0\xa6\x82...'
'naïve'      → UTF-8 bytes: b'na\xc3\xafve'
'Zürich'     → UTF-8 bytes: b'Z\xc3\xbcrich'

'café'       → ভুল decode (latin-1): 'café'
'বাংলা'       → ভুল decode (latin-1): 'বাà¦...'
'naïve'      → ভুল decode (latin-1): 'naïve'
'Zürich'     → ভুল decode (latin-1): 'Zürich'

লক্ষ্য করুন Latin-1 কখনো decode ব্যর্থ হয় না — কারণ Latin-1-এ সবগুলো ২৫৬টা byte value-ই একটা না একটা অক্ষরে সংজ্ঞায়িত। এটাই এর বিপদ: ভুল হলেও চুপচাপ কিছু একটা আউটপুট দেয়, exception ছোঁড়ে না। UTF-8 সেটা করে না (নিচে দেখুন) — এটা UTF-8-এর একটা বড় সুবিধা, পরের লেসনে বিস্তারিত।

এখন উল্টো পথে — mojibake থেকে মূল টেক্সট উদ্ধার:

mojibake = 'café'

# কৌশল: mojibake string-টাকে Latin-1 হিসেবে ফিরিয়ে byte বানান,
# তারপর সেই byte-কে সঠিক codec (UTF-8) দিয়ে decode করুন
recovered = mojibake.encode('latin-1').decode('utf-8')
print(recovered)   # 'café'  — ফিরে এসেছে!

এই ট্রিকটা বাস্তবে extremely common — বহু ওয়েবসাইট আর ডেটাবেসের mojibake bug এভাবেই ঠিক করা হয়, একে বলা হয় “double-decode fix”। এটা কাজ করে কারণ Latin-1 encode/decode bijective — প্রতিটা byte value ঠিক একটা code point-এর সাথে মেলে (একটা injective এবং surjective mapping, ঠিক যেমন mathematics-এর function লেসনে দেখেছি) — তাই এই ধাপে কোনো তথ্য হারায় না, শুধু representation বদলায়।

এটা কী প্রমাণ করে

Mojibake মানে ডেটা হারানো নয়, ভুল lens দিয়ে দেখা। সঠিক lens ফিরিয়ে দিলে (encode ভুল codec-এ, decode সঠিক codec-এ) মূল টেক্সট প্রায়ই ফিরে পাওয়া যায়।

EXPERIMENT

Control character হাতে-কলমে তৈরি করুন

Linux / macOS terminal· ১০ মিনিট

টার্মিনালে Ctrl+A চাপলে কী পাঠানো হয়? উত্তর: অক্ষরের ASCII কোড থেকে উপরের ৩টা bit (0x60 মাস্ক) সরিয়ে ফেলা।

def ctrl_code(letter):
    return ord(letter.upper()) & 0x1F

for c in "ABCJHM":
    print(f"Ctrl+{c} = {ctrl_code(c)} = {chr(ctrl_code(c))!r}")
Ctrl+A = 1 = '\x01'    # SOH
Ctrl+B = 2 = '\x02'
Ctrl+C = 3 = '\x03'    # ETX — টার্মিনালে "interrupt" পাঠায়!
Ctrl+J = 10 = '\n'     # LF — Ctrl+J আর Enter একই কোড পাঠায়
Ctrl+H = 8 = '\x08'    # BS — Ctrl+H আর Backspace ঐতিহাসিকভাবে একই
Ctrl+M = 13 = '\r'     # CR — Ctrl+M আর Enter (কিছু terminal-এ)

এখন সরাসরি টার্মিনালে চেষ্টা করুন:

cat > /tmp/test.txt
# এখন টাইপ করুন কিছু, তারপর Ctrl+D চাপুন (EOF সংকেত, byte নয়!)

Ctrl+C ইন্টারাপ্ট সিগন্যাল পাঠায় কারণ টার্মিনাল driver ETX (code 3) বাইট চিনে রেখেছে বিশেষ অর্থ হিসেবে — এটা SIGINT-এ translate হয় kernel-এর tty layer-এ। এই আচরণটা ASCII-র সংজ্ঞা নয় — ASCII শুধু বলে code 3-এর নাম ETX; “interrupt পাঠাও” অর্থটা Unix terminal driver-এর নিজস্ব convention।

এটাই abstraction layering-এর একটা সুন্দর উদাহরণ: hardware কীবোর্ড → scan code → OS driver byte translate করে ASCII control code-এ → terminal driver সেই byte-কে বিশেষ অর্থ দেয় → application সিগন্যাল পায়। প্রতিটা স্তর একটা নতুন অর্থ যোগ করছে একই কাঁচামালের উপর।

এটা কী প্রমাণ করে

Ctrl+key কম্বিনেশন literally একটা bit-mask অপারেশন — উপরের bit-গুলো মুছে ফেলা।

নিজে বানান

BUILD IT

Mini Mojibake Detector ও Fixer

Python · ●●○○○
  1. একটা byte sequence-কে বিভিন্ন codec (utf-8, latin-1, windows-1252) দিয়ে decode চেষ্টা করুন
  2. কোনটা "valid UTF-8" (exception ছাড়া decode হয়) সেটা চিহ্নিত করুন
  3. যদি UTF-8 হিসেবে decode ব্যর্থ হয়, likely code page অনুমান করুন
  4. Double-decode mojibake pattern (Ã দিয়ে শুরু হওয়া জোড়া) স্বয়ংক্রিয়ভাবে সনাক্ত ও ঠিক করুন
import unicodedata


def is_valid_utf8(data: bytes) -> bool:
    """UTF-8 হিসেবে decode হয় কি না — exception না ছোঁড়াই মানে valid"""
    try:
        data.decode('utf-8')
        return True
    except UnicodeDecodeError:
        return False


def detect_encoding(data: bytes) -> str:
    """সরল heuristic — বাস্তব library (chardet, charset-normalizer)
    অনেক বেশি sophisticated, কিন্তু মূল ধারণাটা এটাই।"""
    if is_valid_utf8(data):
        return 'utf-8'
    # সব byte 0x00–0x7F হলে বিশুদ্ধ ASCII, যেকোনো superset-এই বৈধ
    if all(b \< 0x80 for b in data):
        return 'ascii'
    # সব byte বৈধ — Latin-1 সবসময় decode হবে (fallback)
    return 'latin-1 (অনুমান — নিশ্চিত নয়)'


def looks_like_double_encoded_mojibake(s: str) -> bool:
    """UTF-8 bytes ভুলবশত Latin-1 হিসেবে decode হলে সাধারণত
    'Ã' (U+00C3) দিয়ে শুরু হওয়া জোড়া অক্ষর তৈরি হয়।"""
    return 'Ã' in s or 'â€' in s


def fix_mojibake(s: str) -> str | None:
    """double-decode ভুল উল্টে দেওয়ার চেষ্টা"""
    try:
        recovered = s.encode('latin-1').decode('utf-8')
        return recovered
    except (UnicodeEncodeError, UnicodeDecodeError):
        return None


# ── পরীক্ষা ──────────────────────────────────────────
samples = [
    "café".encode('utf-8'),
    "hello world".encode('ascii'),
    "café".encode('utf-8').decode('latin-1').encode('utf-8'),  # double-mojibake
]

for data in samples:
    enc = detect_encoding(data)
    print(f"{data!r:40} → সনাক্ত: {enc}")
    if enc == 'utf-8':
        text = data.decode('utf-8')
        if looks_like_double_encoded_mojibake(text):
            fixed = fix_mojibake(text)
            print(f"  {'':38} → mojibake সন্দেহ! fix: {fixed!r}")

নিজে বাড়ান:

  1. একটা real-world CSV বা text file নিয়ে বিভিন্ন encoding দিয়ে save করুন (utf-8, latin-1, cp1252), তারপর আপনার detector দিয়ে প্রতিটা চিনতে পারেন কি না দেখুন
  2. chardet বা charset-normalizer library ইনস্টল করে তুলনা করুন — বাস্তব লাইব্রেরি byte-frequency statistics ব্যবহার করে, আপনারটা শুধু pattern matching
  3. BOM (byte order mark) detect করার একটা ফাংশন যোগ করুন — পরের লেসনে এটা কী তা বিস্তারিত দেখব, কিন্তু আপনি আগেই দেখতে পারেন data[:3] == b'\xef\xbb\xbf' UTF-8 BOM নির্দেশ করে
  4. একটা “round-trip test” যোগ করুন — decode করে আবার encode করে মূল byte-এর সাথে মেলে কি না যাচাই করুন

বাস্তব সিস্টেমে

Character encoding যেখানে সিদ্ধান্ত নেয়

PHP/MySQL-এর latin1 default charset। ২০০০-এর দশকের বহু ওয়েব অ্যাপ্লিকেশন MySQL-এর ডিফল্ট charset latin1 রেখেই ইউনিকোড টেক্সট সংরক্ষণ করত — অ্যাপ্লিকেশন লেয়ারে UTF-8 এনকোড হওয়া byte-গুলো ডেটাবেস ভুল করে Latin-1 হিসেবে interpret করত, ফলাফল স্টোর হতো mojibake আকারে। এই ভুল থেকে “WordPress-এর কার্লি quote bug” নামে পরিচিত সমস্যাটা এসেছে — smart quote (') ’ হয়ে যেত।

Windows Notepad-এর default encoding। Windows 10-এর 1903 আপডেট (২০১৯) পর্যন্ত Notepad নতুন ফাইল সংরক্ষণ করত system-এর default ANSI code page দিয়ে (ইংরেজি Windows-এ Windows-1252), UTF-8 নয়। এই কারণে বাংলা বা অন্য non-Latin স্ক্রিপ্টে সেভ করা ফাইল অন্য মেশিনে খুললে প্রায়ই mojibake দেখাত।

Email/SMS-এর 7-bit সীমাবদ্ধতা। পুরনো email transport (SMTP) মূলত 7-bit ASCII-র জন্য ডিজাইন করা, তাই non-ASCII বিষয় (যেমন বাংলা subject line) পাঠাতে Base64 বা Quoted-Printable দিয়ে এনকোড করে MIME header-এ পাঠাতে হয় (=?UTF-8?B?...?= ফরম্যাট)।

Git-এর core.autocrlf CR/LF সমস্যার সরাসরি বাস্তব সমাধান — Windows-এ checkout করার সময় LF → CRLF, commit করার সময় উল্টো। ভুল সেট করলে পুরো ফাইলের প্রতিটা লাইন “বদলে গেছে” দেখায় diff-এ, যদিও আসল কন্টেন্ট একই।

Bijoy/SutonnyMJ থেকে Unicode রূপান্তর। বাংলাদেশ সরকারের বহু পুরনো ডকুমেন্ট এখনো non-Unicode Bijoy এনকোডিং-এ — এগুলো ওপেন করতে নির্দিষ্ট ফন্ট প্রয়োজন, সার্চযোগ্য নয়, কপি-পেস্ট করলে গার্বেজ। “Bijoy to Unicode converter” নামে অসংখ্য টুল শুধু এই সমস্যা সমাধানের জন্যই তৈরি হয়েছে।

C string-এর NUL byte injection। যেহেতু C string \0-তে শেষ হয়, একটা path-এর মাঝখানে NUL byte ঢুকিয়ে ("file.txt\0.jpg") পুরনো নিরাপত্তাহীন কোড path validation বাইপাস করত — validator পুরো string দেখত, কিন্তু OS filesystem call \0-এ থেমে যেত।

HTTP header injection (CRLF injection)। যদি একটা web অ্যাপ্লিকেশন user input সরাসরি HTTP response header-এ বসায় বিনা sanitization-এ, আক্রমণকারী \r\n ঢুকিয়ে নতুন header বা এমনকি সম্পূর্ণ নতুন response body ইনজেক্ট করতে পারে — কারণ \r\n হলো header শেষ হওয়ার সংকেত।

SSH/terminal-এ backspace কাজ না করা। পুরনো/ভিন্ন terminal emulator-এর মাঝে ঐতিহাসিক দ্বন্দ্ব — কেউ Backspace-এ 0x08 (BS) পাঠায়, কেউ 0x7F (DEL)। ভুল mismatch হলে ব্যবহারকারী দেখেন Backspace চাপলে ^H দেখাচ্ছে, মুছছে না — এই bug আজও মাঝে মাঝে দেখা যায় পুরনো embedded system বা serial console-এ।

যে ভুলগুলো সবাই করে

“সব পুরনো কম্পিউটার ASCII ব্যবহার করত।”

না — IBM-এর mainframe (System/360 আর তার উত্তরসূরি) ASCII কখনোই গ্রহণ করেনি। তারা ব্যবহার করে EBCDIC (Extended Binary Coded Decimal Interchange Code) — একটা সম্পূর্ণ ভিন্ন 8-bit এনকোডিং, যেখানে অক্ষরের ক্রমও ASCII থেকে আলাদা।

ASCII-তে:   'A' = 65,  'Z' = 90   (একটানা ক্রমিক)
EBCDIC-তে:  'A' = 193, 'Z' = 233  (মাঝে ফাঁকা জায়গা আছে!)

EBCDIC-তে A–I, J–R, S–Z তিনটা আলাদা block-এ বিভক্ত, মাঝে অব্যবহৃত কোড — একটা উত্তরাধিকার punch-card যুগের bit-pattern নকশা থেকে।

বাস্তব পরিণতি: ASCII আর EBCDIC সিস্টেমের মধ্যে ডেটা আদান-প্রদান করতে গেলে explicit রূপান্তর দরকার হতো — নাহলে সম্পূর্ণ garbage। আজও কিছু legacy mainframe application (ব্যাংকিং, insurance-এর পুরনো COBOL সিস্টেম) EBCDIC-তে ডেটা রাখে, আর আধুনিক সিস্টেমের সাথে সংযোগে একটা conversion layer বাধ্যতামূলক। এটাই দেখায় “8-bit encoding” মানেই ASCII-সামঞ্জস্যপূর্ণ নয় — এমনকি প্রথম ১২৮ code point-এর নিশ্চয়তাও সার্বজনীন নয়।

“ASCII সব ইংরেজি টেক্সট কভার করে।”

আশ্চর্যজনকভাবে না। বাস্তব ইংরেজি টাইপোগ্রাফিতে ব্যবহৃত অনেক চিহ্নই ASCII-র বাইরে:

'  '           curly single quote — ASCII-তে নেই (শুধু সোজা ' আছে)
"  "           curly double quote — ASCII-তে নেই
–  —           en-dash, em-dash — ASCII-তে শুধু hyphen (-) আছে
…              ellipsis — ASCII-তে তিনটা আলাদা ডট (...) লিখতে হয়

Microsoft Word-এর “smart quotes” ফিচার স্বয়ংক্রিয়ভাবে সোজা quote-কে curly quote-এ বদলে দেয় — কিন্তু সেই curly quote ASCII-র বাইরে, Windows-1252 রেঞ্জে (0x91–0x94)। ফাইল যদি ভুল encoding দিয়ে সংরক্ষিত বা পড়া হয়, ঠিক এখান থেকেই mojibake শুরু হয়।

নিয়ম: “ইংরেজি” আর “ASCII” সমার্থক নয়। প্রকৃত ইংরেজি typography-ও 8-bit বা তার বেশি এনকোডিং দাবি করে।

“'Extended ASCII' একটা নির্দিষ্ট, প্রমিত জিনিস।”

এই শব্দটাই বিভ্রান্তিকর। কোনো একক “Extended ASCII” standard নেই — এটা একটা ছাতা-শব্দ যেকোনো 8-bit encoding বোঝাতে ব্যবহৃত হয় যা প্রথম ১২৮ code point-এ ASCII-র সাথে মেলে।

উপরে দেখা টেবিলটা আবার দেখুন — Latin-1, Windows-1252, IBM437, CP866 — সবগুলোই “extended ASCII”, কিন্তু একে অপরের সাথে সম্পূর্ণ অসামঞ্জস্যপূর্ণ 128–255 রেঞ্জে।

ব্যবহারিক পরিণতি: কেউ যদি বলে “এই ফাইলটা extended ASCII-তে”, সেটা আসলে কোনো তথ্য দেয় না — জিজ্ঞেস করুন কোন code page।

“একটা byte সবসময় একটা অক্ষর বোঝায়।”

ASCII-তে এবং বেশিরভাগ single-byte code page-এ সত্য, কিন্তু এটা একটা বিশেষ ক্ষেত্র, সার্বজনীন নিয়ম নয়।

আমরা এইমাত্র দেখেছি বাংলা “café”-এর é UTF-8-এ ২টা byte নেয়। পরের লেসনে দেখব বাংলা অক্ষর সাধারণত ৩টা byte নেয়, আর কিছু emoji ৪টা

“1 byte = 1 character” ধরে নিয়ে লেখা কোড (যেমন string[5] করে ৬ষ্ঠ “অক্ষর” পাওয়ার চেষ্টা, বাইট গণনা করে) ইংরেজি টেক্সটে চলে কিন্তু বাংলা, চীনা, emoji-তে ভেঙে পড়ে — মাঝপথে একটা multi-byte অক্ষরের ভেতরে কেটে ফেলে, ফলে বাকি অংশ decode-ই হয় না।

বুঝেছেন কি না দেখুন

1

ASCII কেন ৭ বিট ব্যবহার করে, ৮ বিট নয় — যেখানে বাইট এমনিতেই ৮ বিট? এই সিদ্ধান্তটা পরবর্তীতে কী সমস্যা তৈরি করেছিল?

যুক্তি

কারণ: ASCII ডিজাইন হয়েছিল serial transmission-এর যুগে, যখন ডেটা phone line-এর মতো শোরগোলপূর্ণ মাধ্যম দিয়ে পাঠানো হতো। ৭টা data bit + ১টা parity bit = ৮ bit মোট — parity bit দিয়ে receiver বুঝতে পারত ট্রান্সমিশনে bit উল্টে গেছে কি না (simple error detection)।

তাই ASCII ইচ্ছাকৃতভাবে মাত্র 2^7 = 128 code point সংজ্ঞায়িত করল, ৮ম বিট error-checking-এর জন্য সংরক্ষিত রাখল।

পরের সমস্যা: যোগাযোগ লাইন যখন নির্ভরযোগ্য হয়ে উঠল আর higher-level protocol (TCP checksum) error detection নিজে সামলাতে লাগল, parity bit অপ্রয়োজনীয় হয়ে গেল। সেই খালি ৮ম বিট তখন “নতুন ১২৮টা code point” হিসেবে ব্যবহৃত হতে শুরু করল — কিন্তু কোনো কেন্দ্রীয় কর্তৃপক্ষ ছাড়া, প্রতিটা vendor নিজের মতো পূরণ করল।

ফলাফল: শত শত পরস্পর-অসামঞ্জস্যপূর্ণ “extended ASCII” code page, আর দশকের পর দশক mojibake। এই বিশৃঙ্খলা এড়াতেই পরবর্তীতে Unicode-এর জন্ম — একটা একক, বিশ্বব্যাপী সংজ্ঞায়িত code point space।

2

এই hex byte sequence-টা দেওয়া হলো: 48 65 6c 6c 6f 2c 20 e9। এটা UTF-8-এ decode করার চেষ্টা করলে কী হবে? Latin-1-এ decode করলে কী দেখাবে? পার্থক্যটা ব্যাখ্যা করুন।

প্রয়োগ

প্রথম ৭টা byte (48 65 6c 6c 6f 2c 20) হলো ASCII printable range-এর মধ্যে — Hello, (যেকোনো encoding-এই একই, কারণ ASCII compatible)।

শেষ byte e9 — বাইনারিতে 1110 1001। UTF-8-এর leading-bit নিয়ম অনুযায়ী (পরের লেসনে বিস্তারিত), 1110xxxx মানে “এটা একটা ৩-byte sequence-এর প্রথম byte, আরো ২টা continuation byte আসবে যা 10xxxxxx প্যাটার্নে শুরু হয়”। কিন্তু এখানে e9-এর পরে আর কোনো byte নেই — sequence অসম্পূর্ণ।

তাই UTF-8 decode ব্যর্থ হবে:

>>> bytes.fromhex("48656c6c6f2c2 0e9").decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 7:
unexpected end of data

Latin-1 decode সবসময় সফল হয় (Latin-1-এ সব ২৫৬টা byte value সংজ্ঞায়িত):

>>> bytes.fromhex("48656c6c6f2c20e9").decode('latin-1')
'Hello, é'

পার্থক্যের কারণ: UTF-8-এর গঠন self-validating — leading bit pattern দিয়ে বলে দেয় কতগুলো byte আসা উচিত, আর সেই প্রত্যাশা না মিললে decoder বুঝতে পারে ডেটা ভুল বা অসম্পূর্ণ। Latin-1-এ এই রক্ষাকবচ নেই — যেকোনো byte value মানেই একটা বৈধ অক্ষর, তাই কখনো “ভুল” ধরা পড়ে না, এমনকি যখন byte sequence-টা আসলে অন্য কোনো encoding-এর ভাঙা অংশ। এটাই কেন mojibake সাইলেন্টলি ঘটে — কোনো error signal ছাড়াই।

3

ch ^ 0x20 দিয়ে case toggle করার trick-টা '5' (digit five) অক্ষরে প্রয়োগ করলে কী হয়? কেন এটা বিপজ্জনক, আর নিরাপদ code কী করা উচিত?

প্রয়োগ

'5' = 53 = 0011_01010x20 = 0010_0000 দিয়ে XOR করলে:

  0011 0101   ('5' = 53)
^ 0010 0000   (0x20)
-----------
  0001 0101   = 21 = 0x15

0x15 হলো NAK (Negative Acknowledge) — একটা control character, কোনো printable glyph নয়। টার্মিনালে এটা প্রিন্ট করলে হয় কিছুই দেখাবে না, নয়তো \x15 এর মতো escape representation দেখাবে, অথবা টার্মিনালের বিশেষ আচরণ ট্রিগার করবে।

কেন বিপজ্জনক: 0x20 bit toggle শুধু letter range-এই কাণ্ডজ্ঞানসম্মত ফল দেয় — কারণ ASCII ডিজাইনাররা বিশেষভাবে uppercase/lowercase জোড়াগুলোকে ঠিক 0x20 দূরত্বে রেখেছিলেন। সংখ্যা, চিহ্ন, বা control character-এ এই সম্পর্ক প্রযোজ্য নয় — সেখানে trick প্রয়োগ করলে এলোমেলো, সম্ভাব্য বিপজ্জনক byte তৈরি হয় (যেমন এখানে একটা control character তৈরি হলো)।

নিরাপদ পদ্ধতি: সবসময় প্রথমে range check করুন:

char to_lower(char c) {
    if (c >= 'A' && c \<= 'Z') return c ^ 0x20;
    return c;   // অক্ষর না হলে অপরিবর্তিত রাখুন
}

C standard library-র tolower()/toupper() ভেতরে এই range check-সহ trick-টাই বাস্তবায়ন করে, শুধু locale-aware সংস্করণে আরো জটিলতা যোগ করে (কিছু ভাষায় case mapping simple bit-flip নয় — যেমন জার্মান ß-এর কোনো uppercase ASCII-সমতুল্য নেই)।

4

আপনি একটা legacy সিস্টেম বানাচ্ছেন যেটা পুরনো Bijoy-এনকোডেড বাংলা ডকুমেন্ট (non-Unicode, custom font-based) স্বয়ংক্রিয়ভাবে সনাক্ত করে Unicode-এ রূপান্তর করবে। এই সমস্যাটা কেন কঠিন, আর আপনার পদ্ধতি কী হবে?

ডিজাইন

কেন কঠিন:

১. কোনো self-identifying marker নেই। UTF-8-এর মতো self-validating গঠন Bijoy এনকোডিং-এ নেই — এটা শুধু single-byte value-কে glyph-এ ম্যাপ করে, ঠিক Latin-1-এর মতো। তাই byte pattern দেখে নিশ্চিতভাবে বলা যায় না এটা Bijoy না অন্য কিছু।

২. Ambiguity-র সাথে লড়াই। একই byte value ASCII, Latin-1, বা Bijoy — তিনটাতেই বৈধ, ভিন্ন অর্থে। শুধু byte pattern দেখে নিশ্চিতভাবে বলা অসম্ভব কোনটা সঠিক।

৩. কোনো আনুষ্ঠানিক spec নেই যাচাই করার জন্য। ISCII একটা সরকারি standard, কিন্তু Bijoy তা অনুসরণ করে না — এটা একটা ভেন্ডর-নির্দিষ্ট font convention, ডকুমেন্টেশন প্রায়ই অসম্পূর্ণ বা অনুপস্থিত।

পদ্ধতি (heuristic-based, নিশ্চিত নয়):

১. প্রথমে UTF-8 হিসেবে decode চেষ্টা করুন। সফল হলে (এবং ফলাফল অর্থবহ বাংলা Unicode range-এ পড়ে), সেটাই সম্ভবত ইতিমধ্যে Unicode।

২. ব্যর্থ হলে, byte frequency analysis করুন। Bijoy-এনকোডেড বাংলা টেক্সটে নির্দিষ্ট byte value-এর একটা পরিসংখ্যানগত প্যাটার্ন থাকে (যেমন কোন byte কত ঘন ঘন আসে) যা ইংরেজি Latin-1 টেক্সট থেকে আলাদা — chardet/charset-normalizer-এর মতো লাইব্রেরি এই পদ্ধতিই ব্যবহার করে, শুধু সাধারণ code page-এর জন্য প্রশিক্ষিত। Bijoy-এর জন্য একটা কাস্টম statistical model প্রয়োজন হবে।

৩. Known mapping table ব্যবহার করুন। ইন্টারনেটে বিদ্যমান Bijoy-to-Unicode conversion tool-গুলোর mapping table reverse engineer বা reuse করুন (এই সমস্যাটা আগেই বহুবার সমাধান করা হয়েছে community দ্বারা)।

৪. কখনো ১০০% নিশ্চিত হবেন না। ব্যবহারকারীকে একটা preview দেখিয়ে নিশ্চিত করতে বলুন রূপান্তরটা সঠিক দেখাচ্ছে কি না — কারণ কোনো automatic detector-ই ভুল করতে পারে না এটা গ্যারান্টি দিতে পারে না।

মূল শিক্ষা: encoding detection মৌলিকভাবে একটা অনুমান — কখনোই সম্পূর্ণ নির্ভরযোগ্য নয়, কারণ কোনো byte sequence-ই নিজে থেকে তার encoding ঘোষণা করে না। সবচেয়ে ভালো সমাধান হলো encoding metadata সংরক্ষণ করা নতুন ডেটার জন্য, যাতে ভবিষ্যতে অনুমানের প্রয়োজনই না পড়ে।

5

CR আর LF-কে কেন আলাদা control character হিসেবে রাখা হয়েছিল, একটাই “newline” কোড না রেখে? আধুনিক কম্পিউটারে এই ঐতিহাসিক সিদ্ধান্তটা কি এখনো প্রাসঙ্গিক?

যুক্তি

ঐতিহাসিক কারণ: যান্ত্রিক টেলিটাইপে “নতুন লাইনে যাওয়া” আসলে দুইটা স্বাধীন শারীরিক কাজ ছিল:

  • Carriage Return — প্রিন্ট হেডকে (carriage) লাইনের শুরুতে ফিরিয়ে আনা, বাম দিকে স্লাইড করে
  • Line Feed — কাগজের রোলার ঘুরিয়ে কাগজটা এক লাইন উপরে/নিচে সরানো

দুটো ভিন্ন মোটর/মেকানিজম দিয়ে নিয়ন্ত্রিত হতো, তাই দুটো আলাদা কোড দরকার ছিল — প্রোগ্রামার প্রয়োজনমতো একটা, দুটো, বা কোনোটাই পাঠাতে পারত (যেমন শুধু CR পাঠিয়ে একই লাইনে ওভাররাইট করে প্রগ্রেস বার দেখানো, \r দিয়ে যা আজও টার্মিনালে ব্যবহৃত হয়)।

আধুনিক প্রাসঙ্গিকতা: যান্ত্রিক টেলিটাইপ নেই, কিন্তু সিদ্ধান্তটা জীবিত কারণ বিভিন্ন OS ভিন্ন সময়ে ভিন্ন convention বেছে নিয়েছিল আর সেই legacy backward-compatibility-র জন্য বদলানো যায়নি:

  • Unix LF-only বেছে নিল সরলতার জন্য (Multics/Unix ডিজাইন সিদ্ধান্ত)
  • DOS/Windows CRLF রেখে দিল সরাসরি টেলিটাইপ ঐতিহ্য থেকে (CP/M হয়ে উত্তরাধিকারসূত্রে)
  • নেটওয়ার্ক প্রোটোকল (HTTP, SMTP, FTP) CRLF আবশ্যিক করে রাখল standard-এ, platform নির্বিশেষে — কারণ প্রোটোকলগুলো ডিজাইন হয়েছিল টেলিটাইপ-সামঞ্জস্যপূর্ণ যুগে, আর পরিবর্তন করলে backward compatibility ভাঙত

এখনো যা প্রাসঙ্গিক: cross-platform ফাইল শেয়ারিং (git-এর autocrlf), শেল স্ক্রিপ্টের shebang লাইন (\r থাকলে ভেঙে যায়), এবং যেকোনো text-based network protocol implement করার সময় (sockets দিয়ে HTTP handle করলে \r\n মেনে চলতে হবে, শুধু \n নয়)।

মূল শিক্ষা: পুরনো হার্ডওয়্যার সিদ্ধান্ত প্রায়ই সফটওয়্যার convention হিসেবে বহু দশক টিকে থাকে, এমনকি মূল কারণ (যান্ত্রিক সীমাবদ্ধতা) বহু আগে অদৃশ্য হয়ে গেলেও — কারণ পরিবর্তনের খরচ (compatibility ভাঙা) মূল কারণের চেয়ে বেশি স্থায়ী প্রমাণিত হয়।

এরপর কী

ASCII-র গল্প শেষ হয় একটা সমস্যায়: বিশ্বের হাজার হাজার ভাষা, বাংলা সহ, ১২৮টা (বা এমনকি ২৫৬টা) code point-এ আঁটে না। আর “extended ASCII”-র সমাধান — শত শত পরস্পর-অসামঞ্জস্যপূর্ণ code page — একটা বিশৃঙ্খলা তৈরি করেছিল যেখানে একই byte পাঁচটা ভিন্ন জিনিস বোঝাতে পারত।

সমাধানটা আসতে হবে একটা একক, বিশ্বজনীন সিস্টেম থেকে — যেখানে পৃথিবীর প্রতিটা লিখন পদ্ধতির প্রতিটা অক্ষরের একটা এবং শুধু একটা স্থায়ী সংখ্যাগত পরিচয় থাকবে, encoding-নিরপেক্ষভাবে।

পরের লেসনে আমরা দেখব সেই সিস্টেম — Unicode — কীভাবে “কোন অক্ষর” (code point) প্রশ্নটাকে “কীভাবে byte-এ সংরক্ষণ করব” (encoding) প্রশ্ন থেকে আলাদা করে দিয়েছে, আর কেন সেই একটা নকশা সিদ্ধান্তই এই পুরো বিশৃঙ্খলা সমাধান করেছে। এরপর UTF-8-এর বিট-স্তরের নিয়মগুলো — কীভাবে একটা variable-length encoding self-synchronizing হতে পারে, আর কেন সেটাই আজ ইন্টারনেটের প্রায় সব টেক্সটের ভিত্তি।

আরও পড়ুন

  • ANSI X3.4 — American Standard Code for Information Interchange · মূল ASCII standard, প্রথম প্রকাশ ১৯৬৩, বড় সংশোধন ১৯৬৭ ও ১৯৮৬
  • RFC 20 — ASCII format for Network Interchange — Vint Cerf, Jack Crocker · ASCII-কে network protocol-এর ভিত্তি হিসেবে সংজ্ঞায়িত করে — HTTP header এখনো এর উপর দাঁড়িয়ে
  • IS 13194:1991 (ISCII) · ভারতীয় সরকারের প্রাক-Unicode Indian script encoding standard — পরের লেসনে Bengali block-এর কাঠামোর পূর্বসূরি হিসেবে ফিরে আসবে