
RSSããªããã¥ãŒã¹ãµã€ãã®æŽæ°ãrobots.txtãšãµã€ããããããPythonã§èŠã€ããæ¹æ³
ãã£ãŒããªãŒããŒã®æåã®ä»äºã¯ãã£ãŒããèŠã€ããããšã§ããã倧ããªãã¥ãŒã¹ãµã€ãã»ã©ãèŠã€ããã¹ããã£ãŒãããããããããŸãããããã§ãããããŒãžãã¹ã¯ã¬ã€ãã³ã°ããã¡ã§ãããå£ããããããã«çžæã«ãåªãããããŸãããå®ã¯ã詊ãé çªã決ããŠããã°ãå¿ èŠãªæ å ±ã®ã»ãšãã©ã¯ãµã€ãèªèº«ãå ¬éããŠããŸãã<link> ã¿ã°ãrobots.txtããããŠãµã€ããããã§ãããã¹ã¯ãããã¢ããªosFoundryã¯ããããRustã§å®è£ ããŠããŸãïŒnews_feed.rs ã® news_discover_feedsãnews_discover_sectionsãnews_fetch_sectionïŒããã®èšäºã§ã¯åãæé ãæšæºã©ã€ãã©ãªã ãã®Pythonã§æžããå®åšãã8ã€ã®ãã¥ãŒã¹ãµã€ãã§åãããŠãããããäœãªã¯ãšã¹ãããããã©ãã«ãã©ãçããããæž¬ããŸããã
æåã®ãªã¯ãšã¹ãã®åã«æ±ºããŠããããš
èŠã€ããåŠçã¯ãåããŠèšªãããµã€ãã«ã¢ã¯ã»ã¹ããŸãããªã®ã§è¡åã®è¯ãã¯åŒã³åºãåŽã«ä»»ããããªã¯ãšã¹ããåºã颿°ã²ãšã€ã«éã蟌ããŠãããŸãã
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ãªã¯ãšã¹ãã¯1æ¬ãã€ãæäœ1ç§ãããŠããµã€ãããšã«äžéãèšããŸããUser-Agentã«ã¯èª°ãã¢ã¯ã»ã¹ããŠããããæžãïŒã¢ããªãéããã®ãšåãã§ãïŒãrobots.txt 以å€ã®URLã¯ãã¹ãŠ urllib.robotparser ã§ç¢ºèªããŠããåãã«è¡ããŸãã以äžã®æ°åã¯ããã®ãã°ããåºããŠããŸãã
robots.txt ã¯ããããããŒãžããå ã«èªã¿ãŸããçµæçã«ãã£ãŒãããã£ããµã€ãã§ã¯1ãªã¯ãšã¹ãäœèšã«ããããŸããã以éã®ãã¹ãŠã«ã«ãŒã«ãå¹ããŸãããåŸã®æé ã§å¿ èŠã«ãªããµã€ããããã®äžèЧãããã«æžããŠãããŸããããã«æ¢ãŸãæ¡ä»¶ã2ã€è¶³ããŠããŸããrobots.txt ããã®User-Agentã«ãµã€ãã®ã«ãŒããçŠæ¢ããŠããå ŽåãšããããããŒãžã401ã»403ã»429ãè¿ããå Žåã¯ããã®ãµã€ãã¯ããã§æã¡åããŸãã
æé 1: ãµã€ããç¥ãããŠãããã£ãŒã
RSSããããµã€ãã¯ãã»ãŒå¿ ãããŒãžã® <head> ã§ç¥ãããŠããŸãããããããŒãžã1ååã£ãŠHTMLã1åããŒã¹ããã ãã§ãã
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"])The VergeãTechCrunchãITmediaã¯ããã§çµãããŸãããã©ãã2ãªã¯ãšã¹ãïŒrobots.txtãšãããããŒãžïŒã§ãåŸ ã¡æéãå«ããŠ4ç§ä»¥å ã§ããITmediaã®ãã£ãŒãã¯å¥ã®ãã¹ãïŒrss.itmedia.co.jpïŒã«ãããŸãããã¹ãåœãŠæšéãããããç¥ãããŠããããªã³ã¯ãä¿¡ããã»ããããçç±ã®ã²ãšã€ã§ãã
æé 2: ãããã9ã€ã®ã¢ãã¬ã¹
äœãç¥ããããŠããªããšãã¯ãã¢ããªã¯ãããããã£ãŒãã®ãã¹ã9ã€è©ŠããRSSãAtomãšããŠèªããæåã®1ã€ã§æ¢ããŸãã/feedã/feed/ã/rssã/rss.xmlã/feed.xmlã/atom.xmlã/index.xmlã/feeds/posts/defaultã/rss/index.xml ã§ããããŒãã <link> ã¿ã°ãå ¥ãå¿ãããµã€ãã§ããWordPressã»Bloggerã»Hugoã»Ghostãªã©ã®æ¢å®ã®ãã£ãŒãã¯ããã§æŸããŸãã
ãã ãä»å詊ããå šåœçŽ3ã€ïŒæ¥çµã»ææ¥ã»èªå£²ïŒã§ã¯ã27åã®è©Šè¡ããã¹ãŠç©ºæ¯ãã§ããã26åã404ã1åïŒasahi.com/rssïŒã403ã§ããåãµã€ãã§ããã£ã13ã14ãªã¯ãšã¹ãã®ãã¡9åãããã§ããã倧æã®çºè¡å ã ããçžæãªãããã®9åããå ã«ãµã€ãããããèªãã°ããããã4ã5ãªã¯ãšã¹ãã§æžã¿ãŸããå°ããªããã°ãæ±ãæ±çšã®ããŒã«ãªãããã®9åã«ãæå³ããããŸãã
æé 3: robots.txtãæããŠããå
robots.txt ã«ã¯ Sitemap: ã®è¡ã§ãµã€ãããããåæãããŠããŸãããµã€ããããã¯ãå¥ã®ãµã€ãããããæã <sitemapindex> ããããŒãžã䞊ã¹ã <urlset> ã®ã©ã¡ããã§ããã¹ã¯ãªããã¯åæããããµã€ãããããæå€§4ã€èªã¿ãã€ã³ããã¯ã¹ãªã1éå±€ã ãäžããŠïŒåã¯æå€§5ã€ïŒã䜿ãããã®ãèŠã€ãã£ãæç¹ã§æ¢ããŸãã
æ°ãã€ãããã®ã¯ã䜿ãããã®å€å®ã§ããããæãã€ãã®ã¯Googleãã¥ãŒã¹ã®åå空é xmlns:news= ã§ããã®åœ¢åŒã¯æè¿å ¬éãããèšäºã®ããã®ãã®ã§ãããšãããæ¥çµãèŠããšãããã ãã§ã¯è¶³ããªãããšãããããŸããæ¥çµã® sections_sitemap.xml ã¯ãã®åå空éã宣èšããŠããŸããã187ä»¶ã®URLã®ãã¡184件㯠/business/startups/ ã®ãããªã»ã¯ã·ã§ã³ããŒãžã§ããåå空éãä¿¡ãããšãããã§ããããŸããããã§ã宣èšã§ã¯ãªãäžèº«ã®URLã§å€å®ããŸãã
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 Nonesection_share ãš article_share ã¯ã<loc> ã®ãã¹ã®ãã¡ãåŸã§åºãŠãã2ã€ã®åœ¢ã®å€å®ãéã£ããã®ã®å²åã§ãã70%ãšãããããå€ã¯ãã¢ããªãã»ã¯ã·ã§ã³ã®ãµã€ããããã«äœ¿ã£ãŠãããã®ãšåãã§ãã
åãµã€ãã®è¡ãçããå ã¯æ¬¡ã®ãšããã§ãã

2026幎11æã«ãMacãã1ãµã€ããã€é ã«æž¬ããŸãããç§æ°ã®ããªãã®éšåã¯1ç§ã®åŸ ã¡æéã§ããåäœã§ãã¡ã°ãé ãã£ãå¿çã¯6.9ç§ã§ãasahi.com/rss ã®403ã§ããã
èªå£²ã𿿥ã¯ããã以äžäœããããŸããããã¥ãŒã¹ãµã€ããããã¯èŠçŽã®ãªããã£ãŒãã®ãããªãã®ã§ãèšäºããšã«URLã»ã¿ã€ãã«ã»å ¬éæå»ãå ¥ã£ãŠããŸããææ¥ã®ã€ã³ããã¯ã¹ã¯ sitemap_national.xmlãsitemap_business.xml ã®ããã«éšéããšã«åãããŠããã®ã§ã远ãããããšãã¯ã»ã¯ã·ã§ã³ãéžã¶ã®ãšåãæèŠã§éšéãéžã¶ããšã«ãªããŸãã
æé 4: ã»ã¯ã·ã§ã³ããŒãžãããªããšã
æ¥çµã®ãµã€ããããã¯ãèšäºã®äžèЧã§ã¯ãªããµã€ãã®ç®æ¬¡ã§ããã»ã¯ã·ã§ã³ã®åœ¢ã®å€å®ã¯ãèšäºã®å€å®ã®è£è¿ãã«ãæ·±ãã®äžéãšçãé€å€ãªã¹ããè¶³ãããã®ã§ãã
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, ...èšäºãšã¿ãªãã®ã¯ããã¹ã«å¹Žæã®äžŠã³ïŒ/2026/11/06/ïŒããããæåŸã®éšåãæ°åãå«ã8æå以äžã®IDã«ãªã£ãŠããããŸãã¯ãã€ãã³ã3ã€ä»¥äžããã¹ã©ãã°ãã®ã©ããã§ããIDã®èŠåã1éå±€ç®ã«äœ¿ããªãã®ã¯ã/coronavirus-2026 ã®ãããªã¿ã°ããŒãžãŸã§é£²ã¿èŸŒãã§ããŸãããã§ãããã€ãã³ã2ã€ã§ãªã3ã€ã«ããŠããã®ã¯ã/news/tech-and-science/ ãã»ã¯ã·ã§ã³ã§ãããµã€ããå€ãããã§ãã
ã»ã¯ã·ã§ã³ã远ããããã®ã¯ã確èª1åã«ã€ã1ãªã¯ãšã¹ãã§ããã»ã¯ã·ã§ã³ããŒãžãåããåããã¹ãã§èšäºã®åœ¢ããããªã³ã¯ã ããæ®ããã¯ããããã®ãèœãšããŸãã
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]æ¥çµã®ã¹ã¿ãŒãã¢ããã®ããŒãžã«ã¯ãåããã¹ããžã®ãªã³ã¯ã271çš®é¡ãããŸããã圢ã®å€å®ãšã倿°æŽŸã®éå±€ã®èŠåïŒäžèŽãããã®ã®60%ãåã1éå±€ç®ãããã§ã¯ /article/ ã§å§ãŸã£ãŠããã°ããã以å€ã¯èœãšãïŒãéããšãæ®ã£ãã®ã¯20ä»¶ã§ããã¹ãŠ /article/DGXZQOUC026TI0S6A101C2000000/ ã®åœ¢ã§ãããèœã¡ã251ä»¶ã¯ãã¡ãã¥ãŒãã¿ã°ãããŒãã®ããŒãžã/loginã/searchãããŒã±ããããŒã¿ã®ããŒãžãªã©ã§ãã
ã¢ããªãåããšããã«ãã©ãçããŸããwww.nikkei.comã远å ãããšãã®ã»ã¯ã·ã§ã³éžæã«ã¯ã衚瀺äžïŒ 60 / 185 åã®ã»ã¯ã·ã§ã³ããšåºãŸãããµã€ããããã®184ä»¶ã«ããããããŒãžãè¶³ããæ°ã§ãããããããŒãžã¯ãããèªäœã«èšäºãžã®ãªã³ã¯ããããšç¢ºãããŠããå ããŠããŸãã

æ¥çµã®ã»ã¯ã·ã§ã³ã®ãµã€ããããããã¯ãããã²ãšã€ãããããšããããŸããã©ã®ã»ã¯ã·ã§ã³ã«ã <lastmod> ãããã187ä»¶ã®ãã¡157ä»¶ã¯çŽè¿24æéã«æŽæ°ãããŠããŸãããããããã®ã»ã¯ã·ã§ã³ã远ããªããå ã«ãã®ãã¡ã€ã«1ã€ãèªãã§ãåããŠããªãã»ã¯ã·ã§ã³ã®ããŒãžã¯åãã«è¡ããªãããšããç¯çŽãã§ããŸãã
æããããšã
APéä¿¡ã¯ãããããŒãžã«403ãè¿ããã®ã§ãã¹ã¯ãªããã¯2ãªã¯ãšã¹ãã§æ¢ãŸããŸããããã®èŠåãå ¥ããåã®çã§ã¯ããã®ããš13ãªã¯ãšã¹ããç¶ããããããã¹ãŠ403ã§ããããã€ã¿ãŒã® robots.txt ã¯èš±å¯ããã¯ããŒã©ãŒãååã§åæãããã以å€ã¯ãã¹ãŠçŠæ¢ããŠããã®ã§ã1ãªã¯ãšã¹ãã§æ¢ãŸãããããããŒãžã¯åãã«è¡ã£ãŠããŸããã
ã©ã¡ãããåé¿çãèããã¹ãè¬ã§ã¯ãããŸãããåä¹ã£ãã¯ã©ã€ã¢ã³ãã«403ãè¿ããµã€ãããrobots.txt ã§æã£ãŠãããµã€ãã¯ããã§ã«çããåºããŠããŸãããã£ãŒãã®ããŒã«ã«ã§ãããã¡ã°ã圹ã«ç«ã€æ¯ãèãã¯ããããã¯ã£ããäŒããŠæ¢ãŸãããšã§ããçžæã®ãµã€ãã«ãããè² æ ããå·¡åã§ã¯ãªã1ã2ãªã¯ãšã¹ãã§æžã¿ãŸãã
osFoundry ã®ãã¹ã¯ãããã¢ããªã¯ããã¥ãŒã¹ç»é¢ã§ãœãŒã¹ã远å ãããšãã«ãã®æ¢çŽ¢ãæå ã§å®è¡ããŸãããã¥ãŒã¹ç»é¢ã®ã»ãã®æ©èœã¯ãã¥ãŒã¹åãèŸŒã¿æ©èœã®ããŒãžã«ãŸãšãŸã£ãŠããŸãã