মিনিফায়েড JSON ডিবাগ করা: line 1 column 23611 থেকে ভাঙা বাইট পর্যন্ত
সংক্ষেপে: একটি 4 MB এক-লাইন JSON ফাইল line 1 column 23611 রিপোর্ট করে, যা কিছুই বলে না। স্থানীয়ভাবে মাপা নমুনা ব্যবহার করে এই নির্দেশিকাটি ব্যাখ্যা করে Python, Node ও jq তিনটি ভিন্ন সংখ্যা কেন দেয়, একটি ক্যারেক্টার অফসেট কীভাবে সরাসরি ভাঙা বাইটে পৌঁছায়, আর সিনট্যাক্স ঠিক করার পরেও কোন তিনটি ফাঁদ নীরবে ডেটা নষ্ট করে।

মূল কথা: লাইন ও কলাম নম্বরের অর্থ কেবল বহু-লাইন ডকুমেন্টে। JSON এক লাইনে মিনিফায়েড হয়ে গেলে প্রতিটি এরর line 1-এ বাস করে — আপনার দরকার লাইন নয়, ক্যারেক্টার ইনডেক্স।
একটি মিনিফায়েড JSON ডকুমেন্টে ঠিক একটি লাইন থাকে, তাই এরর মেসেজের line/column জোড়া চার মিলিয়ন বাইটের একটি অবস্থানকে এক নিরর্থক বাক্যে সংকুচিত করে। উপায় হলো line/column সম্পূর্ণ বাদ দিয়ে বরং ক্যারেক্টার অফসেট পড়া — মেসেজের শেষে থাকা আনাড়ি
(char 23610)-ই সেই সংখ্যা যা সত্যিই আপনাকে বাঁচায়।
নিচের প্রতিটি সংখ্যা স্থানীয়ভাবে উৎপন্ন। নমুনাটি একটি এক-লাইন ডকুমেন্ট যাতে 13,000 বিলিং ইভেন্ট আছে, মোট 2,011,084 অক্ষর, এবং #154 নম্বর ইভেন্টের event_id-এর পরে ইচ্ছাকৃতভাবে একটি কমা মুছে ফেলা হয়েছে। পরিবেশ Python 3.11.1, Node 22.13.1 ও jq 1.7।
১. প্রথমে তথ্য: তিনটি পার্সার, তিনটি সংখ্যা
একই ভাঙা ফাইল তিনটি টুলে দেওয়া হলো:
Python 3.11.1 Expecting ',' delimiter: line 1 column 23611 (char 23610)
Node 22.13.1 Expected ',' or '}' after property value in JSON at position 23610 (line 1 column 23611)
jq 1.7 Expected separator between values at line 1, column 23619 exit code 5
পাশাপাশি:
| টুল | রিপোর্ট করা মান | একক | যেখান থেকে গোনা | line/column কেন ব্যর্থ হয় |
|---|---|---|---|---|
Python JSONDecodeError.pos |
23610 | ক্যারেক্টার অফসেট | বাফারের শুরু | একমাত্র যাকে বিশ্বাস করা যায় |
Python lineno / colno |
1 / 23611 | লাইন ও কলাম | লাইনের শুরু | কেবল এক লাইন, তাই কিছুই বলে না |
Node position |
23610 | ক্যারেক্টার অফসেট | বাফারের শুরু | নির্ভরযোগ্য, যদিও মেসেজ ফিল্ডটি গোপন রাখে |
jq column |
23619 | ক্যারেক্টার অফসেট | অপরাধী টোকেনের শুরু | Python থেকে 9-এ আলাদা |
সিদ্ধান্ত স্পষ্ট: Python-এর char N ও Node-এর position N একই সংখ্যা এবং মিলে যায়। jq নিজস্ব পদ্ধতি ব্যবহার করে — সেটির সঙ্গে মিলিয়ে সময় নষ্ট করবেন না।
তিনটি সংখ্যা আলাদা হওয়ার কারণ কেউ ভুল হিসাব করেনি, বরং তারা ভিন্ন জিনিস গোনে। Python ও Node রিপোর্ট করে পার্সার কোথায় থেমেছে; jq রিপোর্ট করে যে টোকেনটি পার্স করতে ব্যর্থ হয়েছে সেটি কোথায় শুরু হয়। দুইয়ের মাঝে কয়েকটি ইতিমধ্যে গ্রাস করা অক্ষর বসে আছে — এই নমুনায় 9টি।
২. এই ধরনের ফাইলে line 1 column N কেন নিরর্থক
লাইন নম্বর একটি প্রাপ্ত রাশি, যা ধরে নেয় «শেষ নিউলাইনের পর অনেক সময় কেটেছে»। একটি মিনিফায়েড ডকুমেন্টে একটি \n থাকে, সাধারণত শেষে, তাই:
linenoসর্বদা 1;colnoনেমে আসে «সেই এক লাইনের শুরু থেকে কত অক্ষর পেরিয়ে»-এ;- আর সেই লাইনটি চার মিলিয়ন অক্ষর চওড়া।
সম্পাদকে column 23611-এ লাফ দিলে উপকারী কিছু দেখবেন না। আরও খারাপ, less ও vim-এর মতো টুল অতিরিক্ত লম্বা লাইনে হামাগুড়ি দেয়; তীর চিহ্ন দিয়ে কার্সর ঠেলতে ঠেলতে কুড়ি মিনিট পুড়ে যায়।
উল্টো করে ভাবুন: অফসেটই নেটিভ রাশি, আর line/column তা থেকেই গণনা করা হয়। নেটিভ রাশিটাই ব্যবহার করুন।
৩. পদ্ধতি এক: Python-এ e.pos ব্যবহার করুন, lineno উপেক্ষা করুন
JSONDecodeError তিনটি সংখ্যা বহন করে, আর কেবল pos পড়ার যোগ্য। lineno ও colno প্রাপ্ত মান, যা এক-লাইন ডকুমেন্টে সম্পূর্ণ অর্থহীন হয়ে যায়।
# locate.py - Python 3.11.1
import json
import sys
path = sys.argv[1]
raw = open(path, encoding='utf-8').read()
try:
json.loads(raw)
except json.JSONDecodeError as error:
print(f'{type(error).__module__}.{type(error).__name__}: {error}')
start = max(0, error.pos - 60)
end = min(len(raw), error.pos + 60)
print(f'\nchar {start}..{end} of {len(raw)}:')
print(raw[start:end])
print(' ' * (error.pos - start) + '^')
sys.exit(1)
print('valid JSON')
ভাঙা ফাইলের বিরুদ্ধে এটি চালান:
$ python3 locate.py bench/minified-json-error-position/broken.json
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 23611 (char 23610)
char 23550..23670 of 2011083:
ua":"Mozilla/5.0 Chrome/141.0.0.0"},{"event_id":"evt_000154""user_id":100154,"email":"user154@example.com","plan":"pro",
^
ক্যারেট চিহ্নটি দেখায় কোথায় একটি কমা নেই। কুড়ি মিনিট এক কমান্ডে পরিণত হয়।
উইন্ডো কত চওড়া হওয়া উচিত: ±60 অক্ষর প্রায় সবসময় একটি সম্পূর্ণ কি/ভ্যালু জোড়া ঢেকে দেয়। খুব সংকীর্ণ হলে প্রসঙ্গ হারাবেন; খুব চওড়া হলে আউটপুট পর্দা ভরিয়ে ফেলে। গভীরভাবে নেস্টেড ডকুমেন্টে ±120 পর্যন্ত যেতে পারে।
৪. পদ্ধতি দুই: কন্টেইনারে Python নেই — jq আর cut
প্রোডাকশন কন্টেইনার প্রায়ই Python ছাড়া আসে, কিন্তু প্রায় সবসময় jq থাকে, বা একটি apt-get install jq-এ পাওয়া যায়। jq যা দেয় তা হলো উচ্চস্বরে ব্যর্থতা:
$ jq . bench/minified-json-error-position/broken.json
jq: parse error: Expected separator between values at line 1, column 23619
$ echo $?
5
এক্সিট কোড 5 মানে অবৈধ ইনপুট, তাই set -e-এর অধীনে এটি grep 1 ফেরত দিলে যেভাবে স্ক্রিপ্ট থামায়, সেভাবেই থামায়। এটি একটি সুবিধা।
jq-তে অপরাধী বাইট ছাপার কোনো ফ্ল্যাগ নেই, তাই অবস্থানটি cut-কে দিন:
# Note -c (characters), not -b (bytes)
$ cut -c23550-23670 bench/minified-json-error-position/broken.json
"ua":"Mozilla/5.0 Chrome/141.0.0.0"},{"event_id":"evt_000154""user_id":100154,"email":"user154@example.com","plan":"pro",
কমা ফিরে এলে jq তার আসল দক্ষ কাজ করতে পারে:
$ jq -r '.events | length' bench/minified-json-error-position/payload.json
13000
$ jq -r '.events[154] | "\(.event_id) \(.plan) \(.amount_cents)"' bench/minified-json-error-position/payload.json
evt_000154 pro 74479
৫. পদ্ধতি তিন: রানটাইম নেই, বা ডেটা ডিস্কে লেখা যাবে না — ব্রাউজারে পেস্ট করুন
প্রথম দুটি পদ্ধতির জন্য কিছু ইনস্টল থাকা আর ফাইল লেখার অনুমতি দরকার। যদি আপনি একটি খালি ল্যাপটপে থাকেন, বা পেলোড ভর্তি গ্রাহকের ইমেইল ও IP ঠিকানা থাকে এবং «আমি এটি কিছু ওয়েবসাইটে পেস্ট করেছি» বাক্যটি ঘটনার প্রতিবেদনে না দেখতে চান, তৃতীয় বিকল্পটি মানানসই।
পুরো স্ট্রিংটি একটি অনলাইন JSON পার্সার-এ পেস্ট করুন; এটি অপরাধী অক্ষরের উপর ক্যারেট রাখে এবং লাইন ও কলাম চিহ্নিত করে। স্থানীয় টুলের সুবিধা হলো সেই 4 MB কখনো মেশিন ছেড়ে যায় না এবং কারও লগে জমা হয় না।
দুটি সতর্কতা:
- এমন টুল বাছুন যা স্পষ্টভাবে বলে সবকিছু ব্রাউজারে প্রক্রিয়া করে; কখনো এমন টুল নয় যা আপনার কন্টেন্ট ব্যাকএন্ডে POST করে;
- পেস্ট করা খুব বড় ফাইল সহন করে, কিন্তু কয়েক ডজন মেগাবাইটের পর ট্যাব নিজেই মেমরি খায় — তখন পদ্ধতি এক-এ ফিরে যান।
৬. ক্যারেক্টার অফসেট বাইট অফসেট নয়: CJK ডেটার দ্বিতীয় ফাঁদ
এটি চীনা/ইংরেজি মিশ্র পেলোডে উভয় পদ্ধতিতেই কামড়ায়।
JSONDecodeError.pos একটি ক্যারেক্টার অফসেট (Unicode কোড পয়েন্টের গণনা), কারণ এটি একটি str পার্স করার ফল। cut -c অক্ষর ধরে কাটে এবং এর সঙ্গে মিলে যায়। কিন্তু head -c, tail -c ও dd বাইট ধরে কাটে, আর UTF-8-এ একটু হেরফেরেই সব কিছু সরে যায়।
একটি চীনা নমুনা এটিকে মূর্ত করে (5টি অর্ডার, 148 অক্ষর, 222 বাইট):
>>> raw = open('bench/minified-json-error-position/cn.json', encoding='utf-8').read()
>>> raw[25:35] # characters 25-35
'城市":"杭州"},'
>>> open('bench/minified-json-error-position/cn.json','rb').read()[25:35] # bytes 25-35
b'DD00000","'
একই অফসেট: ক্যারেক্টার দৃশ্যে এটি একটি ফিল্ড নামে পৌঁছায়, বাইট দৃশ্যে এটি একটি অর্ডার আইডির লেজে পৌঁছায়। UTF-8-এ একটি CJK অক্ষর 3 বাইট দখল করে, আর সরণ অবস্থানের সঙ্গে জমা হয়।
নিয়ম: e.pos পড়ার পর cut -c বা Python স্লাইস দিয়ে কাটুন, কখনো head -c / dd bs=1 skip=N দিয়ে নয়। যদি কেবল বাইট টুল থাকে, আগে রূপান্তর করুন: len(raw[:error.pos].encode('utf-8'))।
৭. সিনট্যাক্স ঠিক, তবু ডেটা ভুল: তিনটি নীরব ফাঁদ
এই কারণেই ডিবাগিং «এটি পার্স হয়েছে»-তে থামতে পারে না। নিচের প্রতিটি হয় কখনো এরর তোলে না, নয়তো মান ব্যবহারের অনেক পরে তোলে।
ফাঁদ ১: ডুপ্লিকেট কি — শেষটি জেতে, কেউ অভিযোগ করে না
RFC 8259-এর 4 নম্বর ধারা স্পষ্ট: একটি অবজেক্টের ভেতরের নাম SHOULD অনন্য, আর অনন্য না হলে আচরণ অননুমেয় — অধিকাংশ ইমপ্লিমেন্টেশন শেষ অকুর্নেন্স রাখে, আবার কেউ কেউ এরর তোলে। এখানে মাপা তিনটি টুলই শেষটি রাখে:
Input {"order_id":"A-1001","total":4200,"order_id":"A-1002"}
Node {"order_id":"A-1002","total":4200}
Python {'order_id': 'A-1002', 'total': 4200}
jq {"order_id":"A-1002","total":4200}
একটিও সতর্কবার্তা নেই। যদি ওভাররাইট হওয়া ফিল্ডটি amount_cents হয়, আপনি জানতে পারবেন যখন মিলকরণ তিন সেন্টে গরমিল দেখায়।
ফাঁদ ২: NaN আর Infinity — Python গ্রহণ করে, Node করে না
RFC 8259-এর 6 নম্বর ধারা স্পষ্টভাবে বলে সংখ্যার লিটারালে NaN নেই এবং Infinity নেই। Python-এর json.loads ডিফল্টে দুটোই গ্রহণ করে, কারণ এর নিজস্ব float-এ সেই মানগুলো আছে:
>>> json.loads('{"discount":NaN}')
{'discount': nan}
>>> json.dumps(json.loads('{"discount":NaN}'))
'{"discount": NaN}' # written straight back out
সেই আউটপুট Node-কে দিলে পাবেন:
SyntaxError: Unexpected token 'N', "{"discount":NaN}" is not valid JSON
অর্থাৎ: খারাপ ডেটা Python-এর ছাঁকনি পেরোয়, আর বিস্ফোরিত হয় ডাউনস্ট্রিম ভোক্তার কাছে।
ফাঁদ ৩: 2^53-এর বেশি ইন্টিজার নীরবে নির্ভুলতা হারায়
এক ডকুমেন্ট, দুটি ভিন্ন মান:
JSON source {"order_no":12345678901234567890}
Python 12345678901234567890 <- kept as-is
Node 12345678901234567000 <- last three digits flattened
কারণ এক লাইনে: JavaScript-এর Number.MAX_SAFE_INTEGER হলো 9007199254740991 (2^53-1)। MDN বিশেষভাবে উল্লেখ করে যে reviver চলে মানটি ইতিমধ্যে একটি JavaScript number-এ পার্স হওয়ার পরে, তাই নির্ভুলতা reviver-এর আগেই হারায় — ডিসিরিয়ালাইজিং পাশে তা ফেরানো যায় না। একমাত্র সমাধান হলো প্রোডিউসার বড় ID-গুলো স্ট্রিং হিসেবে সিরিয়ালাইজ করবে।
তিনটি ফাঁদ তুলনা করে:
| ফাঁদ | কিছু এরর তোলে? | আগে কে টের পায় | কীভাবে আটকাবেন |
|---|---|---|---|
| ডুপ্লিকেট কি | না | ডাউনস্ট্রিম মিলকরণ | object_pairs_hook দিয়ে স্পষ্ট যাচাই |
| NaN / Infinity | Node তোলে, Python নীরব | ডাউনস্ট্রিম ভোক্তা | parse_constant দিয়ে প্রান্তে প্রত্যাখ্যান |
| 2^53-এর বেশি ইন্টিজার | না | সমতা যাচাই করা কোড | স্ট্রিং হিসেবে পাঠান |
প্রথম দুটি এক পাসে ধরুন
# check.py - Python 3.11.1
import json
import sys
from collections import Counter
def no_dupes(pairs):
counts = Counter(key for key, _ in pairs)
dupes = sorted(key for key, n in counts.items() if n > 1)
if dupes:
raise ValueError(f'duplicate key(s): {dupes}')
return dict(pairs)
def reject_constant(name):
raise ValueError(f'non-standard JSON constant: {name}')
raw = open(sys.argv[1], encoding='utf-8').read()
try:
data = json.loads(raw, object_pairs_hook=no_dupes, parse_constant=reject_constant)
except json.JSONDecodeError as error:
start, end = max(0, error.pos - 60), min(len(raw), error.pos + 60)
print(f'{error}\n\nchar {start}..{end} of {len(raw)}:')
print(raw[start:end])
print(' ' * (error.pos - start) + '^')
sys.exit(1)
except ValueError as error:
print(f'rejected: {error}')
sys.exit(1)
print(f'valid JSON, {len(raw)} chars')
খরচ সৎভাবে বলা: একই 2,011,084 অক্ষরের নমুনায় এই কঠোর সংস্করণ 35.2-36.2 ms-এ দাঁড়ায়, যেখানে শিথিলটি 9.4-9.9 ms, প্রায় 3.7 গুণ ধীর (প্রতি রাউন্ডে পাঁচ রানের পাঁচ রাউন্ডে 3.6x থেকে 3.8x, প্রতি রাউন্ডে মিডিয়ান) — কারণ object_pairs_hook প্রতিটি অবজেক্টের জন্য একটি Counter চালায়। এটি CI-এর সুখী পথ থেকে দূরে রাখুন এবং কেবল ডেলিভারি সন্দেহজনক দেখালে এতে স্যুইচ করুন।
৮. চেকলিস্ট
ফাইল ঠিক করার পর পাঠযোগ্যতার জন্য এটি প্রিটি-প্রিন্ট করতে মন চায়, কিন্তু একই নমুনা মিনিফায়েড (separators=(',',':')) অবস্থায় 2,011,084 অক্ষর আর indent=2-এ 2,869,100 অক্ষর মাপে — 43% বেশি। এই ভার Git ইতিহাস, CI ক্যাশ ও নেটওয়ার্ক ট্রান্সফারে যায়, তাই প্রিটি সংস্করণ স্থানীয় রাখুন এবং শিপিংয়ের আগে এক লাইনে সংকুচিত করুন।
- [ ] আগে
(char N)/position Nখুঁজুন;line 1 column Nউপেক্ষা করুন; - [ ] Python
e.posব্যবহার করে, jqcut -c-এর সঙ্গে জোড়া লাগে — একই ক্যারেক্টার অফসেট, তাই jq-এরcolumnতাদের সঙ্গে মেলাবেন না; - [ ] CJK ডকুমেন্টে অক্ষর ধরে কাটুন (
cut -c, Python স্লাইস), কখনোhead -c/ddনয়; - [ ] কন্টেইনার স্ক্রিপ্টে jq-এর এক্সিট কোড 5-কে «ইনপুট অবৈধ» ধরে নিন;
- [ ] সন্দেহজনক ডেটা
object_pairs_hook+parse_constantদিয়ে আবার যাচাই করুন এবং প্রায় 3.7 গুণ খরচ মেনে নিন; - [ ] 2^53-এর বেশি যেকোনো ID স্ট্রিং হিসেবে শিপ করুন; reviver তাকে বাঁচাবে এমন আশা করবেন না;
- [ ] প্রিটি-প্রিন্ট আউটপুট (মাপা +43%) স্থানীয় রাখুন: শিপিং বা কমিটের আগে /minify দিয়ে আবার এক লাইনে সংকুচিত করুন; কয়েক MB-এর বেশি হলে বরং স্ট্রিমিংয়ে যান, যা বড় JSON পার্সিং, রেন্ডারিং ও স্ট্রিমিং-এ আলোচিত।
সাধারণ প্রশ্ন
তিনটি টুল কেন ভিন্ন অবস্থান রিপোর্ট করে?
কারণ তারা ভিন্ন জিনিস গোনে। Python-এর char N ও Node-এর position N দুটোই মানে «পার্সার যেখানে থেমেছে» — এই নমুনায় দুটোই 23610, তাই মিলে যায়। jq রিপোর্ট করে অপরাধী টোকেন কোথায় শুরু, এখানে 23619, নয়টি বেশি। কেউ ভুল করেনি এবং কোনো সংশোধন দরকার নেই; কেবল একই টুলের ভেতরে সংখ্যা তুলনা করুন।
cut -c নাকি head -c ব্যবহার করব?
cut -c ব্যবহার করুন। JSONDecodeError.pos Unicode অক্ষরে গোনা একটি অফসেট, আর cut -c অক্ষর ধরে কাটে, তাই দুটো মিলে যায়। head -c, tail -c ও dd বাইট ধরে কাটে, যা CJK টেক্সটযুক্ত UTF-8 ডকুমেন্টে সরে যায় এবং একটি অক্ষরকেও অর্ধেক কেটে ফেলতে পারে। চীনা নমুনায় একই অফসেটে ক্যারেক্টার দৃশ্য দেয় 城市":"杭州"}, আর বাইট দৃশ্য দেয় DD00000","।
ডুপ্লিকেট কি হ্যান্ডলিং কি সত্যিই সর্বত্র অরক্ষিত?
সত্যিই। RFC 8259-এর 4 নম্বর ধারা কেবল বলে নাম SHOULD অনন্য এবং অনন্য না হলে আচরণ অননুমেয়। এখানে মাপা Python, Node ও jq তিনটিই কোনো সতর্কবার্তা ছাড়াই শেষটি-জেতে নীতি প্রয়োগ করে। এটি ধরতে নিজের object_pairs_hook দরকার, যার খরচ প্রায় 3.7 গুণ পার্স সময়।
Python কেন NaN ঠিকঠাক পড়ে, অথচ Node এরর তোলে?
RFC 8259-এর 6 নম্বর ধারা NaN ও Infinity-কে সংখ্যা লিটারাল হিসেবে নিষিদ্ধ করে। Python-এর json.loads ডিফল্টে সেগুলো গ্রহণ করে কারণ Python-এর নিজস্ব float-এ সেই মান আছে, তাই খারাপ ডেটা Python পেরিয়ে সোজা আবার লেখা হয়ে যায়। Node-এর JSON.parse কঠোরভাবে স্পেক মানে এবং সেই আউটপুটে SyntaxError তোলে। এন্ট্রি পয়েন্টে parse_constant যোগ করলে তা সঙ্গে সঙ্গে প্রত্যাখ্যাত হয়।
20 অঙ্কের অর্ডার নম্বর কি JSON-এ সবসময় নির্ভুলতা হারায়?
কেবল যখন ভোক্তা সেটিকে IEEE 754 ডাবল হিসেবে পার্স করে। Python-এর int যেকোনো নির্ভুলতার, তাই 12345678901234567890 রাখে; JavaScript-এর Number.MAX_SAFE_INTEGER হলো 9007199254740991, তাই একই ফিল্ড 12345678901234567000 হয়ে যায়। MDN স্পষ্টভাবে বলে reviver পার্সের পরে চলে, তাই ডাউনস্ট্রিমে কিছুই ফেরানো যায় না — নির্ভরযোগ্য একমাত্র সমাধান প্রোডিউসারকে স্ট্রিং আউটপুট দিতে হবে।