रेगेक्स टेस्टर
असली टेक्स्ट पर पैटर्न जाँचें, मैच हाइलाइट के साथ और बेकाबू बैकट्रैकिंग पर सख़्त रोक।
रेगेक्स टेस्टर को वर्कर की ज़रूरत क्यों है
इस पेज की ज़्यादातर मशीनरी एक ही तरह की नाकामी के लिए है। ज़रा /(a+)+b/ को तीस a के ख़िलाफ़ चलाकर देखिए, जहाँ कोई b न हो। इंजन को तय करना होता है कि उन तीस अक्षरों को भीतरी और बाहरी क्वांटिफ़ायर के बीच कैसे बाँटे, और बाँटने के 2³⁰ तरीक़े हैं। मैच नामुमकिन घोषित करने से पहले हर एक आज़माया जाता है।
अहम बात यह है कि चल रहे रेगेक्स को बीच में रोका नहीं जा सकता। जावास्क्रिप्ट सिंगल-थ्रेडेड है, इसलिए setTimeout वाला पहरेदार तब तक चल ही नहीं सकता जब तक रेगेक्स लौट न आए — और उस इनपुट के लिए वह घड़ी ब्रह्मांड की ऊष्मीय मृत्यु के बाद कहीं आती है। टैब गया।
इसलिए पैटर्न एक Web Worker में, अपने अलग थ्रेड पर चलता है, और मुख्य पेज एक टाइमर थामे रहता है। जब टाइमर जीत जाता है तो worker.terminate() उस थ्रेड को सीधे मार देता है। यही अकेली व्यवस्था है जो सचमुच काम करती है, और इसीलिए यह टेस्टर हैंग हुए टैब में बदलने के बजाय आपको बता देता है कि आपका पैटर्न ख़तरनाक है।
इसे मन में बैठा लेना चाहिए, क्योंकि प्रोडक्शन कोड में वही पैटर्न एक डिनायल-ऑफ़-सर्विस है: उपयोगकर्ता का दिया इनपुट किसी कमज़ोर रेगेक्स से मिलाया जाए, और एक ही रिक्वेस्ट अनिश्चित काल तक एक CPU कोर को जकड़ ले। बग की इस श्रेणी को ReDoS कहते हैं।
जिन बनावटों से बचना चाहिए
| ख़तरनाक | क्यों | ज़्यादा सुरक्षित |
|---|---|---|
(a+)+ | नेस्टेड क्वांटिफ़ायर — घातांकीय बँटवारे | a+ |
(\w|\s)* | क्वांटिफ़ायर के भीतर विकल्प, और शाखाएँ आपस में ओवरलैप करतीं | [\w\s]* |
(\d+)*$ | क्वांटिफ़ायर वाला समूह और ऐसा ऐंकर जो पूरी छानबीन पर मजबूर कर दे | \d*$ |
.*.*= | दो असीमित वाइल्डकार्ड एक ही टेक्स्ट के लिए लड़ते हुए | [^=]*= |
साझा तत्व है द्वैधता: पैटर्न के पास उन्हीं अक्षरों को खाने के एक से ज़्यादा रास्ते होना। कैरेक्टर क्लास वही काम कर देती है जो विकल्प करता है, पर द्वैधता के बिना — इसीलिए «ज़्यादा सुरक्षित» वाला स्तंभ दरअसल ज़्यादातर | की जगह [] रख देना भर है।
लालची, आलसी, और वह बग जो हर कोई एक बार लिखता है
क्वांटिफ़ायर डिफ़ॉल्ट रूप से लालची होते हैं: .+ जितना ले सकता है उतना ले लेता है और अक्षर सिर्फ़ विरोध करते-करते लौटाता है। <b>bold</b> के ख़िलाफ़:
| पैटर्न | क्या मैच होता है |
|---|---|
<.+> | <b>bold</b> — पूरा का पूरा |
<.+?> | <b> — आलसी, पहले ही > पर रुक जाता है |
<[^>]+> | <b> — और तेज़ भी, क्योंकि बैकट्रैक करने को कुछ है ही नहीं |
तीसरा रूप ही उठाने लायक़ है। नकारी गई कैरेक्टर क्लास हद से आगे जा ही नहीं सकती, इसलिए पीछे लौटकर कुछ मिटाना नहीं पड़ता — यह इरादा भी ज़्यादा साफ़ बताती है और आलसी रूप से चलने में सस्ती भी है।
lastIndex, और हर दूसरा मैच ग़ायब क्यों हो जाता है
g फ़्लैग वाला RegExp अवस्था रखता है। वह एक lastIndex सँभालता है जो हर exec या test के बाद आगे खिसकता है, इसलिए अलग-अलग कॉल में एक ही ऑब्जेक्ट दोबारा इस्तेमाल करने पर खोज वहीं से चलती है जहाँ वह रुकी थी:
const re = /\d+/g;
re.test('123'); // true, lastIndex is now 3
re.test('123'); // false! it resumed from index 3रेगेक्स को लूप के भीतर बनाइए, या lastIndex = 0 कर दीजिए, या matchAll लीजिए जो यह ख़ुद सँभालता है। ऊपर वाला टेस्टर हर बार नया ऑब्जेक्ट बनाता है, इसलिए यहाँ आप पहली कॉल वाला बरताव देख रहे हैं।
जावास्क्रिप्ट का रेगेक्स कहाँ अलग है
यहाँ के नतीजे Node और ब्राउज़र कोड से हूबहू मिलते हैं, क्योंकि इंजन वही है। दूसरी भाषाओं के मुक़ाबले जो फ़र्क़ लोगों को फँसाते हैं वे ये हैं:
- कोई एटॉमिक ग्रुप या पज़ेसिव क्वांटिफ़ायर नहीं। PCRE में बैकट्रैकिंग को सिरे से रोकने के लिए
(?>...)औरa++हैं; जावास्क्रिप्ट में दोनों नहीं, इसीलिए यहाँ ReDoS लिखना आसान है। - कोई रिकर्ज़न नहीं। जावास्क्रिप्ट रेगेक्स से संतुलित कोष्ठक मैच नहीं किए जा सकते। अगर ज़रूरत वही है तो आपको पार्सर चाहिए।
- लुकबिहाइंड देर से आया।
(?<=...)मौजूदा ब्राउज़रों में समर्थित है पर पुराने Safari में नहीं, इसलिए अपने लक्ष्य परखिए। - Go का RE2 अलग ही मशीन है। रैखिक समय की गारंटी, न बैकरेफ़रेंस, न लुकअराउंड। अगर आपने कभी सोचा कि Go ने सुविधाएँ क्यों छोड़ीं, तो वजह यही पेज है।
संबंधित
अगर जिस टेक्स्ट से आप मिलान कर रहे हैं वह JSON है, तो पहले उसे फ़ॉर्मैट कर लेना आम तौर पर पैटर्न की ज़रूरत ही ख़त्म कर देता है — संरचित डेटा पर पार्सर हर बार रेगेक्स को हरा देता है। और अगर आपको मिलान नहीं, शेड्यूल करना है, तो पड़ोस का टूल cron पार्सर है।
रेगेक्स से जुड़े सवाल
मेरा पैटर्न दो सेकंड बाद क्यों रोक दिया गया?
क्योंकि वह विनाशकारी ढंग से बैकट्रैक कर रहा था। कुछ बनावटें — जैसे (a+)+ या (\w|\s)* जैसे नेस्टेड क्वांटिफ़ायर — इंजन से मिलान के घातांकीय रूप से कई रास्ते आज़मवा देती हैं, इसलिए चालीस अक्षरों की स्ट्रिंग में ब्रह्मांड की उम्र से ज़्यादा समय लग सकता है। जावास्क्रिप्ट से चल रहे रेगेक्स को बीच में रोका नहीं जा सकता, इसीलिए यहाँ पैटर्न एक Web Worker में चलता है: वर्कर को ख़त्म किया जा सकता है और पेज चलता रहता है। मुख्य थ्रेड पर लगाया गया टाइमआउट तो रेगेक्स के पूरा होने पर ही चलेगा, और ऐसे पैटर्नों के लिए वह घड़ी कभी आती ही नहीं।
क्या यह वही रेगेक्स इंजन इस्तेमाल करता है जो मेरे कोड में है?
यह आपके ब्राउज़र का, यानी जावास्क्रिप्ट इंजन इस्तेमाल करता है — इसलिए नतीजे Node और ब्राउज़र कोड से हूबहू मिलते हैं। PCRE, Python, Go या Java से हमेशा नहीं मिलेंगे। जावास्क्रिप्ट में एटॉमिक ग्रुप नहीं हैं, पज़ेसिव क्वांटिफ़ायर नहीं हैं और रिकर्ज़न नहीं है; लुकबिहाइंड मौजूदा ब्राउज़रों में है पर देर से आया। Go का RE2 तो पूरी तरह अलग डिज़ाइन है और उसमें बैकरेफ़रेंस जान-बूझकर नहीं हैं — ऊपर बताई बैकट्रैकिंग की समस्या से वह ठीक इसी तरह बचता है।
मेरा ग्लोबल रेगेक्स एक छोड़कर एक मैच ही क्यों ढूँढ़ता है?
g फ़्लैग वाला RegExp ऑब्जेक्ट अपने साथ एक lastIndex रखता है जो हर कॉल पर आगे खिसकता है, इसलिए अलग-अलग exec या test कॉल में वही ऑब्जेक्ट दोबारा इस्तेमाल करने पर खोज वहीं से शुरू होती है जहाँ छूटी थी। यह इस API का सबसे भरोसेमंद ढंग से उलझाने वाला हिस्सा है। या तो हर बार नया रेगेक्स बनाइए, या इस्तेमाल से पहले lastIndex को 0 कर दीजिए, या matchAll लीजिए जो यह ख़ुद सँभाल लेता है।
लालची और आलसी क्वांटिफ़ायर में क्या फ़र्क़ है?
लालची क्वांटिफ़ायर जितना ले सकता है उतना ले लेता है और मजबूरी में ही अक्षर लौटाता है; आलसी (जिसके आख़िर में ? लगाया जाता है) जितना कम हो सके उतना लेता है और ज़रूरत पड़ने पर ही फैलता है। <b>bold</b> पर पैटर्न <.+> पूरी स्ट्रिंग से मैच करेगा, क्योंकि .+ सब कुछ निगल जाता है और सिर्फ़ इतना पीछे हटता है कि आख़िरी > मिल जाए। <.+?> लिखिए तो सिर्फ़ <b> मैच होगा। किसी पैटर्न के इरादे से कहीं ज़्यादा पकड़ लेने की सबसे आम वजह यही है।
क्या यहाँ असली डेटा चिपकाना सुरक्षित है?
हाँ। पैटर्न और टेक्स्ट इसी पेज के भीतर एक वर्कर को सौंपे जाते हैं और वहीं जाँचे जाते हैं। कुछ भी किसी सर्वर पर नहीं भेजा जाता — और बेक़ाबू पैटर्न को रद्द कर पाना इसी वजह से संभव है, क्योंकि इंतज़ार करने लायक़ कोई रिक्वेस्ट है ही नहीं।
अंतिम समीक्षा । कुछ पुराना पड़ा हुआ मिला? हमें बताइए.
