Phase 0-2: Schema cleanup, typed relations, event-driven automation
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd) - Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py) - Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks - Fix .gitignore to track tools/ and .githooks/ - Fix git remote URL (remove plaintext token) - New wiki pages: 504 pages, 34 raw sources
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user