DEV Community

Cover image for Python में robots.txt और sitemap से किसी न्यूज़ साइट का फ़ीड या सेक्शन पेज कैसे ढूंढें

Python में robots.txt और sitemap से किसी न्यूज़ साइट का फ़ीड या सेक्शन पेज कैसे ढूंढें

किसी फ़ीड रीडर का पहला काम फ़ीड ढूंढना होता है, और कई बड़ी न्यूज़ साइटों पर ढूंढने के लिए कोई फ़ीड होता ही नहीं। ऐसे में ज़्यादातर लोग होमपेज को scrape करने लगते हैं, जो नाज़ुक भी है और साइट के साथ बदतमीज़ी भी। इससे बेहतर एक क्रम है, और उसका ज़्यादातर हिस्सा साइट खुद बता देती है: <link> टैग में, robots.txt में और sitemaps में। osFoundry डेस्कटॉप ऐप यह काम Rust में करता है (news_feed.rs, फ़ंक्शन news_discover_feeds, news_discover_sections और news_fetch_section)। नीचे वही प्रक्रिया सिर्फ़ standard library वाले Python में है, जिसे आठ असली न्यूज़ साइटों पर चलाया गया, साथ में हर साइट पर कितने request लगे और नतीजा कहां पहुंचा।

पहले request से पहले बुनियादी नियम

Discovery स्क्रिप्ट ऐसी साइटों पर जाती है जिन्हें उसने पहले कभी नहीं देखा, इसलिए शिष्टता उसी एक फ़ंक्शन में होनी चाहिए जो request भेजता है, caller के भरोसे नहीं छोड़ी जा सकती।

UA = "Mozilla/5.0 (compatible; osFoundryNews/1.0; +https://osfoundry.io)"
PAUSE = 1.0          # seconds between requests to the same site
MAX_REQUESTS = 25    # per site, everything included

class Site:
    def __init__(self, host):
        self.root = f"https://{host}/"
        self.log, self.robots, self._last = [], None, 0.0

    def get(self, url, step):
        if len(self.log) >= MAX_REQUESTS:
            raise Budget(url)
        if self.robots and not url.endswith("/robots.txt") and not self.robots.can_fetch(UA, url):
            self.log.append((step, url, "robots-disallow", 0, 0))
            return None, None
        wait = self._last + PAUSE - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        t0 = time.monotonic()
        req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept-Encoding": "gzip"})
        status, body = None, None
        try:
            with urllib.request.urlopen(req, timeout=15) as r:
                raw = r.read(8 * 1024 * 1024)
                if r.headers.get("Content-Encoding") == "gzip" or url.endswith(".gz"):
                    raw = gzip.decompress(raw)
                status, body = r.status, raw.decode("utf-8", "replace")
        except urllib.error.HTTPError as e:
            status = e.code
        except Exception as e:
            status = type(e).__name__
        self._last = time.monotonic()
        self.log.append((step, url, status, len(body or ""), round((self._last - t0) * 1000)))
        return status, body
Enter fullscreen mode Exit fullscreen mode

एक बार में एक request, हर request के बीच कम से कम एक सेकंड, हर साइट के लिए एक सख़्त सीमा, ऐसा User-Agent जो बताए कि पूछ कौन रहा है (वही जो ऐप भेजता है), और हर दूसरे URL से पहले urllib.robotparser के ज़रिए robots.txt की जांच। नीचे के सारे आंकड़े इसी log से आए हैं।

robots.txt सबसे पहले, होमपेज से भी पहले, लाया जाता है। जिन साइटों पर आख़िर में फ़ीड मिल जाता है वहां इससे एक request ज़्यादा लगता है, लेकिन इसका फ़ायदा यह है कि उसके बाद की हर चीज़ पर नियम लागू होते हैं, और इसी फ़ाइल में sitemaps की वह सूची भी होती है जो आगे के steps को वैसे भी चाहिए। इसके ऊपर दो और रुकने की शर्तें हैं: अगर robots.txt इस User-Agent के लिए साइट का root disallow करता है, या होमपेज 401, 403 या 429 लौटाता है, तो स्क्रिप्ट उस साइट पर वहीं रुक जाती है।

Step 1: साइट ने जो फ़ीड घोषित किया है

जिन साइटों पर RSS होता है, वे लगभग हमेशा पेज के head में यह बता देती हैं। होमपेज के लिए एक request, एक HTML parse:

class Links(HTMLParser):
    def __init__(self):
        super().__init__()
        self.alternates, self.anchors = [], []

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "link" and "alternate" in (a.get("rel") or "").lower().split():
            t = (a.get("type") or "").lower()
            if ("rss" in t or "atom" in t) and a.get("href"):
                self.alternates.append(a["href"])
        elif tag == "a" and a.get("href"):
            self.anchors.append(a["href"])
Enter fullscreen mode Exit fullscreen mode

The Verge, TechCrunch और ITmedia पर बात यहीं ख़त्म हो गई: हर साइट पर दो request (robots.txt और होमपेज), pause मिलाकर 4 सेकंड से कम में। ITmedia का फ़ीड दूसरे host (rss.itmedia.co.jp) पर है, और path का अंदाज़ा लगाने के बजाय घोषित link पर भरोसा करने की यह एक वजह है।

Step 2: नौ आम पते

जब कुछ भी घोषित नहीं होता, तो ऐप नौ आम फ़ीड paths आज़माता है और उस पहले path पर रुक जाता है जो RSS या Atom के रूप में parse हो जाए: /feed, /feed/, /rss, /rss.xml, /feed.xml, /atom.xml, /index.xml, /feeds/posts/default, /rss/index.xml। ये उन साइटों पर CMS के default (WordPress, Blogger, Hugo, Ghost) पकड़ लेते हैं जिनकी theme <link> टैग लगाना भूल गई।

टेस्ट में शामिल जापान के तीन राष्ट्रीय अख़बारों, Nikkei, Asahi और Yomiuri, पर सभी 27 probes फ़ेल हुए: 26 ने 404 लौटाया और एक (asahi.com/rss) ने 403। हर साइट पर लगे 13 या 14 requests में से 9 यही थे। अगर आपको सिर्फ़ बड़े publishers से मतलब है, तो इन probes से पहले sitemaps पढ़ने से हर run 4 या 5 requests में निपट जाएगा। लेकिन छोटे ब्लॉग भी संभालने वाले general tool के लिए ये probes अपनी जगह के लायक हैं।

Step 3: robots.txt किस ओर इशारा करता है

robots.txt में sitemaps Sitemap: lines के ज़रिए लिखे होते हैं। कोई sitemap या तो <sitemapindex> होता है जो और sitemaps की ओर इशारा करता है, या पेजों का <urlset>। स्क्रिप्ट सूची में से ज़्यादा से ज़्यादा चार sitemaps पढ़ती है, index के अंदर एक level तक जाती है (ज़्यादा से ज़्यादा पांच children), और पहले काम के sitemap पर रुक जाती है।

असली सावधानी इस बात में है कि कौन सा sitemap काम का है। सबसे सीधा टेस्ट Google News namespace, xmlns:news=, है, जिसे यह format हाल में छपे लेखों के लिए रखता है। Nikkei दिखाता है कि यह टेस्ट काफ़ी क्यों नहीं है। उसका sections_sitemap.xml यह namespace declare करता है, फिर भी उसके 187 में से 184 URLs /business/startups/ जैसे सेक्शन पेज हैं। इसलिए स्क्रिप्ट sitemap को उसके अंदर के URLs से परखती है:

def usable(info):
    """Judge a sitemap by the URLs in it, not by the namespace it declares."""
    if info.get("section_share", 0) >= 0.7:
        return "sections"
    if info.get("article_share", 0) >= 0.7 and info["kind"] == "news_sitemap":
        return "news sitemap"
    if info.get("article_share", 0) >= 0.7 and info.get("dated"):
        return "article sitemap"
    return None
Enter fullscreen mode Exit fullscreen mode

section_share और article_share उन <loc> paths का अनुपात हैं जो नीचे दिए दो shape टेस्ट पास करते हैं। 70 % की सीमा वही है जो ऐप सेक्शन sitemaps के लिए इस्तेमाल करता है।

हर साइट का नतीजा यह रहा:

साइट Requests सेकंड नतीजा कहां पहुंचा
www.theverge.com 2 3.7 <link rel="alternate"> से RSS
techcrunch.com 2 3.4 <link rel="alternate"> से RSS
www.itmedia.co.jp 2 3.6 <link rel="alternate"> से RSS (rss.itmedia.co.jp पर)
www.yomiuri.co.jp 13 27.2 News sitemap, 340 लेख, सभी पिछले 24 घंटों के
www.asahi.com 13 47.3 News sitemap (national), 61 लेख, 36 पिछले 24 घंटों के
www.nikkei.com 14 30.8 सेक्शन sitemap, 184 सेक्शन; Startups पेज से 20 लेख मिले
apnews.com 2 2.9 रुका: होमपेज ने 403 लौटाया
www.reuters.com 1 1.5 रुका: robots.txt इस User-Agent को disallow करता है

नवंबर 2026 में एक Mac से, एक के बाद एक साइट पर मापा गया। सेकंडों का अच्छा-ख़ासा हिस्सा एक सेकंड के pause का है; सबसे धीमा single response 6.9 s का था, asahi.com/rss से आया 403।

Yomiuri और Asahi के लिए इससे आगे कुछ नहीं चाहिए: news sitemap असल में बिना summary वाला फ़ीड है, जिसमें हर लेख का URL, title और प्रकाशन समय होता है। Asahi का index इसे desk के हिसाब से बांटता है (sitemap_national.xml, sitemap_business.xml वगैरह), इसलिए इसे follow करने का मतलब है desks चुनना, लगभग सेक्शन चुनने जैसा।

Step 4: सेक्शन पेज, जब सिर्फ़ वही मौजूद हों

Nikkei का sitemap लेखों की सूची कम और साइट की directory ज़्यादा है। सेक्शन का shape टेस्ट लेख वाले टेस्ट का उल्टा है, साथ में depth की सीमा और एक छोटी blocklist:

def looks_like_article(path):
    segs = [s for s in path.split("/") if s]
    if not segs:
        return False
    last = re.sub(r"(\.html?)+$", "", segs[-1])
    if len(last) < 6:
        return False
    if len(segs) < 2:
        return last.count("-") >= 3
    if any(re.fullmatch(r"\d{4}", a) and re.fullmatch(r"\d{2}", b) for a, b in zip(segs, segs[1:])):
        return True
    if len(last) >= 8 and re.search(r"\d", last):
        return True
    return last.count("-") >= 3

def looks_like_section(path):
    if looks_like_article(path):
        return False
    segs = [s for s in path.split("/") if s]
    if not segs or len(segs) > 3:
        return False
    if "." in segs[-1] and not segs[-1].endswith(".html"):
        return False
    return not any(s.lower() in BLOCKED for s in segs)   # search, login, about, privacy, ...
Enter fullscreen mode Exit fullscreen mode

लेख वह path है जिसमें तारीख़ की जोड़ी हो (/2026/11/06/), या आख़िरी segment ID जैसा हो, 8 या ज़्यादा characters का और उसमें कोई अंक हो, या कम से कम तीन hyphens वाला slug हो। ID वाला नियम depth 1 पर लागू नहीं होता, क्योंकि वहां वह /coronavirus-2026 जैसे tag पेजों को भी निगल लेता। दो के बजाय तीन hyphens इसलिए, क्योंकि बहुत सी साइटों पर /news/tech-and-science/ एक सेक्शन होता है।

इसके बाद किसी सेक्शन को follow करने में हर check पर एक request लगता है: सेक्शन पेज लाओ, उसी host के वे links रखो जिनका shape लेख जैसा हो, और भटके हुए links हटा दो।

shaped = [u for u in unique if looks_like_article(urlparse(u).path)]
first = lambda u: next((s for s in urlparse(u).path.split("/") if s), None)
counts = {}
for u in shaped:
    counts[first(u)] = counts.get(first(u), 0) + 1
if counts:
    seg, n = max(counts.items(), key=lambda kv: kv[1])
    if n * 10 >= len(shaped) * 6:
        shaped = [u for u in shaped if first(u) == seg]
Enter fullscreen mode Exit fullscreen mode

Nikkei के Startups पेज पर उसी host के 271 अलग-अलग links थे। Shape टेस्ट और dominant-segment नियम (अगर 60 % matches एक ही पहले segment से शुरू होते हैं, यहां /article/, तो बाकी हटा दिए जाते हैं) के बाद 20 बचे, सभी /article/DGXZQOUC026TI0S6A101C2000000/ जैसे। हटाए गए 251 में menus, tag और theme पेज, /login, /search, market data पेज और इसी तरह की चीज़ें थीं।

ऐप भी इसी नतीजे पर पहुंचता है। www.nikkei.com के लिए उसका section picker "Showing 60 of 185 sections" दिखाता है: sitemap के 184 और होमपेज, जिसे वह तभी जोड़ता है जब जांच ले कि होमपेज पर खुद लेखों के links हैं।

Nikkei के लिए osFoundry का section picker, publisher के sitemap से बना

Nikkei का सेक्शन sitemap एक और बात बता देता है: हर सेक्शन का <lastmod> होता है, और 187 में से 157 पिछले 24 घंटों में बदले थे। कई सेक्शन देखने वाला follower पहले यही एक फ़ाइल पढ़ सकता है और जो सेक्शन पेज नहीं बदले उन्हें छोड़ सकता है।

जब जवाब "नहीं" हो

AP ने होमपेज पर 403 लौटाया, इसलिए स्क्रिप्ट दो requests के बाद रुक गई। उस नियम के बिना वह 13 और requests भेजती जो सब 403 ही लौटाते, जैसा इस स्क्रिप्ट के पिछले version में हुआ था। Reuters का robots.txt उन चुनिंदा crawlers के नाम लिखता है जिन्हें वह अनुमति देता है और बाकी सबको disallow करता है, इसलिए स्क्रिप्ट एक request के बाद रुक गई और होमपेज कभी लाया ही नहीं।

इनमें से कोई भी ऐसी पहेली नहीं है जिसका तोड़ निकाला जाए। जो साइट अपनी पहचान बताने वाले client को 403 देती है, या robots.txt में मना करती है, उसने सवाल का जवाब दे दिया है। फ़ीड tool के लिए सही व्यवहार यही है कि यह बात साफ़ बताए और रुक जाए, जिससे साइट पर पूरे crawl के बजाय एक या दो requests का ही बोझ पड़े।

osFoundry डेस्कटॉप ऐप में जब आप News स्क्रीन पर कोई source जोड़ते हैं, तो यह discovery लोकल तौर पर चलती है; वह स्क्रीन और क्या-क्या करती है, यह news firehose पेज पर बताया गया है।

Top comments (0)