JSON Toolkit

মিনিফায়েড JSON ডিবাগ করা: line 1 column 23611 থেকে ভাঙা বাইট পর্যন্ত

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

উন্নত~7 মিনিট পড়াপ্রকাশ 2026-11-01আপডেট 2026-11-01
মিনিফায়েড JSON-এ এরর লোকেট করার কভার

মূল কথা: লাইন ও কলাম নম্বরের অর্থ কেবল বহু-লাইন ডকুমেন্টে। 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 কখনো মেশিন ছেড়ে যায় না এবং কারও লগে জমা হয় না।

দুটি সতর্কতা:

  1. এমন টুল বাছুন যা স্পষ্টভাবে বলে সবকিছু ব্রাউজারে প্রক্রিয়া করে; কখনো এমন টুল নয় যা আপনার কন্টেন্ট ব্যাকএন্ডে POST করে;
  2. পেস্ট করা খুব বড় ফাইল সহন করে, কিন্তু কয়েক ডজন মেগাবাইটের পর ট্যাব নিজেই মেমরি খায় — তখন পদ্ধতি এক-এ ফিরে যান।

৬. ক্যারেক্টার অফসেট বাইট অফসেট নয়: 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 ব্যবহার করে, jq cut -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 পার্সের পরে চলে, তাই ডাউনস্ট্রিমে কিছুই ফেরানো যায় না — নির্ভরযোগ্য একমাত্র সমাধান প্রোডিউসারকে স্ট্রিং আউটপুট দিতে হবে।