web.XMLSiteMap のソースコード

"""
XMLサイトマップを生成するためのウェブクローラー

概要:
    指定されたルートURLからウェブページを再帰的にクロールし、各ページのタイトル、
    最終更新日時(Last-Modifiedヘッダーから)、およびURLを含むXMLサイトマップを作成します。

詳細説明:
    このスクリプトは、コマンドライン引数で指定された、またはデフォルトのルートURLからクロールを開始します。
    HTTPリクエストを送信し、HTMLコンテンツから関連するリンクを抽出します。
    抽出されたリンクは同じドメイン内のものであれば再帰的にクロールされます。
    各ページについて、<title>タグからタイトルを抽出し、HTTPレスポンスヘッダーの
    'Last-Modified'フィールドから最終更新日時を取得します。
    収集されたすべてのURLとそれに対応するメタデータ(タイトル、最終更新日時)は、
    Sitemapプロトコルに準拠したXMLサイトマップとして整形され、指定されたファイルに出力されます。
    クエリパラメータを含むURLも適切に処理され、対象は.html/.shtmlファイルまたはディレクトリパスに限定されます。

関連リンク:
    :doc:`XMLSiteMap_usage`
"""

import sys
import re
import requests
from urllib.parse import urljoin, urlparse, urlunparse
from xml.dom.minidom import parseString
import xml.etree.ElementTree as ET
from datetime import datetime
import charset_normalizer


RootURL = "http://d2mate.mdxes.iir.isct.ac.jp/D2MatE/D2MatE_programs.html?page=statistcs"
outpath = "sitemap.xml"

argv = sys.argv
nargs = len(argv)
if nargs > 1:
    RootURL = argv[1]
if nargs > 2:
    outpath = argv[2]

base_domain = urlparse(RootURL).netloc

print()
print("Crawling and generating XML Sitemap with metadata...")
print()
print(f"usage: python {argv[0]} [RootURL] [OutputFile]")
print()
print(f"RootURL: {RootURL}")
print(f"Output File: {outpath}")
print(f"Base Domain: {base_domain}")
print()

[ドキュメント] def clean_html_content(html_content): """ HTMLコンテンツから余分な空白文字や改行を削除し、整形します。 概要: HTMLコンテンツから不要な改行やスペースを削除し、前後のスペースをトリムする関数。 詳細説明: この関数は、HTMLコンテンツ内の複数の連続する空白文字や改行文字を単一のスペースに変換し、 文字列の前後の空白をトリムすることで、コンテンツをきれいにします。 これは、HTMLの解析やテキスト抽出を容易にするために行われます。 :param html_content: 処理対象のHTML文字列。 :type html_content: str :returns: 整形されたHTML文字列。 :rtype: str """ return " ".join(html_content.split())
[ドキュメント] def get_page_title(html_content): """ HTMLコンテンツからページのタイトルを抽出します。 概要: HTMLコンテンツ内から<title>を抽出する関数。 詳細説明: 正規表現を使用して`<title>`タグの内容を検索し、そのテキストを返します。 タイトルが見つからない場合は、「No Title」を返します。検索は大文字小文字を区別しません。 :param html_content: タイトルを抽出するHTML文字列。 :type html_content: str :returns: 抽出されたページのタイトル、または「No Title」。 :rtype: str """ title_match = re.search(r"<title>(.*?)</title>", html_content, re.IGNORECASE) if title_match: return title_match.group(1).strip() return "No Title"
[ドキュメント] def is_target_url(url): """ 指定されたURLがクロールの対象となるHTMLページまたはディレクトリパスであるかを判定します。 概要: URLがHTMLファイル(*.html)またはファイル名なし(ディレクトリパス)の場合にTrueを返す。 詳細説明: この関数はURLを解析し、そのパスが`.html`または`.shtml`で終わるか、 あるいはスラッシュ`/`で終わる(ディレクトリを示唆する)場合にTrueを返します。 これにより、画像、CSS、JavaScriptなどの非HTMLリソースがクロール対象から除外されます。 :param url: 判定するURL。 :type url: str :returns: URLが対象の場合True、それ以外はFalse。 :rtype: bool """ parsed_url = urlparse(url) path = parsed_url.path return path.endswith(".html") or path.endswith(".shtml") or path.endswith("/")
[ドキュメント] def normalize_url(url): """ URLを標準化し、フラグメント(#以降の部分)を削除します。 概要: URLを標準化する(クエリパラメータを保持する)関数。 詳細説明: クエリパラメータは保持しつつ、URLのフラグメント部分(ページ内の特定の位置を示す部分)を 削除することで、同じコンテンツを指す異なるURLが重複して処理されるのを防ぎます。 これにより、サイトマップのURLの重複を避けることができます。 :param url: 標準化するURL。 :type url: str :returns: フラグメントが削除された標準化されたURL。 :rtype: str """ parsed_url = urlparse(url) normalized = urlunparse(parsed_url._replace(fragment="")) # フラグメントを削除 return normalized
[ドキュメント] def find_urls_in_html(html_content, base_url): """ HTMLコンテンツ内からすべてのリンク(`href`, `src`, `url`属性)を抽出し、絶対URLに変換します。 概要: HTMLコンテンツからリンクを抽出し、FQDNを生成する関数。 詳細説明: この関数は、HTMLコンテンツ内のアンカータグの`href`属性、画像タグの`src`属性、 JavaScript内の`url`プロパティなどを正規表現で検索します。 見つかった相対URLは`urljoin`を使用して絶対URLに変換され、クロール対象のURLのみがセットとして返されます。 :param html_content: リンクを抽出するHTML文字列。 :type html_content: str :param base_url: 相対URLを解決するための基準URL。 :type base_url: str :returns: 抽出され、標準化された絶対URLのセット。 :rtype: set[str] """ urls = set() pattern = re.compile(r"url:\s*[\"']([^\"']+)[\"']|href=[\"']([^\"']+)[\"']|src=[\"']([^\"']+)[\"']") matches = pattern.findall(html_content) for match in matches: url = match[0] or match[1] or match[2] if url.startswith("/"): full_url = urljoin(base_url, url) elif not url.startswith("http"): full_url = urljoin(base_url, url) else: full_url = url if is_target_url(full_url): urls.add(full_url) return urls
[ドキュメント] def detect_encoding(response_content): """ HTTPレスポンスのバイトコンテンツから最も適切な文字エンコーディングを検出します。 概要: 文字コードを判別する関数。 詳細説明: `charset_normalizer`ライブラリを利用して、与えられたバイト列の文字コードを自動的に判別します。 これにより、様々なエンコーディングで提供されるウェブページを正しくデコードできます。 :param response_content: HTTPレスポンスのバイト列コンテンツ。 :type response_content: bytes :returns: 検出された文字エンコーディングの文字列。 :rtype: str """ result = charset_normalizer.detect(response_content) return result['encoding']
[ドキュメント] def crawl_recursive(start_url, visited, base_domain, parent = ""): """ 指定された開始URLから再帰的にリンクをたどり、各ページのタイトルと最終更新日時を収集します。 概要: 再帰的にリンクをたどる関数(クエリパラメータ含めた情報の取得対応)。 詳細説明: 1. URLを標準化し、既に訪問済みのURLはスキップします。 2. `requests`ライブラリを使用してURLからコンテンツを取得します。 3. 取得したコンテンツの文字コードを検出し、デコードします。 4. HTTPヘッダーから`Last-Modified`情報を抽出し、YYYY-MM-DD形式にフォーマットします。 5. HTMLコンテンツからページのタイトルと、同じドメイン内の他のリンクを抽出します。 6. 抽出されたリンク(対象URLのみ)に対して再帰的にこの関数を呼び出し、 全ての関連するページ情報を収集します。 :param start_url: クロールを開始するURL。 :type start_url: str :param visited: 既に訪問済みの正規化されたURLを格納するセット。 :type visited: set :param base_domain: クロール対象を制限するベースドメイン。このドメイン外のURLはスキップされます。 :type base_domain: str :param parent: 現在クロール中のURLの親URL(エラーメッセージ用)。デフォルトは空文字列。 :type parent: str :returns: クロールされた全てのURLと、それに対応するタイトルおよびLast-Modified日時を含む辞書。 エラーが発生した場合は空の辞書を返します。 :rtype: dict[str, dict[str, str | None]] """ normalized_url = normalize_url(start_url) if normalized_url in visited: return {} print(f"Crawling: {start_url}") visited.add(normalized_url) try: response = requests.get(start_url) if response.status_code != 200: print(f"Failed to fetch: {start_url} in {parent}") return {} # 文字コード判別 encoding = detect_encoding(response.content) decoded_content = response.content.decode(encoding) print(f" Encoding: {encoding}") # Last-Modifiedヘッダーを取得 lastmod = response.headers.get("Last-Modified") if lastmod: lastmod = datetime.strptime(lastmod, "%a, %d %b %Y %H:%M:%S %Z").strftime("%Y-%m-%d") print(f" lastmod: {lastmod}") else: lastmod = None except Exception as e: print(f"Error fetching {start_url}: {e}") return {} html_content = clean_html_content(decoded_content) title = get_page_title(html_content) print(f" title: {title}") found_urls = find_urls_in_html(html_content, start_url) filtered_urls = {url for url in found_urls if urlparse(url).netloc == base_domain} all_urls = {start_url: {"title": title, "lastmod": lastmod}} for url in filtered_urls: all_urls.update(crawl_recursive(url, visited, base_domain, parent = start_url)) return all_urls
[ドキュメント] def generate_xml_sitemap_with_metadata(urls_with_metadata, output_file=None): """ 収集されたURLとそのメタデータ(タイトル、最終更新日時)から、 Sitemapプロトコルに準拠したXMLサイトマップファイルを生成します。 概要: URLとメタデータ(タイトル、Last-Modified)から整形済みXMLサイトマップを生成する関数。 詳細説明: `xml.etree.ElementTree`を使用して、`<urlset>`、`<url>`、`<loc>`、`<title>`、 `<lastmod>`、`<changefreq>`、`<priority>`といったSitemapプロトコル要素を含むXML構造を構築します。 `title`と`lastmod`は収集されたメタデータから設定されます。 XMLは整形され(pretty-print)、指定された出力ファイルにUTF-8エンコーディングで保存されます。 `output_file`が指定されない場合、グローバル変数`outpath`が使用されます。 :param urls_with_metadata: URLをキーとし、タイトルとLast-Modified日時を値とする辞書。 :type urls_with_metadata: dict[str, dict[str, str | None]] :param output_file: サイトマップを保存するファイルパス。省略された場合はグローバル設定に従います。 :type output_file: str, optional :returns: なし。サイトマップファイルが指定されたパスに生成されます。 :rtype: None """ if output_file is None: output_file = outpath; urlset = ET.Element("urlset", xmlns="http://www.sitemaps.org/schemas/sitemap/0.9") for url, metadata in sorted(urls_with_metadata.items()): url_element = ET.SubElement(urlset, "url") loc = ET.SubElement(url_element, "loc") loc.text = url title = metadata.get("title", "No Title") title_element = ET.SubElement(url_element, "title") title_element.text = title lastmod = metadata.get("lastmod") if lastmod: lastmod_element = ET.SubElement(url_element, "lastmod") lastmod_element.text = lastmod changefreq = ET.SubElement(url_element, "changefreq") changefreq.text = "monthly" priority = ET.SubElement(url_element, "priority") priority.text = "1.0" raw_xml = ET.tostring(urlset, encoding="utf-8") parsed_xml = parseString(raw_xml) pretty_xml = parsed_xml.toprettyxml(indent=" ") with open(output_file, "w", encoding="utf-8") as f: f.write(pretty_xml)
[ドキュメント] def main(): """ XMLサイトマップ生成スクリプトのメイン実行関数です。 概要: スクリプトの主処理を実行。 詳細説明: 1. 訪問済みURLを記録するためのセットを初期化します。 2. `RootURL`から`crawl_recursive`関数を呼び出してウェブサイトのクロールを開始します。 3. クロールによって見つかった全てのURLとそのメタデータ(タイトル、最終更新日時)をコンソールに出力します。 4. `generate_xml_sitemap_with_metadata`関数を呼び出して、 収集した情報に基づいてXMLサイトマップを生成し、ファイルに保存します。 :param None: 引数なし。 :returns: なし。サイドエフェクトとしてサイトマップファイルが生成されます。 :rtype: None """ visited = set() print(f"Starting crawl from: {RootURL}") all_urls_with_metadata = crawl_recursive(RootURL, visited, base_domain) print(f"Found total URLs: {len(all_urls_with_metadata)}") for idx, (url, metadata) in enumerate(sorted(all_urls_with_metadata.items()), start=1): print(f" [{idx}] {url} - Title: {metadata['title']} - LastMod: {metadata['lastmod']}") generate_xml_sitemap_with_metadata(all_urls_with_metadata) print("XML Sitemap with metadata has been saved as '{outpath}'")
if __name__ == "__main__": main()