.github/scripts/check_links.py (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
#!/usr/bin/env python3
import concurrent.futures
import re
import socket
import urllib.error
import urllib.request
README = "README.md"
MARKER = " **(link no longer exists)**"
# Match a list entry and capture its primary URL: "- [Name](https://...) - ..."
# Table rows ("|...") and Table-of-Contents anchors ("#...") never match.
ENTRY_RE = re.compile(r"^- \[[^\]]+\]\((https?://[^)]+)\)")
UA = (
"Mozilla/5.0 (compatible; awesome-indieweb-linkcheck/1.0; "
"+https://github.com/runawaydevil/awesome-indieweb)"
)
TIMEOUT = 20
def is_dead(url):
"""Return True only on a definitive dead signal (404/410/NXDOMAIN)."""
req = urllib.request.Request(url, method="GET", headers={"User-Agent": UA})
try:
urllib.request.urlopen(req, timeout=TIMEOUT)
return False
except urllib.error.HTTPError as e:
return e.code in (404, 410)
except urllib.error.URLError as e:
# DNS resolution failure means the domain itself is gone.
return isinstance(e.reason, socket.gaierror)
except Exception:
# Anything else is inconclusive; never flag on uncertainty.
return False
def strip_marker(line):
"""Remove a trailing marker (if present), preserving the newline."""
body = line.rstrip("\n")
if body.endswith(MARKER):
nl = "\n" if line.endswith("\n") else ""
return body[: -len(MARKER)] + nl
return line
def main():
with open(README, encoding="utf-8") as f:
lines = f.readlines()
# (line index, url) for every entry, ignoring any existing marker.
targets = []
for i, line in enumerate(lines):
m = ENTRY_RE.match(strip_marker(line))
if m:
targets.append((i, m.group(1)))
urls = {url for _, url in targets}
dead = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as ex:
futures = {ex.submit(is_dead, u): u for u in urls}
for fut in concurrent.futures.as_completed(futures):
url = futures[fut]
try:
dead[url] = fut.result()
except Exception:
dead[url] = False
changed, newly_dead, recovered = 0, [], []
for i, url in targets:
base = strip_marker(lines[i])
was_marked = lines[i] != base
if dead.get(url):
nl = "\n" if base.endswith("\n") else ""
updated = base.rstrip("\n") + MARKER + nl
if updated != lines[i]:
lines[i], changed = updated, changed + 1
newly_dead.append(url)
elif was_marked:
lines[i], changed = base, changed + 1
recovered.append(url)
if changed:
with open(README, "w", encoding="utf-8", newline="") as f:
f.writelines(lines)
print(f"Checked {len(urls)} unique links across {len(targets)} entries.")
print(f"Newly marked dead: {len(newly_dead)}")
for u in newly_dead:
print(f" DEAD {u}")
print(f"Recovered (marker removed): {len(recovered)}")
for u in recovered:
print(f" ALIVE {u}")
print(f"Lines changed: {changed}")
if __name__ == "__main__":
main()
|