summaryrefslogtreecommitdiff
path: root/scripts/analyze_links.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/analyze_links.py')
-rw-r--r--scripts/analyze_links.py170
1 files changed, 170 insertions, 0 deletions
diff --git a/scripts/analyze_links.py b/scripts/analyze_links.py
new file mode 100644
index 00000000..598f4a94
--- /dev/null
+++ b/scripts/analyze_links.py
@@ -0,0 +1,170 @@
+import os
+import re
+import json
+from urllib.parse import urlparse
+from collections import Counter
+
+content_dir = "content"
+internal_link_pattern = re.compile(r"\[[^\]]*\]\((/[^)]+)\)")
+markdown_link_re = re.compile(r"\[.*?\]\((http.*?)\)")
+
+url_map = {} # file path -> canonical url
+alias_map = {} # alias url -> canonical url
+link_graph = {} # canonical url -> list of canonical urls
+title_map = {} # canonical url -> title
+external_links = Counter()
+
+def parse_frontmatter(filepath):
+ with open(filepath, encoding="utf-8") as f:
+ lines = f.readlines()
+
+ if not lines or lines[0].strip() != "---":
+ return {}, 0
+
+ frontmatter = {}
+ aliases, related_cards, mentioned_books = [], [], []
+ slug, draft, title = None, False, None
+ i = 1
+ while i < len(lines):
+ line = lines[i].strip()
+ if line == "---":
+ break
+ if ":" in line:
+ key, value = line.split(":", 1)
+ key = key.strip()
+ value = value.strip().strip('"').strip("'")
+
+ if key == "aliases":
+ aliases = []
+ i += 1
+ while i < len(lines) and lines[i].startswith(" - "):
+ aliases.append(lines[i].strip()[3:].strip())
+ i += 1
+ continue
+ elif key == "slug":
+ slug = value
+ elif key == "draft":
+ draft = value.lower() == "true"
+ elif key == "title":
+ title = value
+ elif key == "related_cards":
+ related_cards = []
+ i += 1
+ while i < len(lines) and lines[i].strip().startswith("- "):
+ related_cards.append(lines[i].strip()[2:].strip())
+ i += 1
+ continue
+ elif key == "mentioned_books":
+ mentioned_books = []
+ i += 1
+ while i < len(lines) and lines[i].strip().startswith("- "):
+ mentioned_books.append(lines[i].strip()[2:].strip())
+ i += 1
+ continue
+ i += 1
+
+ return {
+ "slug": slug,
+ "aliases": aliases,
+ "draft": draft,
+ "title": title,
+ "related_cards": related_cards,
+ "mentioned_books": mentioned_books
+ }, i + 1
+
+def get_canonical_url(filepath, frontmatter):
+ rel_path = os.path.relpath(filepath, content_dir)
+ dirname, filename = os.path.split(rel_path)
+ name = os.path.splitext(filename)[0]
+ slug = frontmatter.get("slug") or name
+ parts = dirname.split(os.sep)
+ return f"/{'/'.join(parts)}/{slug}/" if parts != [""] else f"/{slug}/"
+
+def resolve_url(url):
+ return alias_map.get(url, url)
+
+def resolve_url_from_dir(link, subdir):
+ target_dir = os.path.join(content_dir, subdir)
+ for path, canonical in url_map.items():
+ if path.startswith(target_dir):
+ name = os.path.splitext(os.path.basename(path))[0]
+ if name == link:
+ return canonical
+ return None
+
+def get_icon(url):
+ if url.startswith("/cards/"): return "📄"
+ if url.startswith("/posts/"): return "📜"
+ if url.startswith("/library/"): return "📖"
+ if url.startswith("/fictions/"): return "🧙‍♀️"
+ if url.startswith("/en/"): return "🇬🇧"
+ return "📁"
+
+def collect_urls():
+ for root, _, files in os.walk(content_dir):
+ for file in files:
+ if not file.endswith(".md") or file == "_index.md":
+ continue
+ path = os.path.join(root, file)
+ frontmatter, _ = parse_frontmatter(path)
+ if frontmatter.get("draft"):
+ continue
+
+ canonical_url = get_canonical_url(path, frontmatter)
+ url_map[path] = canonical_url
+ link_graph[canonical_url] = []
+ title = frontmatter.get("title") or canonical_url.strip("/").split("/")[-1]
+ title_map[canonical_url] = f"{get_icon(canonical_url)} {title}"
+
+ for alias in frontmatter.get("aliases", []):
+ alias_map[alias] = canonical_url
+
+def build_link_graph_and_external_links():
+ for path, canonical_url in url_map.items():
+ with open(path, encoding="utf-8") as f:
+ content = f.read()
+
+ # 内部链接
+ for match in internal_link_pattern.finditer(content):
+ link = match.group(1)
+ normalized = resolve_url(link)
+ if normalized and normalized in link_graph:
+ link_graph[canonical_url].append(normalized)
+
+ # 外部链接
+ for match in markdown_link_re.findall(content):
+ domain = urlparse(match).netloc
+ if domain and domain not in {"image.guhub.cn", "www.geedea.pro"}:
+ external_links[domain] += 1
+
+ # 相关卡片和图书
+ frontmatter, _ = parse_frontmatter(path)
+ for card in frontmatter.get("related_cards", []):
+ target = resolve_url_from_dir(card, "cards")
+ if target and target in link_graph:
+ link_graph[canonical_url].append(target)
+ for book in frontmatter.get("mentioned_books", []):
+ target = resolve_url_from_dir(book, "library")
+ if target and target in link_graph:
+ link_graph[canonical_url].append(target)
+
+def main():
+ collect_urls()
+ build_link_graph_and_external_links()
+
+ # 写入内部链接图
+ os.makedirs("public", exist_ok=True)
+ nodes = [{"id": url, "label": title_map[url], "url": url} for url in link_graph]
+ edges = [{"from": src, "to": tgt} for src, targets in link_graph.items() for tgt in targets]
+ with open("public/link-graph.json", "w", encoding="utf-8") as f:
+ json.dump({"nodes": nodes, "edges": edges}, f, indent=2, ensure_ascii=False)
+ print("✅ Link graph generated: public/link-graph.json")
+
+ # 写入外部链接统计
+ os.makedirs("data", exist_ok=True)
+ with open("data/externallinks.json", "w", encoding="utf-8") as f:
+ json.dump(external_links.most_common(), f, indent=2, ensure_ascii=False)
+ print("✅ External link data written to data/externallinks.json")
+
+if __name__ == "__main__":
+ main()