zendriver + Scrapeless: CDP के माध्यम से एक रिमोट ब्राउज़र चलाना
Specialist in Anti-Bot Strategies
TL;DR:
- zendriver एक सामुदायिक फ़र्क है nodriver का जो कच्चे CDP के माध्यम से Chrome को चलाता है, और nodriver की तरह इसे स्वयं ब्राउज़र लॉन्च करने के लिए बनाया गया है।
Browser.start()का एकconnect_existingपथ है, लेकिन खोजHTTPApiके माध्यम से चलती है, जो एक होस्ट और एक पोर्ट के साथ साधारण HTTP को हार्डकोड करती है - एक टोकन ले जाने वाले सुरक्षित WebSocket एंडपॉइंट को उस तरीके से व्यक्त नहीं किया जा सकता।- असली बाधा टैब पता है। zendriver हर टैब को एक होस्ट, एक पोर्ट और एक लक्ष्य आईडी से बनाता है, और एक दूरस्थ एंडपॉइंट ऐसी कोई मार्ग नहीं प्रदान करता।
- एक CDP URL का मतलब एक ब्राउज़र है: एक ही एंडपॉइंट से दूसरे कनेक्शन को एक अलग दूरस्थ ब्राउज़र मिलता है और पहले वाले के लक्ष्यों में से किसी को भी नहीं देखता।
- सीवन फ्लैट-सेशन मल्टीप्लेक्सिंग है -
Target.attachToTarget(flatten=True)एक sessionId लौटाता है, और इसे बाहर जाने वाले फ़्रेमों पर अंकित करना zendriver के अपनेTabसहायक प्रोग्रामों को दूरस्थ पृष्ठ तक पहुँचाता है। - सत्र संलग्न होने के साथ, एक असली निकासी ने एक पूरी तरह से प्रस्तुत पृष्ठ और सही शीर्षकों और कीमतों के साथ 20 उत्पाद कार्ड लौटाए।
- Scrapeless मुफ्त योजना पर शुरू करें और एक ब्राउज़र चलाएँ जिसे आपको लॉन्च करने की आवश्यकता नहीं थी।
Scrapeless Scraping Browser एक क्लाउड ब्राउज़र है जिससे आप कनेक्ट करते हैं, न कि लॉन्च करते हैं। यह एक सुरक्षित WebSocket CDP एंडपॉइंट पर सुनता है, जो वही प्रोटोकॉल है जो zendriver बोलता है - इसलिए कागज पर दोनों एक पंक्ति में मिलना चाहिए।
वे नहीं मिलते। Playwright में connect_over_cdp है। Puppeteer में connect है। zendriver का कोई समकक्ष नहीं है, और एक के लिए अनुरोध ट्रैकर में अप्रैल 2025 से बिलकुल इसी के लिए एक खुला मुद्दा है, जिसका उत्तर नहीं मिला है। आधिकारिक कच्चा-CDP ट्यूटोरियल page.send() और इवेंट हैंडलर्स को कवर करता है और कभी भी दूरस्थ ब्राउज़र्स का उल्लेख नहीं करता।
यह गाइड इस बात का काम करती है कि वास्तव में रास्ते में क्या खड़ा है - जो कि कनेक्शन नहीं है - और फिर केवल लाइब्रेरी के अपने प्राइमिटिव का उपयोग करके एक दूरस्थ ब्राउज़र संलग्न करती है।
How zendriver Starts a Browser
zendriver nodriver का एक फ़र्क है जिसे अनमर्ज किए गए बगफिक्स को मर्ज करने और योगदानों के लिए परियोजना को फिर से खोलने के लिए बनाया गया है। आर्किटेक्चरल रूप से दोनों एक ही विचार हैं: Chrome DevTools Protocol के माध्यम से बिना WebDriver परत के सीधे Chrome को चलाना, जो ड्राइवर-आधारित स्टैक्स द्वारा उजागर की गई ऑटोमेशन सतह को हटा देता है। फ़र्क में मानक asyncio.run() एंट्री, डॉकर समर्थन, और कुकी स्थिरता जोड़ी गई है।
सामान्य प्रवेश बिंदु एक ब्राउज़र लॉन्च करता है:
python
import asyncio
import zendriver as zd
async def main():
browser = await zd.start(headless=True, no_sandbox=True)
page = await browser.get("https://books.toscrape.com/")
await page.wait_for("h3 a", timeout=20)
await page.sleep(2)
print("cards:", len(await page.select_all("article.product_pod")))
await browser.stop()
asyncio.run(main())
text
cards: 20
no_sandbox=True की आवश्यकता होती है जब प्रक्रिया रूट के रूप में चलती है, जो सामान्य रूप से कंटेनर के अंदर होती है; इसे डेस्कटॉप पर छोड़ दें। sleep कॉल इसे आगे की चर्चा के लिए एक कारण के लिए अपनी जगह कमाता है, और वह कारण दूरस्थ पथ पर समान रूप से लागू होता है।
यह उस मशीन पर एक बाल प्रक्रिया के रूप में Chrome को उत्पन्न करता है जो स्क्रिप्ट चलाता है। परिणाम सेट में हर zendriver गाइड इस तरह कार्य करता है: एक ब्राउज़र को चलाना जिसे स्क्रिप्ट ने स्वयं लॉन्च किया, और इसे browser_args के माध्यम से आकार देना - एक Chromium --proxy-server स्विच होना आम उदाहरण है। इनमें से कोई भी उस ब्राउज़र से नहीं जुड़ता जिसे उसने उत्पन्न नहीं किया।
Prerequisites
- Python 3.10 या बाद का।
- डैशबोर्ड से एक Scrapeless API कुंजी, जिसे
SCRAPELESS_KEYके रूप में निर्यात किया गया है। - एक स्थानीय Chrome या Chromium इंस्टॉलेशन, केवल यदि आप उपरोक्त स्थानीय-लॉन्च ब्लॉक को चलाना चाहते हैं। दूरस्थ पथ को किसी ब्राउज़र बाइनरी की आवश्यकता नहीं है।
Install
bash
pip install "zendriver==0.15.5"
bash
export SCRAPELESS_KEY="your_api_key_here"
What connect_existing Actually Does
Browser.start() को पढ़ने से यह धारणा मिलती है कि दूरस्थ संलग्नक पहले से ही समर्थित है। जब दोनों config.host और config.port सेट होते हैं, तो यह एक फ्लैग को पलटता है और पूरी तरह से एक प्रक्रिया लॉन्च करने को छोड़ देता है। स्थापित पैकेज आपको दिखाएगा:
python
import inspect
import textwrap
from zendriver.core.browser import Browser
source = inspect.getsource(Browser.start).splitlines()
start = next(i for i, line in enumerate(source) if "connect_existing = False" in line)
print(textwrap.dedent("\n".join(source[start:start + 6])))
text
connect_existing = False
if self.config.host is not None and self.config.port is not None:
connect_existing = True
else:
self.config.host = "127.0.0.1"
self.config.port = util.free_port()
यह वास्तविक है - यह एक ब्राउज़र से कनेक्ट करता है जिसे उसने शुरू नहीं किया। सीमा वह है जो आगे आती है। खोज HTTPApi के माध्यम से चलती है, जो एक होस्ट-और-पोर्ट जोड़ी लेती है और उसे एक निश्चित पते के टेम्पलेट में इंटरपोलेट करती है, फिर /json/version को urllib के साथ लाने के लिए लाती है ताकि ब्राउज़र का WebSocket URL जान सके। आप बिना Python छोड़े उस पते का आकार देख सकते हैं:
python
from zendriver.core.browser import HTTPApi
api = HTTPApi(("example-host", 9222))
print("scheme:", api.api.split("://")[0])
print("room for a query string:", "?" in api.api)
print("room for a path:", api.api.count("/") > 2)
text
scheme: http
room for a query string: False
room for a path: False
एक क्लाउड एंडपॉइंट के लिए तीन चीजें एक साथ टूटती हैं। स्कीम को साधारण http पर स्थिर किया गया है। पता एक होस्ट और एक पोर्ट है, इसलिए वहाँ एक पथ, एक क्वेरी स्ट्रिंग या एक प्रमाणीकरण टोकन रखने के लिए कोई जगह नहीं है। और एक स्थायी एंडपॉइंट आमतौर पर सार्वजनिक इंटरनेट पर CDP HTTP खोज मार्गों को उजागर नहीं करता - Scrapeless होस्ट पर /json/version और /json/list की मांग करने पर HTTP 403 दोनों मामलों में लौटता है।
सो connect_existing आपके द्वारा शुरू किए गए एक क्रोम के लिए है --remote-debugging-port के साथ, एक होस्ट और पोर्ट पर जिसे आप नियंत्रित करते हैं। यह एक रिमोट-अटैच फीचर नहीं है।
असली अवरोधक टैब URL है
मान लीजिए कि खोज हल हो गई। कनेक्शन तब भी उपयोग योग्य नहीं होगा, और इसका कारण उस परत के एक स्तर नीचे है जहां अधिकांश लोग देखते हैं।
zendriver टैब कनेक्शनों का निर्माण एक ही होस्ट और पोर्ट से स्ट्रिंग-बिल्डिंग करके करता है, जिसे लक्षित आईडी के साथ एक /devtools/page/<target_id> पाथ में इंटरपोलेट किया जाता है। यह browser.py में दो अलग-अलग स्थानों पर करता है, जिसे आप स्थापित पैकेज से पुष्टि कर सकते हैं:
python
import inspect
from zendriver.core import browser
source = inspect.getsource(browser)
print("tab addresses built from host and port:", source.count("{self.config.host}:{self.config.port}"))
print("devtools path template present:", "/devtools/" in source)
text
tab addresses built from host and port: 2
devtools path template present: True
एक स्थानीय क्रोम उस मार्ग की सेवा करता है। एक क्लाउड ब्राउज़र नहीं करता — यह एक एंडपॉइंट को उजागर करता है, और लक्षित डेवलपर उपकरणों के पथ इसके सार्वजनिक सतह का हिस्सा नहीं हैं। चार संभावित आकार एक लाइव सत्र के खिलाफ प्रयास किए गए: टोकन के साथ, इसके बिना, /api/v2/browser प्रीफिक्स के तहत, और एक /page/ मार्ग के रूप में। सभी चार HTTP 404 के साथ अस्वीकृत कर दिए गए।
इसलिए समस्या को सही URL खोजकर हल नहीं किया जा सकता। खोजने के लिए कोई प्रति-टैब URL नहीं है।
एक CDP URL, एक ब्राउजर
अगली प्रवृत्ति टैब के लिए एक दूसरा कनेक्शन खोलना है। यह भी काम नहीं करता, और यह एक ऐसा तरीके से असफल होता है जो एक दोपहर बर्बाद करने के लिए पर्याप्त चुप है।
python
import asyncio
import os
from urllib.parse import urlencode
import zendriver as zd
from zendriver import cdp
def cdp_url():
return "wss://browser.scrapeless.com/api/v2/browser?" + urlencode(
{"token": os.environ["SCRAPELESS_KEY"], "sessionTTL": 300, "proxyCountry": "US"}
)
async def main():
a = zd.Connection(cdp_url())
b = zd.Connection(cdp_url())
tid = await a.send(cdp.target.create_target("https://books.toscrape.com/"))
a_ids = {str(t.target_id) for t in await a.send(cdp.target.get_targets())}
b_ids = {str(t.target_id) for t in await b.send(cdp.target.get_targets())}
print("A sees its own target:", str(tid) in a_ids)
print("B sees it:", str(tid) in b_ids)
print("shared target ids:", len(a_ids & b_ids))
await a.aclose()
await b.aclose()
asyncio.run(main())
text
A sees its own target: True
B sees it: False
shared target ids: 0
एंडपॉइंट के लिए प्रत्येक कनेक्शन अपना खुद का ब्राउज़र है। दो सत्र कुछ भी साझा नहीं करते — न वह लक्ष्य जिसे आपने अभी बनाया है, न ही एकल लक्षित आईडी। "प्रति टैब एक सॉकेट" पर आधारित कुछ भी चुपचाप उस ब्राउज़र को चला रहा है जिसे यह समझता है।
हालांकि, नोट करें कि क्या काम किया। zd.Connection(cdp_url()) कनेक्ट हुआ और CDP कमांड का उत्तर दिया। परिवहन कभी समस्या नहीं था।
क्या आप उस ब्राउज़र को चलाने के लिए तैयार हैं जिसे आपको लॉन्च करने की आवश्यकता नहीं थी? एक मुफ्त Scrapeless खाता बनाएं और इसे zendriver की ओर इंगित करें।
एक फ़्लैट सत्र संलग्न करें
हर चीज़ को एक कनेक्शन के जरिए यात्रा करनी होती है, जो कि CDP Target.attachToTarget मेथड के लिए है। flatten सेट करने पर यह एक sessionId लौटाता है, और किसी भी फ्रेम जो उस sessionId को ले जाती है उसे जुड़े लक्षित पर रूट किया जाता है बजाय ब्राउज़र के।
zendriver इसका उपयोग नहीं करता। प्रत्येक आउटगोइंग फ्रेम को एक प्रॉपर्टी द्वारा सीरियलाइज़ किया जाता है, Transaction.message:
python
import inspect
from zendriver.core.connection import Transaction
body = inspect.getsource(Transaction.message.fget)
print(body.strip().splitlines()[-1].strip())
print("sessionId present:", "sessionId" in body)
text
return json.dumps({"method": self.method, "params": self.params, "id": self.id})
sessionId present: False
कोई sessionId फील्ड नहीं है, इसलिए कमांड हमेशा ब्राउज़र पर लैंड होते हैं। एक जोड़ना पूरी एकीकरण है: सत्र को संलग्न करना, फिर इसे बाहर जाते फ्रेम पर स्टैम्प करना।
python
import asyncio
import json
import os
from urllib.parse import urlencode
import zendriver as zd
from zendriver import cdp
CDP_URL = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode(
{"token": os.environ["SCRAPELESS_KEY"], "sessionTTL": 300, "proxyCountry": "US"}
)
class ScrapelessTab(zd.Tab):
"""A zendriver Tab bound to a remote browser over a single websocket."""
def __init__(self, url):
super().__init__(url, target=None)
self._session_id = None
async def open_page(self, url):
tid = await self.send(cdp.target.create_target(url))
infos = await self.send(cdp.target.get_targets())
self._target = next(t for t in infos if str(t.target_id) == str(tid))
self._session_id = await self.send(
cdp.target.attach_to_target(tid, flatten=True)
)
websocket, session_id = self.websocket, self._session_id
original_send = websocket.send
async def send_with_session(message, *args, **kwargs):
frame = json.loads(message)
frame.setdefault("sessionId", str(session_id))
return await original_send(json.dumps(frame), *args, **kwargs)
websocket.send = send_with_session
return self
async def main():
tab = ScrapelessTab(CDP_URL)
await tab.open_page("https://books.toscrape.com/")
print("session attached:", bool(tab._session_id))
await tab.aclose()
asyncio.run(main())
text
session attached: True
Tab की उपकाम करने वाले वर्ग को Connection की जगह लेना जानबूझकर है: Tab पहले से ही हर पृष्ठ हेल्पर को ले जाता है, और यह Connection को बढ़ाता है, इसलिए एक ऑब्जेक्ट एक सॉकेट, एक लिसनर, और एक रिस्पॉन्स मैप रखता है। कंस्ट्रक्टर को target=None पास करना ठीक है क्योंकि असली लक्ष्य एक बार सौंपा जाता है जब यह मौजूद होता है। उत्तर उसी id के साथ आते हैं जिससे उन्हें भेजा गया था, इसलिए zendriver का मौजूदा डिस्पैच उन्हें बिना छेड़े हल कर देता है।
एक वास्तविक निष्कर्षण चलाएँ
सत्र संलग्न होने के साथ, साधारण API कार्य करती है। दो समय बिंदु पहले बाइट करते हैं, और इनमें से कोई भी रिमोट ब्राउज़रों के लिए विशिष्ट नहीं है — दोनों स्थायी रूप से चालू की गई एक स्थानीय ब्राउज़र के खिलाफ समान व्यवहार करते हैं।
get_content() तुरंत अपना आदेश भेजता है बजाय इसके कि यह मतदान करे। इसे बहुत जल्दी कॉल करें और यह खाली दस्तावेज़ कंकाल लौटाता है, 39 अक्षर, बिल्कुल बिना किसी त्रुटि के — जो एक टूटे हुए कनेक्शन जैसा पढ़ता है बजाय एक पृष्ठ के जो कि रेंडर नहीं हुआ है।
wait_for() मतदान करता है, लेकिन यह तब लौटाता है जब इसका चयनकर्ता एक बार मेल खाता है। एक ऐसे पृष्ठ पर जिसका मार्कअप अभी भी आ रहा है, यह दिखता है कि यह कमजोर गारंटी है: चयन wait_for("h3 a") के लौटने के बाद सीधे करना एक स्थानीय रन पर 9 कार्ड्स और अगले पर 4 पर रहा, DOM के सैटल होने पर 20 के मुकाबले। किसी भी चीज़ को गिनने से पहले पृष्ठ को एक पल दें।
python
await tab.wait_for("h3 a", timeout=20)
await tab.sleep(2)
html = await tab.get_content()
print("fully rendered page:", len(html) > 40000)
print("catalogue marker present:", "All products" in html)
cards = await tab.select_all("article.product_pod")
print("product cards:", len(cards))
for card in cards[:5]:
link = await card.query_selector("h3 a")
price = await card.query_selector("p.price_color")
print(f" {link.attrs.get('title')} — {price.text}")
text
fully rendered page: True
catalogue marker present: True
product cards: 20
A Light in the Attic — £51.77
Tipping the Velvet — £53.74
Soumission — £50.10
Sharp Objects — £47.82
Sapiens: A Brief History of Humankind — £54.23
चयनकर्ता और तत्व क्वेरीज स्थानीय ब्राउज़र के खिलाफ ठीक उसी तरह व्यवहार करते हैं, क्योंकि zendriver की ओर से कुछ भी नहीं बदला है सिवाय इसके कि फ्रेम कहाँ जा रहे हैं। proxyCountry एक दो-अक्षर कोड लेता है जब अनुरोध को एक विशेष देश से बाहर निकलना होता है, और sessionTTL सीमित करता है कि रिमोट सत्र कितनी देर तक खुला रहता है।
निष्कर्ष
zendriver के पास कोई connect_over_cdp नहीं है, और इसका कारण यह नहीं है कि वेब सॉकेट परिवहन गायब है — zd.Connection पहले प्रयास पर एक रिमोट एंडपॉइंट से बात करता है। बाधा यह है कि टैब कनेक्शन एक होस्ट और पोर्ट से स्ट्रिंग-बिल्ड होते हैं, और एक क्लाउड ब्राउज़र के पास उन्हें इंगित करने के लिए कोई प्रति-टैब मार्ग नहीं है।
फ्लैट सत्र उस अंतर को बंद करते हैं। एक कनेक्शन, एक Target.attachToTarget कॉल, और आउटगोइंग फ्रेम्स पर स्टैम्प किया गया sessionId पुस्तकालय की अपनी Tab को एक रिमोट पृष्ठ पर एक हैंडल में बदल देता है, प्रत्येक चयनकर्ता हेल्पर के साथ।
जब ऐसी सेटअप ठीक से काम नहीं करती है, तो आमतौर पर दो जांचें इसमें मदद करती हैं। यदि परिणाम गलत की जगह खाली लगते हैं, तो सामग्री पढ़ने से पहले चयनकर्ता के लिए प्रतीक्षा करें — get_content() प्रतीक्षा नहीं करता है। यदि टैब खुलते हुए दिखाई देते हैं लेकिन उनमें कुछ भी नहीं मिलता है, तो पुष्टि करें कि आप दूसरा कनेक्शन नहीं खोल रहे हैं, क्योंकि यह एक अलग ब्राउज़र है।
प्रोटोकॉल की पृष्ठभूमि के लिए, Chrome DevTools प्रोटोकॉल क्या है देखें और nodriver और Patchright का तुलना ड्राइवर-स्तरीय स्टेल्थ उपकरणों के रूप में देखें। योजना के विवरण Scrapeless कीमतों के पृष्ठ पर हैं, और सत्र पैरामीटर Scrapeless दस्तावेज़ीकरण में हैं।
FAQ
प्रश्न: क्या zendriver में connect_over_cdp विधि है?
नहीं। Playwright के connect_over_cdp या Puppeteer के connect के समान कोई समकक्ष नहीं है। connect_existing का व्यवहार Browser.start() के भीतर निकटता से संबंधित है लेकिन यह एक स्थानीय रूप से पहुंच योग्य होस्ट और पोर्ट को लक्षित करता है, न कि एक टोकन के साथ एक दूरस्थ वेबस्कॉकेट यूआरएल।
प्रश्न: दूरस्थ एंडपॉइंट पर होस्ट और पोर्ट सेट करने से क्यों काम नहीं होता?
क्योंकि HTTPApi एक निश्चित सामान्य-HTTP पते में एक होस्ट और एक पोर्ट को इंटरपोलेट करता है और इससे /json/version को लाता है। एक सुरक्षित वेबस्कॉकेट यूआरएल जो क्वेरी स्ट्रिंग के साथ होता है, उसे होस्ट और पोर्ट के रूप में प्रदर्शित नहीं किया जा सकता है, और होस्टेड एंडपॉइंट आमतौर पर उन खोज मार्गों को सार्वजनिक रूप से उजागर नहीं करते हैं।
प्रश्न: क्या मैं एक दूरस्थ ब्राउज़र पर कई टैब खोल सकता हूँ?
हाँ, लेकिन उन्हें कनेक्शन साझा करना चाहिए। प्रत्येक लक्ष्य के लिए Target.attachToTarget को एक बार कॉल करें और मिलती-जुलती sessionId को उस टैब के फ़्रेम पर स्टाम्प करें। एंडपॉइंट से एक और कनेक्शन खोलने पर आपको एक अलग ब्राउज़र मिलता है।
प्रश्न: क्या फोर्क के एंटी-डिटेक्शन पैचिंग दूरस्थ ब्राउज़र पर भी लागू होती है?
ये पैच इस पर कार्य करते हैं कि एक ब्राउज़र कैसे लॉन्च और कॉन्फ़िगर किया जाता है, इसलिए ये उस प्रक्रिया से संबंधित हैं जिसे zendriver शुरू करता है। जब आप किसी ब्राउज़र से संलग्न होते हैं जिसे आपने लॉन्च नहीं किया, तो इसकी कॉन्फ़िगरेशन वही होती है जो प्रदाता ने सेट की है, और zendriver पूरी तरह से एक प्रोटोकॉल क्लाइंट की तरह कार्य कर रहा है।
प्रश्न: get_content() ने लगभग खाली दस्तावेज़ क्यों लौटाया?
क्योंकि यह प्रतीक्षा नहीं करता। यह तुरंत अपना आदेश जारी करता है, इसलिए एक पृष्ठ जो रेंडरिंग समाप्त नहीं हुआ है वह खाली दस्तावेज़ का कंकाल लौटाता है — 39 वर्ण — बिना किसी त्रुटि के। पहले wait_for() के साथ चयनकर्ता पर प्रतीक्षा करें।
प्रश्न: क्या मुझे दूरस्थ पथ के लिए Chrome को स्थानीय रूप से स्थापित करने की आवश्यकता है?
नहीं। आपके मशीन पर कुछ भी लॉन्च नहीं होता है, इसलिए किसी भी ब्राउज़र बाइनरी की आवश्यकता नहीं है। आपको केवल स्थानीय zd.start() उदाहरण चलाने के लिए एक की आवश्यकता है।
प्रश्न: sessionTTL क्या नियंत्रित करता है?
कितने समय तक दूरस्थ ब्राउज़र सत्र खुला रहता है, सेकंड में। इसे उस समय के ऊपर सेट करें जिसकी आपकी आवश्यकता है; सत्र तब समाप्त होता है जब कनेक्शन बंद होता है या विंडो समाप्त होती है, जो पहले आती है।
स्क्रैपलेस में, हम केवल सार्वजनिक रूप से उपलब्ध डेटा का उपयोग करते हैं, जबकि लागू कानूनों, विनियमों और वेबसाइट गोपनीयता नीतियों का सख्ती से अनुपालन करते हैं। इस ब्लॉग में सामग्री केवल प्रदर्शन उद्देश्यों के लिए है और इसमें कोई अवैध या उल्लंघन करने वाली गतिविधियों को शामिल नहीं किया गया है। हम इस ब्लॉग या तृतीय-पक्ष लिंक से जानकारी के उपयोग के लिए सभी देयता को कोई गारंटी नहीं देते हैं और सभी देयता का खुलासा करते हैं। किसी भी स्क्रैपिंग गतिविधियों में संलग्न होने से पहले, अपने कानूनी सलाहकार से परामर्श करें और लक्ष्य वेबसाइट की सेवा की शर्तों की समीक्षा करें या आवश्यक अनुमतियाँ प्राप्त करें।



