एक वाक्य में
यूनिकोड टेक्स्ट के लिए एक यूनिवर्सल स्टैंडर्ड है जो हमारे ग्लोबल इंटरनेट को संभव बनाता है, लेकिन इसकी विशालता में अदृश्य कैरेक्टर्स, हमशक्ल, और अन्य अजीब चीजें शामिल हैं जो एक सीधे-सादे स्ट्रिंग को बग्स की एक माइनफील्ड में बदल सकती हैं।
यह क्या समस्या हल करता है
कंप्यूटिंग के शुरुआती दौर में, जीवन सरल था। हमारे पास ASCII था। इसने हमें 127 कैरेक्टर दिए: बड़े अक्षर, छोटे अक्षर, संख्याएं, विराम चिह्न, और कुछ कंट्रोल कोड। यह साफ-सुथरा था, और एक सिंगल बाइट में पूरी तरह से फिट हो जाता था। यह पूरी तरह से अंग्रेज़ी-केंद्रित भी था। अगर आप ¿Qué pasa? या 你好 या спасибо लिखना चाहते थे, तो आपकी किस्मत खराब थी।
इसने एक अराजक युग को जन्म दिया जिसे "कोडपेज हेल" (codepage hell) के नाम से जाना जाता है। विभिन्न क्षेत्रों के कंप्यूटर अलग-अलग 8-बिट कैरेक्टर सेट का उपयोग करते थे जो ASCII का विस्तार करते थे। Windows-1252 (पश्चिमी यूरोपीय) में लिखा गया एक डॉक्यूमेंट KOI8-R (रूसी) का उपयोग करने वाले सिस्टम पर खोले जाने पर एक गड़बड़झाला (जिसे हम प्यार से mojibake कहते हैं) में बदल जाता था। अंतरराष्ट्रीय स्तर पर टेक्स्ट साझा करना एक खराब लाइन वाले टेलीफोन से खेलने जैसा था।
फिर, यूनिकोड कंसोर्टियम एक सफेद घोड़े पर सवार होकर आया। उनका मिशन: सभी आधुनिक और ऐतिहासिक लेखन प्रणालियों के लिए एक एकल, एकीकृत कैरेक्टर सेट। सभी पर राज करने के लिए एक स्टैंडर्ड। हर कैरेक्टर—'A' से लेकर '€' तक, और 'पाइल ऑफ पू' इमोजी (💩) तक—को अपना एक यूनिक नंबर, एक "कोड पॉइंट" मिलेगा।
यह एक स्मारकीय उपलब्धि थी जो हमारी आधुनिक दुनिया को शक्ति प्रदान करती है। लेकिन इस भव्य एकीकरण ने अपनी ही कुछ अद्भुत nerdy समस्याएं पैदा कीं। मानव भाषा की जटिलताओं को समायोजित करने के लिए, यूनिकोड को केवल दृश्य अक्षरों से अधिक शामिल करना पड़ा। इसे चाहिए था:
- कंबाइनिंग कैरेक्टर्स (Combining characters): एक एक्सेंट मार्क (
´) जो एक अलग कैरेक्टर है, जिसे दूसरे (e) के ऊपर रखने के लिए डिज़ाइन किया गया है। - ज़ीरो-विड्थ कैरेक्टर्स (Zero-width characters): अदृश्य मार्कर जो लाइन ब्रेक (
U+200B ज़ीरो-विड्थ स्पेस) का सुझाव दे सकते हैं या इमोजी को एक साथ जोड़ सकते हैं (U+200D ज़ीरो-विड्थ जॉइनर)। - अस्पष्ट कैरेक्टर्स (Ambiguous characters): दर्जनों विभिन्न प्रकार के स्पेस, डैश और कोट्स।
- हमशक्ल (Homoglyphs): लैटिन अक्षर
aऔर सिरिलिक अक्षरаकई फॉन्ट में एक जैसे दिखते हैं, लेकिन एक कंप्यूटर के लिए, वेaऔरbजितने ही अलग हैं।
अचानक, जो आप देखते हैं वह नहीं है जो आपको मिलता है। एक स्ट्रिंग जो "cat" जैसी दिखती है, उसमें एक अदृश्य कैरेक्टर हो सकता है, जिससे उसकी लंबाई 4 हो जाती है, 3 नहीं। एक वेरिएबल नाम safeString लैटिन 'a' के बजाय एक ग्रीक 'α' छिपा सकता है। यही वह समस्या है जिसे एक टेक्स्ट इंस्पेक्टर हल करता है: यह आपको यह दिखाने के लिए एक्स-रे चश्मे लगाता है कि आपकी स्ट्रिंग वास्तव में किस चीज़ से बनी है, और मशीन में छिपे भूतों को उजागर करता है।
यह अंदर से कैसे काम करता है
एक स्ट्रिंग को चीर-फाड़ करने के लिए, हमें इसकी तीन मूलभूत परतों को समझने की आवश्यकता है: एब्स्ट्रैक्ट कैरेक्टर, इसका बाइट रिप्रेजेंटेशन, और बीच में छिपी अजीब चीजें।
कोड पॉइंट्स: एक कैरेक्टर का पता
इसके मूल में, यूनिकोड सिर्फ एक विशाल सूची है। प्रत्येक कैरेक्टर को एक यूनिक नंबर दिया जाता है जिसे कोड पॉइंट कहा जाता है। यह यूनिकोड ब्रह्मांड में उस कैरेक्टर का स्थायी पता है। हम उन्हें U+XXXX नोटेशन का उपयोग करके लिखते हैं, जहाँ XXXX एक हेक्साडेसिमल संख्या है।
U+0041हैA(लैटिन कैपिटल लेटर A)U+00E9हैé(लैटिन स्मॉल लेटर E with Acute)U+20ACहै€(यूरो साइन)U+1F4A9है💩(पाइल ऑफ पू)
एक कोड पॉइंट एक एब्स्ट्रैक्ट विचार है। यह एक बाइट या एक फॉन्ट नहीं है। यह सिर्फ एक कैरेक्टर से मैप की गई एक संख्या है। हम उस संख्या को कैसे स्टोर करते हैं यह एक और कहानी है।
एनकोडिंग्स: बाइट्स में कोड पॉइंट्स को स्टोर करना
आप एक "कोड पॉइंट" को फ़ाइल में सेव नहीं कर सकते। आपको बाइट्स सेव करने होते हैं। एक एनकोडिंग नियमों का एक सेट है जो कोड पॉइंट्स के एक क्रम को बाइट्स के एक क्रम में परिवर्तित करता है।
आज एनकोडिंग का राजा UTF-8 है। इसकी प्रतिभा इसके वेरिएबल-लेंथ डिज़ाइन में निहित है।
- किसी भी कैरेक्टर के लिए जो मूल ASCII सेट में भी है (जैसे
A,U+0041), UTF-8 एक सिंगल बाइट का उपयोग करता है—वही बाइट जो ASCII उपयोग करता था। इसने इसे बैकवर्ड-कम्पैटिबल और अपनाने में आसान बना दिया। - अन्य कैरेक्टर्स के लिए, यह 2, 3, या 4 बाइट्स के एक क्रम का उपयोग करता है। प्रत्येक बाइट के पहले कुछ बिट्स सिग्नल के रूप में कार्य करते हैं, जो कंप्यूटर को बताते हैं कि वर्तमान कैरेक्टर के हिस्से के रूप में कितने बाइट्स हैं।
आइए ¡Hola! को देखें:
| कैरेक्टर | कोड पॉइंट | UTF-8 बाइट्स (हेक्स) |
|---|---|---|
¡ |
U+00A1 |
C2 A1 |
H |
U+0048 |
48 |
o |
U+006F |
6F |
l |
U+006C |
6C |
a |
U+0061 |
61 |
! |
U+0021 |
21 |
एक टेक्स्ट इंस्पेक्टर इस रिवर्स प्रक्रिया को करता है। यह आपकी स्ट्रिंग के रॉ बाइट्स को पढ़ता है, उन्हें एक एनकोडिंग (आमतौर पर UTF-8) के अनुसार इंटरप्रेट करता है, और आपको कोड पॉइंट्स का वह क्रम दिखाता है जिससे यह बना है।
अदृश्य उपद्रवी
यहाँ से मज़ा शुरू होता है। एक टेक्स्ट इंस्पेक्टर का मुख्य काम उन कैरेक्टर्स पर रोशनी डालना है जो कुछ भी नहीं दिखते हैं।
| श्रेणी | उदाहरण कैरेक्टर और कोड पॉइंट | कुटिल उद्देश्य |
|---|---|---|
| ज़ीरो-विड्थ स्पेस | U+200B |
कुछ नहीं जैसा दिखता है। एक अदृश्य कैरेक्टर जो एक लंबे शब्द या URL में लाइन-ब्रेक के लिए एक अच्छी जगह का सुझाव देता है। |
| ज़ीरो-विड्थ जॉइनर | U+200D |
इमोजी के लिए जादुई गोंद। 👨 + ZWJ + 👩 + ZWJ + 👧 = 👨👩👧। यह उन कैरेक्टर्स को जोड़ता है जो सामान्य रूप से नहीं जुड़ते। |
| नॉन-ब्रेकिंग स्पेस | U+00A0 |
एक सामान्य स्पेस जैसा दिखता है, लेकिन लाइन ब्रेक को रोकता है। 100 km या Dr. Strange जैसी चीजों के लिए उपयोगी है। |
| कंबाइनिंग मार्क | U+0301 (कंबाइनिंग एक्यूट एक्सेंट) |
एक एक्सेंट (´) जो अपने आप में एक कैरेक्टर है। यह पिछले कैरेक्टर के ऊपर खींचा जाता है। |
| हमशक्ल (Homoglyph) | U+0430 (सिरिलिक स्मॉल लेटर A) |
अधिकांश फॉन्ट में लैटिन a (U+0061) के समान दिखता है, लेकिन एक पूरी तरह से अलग कोड पॉइंट है। |
यह नॉर्मलाइजेशन (normalization) की अवधारणा की ओर ले जाता है। कैरेक्टर é को दो तरीकों से दर्शाया जा सकता है:
- कम्पोस्ड (NFC): एक सिंगल कोड पॉइंट,
U+00E9। - डीकम्पोस्ड (NFD): दो कोड पॉइंट्स,
e(U+0065) जिसके बाद कंबाइनिंग एक्सेंट´(U+0301) आता है।
देखने में, वे समान हैं। लेकिन एक कंप्यूटर के लिए जो एक साधारण बाइट-दर-बाइट तुलना कर रहा है, "\u00E9" "e\u0301" के बराबर नहीं है। एक टेक्स्ट इंस्पेक्टर यह बता सकता है कि आपके पास कौन सा रूप है और आपको उनके बीच बदलने में मदद कर सकता है।
वास्तविक दुनिया की कहानियाँ
कॉपी-पेस्ट की तबाही
एक जूनियर डेवलपर देर रात तक एक बग को ठीक करने की कोशिश कर रहा है। उसे एक ब्लॉग पर एक समाधान मिलता है, जावास्क्रिप्ट की एक सिंगल लाइन: const timeout = 100;। वह इसे कॉपी करता है, अपने कोड एडिटर में पेस्ट करता है, और सेव दबाता है। पूरा एप्लिकेशन बिल्ड एक रहस्यमय SyntaxError: Invalid or unexpected token के साथ विफल हो जाता है।
वह उस लाइन को घूरता है। यह एकदम सही है। वह इसे मैन्युअल रूप से फिर से टाइप करता है। यह काम करता है। वह कॉपी की गई लाइन को फिर से पेस्ट करता है। यह टूट जाता है। क्या वह पागल हो रहा है? एक घंटे तक अपने बाल नोचने के बाद, एक सीनियर डेवलपर उस लाइन को घूरता है और कहता है, "इसे एक टेक्स्ट इंस्पेक्टर में पेस्ट करो।"
परिणाम: const[U+00A0]timeout[U+00A0]=[U+00A0]100;। ब्लॉग के CSS ने कोड को सुंदर बना दिया था, मानक स्पेस (U+0020) को नॉन-ब्रेकिंग स्पेस (U+00A0) से बदल दिया था। वे एक जैसे दिखते हैं, लेकिन जावास्क्रिप्ट इंजन को कोई पता नहीं है कि उस संदर्भ में "नॉन-ब्रेकिंग स्पेस" क्या है।
सबक: वेब (या PDF, या Word डॉक्स) से कॉपी किया गया टेक्स्ट जब तक निर्दोष साबित न हो जाए, तब तक दोषी है। यह अक्सर "स्मार्ट" कोट्स, गैर-मानक स्पेस, और अन्य अदृश्य शैतानों से दूषित होता है।
वह फैंटम यूजर जो लॉग इन नहीं कर सका
एक नया यूजर François नाम से एक सर्विस के लिए साइन अप करता है। सिस्टम खुशी-खुशी अकाउंट बना देता है। अगले दिन, François लॉग इन करने की कोशिश करता है। वह अपना नाम टाइप करता है, एंटर दबाता है... "अमान्य यूजरनेम या पासवर्ड।" वह फिर से सावधानी से कोशिश करता है। वही परिणाम। वह लॉक हो गया है।
डेटाबेस में, उसका नाम डीकम्पोस्ड कैरेक्टर्स का उपयोग करके संग्रहीत किया गया था: F, r, a, n, c, o, i, s और एक U+0327 (कंबाइनिंग सेडिला)। हालांकि, लॉगिन फॉर्म प्रीकम्पोस्ड कैरेक्टर ç (U+00E7) भेज रहा था। देखने में, c + ¸ ç के समान है। लेकिन सर्वर एक साधारण स्ट्रिंग तुलना कर रहा था: François (डीकम्पोस्ड) François (कम्पोस्ड) के बराबर नहीं है। WHERE username = '...' क्वेरी विफल हो गई।
सबक: डेटाबेस में स्टोर करने या तुलना करने से पहले हमेशा यूजर इनपुट को एक सुसंगत रूप (NFC सबसे आम विकल्प है) में नॉर्मलाइज करें।
धोखेबाज डोमेन
एक कर्मचारी को एक ईमेल मिलता है जो ऐसा लगता है कि यह उसकी कंपनी के आईटी विभाग से है। "सुरक्षा अपडेट आवश्यक: कृपया अपने खाते को सुरक्षित करने के लिए microsоft.com/update पर लॉग इन करें।" लिंक वैध लगता है। डोमेन नाम वहीं है। वह उस पर क्लिक करता है, एक ऐसे पेज पर अपनी क्रेडेंशियल दर्ज करता है जो बिल्कुल असली जैसा दिखता है, और अपने दिन के साथ आगे बढ़ता है।
वह अभी-अभी फिशिंग का शिकार हुआ है। डोमेन microsoft.com नहीं था। यह microsоft.com था। दूसरा 'o' लैटिन 'o' (U+006F) नहीं बल्कि सिरिलिक 'о' (U+043E) था। यह एक IDN होमोग्राफ अटैक है। मानव आंख के लिए, यह एक आदर्श जालसाजी है। DNS सिस्टम के लिए, यह एक पूरी तरह से अलग पता है, जो स्कैमर के सर्वर की ओर ले जाता है।
सबक: उन आइडेंटिफायर्स से बहुत सावधान रहें जो कैरेक्टर सेट को मिलाते हैं। जबकि आधुनिक ब्राउज़रों में कुछ सुरक्षा उपाय हैं, होमोग्राफ हमलों का सिद्धांत यूजरनेम, वैलिडेशन रूल्स, और कहीं भी जहाँ स्ट्रिंग्स का उपयोग सुरक्षा के लिए किया जाता है, में एक निरंतर खतरा है।
आम गलतियाँ और जाल
- यह मान लेना कि
string.lengthकैरेक्टर्स की गिनती करता है। कई भाषाओं (जैसे जावास्क्रिप्ट) में, यह कोड यूनिट्स की गिनती करता है, न कि समझे गए कैरेक्टर्स की। उदाहरण के लिए, JS में"👍🏽".length4 है, क्योंकि यह "थम्ब्स अप" इमोजी (👍, 2 यूनिट्स) और "मीडियम स्किन टोन मॉडिफायर" (🏽, 2 यूनिट्स) से बना है। - सभी व्हाइटस्पेस को बराबर मानना। एक स्ट्रिंग पर
trim()चलाने से बीच में छिपाU+200B ज़ीरो-विड्थ स्पेसनहीं हटेगा।\s+के लिए एक regex शायदU+00A0 नॉन-ब्रेकिंग स्पेसको न पकड़े। आपको पता होना चाहिए कि आप किसका शिकार कर रहे हैं। - नॉर्मलाइजेशन को अनदेखा करना। जैसा कि François के साथ देखा गया, उन स्ट्रिंग्स की तुलना करना जो एक जैसी दिखती हैं लेकिन जिनके अंतर्निहित बाइट रिप्रेजेंटेशन अलग-अलग हैं, एक क्लासिक, निराशाजनक बग है।
string1.normalize() === string2.normalize()आपका दोस्त है। - अपनी आँखों पर भरोसा करना। आप इन मुद्दों को रेंडर किए गए टेक्स्ट को देखकर डीबग नहीं कर सकते। एक टेक्स्ट इंस्पेक्टर जो व्यक्तिगत कोड पॉइंट्स और उनके नाम दिखाता है, यह सुनिश्चित करने का एकमात्र तरीका है कि वास्तव में क्या है।
- अपना खुद का "खराब कैरेक्टर" स्ट्रिपर बनाना। सभी "अजीब" कैरेक्टर्स को हटाने के लिए एक regex लिखने की कोशिश करना एक मूर्खतापूर्ण काम है। आप या तो कुछ को चूक जाएंगे या, इससे भी बदतर, अन्य भाषाओं के लिए आवश्यक वैध कैरेक्टर्स को हटा देंगे, जिससे आपके यूजर्स के नाम और टेक्स्ट खराब हो जाएंगे।
यह आपके रडार पर क्यों होना चाहिए
जब भी टेक्स्ट अप्रत्याशित रूप से व्यवहार करता है तो आपको एक यूनिकोड टेक्स्ट इंस्पेक्टर का उपयोग करना चाहिए। यह एक अनिवार्य डीबगिंग टूल है। इसके बारे में तब सोचें जब:
- एक स्ट्रिंग तुलना विफल हो जाती है जब यह "स्पष्ट रूप से" सफल होनी चाहिए।
- आपको कोड की एक लाइन पर सिंटैक्स एरर मिलता है जो पूरी तरह से वैध लगती है।
- आप यूजर द्वारा प्रदान किए गए इनपुट जैसे यूजरनेम, ईमेल या URL को वैलिडेट कर रहे हैं।
- आप कई सिस्टम से डेटा के साथ काम कर रहे हैं, खासकर यदि उनमें विभिन्न भाषाएं शामिल हैं।
- आपको यह समझने की आवश्यकता है कि
string.lengthआपको "गलत" नंबर क्यों दे रहा है। - आप कोई ऐसा सिस्टम बना रहे हैं जिसे मजबूत, सुरक्षित और वैश्विक दर्शकों के लिए काम करने की आवश्यकता है।
संक्षेप में, किसी भी समय जब एक कंप्यूटर और एक इंसान इस बात पर असहमत होते हैं कि टेक्स्ट का एक टुकड़ा क्या कहता है, तो कंप्यूटर शायद बाइट्स के बारे में सही है, और एक टेक्स्ट इंस्पेक्टर आपका अनुवादक है।
और गहराई में जाएं
- The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) — जोएल स्पोल्स्की का प्रसिद्ध निबंध। इसे पढ़ना ज़रूरी है।
- Unicode (Wikipedia) — मानक के इतिहास और तकनीकी विवरणों का एक गहरा, संपूर्ण अवलोकन।
- UTF-8 (RFC 3629) — इंटरनेट के प्रमुख कैरेक्टर एनकोडिंग के लिए तकनीकी विनिर्देश। घना, लेकिन आधिकारिक।
- String.prototype.normalize() (MDN Web Docs) — जावास्क्रिप्ट में नॉर्मलाइजेशन को संभालने के लिए व्यावहारिक मार्गदर्शन।
- The Unicode Consortium — आधिकारिक स्रोत। वे मानक, कोड चार्ट और तकनीकी रिपोर्ट प्रकाशित करते हैं।