#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import io import re import datetime from pathlib import Path from typing import Dict, List, Set, Tuple from collections import defaultdict if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") class WikiLinkAnalyzer: def __init__(self, wiki_dir: Path): self.wiki_dir = wiki_dir self.outgoing_links: Dict[str, Set[str]] = defaultdict(set) self.incoming_links: Dict[str, Set[str]] = defaultdict(set) self.all_files: Set[str] = set() self.files_with_content: Dict[str, int] = {} def extract_wikilinks(self, content: str) -> List[str]: pattern = r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]" return re.findall(pattern, content) def normalize_link(self, link: str) -> str: link = link.strip() link = link.replace(" ", "-") return link def analyze_file(self, file_path: Path) -> Tuple[Set[str], int]: try: content = file_path.read_text(encoding="utf-8") links = self.extract_wikilinks(content) normalized_links = {self.normalize_link(l) for l in links} return normalized_links, len(content) except Exception as e: return set(), 0 def analyze_all(self): md_files = list(self.wiki_dir.glob("*.md")) print(f"分析 {len(md_files)} 个Wiki文件...") for md_file in md_files: filename = md_file.stem self.all_files.add(filename) outgoing, content_length = self.analyze_file(md_file) self.outgoing_links[filename] = outgoing self.files_with_content[filename] = content_length for link in outgoing: self.incoming_links[link].add(filename) def find_orphaned_files(self) -> List[str]: orphaned = [] for filename in self.all_files: if ( filename not in self.incoming_links or len(self.incoming_links[filename]) == 0 ): orphaned.append(filename) return orphaned def find_dangling_links(self) -> Dict[str, List[str]]: dangling = {} for filename, links in self.outgoing_links.items(): broken = [] for link in links: if link not in self.all_files: broken.append(link) if broken: dangling[filename] = broken return dangling def calculate_link_stats(self) -> Dict: stats = { "total_files": len(self.all_files), "files_with_outgoing": 0, "files_with_incoming": 0, "total_outgoing_links": 0, "total_incoming_links": 0, "avg_outgoing_per_file": 0, "avg_incoming_per_file": 0, } for links in self.outgoing_links.values(): stats["total_outgoing_links"] += len(links) for links in self.incoming_links.values(): stats["total_incoming_links"] += len(links) stats["files_with_outgoing"] = sum(1 for l in self.outgoing_links.values() if l) stats["files_with_incoming"] = sum(1 for l in self.incoming_links.values() if l) if stats["total_files"] > 0: stats["avg_outgoing_per_file"] = ( stats["total_outgoing_links"] / stats["total_files"] ) stats["avg_incoming_per_file"] = ( stats["total_incoming_links"] / stats["total_files"] ) return stats def get_top_linked_files(self, limit: int = 20) -> List[Tuple[str, int]]: files = [(f, len(links)) for f, links in self.incoming_links.items()] files.sort(key=lambda x: x[1], reverse=True) return files[:limit] def generate_report(self) -> str: stats = self.calculate_link_stats() orphaned = self.find_orphaned_files() dangling = self.find_dangling_links() top_linked = self.get_top_linked_files() report = f"""# Wiki双链分析报告 ## 总体统计 | 指标 | 值 | |------|-----| | 总文件数 | {stats["total_files"]} | | 有外链的文件 | {stats["files_with_outgoing"]} ({stats["files_with_outgoing"] / stats["total_files"] * 100:.1f}%) | | 有入链的文件 | {stats["files_with_incoming"]} ({stats["files_with_incoming"] / stats["total_files"] * 100:.1f}%) | | 外链总数 | {stats["total_outgoing_links"]} | | 入链总数 | {stats["total_incoming_links"]} | | 平均每文件外链 | {stats["avg_outgoing_per_file"]:.1f} | | 平均每文件入链 | {stats["avg_incoming_per_file"]:.1f} | ## 孤立文件(无入链) 共 {len(orphaned)} 个文件没有任何页面引用: """ for f in sorted(orphaned): content_size = self.files_with_content.get(f, 0) outgoing = len(self.outgoing_links.get(f, [])) report += f"- [[{f}]] ({content_size}字, {outgoing}个外链)\n" report += f""" ## 悬空链接(指向不存在的页面) """ if not dangling: report += "无悬空链接 ✅\n" else: for filename, links in sorted(dangling.items()): report += f"**{filename}**: {', '.join(links)}\n" report += f""" ## 高链接页面(入链最多) | 页面 | 入链数 | |------|--------| """ for filename, count in top_linked: report += f"| [[{filename}]] | {count} |\n" report += f""" ## 双向链接分析 """ bidirectional = 0 one_way = 0 no_links = 0 for filename in self.all_files: outgoing = self.outgoing_links.get(filename, set()) incoming = self.incoming_links.get(filename, set()) common = outgoing & incoming if common: bidirectional += 1 elif outgoing or incoming: one_way += 1 else: no_links += 1 report += f"""| 类型 | 数量 | 占比 | |------|------|------| | 双向链接 | {bidirectional} | {bidirectional / stats["total_files"] * 100:.1f}% | | 单向链接 | {one_way} | {one_way / stats["total_files"] * 100:.1f}% | | 无链接 | {no_links} | {no_links / stats["total_files"] * 100:.1f}% | ## 改进建议 ### 1. 消除孤立文件 - 为 {len(orphaned)} 个孤立文件添加相关引用 - 优先处理内容丰富的孤立文件 ### 2. 修复悬空链接 """ if dangling: total_dangling = sum(len(links) for links in dangling.values()) report += f"- 修复 {total_dangling} 个悬空链接\n" else: report += "- 无需修复 ✅\n" report += """ ### 3. 建立双向链接 - 增加页面间的相互引用 - 建议每个概念页面至少有2-3个双向链接 ### 4. 优化链接结构 - 入链最多的页面应该是核心概念页面 - 确保知识网络连通 --- *报告生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}* """ return report def save_report(self, output_path: Path): report = self.generate_report() output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: f.write(report) print(f"✅ 分析报告已保存: {output_path}") def main(): print("=" * 60) print("Wiki双链深度分析工具") print("=" * 60) script_dir = Path(__file__).parent project_root = script_dir.parent.parent.parent wiki_dir = project_root / "wiki" print(f"项目根目录: {project_root}") print(f"Wiki目录: {wiki_dir}") if not wiki_dir.exists(): print(f"❌ Wiki目录不存在: {wiki_dir}") return output_path = ( project_root / "raw" / "教育AI研究" / "outputs" / f"wiki_link_analysis_{datetime.date.today()}.md" ) analyzer = WikiLinkAnalyzer(wiki_dir) analyzer.analyze_all() analyzer.save_report(output_path) stats = analyzer.calculate_link_stats() orphaned = len(analyzer.find_orphaned_files()) dangling = len(analyzer.find_dangling_links()) print() print("=" * 60) print("分析完成") print("=" * 60) print(f"总文件数: {stats['total_files']}") print(f"平均每文件外链: {stats['avg_outgoing_per_file']:.1f}") print(f"平均每文件入链: {stats['avg_incoming_per_file']:.1f}") print(f"孤立文件数: {orphaned}") print(f"悬空链接数: {dangling}") print("=" * 60) if __name__ == "__main__": main()