html.parser দিয়ে Python-এ HTML link ও image checker বানান

ছোট link scanner বানিয়ে HTML parsing, URL সামলানো এবং মৌলিক SEO ও নিরাপত্তা পরীক্ষা শিখুন। নমুনা পাতায় কোড চালিয়ে পরীক্ষিত।

সমস্যা চিহ্নিত internal ও external লিংকের তালিকা

প্রতিটি ওয়েবসাইটের লিংক ও ছবি দ্রুত পরীক্ষার উপায় দরকার: খালি লিংক আছে কি, বর্ণনাহীন ছবি আছে কি, বা নতুন tab-এ অনিরাপদভাবে খোলা external লিংক আছে কি? এই প্রজেক্ট শুধু Python-এর standard library দিয়ে একটি scanner বানায়, যাতে শেখা যায় HTML parsing, URL সামলানো এবং accessibility, SEO ও নিরাপত্তার জন্য গুরুত্বপূর্ণ কয়েকটি পরীক্ষা। কোড ও নিচের আউটপুট প্রকাশের আগে Python 3-এ চালানো হয়েছে। এটি আপনার কাছে থাকা HTML ফাইলে কাজ করে এবং ওয়েবসাইট crawl করে না বা কোনো request পাঠায় না।

নমুনা পাতা

page.html-এ ভালো ও খারাপ লিংকের মিশ্রণ:

<html><body>
<h1>Demo page</h1>
<a href="/about">About us</a>
<a href="contact.html">Contact</a>
<a href="https://example.org/docs" target="_blank">Docs</a>
<a href="https://example.org/blog" target="_blank" rel="noopener noreferrer">Blog</a>
<a href="mailto:hi@example.com">Email</a>
<a href="javascript:void(0)">Click</a>
<a href="/empty"><img src="/logo.png"></a>
<img src="/team.jpg" alt="Our team">
</body></html>

কোড

linkscan.py নামে সংরক্ষণ করুন:

#!/usr/bin/env python3
"""List the links and images in an HTML file and flag simple problems (standard library only)."""
import sys
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse


class LinkParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links, self.images, self._href, self._text = [], [], None, []

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if tag == "a" and attrs.get("href"):
            self._href, self._text = attrs["href"], []
            self._attrs = attrs
        elif tag == "img":
            self.images.append({"src": attrs.get("src", ""), "alt": attrs.get("alt")})

    def handle_data(self, data):
        if self._href is not None:
            self._text.append(data)

    def handle_endtag(self, tag):
        if tag == "a" and self._href is not None:
            text = " ".join("".join(self._text).split())
            self.links.append({"href": self._href, "text": text, "rel": self._attrs.get("rel", ""),
                               "target": self._attrs.get("target", "")})
            self._href = None


def analyse(html_text, base_url):
    parser = LinkParser()
    parser.feed(html_text)
    host = urlparse(base_url).netloc
    report = {"internal": [], "external": [], "problems": []}
    for link in parser.links:
        url = urljoin(base_url, link["href"])
        scheme = urlparse(url).scheme
        if scheme not in ("http", "https"):
            if scheme == "javascript":
                report["problems"].append(f"javascript: link: {link['href']!r}")
            continue  # mailto:, tel: and so on are not web links
        kind = "internal" if urlparse(url).netloc == host else "external"
        report[kind].append(url)
        if not link["text"]:
            report["problems"].append(f"link with no text: {url}")
        if link["target"] == "_blank" and "noopener" not in link["rel"] and kind == "external":
            report["problems"].append(f"target=_blank without rel=noopener: {url}")
    for image in parser.images:
        if image["alt"] is None:
            report["problems"].append(f"image without alt text: {image['src']}")
    return report


if __name__ == "__main__":
    path, base = sys.argv[1], sys.argv[2]
    with open(path, encoding="utf-8") as f:
        result = analyse(f.read(), base)
    print(f"internal links: {len(result['internal'])}, external links: {len(result['external'])}")
    for url in sorted(set(result["external"])):
        print("  external:", url)
    for problem in result["problems"]:
        print("  PROBLEM:", problem)

চালান

$ python3 linkscan.py page.html https://mysite.com/index.html
internal links: 3, external links: 2
  external: https://example.org/blog
  external: https://example.org/docs
  PROBLEM: target=_blank without rel=noopener: https://example.org/docs
  PROBLEM: javascript: link: 'javascript:void(0)'
  PROBLEM: link with no text: https://mysite.com/empty
  PROBLEM: image without alt text: /logo.png

Scanner ৩টি internal ও ২টি external লিংক পেয়েছে (mailto: লিংক সঠিকভাবে এড়ানো হয়েছে), এবং চারটি সমস্যা।

এটি কীভাবে কাজ করে

html.parser.HTMLParser HTML স্রোত হিসেবে পড়ে এবং প্রতিটি অংশের জন্য আপনার method ডাকে: খোলা tag-এর জন্য handle_starttag, লেখার জন্য handle_data, বন্ধ tag-এর জন্য handle_endtag। এটি regular expression-এর চেয়ে আসল জগতের এলোমেলো HTML ভালো সামলায়। HTML কখনো regex দিয়ে পার্স করবেন না: nested tag, যেকোনো ক্রমের attribute ও comment তা ভেঙে দেবে।

লিংকের লেখা জমানো। <a href=...> দেখলে লিংকটি মনে রাখি এবং </a> পর্যন্ত লেখা জমাই। জমা লেখা খালি হলে, যেমন <a href="/empty"><img ...></a>-এ, screen reader-এর পড়ার কিছু থাকে না, আর আমরা "link with no text" জানাই।

Relative URL সমাধান। urljoin(base_url, "contact.html") পাতার নিজের ঠিকানা দিয়ে relative ঠিকানাকে পূর্ণ করে। /about ও contact.html দুটিই https://mysite.com/... হয়। এটি ছাড়া লিংক internal কি না বোঝা যায় না।

Internal না external। Host (netloc) base ঠিকানার সঙ্গে তুলনা করি।

শুধু ওয়েব লিংক। urlparse(url).scheme দিয়ে mailto: ও tel: এড়াই, আর javascript: লিংক চিহ্নিত করি, কারণ এগুলো খারাপ অভ্যাস: script ছাড়া কাজ করে না এবং ঠিকানা কখনো ব্যবহারকারীর ইনপুট থেকে বানালে ঝুঁকি।

পরীক্ষাগুলো ও কেন গুরুত্বপূর্ণ

পরীক্ষা কেন
alt ছাড়া ছবি Screen-reader ব্যবহারকারী বর্ণনা পায় না, আর search engine কম প্রসঙ্গ পায়। শুধু সাজসজ্জার ছবির জন্য খালি alt="" বৈধ, তাই আমরা খালি নয়, অনুপস্থিত attribute (None) পরীক্ষা করি।
লেখাহীন লিংক সহায়ক প্রযুক্তির পড়া বা click করার কিছু নেই, আর search engine-এর জন্য anchor text নেই।
rel="noopener" ছাড়া target="_blank" পুরোনো browser-এ খোলা পাতা window.opener ধরতে পারত এবং মূল tab অন্যত্র পাঠাতে পারত। আধুনিক browser ডিফল্টে _blank লিংককে noopener ধরে, তবে পুরোনো browser-এর জন্য স্পষ্ট rel="noopener noreferrer" এখনো ভালো অভ্যাস।
javascript: লিংক script ছাড়া ভাঙে এবং accessible করা কঠিন। আসল লিংক বা <button> ব্যবহার করুন।

Test

test_linkscan.py নামে সংরক্ষণ করুন:

import unittest

from linkscan import analyse

BASE = "https://mysite.com/index.html"


class LinkScanTest(unittest.TestCase):
    def test_splits_internal_and_external(self):
        r = analyse('<a href="/a">A</a><a href="https://other.com/x">X</a>', BASE)
        self.assertEqual(r["internal"], ["https://mysite.com/a"])
        self.assertEqual(r["external"], ["https://other.com/x"])

    def test_relative_links_are_resolved(self):
        r = analyse('<a href="page.html">P</a>', BASE)
        self.assertEqual(r["internal"], ["https://mysite.com/page.html"])

    def test_flags_problems(self):
        html = '<a href="https://o.com" target="_blank">O</a><a href="javascript:x()">J</a><img src="a.png">'
        problems = " | ".join(analyse(html, BASE)["problems"])
        self.assertIn("noopener", problems)
        self.assertIn("javascript:", problems)
        self.assertIn("alt text", problems)

    def test_mailto_is_ignored(self):
        r = analyse('<a href="mailto:a@b.c">mail</a>', BASE)
        self.assertEqual(r["internal"] + r["external"], [])


if __name__ == "__main__":
    unittest.main()
$ python3 -m unittest -v
Ran 4 tests in 0.002s

OK

সীমা

  • এটি HTML-এ কী লেখা আছে দেখে, লিংক আসলে কাজ করে কি না নয়। ভাঙা লিংক খুঁজতে HTTP request (HEAD বা GET) লাগে, ভদ্র বিরতি, timeout ও robots.txt সম্মান সহ। শুধু নিজের বা পরীক্ষার অনুমতি থাকা সাইট পরীক্ষা করুন।
  • এটি JavaScript চালায় না, তাই script-যোগ করা লিংক দেখে না।
  • <link>, <script> ও <iframe>-এর লিংক এবং srcset-এর ছবি ধরা হয় না।
  • <base> tag উপেক্ষা করে, যা relative URL-এর সমাধান বদলাতে পারে।

চ্যালেঞ্জ

১. প্রতিটি external domain গুনে শীর্ষ ৫টি ছাপুন। ২. ছবিতে width ও height আছে কি না দেখুন (layout shift ঠেকায়)। ৩. ডুপ্লিকেট লিংক ও "click here"-এর মতো অস্পষ্ট লেখার লিংক জানান। ৪. HTML ফাইলের পুরো folder scan করুন। ৫. ঐচ্ছিক --check flag যোগ করুন, যা timeout ও ছোট বিরতিসহ urllib.request দিয়ে লিংক পরীক্ষা করে, শুধু আপনার নিজের সাইটে। ৬. রিপোর্ট JSON-এ দিন।

মূল কথা: HTML-এর জন্য আসল parser ব্যবহার করুন, URL সঠিকভাবে সমাধান করুন, এবং accessibility, SEO ও নিরাপত্তার নিয়মকে ছোট স্বয়ংক্রিয় পরীক্ষায় বদলান। এমন script প্রতিটি build-এ চালানো যায়।


হাতে-কলমে প্রজেক্ট সিরিজ, পর্ব ২ (পুনর্ব্যবহারযোগ্য টুল): 5 নম্বর (মোট 5)। আগের লেখা: Python-এ Markdown to HTML converter বানান, XSS-নিরাপদ.

আরও পড়ুন

Background job ও queue: retry, backoff ও dead job কোডসহ

Web request-এর ভেতরে email পাঠানো বা ছবি ছোট করলে ব্যবহারকারী অপেক্ষা করেন ও ব্যর্থতায় কাজ হারায়। চালানো যায় এমন কোডে job queue, retry ও backoff শিখুন।

Backend ও Database•৬ মিনিটে পড়া

Webhook সঠিকভাবে: HMAC signature যাচাই ও replay ঠেকান

Webhook URL প্রকাশ্য, তাই যে কেউ POST করতে পারে। HMAC, timestamp ও event id দিয়ে request আসল কিনা প্রমাণ করা শিখুন। চালানো যায় এমন কোড।

Backend ও Database•৫ মিনিটে পড়া

Idempotency key: API retry নিরাপদ করুন, দুবার চার্জ রুখুন

Network ব্যর্থ হয়, client retry করে, আর সুরক্ষা না থাকলে গ্রাহক দুবার চার্জ হন। Idempotency key এটি ঠিক করে। চালানো যায় এমন উদাহরণসহ।

Backend ও Database•৫ মিনিটে পড়া