Sitemap自動更新ツール『追放された俺でも、サイトマップ更新で無双する件。』 最新版ソースコード

『追放された俺でも、サイトマップ更新で無双する件。』は、既存の sitemap.xml を元に、指定フォルダ内のファイルの更新日時をチェックして を自動更新するGUIアプリです。

>>GUI外観 >>使い方

『追放された俺でも、サイトマップ更新で無双する件。』の最新版ソースコード全文です:

 
        import tkinter as tk
        from tkinter import filedialog, messagebox, scrolledtext
        import os
        import xml.etree.ElementTree as ET
        from datetime import datetime, timezone
        from urllib.parse import urlparse, unquote
        
        
        class SitemapUpdater:
            def __init__(self, master):
                self.master = master
                self.master.title("Sitemap 自動更新ツール - 追放された俺でも、サイトマップ更新で無双する件。")
                self.sitemap_path = ""
                self.folder_path = ""
        
                # パス表示ラベル
                self.label_sitemap = tk.Label(master, text="sitemap.xml: 未選択", anchor="w", wraplength=600)
                self.label_sitemap.pack(fill="x", padx=10, pady=2)
        
                self.label_folder = tk.Label(master, text="対象フォルダ: 未選択", anchor="w", wraplength=600)
                self.label_folder.pack(fill="x", padx=10, pady=2)
        
                # 操作ボタン
                tk.Button(master, text="XMLファイルを選択", command=self.load_sitemap).pack(pady=4)
                tk.Button(master, text="フォルダを選択", command=self.select_folder).pack(pady=4)
                tk.Button(master, text="更新・保存", command=self.update_sitemap).pack(pady=10)
        
                # ステータス表示欄
                self.status_text = scrolledtext.ScrolledText(master, height=14, wrap=tk.WORD)
                self.status_text.pack(fill="both", expand=True, padx=10, pady=5)
        
                # バージョン表示(最下部中央揃え)
                self.version_label = tk.Label(master, text="ver.2.0.1", anchor="center", fg="gray")
                self.version_label.pack(side="bottom", pady=6)
        
            def log(self, message):
                """ステータスログに出力"""
                timestamp = datetime.now().strftime("[%H:%M:%S] ")
                self.status_text.insert(tk.END, timestamp + message + "\n")
                self.status_text.see(tk.END)
                self.status_text.update_idletasks()
        
            def load_sitemap(self):
                path = filedialog.askopenfilename(filetypes=[("XML files", "*.xml")])
                if path:
                    self.sitemap_path = path
                    self.label_sitemap.config(text=f"XMLファイル: {path}")
                    self.log("XMLファイルを選択しました。")
        
            def select_folder(self):
                folder = filedialog.askdirectory()
                if folder:
                    self.folder_path = folder
                    self.label_folder.config(text=f"対象フォルダ: {folder}")
                    self.log("対象フォルダを選択しました。")
        
            def parse_lastmod(self, text):
                """
                <lastmod>文字列をUTC aware datetimeに変換
                例:
                  2025-03-01
                  2025-03-01T12:34:56
                  2025-03-01T12:34:56Z
                  2025-03-01T12:34:56+09:00
                """
                if not text:
                    raise ValueError("lastmodが空です")
        
                s = text.strip()
        
                if s.endswith("Z"):
                    s = s[:-1] + "+00:00"
        
                dt = datetime.fromisoformat(s)
        
                if dt.tzinfo is None:
                    dt = dt.replace(tzinfo=timezone.utc)
                else:
                    dt = dt.astimezone(timezone.utc)
        
                return dt
        
            def format_utc_z(self, dt):
                """UTC datetime を sitemap向けの Z 形式に整形"""
                return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        
            def url_to_path(self, url):
                """
                URLからファイルパスを抽出
                例:
                  https://example.com/a/b.html -> a/b.html
                  https://example.com/         -> index.html
                """
                parsed = urlparse(url)
                path = unquote(parsed.path.lstrip("/"))
        
                if not path or path.endswith("/"):
                    path = os.path.join(path, "index.html") if path else "index.html"
        
                return path
        
            def update_sitemap(self):
                if not self.sitemap_path or not self.folder_path:
                    messagebox.showwarning("エラー", "XMLファイルとフォルダの両方を選択してください。")
                    return
        
                try:
                    self.log("XMLファイルを読み込み中...")
                    tree = ET.parse(self.sitemap_path)
                    root = tree.getroot()
                except Exception as e:
                    messagebox.showerror("読み込みエラー", f"XMLの読み込みに失敗しました。\n{e}")
                    self.log(f"❌ XML読み込み失敗: {e}")
                    return
        
                ns = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
                ET.register_namespace('', ns['ns'])
        
                updated = False
                updated_count = 0
                checked_count = 0
                missing_count = 0
                invalid_date_count = 0
        
                for url in root.findall("ns:url", ns):
                    loc = url.find("ns:loc", ns)
                    lastmod = url.find("ns:lastmod", ns)
        
                    if loc is None or not (loc.text and loc.text.strip()):
                        self.log("⚠ <loc> が見つからない、または空です。スキップします。")
                        continue
        
                    if lastmod is None:
                        lastmod = ET.SubElement(url, f"{{{ns['ns']}}}lastmod")
                        lastmod.text = ""
                        self.log(f"ℹ <lastmod> が無かったため新規追加: {loc.text.strip()}")
        
                    url_text = loc.text.strip()
                    url_path = self.url_to_path(url_text)
                    full_path = os.path.join(self.folder_path, *url_path.split("/"))
                    checked_count += 1
        
                    if not os.path.isfile(full_path):
                        self.log(f"⚠ ファイルが見つかりません: {url_path}")
                        missing_count += 1
                        continue
        
                    try:
                        file_mtime = os.path.getmtime(full_path)
                        file_dt = datetime.fromtimestamp(file_mtime, tz=timezone.utc)
                        file_dt_cmp = file_dt.replace(microsecond=0)
        
                        if not (lastmod.text and lastmod.text.strip()):
                            lastmod_dt_cmp = None
                        else:
                            lastmod_dt = self.parse_lastmod(lastmod.text)
                            lastmod_dt_cmp = lastmod_dt.replace(microsecond=0)
        
                    except ValueError as e:
                        self.log(f"⚠ 日付形式不正: {lastmod.text} ({e})")
                        invalid_date_count += 1
                        continue
                    except Exception as e:
                        self.log(f"⚠ 処理中エラー: {url_path} ({e})")
                        continue
        
                    if lastmod_dt_cmp is None or file_dt_cmp > lastmod_dt_cmp:
                        old_value = lastmod.text.strip() if lastmod.text else "(空)"
                        new_value = self.format_utc_z(file_dt_cmp)
                        lastmod.text = new_value
                        self.log(f"✅ 更新: {url_path}  [{old_value} -> {new_value}]")
                        updated = True
                        updated_count += 1
                    else:
                        self.log(f"🟢 最新です: {url_path}")
        
                if updated:
                    now = datetime.now().strftime("%Y%m%d-%H%M%S")
                    save_path = os.path.join(os.path.dirname(self.sitemap_path), f"sitemap-{now}.xml")
                    try:
                        tree.write(save_path, encoding="utf-8", xml_declaration=True)
                        self.log(f"💾 {updated_count} 件更新し、保存しました: {save_path}")
                        self.log(
                            f"📊 チェック: {checked_count} 件 / 更新: {updated_count} 件 / "
                            f"未検出: {missing_count} 件 / 日付不正: {invalid_date_count} 件"
                        )
                        messagebox.showinfo(
                            "保存完了",
                            f"{updated_count} 件の<lastmod>を更新し、保存しました。\n\n保存先:\n{save_path}"
                        )
                    except Exception as e:
                        self.log(f"❌ 保存失敗: {e}")
                        messagebox.showerror("保存エラー", f"XMLの保存に失敗しました。\n{e}")
                else:
                    self.log(
                        f"🔍 更新不要。チェック: {checked_count} 件 / "
                        f"未検出: {missing_count} 件 / 日付不正: {invalid_date_count} 件"
                    )
                    messagebox.showinfo("更新不要", "すべてのファイルが最新の状態です。")
        
        
        if __name__ == "__main__":
            root = tk.Tk()
            
            ##タイトルバーアイコン表示用
            data = '''R0lGODdhEAAQAIcAAOStRN6lPdyiOtmeONidN9ebNtaZNdSYNNCTMdCQLsiLLceJLMaHKsSFKsCCKcCBKb1+KLx+KL1+Jbx8I7Z6J7d4JLZ3I7R3JLN3JrV2IrF1JLR0ILJ0JbJ0JLJ0IqxvI6tvIapvIqpvIadrHqBmIJ9mHp9lHZxlH6BkG5tjH5tjHJphHJRhIZRhIJlgGphfGZVfG5RgHpNfHZNfHJZeGpNdHZVcG5FeHpFdH5BbGo9aG45aHIlaIJBZF4lWG4pTFohVGIRTG4ZSF4ZSFYJRF4JPFIFPF3xNGIBJD3xKFHpLFXlIE3xHEHhHE3dDDnZEEG5BEWlEGW1BEWxBEmw/EGk9DWg9EGg9D2w8DWc8EGU7DFw2DWE1C1w1DVU1EFo0DlYyDVUyDFcxDFUxC1ExDU8wD08uC0wtDEosDEktD0csDlQqBVQqBE4nBU4nBEsnBkoqC0cqDEcoCkUqDEIoDEEpD0EoDEApDz8oDkAnDj4qEzopFDooEzkoEz4nDj0nDkcjA0ElCUEiBkMhAz8mDDwlDDomDzkmEDklDTkkDjcmEzYmEjgkDjYjDTQjDzMjDzMiDjMhDTIkEzIiEDAhDzAhDi8kEyshEjEgDDAgDS4gDy4gDiwgDy4fDi4fDS0fDiofETceBzUdBjEdBy8cByweDiweDSweDCseDSscCioeDikeDyodDikdDykdDSocDCkcDSkcDDEbBjAaBisZBi0YBSsXBSgaCigXBigWBSgWBCcfEiceEScdECYdECYdDyccDiccDSccDCUcDyMcECIcEScbDCcbCyUbDCQbDiMbDyIbDyEbECQaDSQaDCMaDiIaDyEaDyYZCSMZDSIZDSIYCiEYDCEYCycWBSYVBCIXCSIWByAZDh8ZDx8XDB8XCh0ZEB0ZDx4YDR0YDxwYDhsYDx4XDB0XDRwXDRsXDh0VChwVChoWDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAEAI/wDLhSu2y5IiPnr29JF0iZcybul8OVJTRYQDBAEADGBQoceYO5WgBZOmS9CbNmvYuAE0KBCkTcCiwfI0So6PHDReuFBxooaXV826/fpUiMcNGTNgxGjBIgqrYNyIqcKExkgKDBEeQPhg48qcSa2UbcJDRsgKEBUmbEDxQ8oZRK7Qmbu2DZesWbVs5dKVDdstY9bGLQO16FCdNGXKqMmTSNOqZ+TADUslCgcHDRYyeAhB4kgmU8mYwdJGCs4XKE2SLLEiJg4jTsN8UfqzpQQFBQUEHGjQoYgZQ516PVKjxcSFBQYIJJAwYkgYP56ilWpEJwuVJ0yQOMHCBYydSFCdHSOjFSrIDh1AiCiZ0oVQrGnsznlbp+5bNWOoTJ0SdgwZNXEBAQA7''' 
            root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
            ####
            
            root.geometry("760x520")
            app = SitemapUpdater(root)
            root.mainloop()
    

📦 ダウンロード 🏠 ホームへ