『追放された俺でも、サイトマップ更新で無双する件。』は、既存の sitemap.xml を元に、指定フォルダ内のファイルの更新日時をチェックして
『追放された俺でも、サイトマップ更新で無双する件。』の最新版ソースコード全文です:
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()