diff --git a/.claude/skills/wiki-sync-translate/SKILL.md b/.claude/skills/wiki-sync-translate/SKILL.md index 14cf3b6..425918a 100644 --- a/.claude/skills/wiki-sync-translate/SKILL.md +++ b/.claude/skills/wiki-sync-translate/SKILL.md @@ -75,10 +75,18 @@ scripts/wiki_sync.py --title "<页面名称>" --since <上次同步时间> --run } ``` -变更类型: -- `replaced`: 替换,`old_line` 表示需要修改的行号 -- `added`: 新增,`new_line` 表示插入位置 -- `removed`: 删除,`old_line` 表示要删除的行 +变更类型: +- `replaced`: 替换,`old_line` 表示需要修改的行号 +- `added`: 新增,`new_line` 表示插入位置 +- `removed`: 删除,`old_line` 表示要删除的行 + +**配对算法说明:** +从 v2 版本开始,脚本使用**内容相似度**来判断 `removed` 和 `added` 是否应该配对为 `replaced`: +- 相似度阈值设为 0.5(50%) +- 只有当 removed 和 added 的内容相似度 ≥ 0.5 时,才会被配对 +- 这避免了将完全不同内容的行错误配对 + +**注意:** 如果 `old_line` 和 `new_line` 差距很大但内容相似(如行号从171变到193),这通常意味着中间有其他行被插入或删除,需要仔细检查变更是否真的相关。 ### Step 4: 更新中文文档 diff --git a/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py index a0f3a30..d273eca 100644 --- a/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py +++ b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py @@ -180,9 +180,26 @@ def parse_diff_to_changes(diff_text): return changes -def group_changes_by_line(changes): +def calculate_similarity(text1, text2): + """ + 计算两段文本的相似度(0-1) + 使用 difflib.SequenceMatcher + """ + if not text1 or not text2: + return 0.0 + # 去除首尾空白后比较 + t1 = text1.strip() + t2 = text2.strip() + return difflib.SequenceMatcher(None, t1, t2).ratio() + + +def group_changes_by_line(changes, similarity_threshold=0.5): """ 将变更按行号分组,将连续的删除和添加合并为替换操作 + + 改进:使用内容相似度来判断 removed 和 added 是否应该配对 + - 如果 removed 和 added 的内容相似度 >= threshold,才配对为 replaced + - 否则分别标记为 removed 和 added """ # 先收集所有的删除和添加 removed_by_line = {} # old_line -> content @@ -194,27 +211,40 @@ def group_changes_by_line(changes): elif c["type"] == "added": added_by_line[c["new_line"]] = c["new_content"] - # 尝试将删除和添加配对 + # 使用贪心算法,基于内容相似度进行配对 grouped = [] used_added = set() + used_removed = set() - for old_line, old_content in sorted(removed_by_line.items()): - # 找一个未使用的添加行来配对 - paired = False - for new_line, new_content in sorted(added_by_line.items()): + # 第一步:找出所有高相似度的配对 + pairings = [] + for old_line, old_content in removed_by_line.items(): + for new_line, new_content in added_by_line.items(): if new_line not in used_added: - grouped.append({ - "type": "replaced", - "old_line": old_line, - "new_line": new_line, - "old_content": old_content, - "new_content": new_content - }) - used_added.add(new_line) - paired = True - break + similarity = calculate_similarity(old_content, new_content) + if similarity >= similarity_threshold: + pairings.append((similarity, old_line, new_line, old_content, new_content)) - if not paired: + # 按相似度降序排序,优先处理最相似的配对 + pairings.sort(key=lambda x: x[0], reverse=True) + + # 第二步:贪心配对 + for similarity, old_line, new_line, old_content, new_content in pairings: + if old_line not in used_removed and new_line not in used_added: + grouped.append({ + "type": "replaced", + "old_line": old_line, + "new_line": new_line, + "old_content": old_content, + "new_content": new_content, + "_similarity": round(similarity, 2) # 调试用,可选 + }) + used_removed.add(old_line) + used_added.add(new_line) + + # 第三步:处理未配对的 removed + for old_line, old_content in sorted(removed_by_line.items()): + if old_line not in used_removed: grouped.append({ "type": "removed", "old_line": old_line, @@ -223,7 +253,7 @@ def group_changes_by_line(changes): "new_content": None }) - # 添加未配对的新增行 + # 第四步:处理未配对的 added for new_line, new_content in sorted(added_by_line.items()): if new_line not in used_added: grouped.append({ diff --git a/CLAUDE.md b/CLAUDE.md index e69de29..0bb8fe3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# PD2 Wiki Sync Tool - Claude Code 配置 + +## 项目概述 + +Project Diablo 2 Wiki 中英文同步工具。 + +## 环境要求 + +### Python 虚拟环境 + +**必须**先激活虚拟环境再运行任何 Python 脚本: + +```bash +source venv/bin/activate +``` + +### 依赖 + +```bash +pip install -r requirements.txt +``` + +## 使用方式 + +本工具通过 Claude Code 的 Skill 方式使用,不提供命令行直接调用。 + +### 基本用法 + +启动 Claude Code 后,使用 `/wiki-sync-translate` 命令: + +``` +/wiki-sync-translate <描述你想要同步的内容> +``` + +### 使用示例 + +| 场景 | 命令示例 | +|------|---------| +| 同步所有变更 | `/wiki-sync-translate 同步从2026-01-02以来的所有变更` | +| 同步单个页面 | `/wiki-sync-translate 同步 Maps 页面` | +| 同步特定时间范围的页面 | `/wiki-sync-translate 同步从2026-01-01开始的 General Changes 页面` | +| 同步最近变更 | `/wiki-sync-translate 同步最近一周的所有变更` | + +## Skill 位置 + +`.claude/skills/wiki-sync-translate/` + +## 输出目录 + +`wiki_sync_output/<时间戳>/` + +## 注意事项 + +- 中文 Wiki 行号必须与英文完全对应 +- 变更配对使用内容相似度算法(阈值 0.5) diff --git a/README.md b/README.md index 112ecc0..a0a5d09 100644 --- a/README.md +++ b/README.md @@ -1,257 +1,119 @@ -# Wiki Sync Tool - Enhanced Version +# PD2 Wiki Sync Tool -一个用于同步和跟踪 MediaWiki 网站变更的 Python 工具,支持双语对比和精确的行号定位。 +Project Diablo 2 Wiki 中英文同步工具,用于同步英文 Wiki 变更到中文翻译文档。 -## 功能特点 +## 前置要求 -- 🔄 自动同步 MediaWiki 网站的最新更改 -- 📝 生成带语法高亮的 HTML diff 文件,清晰展示变更内容 -- 💾 保存页面完整内容供离线查阅 -- ⏰ 支持增量同步,只获取上次同步后的新变更 -- 🔍 支持按时间点或特定页面进行同步 -- 📁 智能文件组织,区分新页面和变更页面 -- 🌐 **新增**:自动同步中文翻译版本 -- 🎯 **新增**:精确的行号映射,点击英文行自动定位到中文对应行 -- 📊 **新增**:生成精美的双语对比网页 -- 🎨 **新增**:现代化的UI设计,支持同步滚动和高亮显示 -- 🧭 **新增**:导航浮窗功能,快速跳转到变更点 -- 🗂️ **新增**:分类存储,主文件和参考文件分开管理 +### 1. 安装 Claude Code -## 安装 - -1. 确保你已经安装了 Python 3.6+ -2. 克隆此仓库: - ```bash - git clone - cd wiki-sync-tool - ``` -3. 安装依赖: - ```bash - pip install requests python-dotenv - ``` - -## 配置 - -创建一个 `.env` 文件并配置你的 MediaWiki API 地址: - -```env -# 英文版 Project Diablo 2 Wiki API 地址 -WIKI_API_URL_EN=https://wiki.projectdiablo2.com/w/api.php - -# 中文版 Project Diablo 2 Wiki API 地址 -WIKI_API_URL_CN=https://wiki.projectdiablo2.cn/w/api.php -``` - -或者复制提供的示例配置文件: +本工具需要通过 [Claude Code](https://github.com/anthropics/claude-code) 来使用。 ```bash -cp .env.example .env +# 安装 Claude Code +npm install -g @anthropic-ai/claude-code +``` + +### 2. 克隆仓库 + +```bash +git clone +cd sync-pd2-wiki +``` + +### 3. 创建虚拟环境并安装依赖 + +```bash +python -m venv venv +source venv/bin/activate # Linux/macOS +# 或 +.\venv\Scripts\activate # Windows + +pip install -r requirements.txt ``` ## 使用方法 -### 基本全量同步 +### 启动 Claude Code -同步自上次运行以来的所有更改: +在项目目录下运行: ```bash -python sync.py --run +claude ``` -首次运行时,会同步过去 24 小时内的更改。 +### 使用 Skill 同步 Wiki -### 指定时间起点同步 +在 Claude Code 中使用 `/wiki-sync-translate` 命令: -从指定时间开始同步: +#### 示例 1:同步特定时间的所有页面变更 -```bash -python sync.py --since 2025-11-28T00:00:00Z --run +``` +/wiki-sync-translate 帮我同步从2026-01-02以来的所有变更 ``` -### 同步特定页面 +#### 示例 2:同步特定页面 -只同步特定页面的最新更改: - -```bash -python sync.py --title "Amazon Basin" --run +``` +/wiki-sync-translate 同步 Maps 页面的变更 ``` -### 同步特定页面并更新时间戳 +#### 示例 3:同步特定时间范围的单个页面 -同步特定页面并在完成后更新全局时间戳: - -```bash -python sync.py --title "Amazon Basin" --update-timestamp --run +``` +/wiki-sync-translate 同步从2026-01-01开始的 General Changes 页面 ``` -### 查看帮助 +#### 示例 4:同步最近的变更 -```bash -python sync.py --help +``` +/wiki-sync-translate 同步最近一周的所有变更 ``` ## 输出文件 -每次运行都会在 `wiki_sync_output` 目录下创建一个以时间戳命名的子目录,并按照以下结构组织文件: +同步完成后,文件会保存在 `wiki_sync_output/<时间戳>/` 目录: ``` -wiki_sync_output/[时间戳]/ -├── new_pages/ # 新创建的页面目录 -│ └── [页面名].full.txt # 只保存页面的完整内容 +wiki_sync_output/20260322_145110/ +├── new_pages/ # 新创建的页面 +│ └── [页面名].full.txt # 英文完整内容 │ -└── changed_pages/ # 有变更的页面目录 - ├── [页面名].full.txt # 页面的最新完整内容 - ├── [页面名].cn.txt # 对应的中文翻译内容 - ├── [页面名].comparison.html # 双语对比页面(带导航功能) - │ - └── files/ # 参考文件目录 - ├── [页面名].diff.html # 官方 MediaWiki diff HTML - ├── [页面名].diff.txt # 文本格式的 diff - └── [页面名].old.txt # 历史版本内容 -``` - -### 文件说明 - -**主要文件(日常使用)**: -- `.full.txt` - 页面的完整内容(新页面和变更页面都有) -- `.cn.txt` - 中文翻译内容(仅变更页面) -- `.comparison.html` - 双语对比网页(仅变更页面) - -**参考文件(技术细节)**: -- `.diff.html` - MediaWiki原生HTML diff -- `.diff.txt` - 文本格式的diff(类似git diff) -- `.old.txt` - 页面的历史版本内容 - -### 双语对比网页特性 - -生成的双语对比网页具有以下高级功能: - -1. **导航浮窗** 🧭 - - 左侧浮窗显示所有变更点的导航列表 - - 点击导航项可快速跳转到对应位置 - - 滚动页面时自动高亮当前可见的变更 - - 支持使用 Ctrl+1~9 快速跳转到对应编号的变更 - -2. **精确行号映射**: - - 英文diff中的每一行都标注了对应的中文行号 - - 点击英文任意行,自动高亮并滚动到对应的中文行 - -3. **交互式体验**: - - 鼠标悬停时预览对应的中文行 - - 点击时高亮显示对应关系 - - 平滑滚动动画效果 - -4. **视觉设计**: - - 现代化的UI设计 - - 标准的diff配色(绿色新增、红色删除、灰色未变更) - - 响应式布局,支持移动端查看 - -5. **字符级diff高亮**: - - 显示精确到字符级别的变更差异 - - 新增内容使用绿色背景高亮 - - 删除内容使用红色背景高亮 - -### Diff 文件示例 - -文本diff格式示例: - -``` ---- old_version -+++ new_version -@@ -10,7 +10,7 @@ - This is line 10 --This line will be removed -+This line will be added - This is line 12 -``` - -HTML diff特性: -- 绿色背景表示新增内容 -- 红色背景表示删除内容 -- 左侧彩色竖线标识变更类型 -- +/- 标记清晰显示变更位置 -- 删除内容带有删除线效果 - -## 技术细节 - -### 智能文件组织策略 - -工具根据页面是否为新建来组织文件: - -- **新页面**:判断依据是diff文本以"新创建页面"开头 - - 只保存 `.full.txt` 文件 - - 不生成中英对比(因为不存在变更) - -- **变更页面**: - - 保存主要文件在 `changed_pages` 目录 - - 将技术细节文件放在 `files` 子目录 - -### 行号解析机制 - -工具使用自定义的diff解析器,能够精确提取: -- Hunk头部的行号范围信息 -- 每一行变更对应的旧版本和新版本行号 -- 增删改上下文行的准确位置 - -### 导航系统实现 - -1. **动态生成导航项**: - - 扫描所有 `.change-block` 元素 - - 自动提取变更类型(新增/替换/删除) - - 生成预览文本和行号信息 - -2. **模板字符串修复**: - - 使用正确的 `"${changeId}"` 语法 - - 确保选择器能够正确匹配DOM元素 - -### 中文页面搜索策略 - -1. 首先尝试精确匹配页面标题 -2. 如果失败,则进行模糊搜索 -3. 支持标题中的空格和特殊字符处理 - -### 使用说明 - -**查看新页面**: -- 进入 `new_pages` 目录 -- 查看对应的 `.full.txt` 文件获取页面完整内容 - -**处理变更页面**: -- 主要查看 `changed_pages` 目录下的文件 -- 使用 `.comparison.html` 进行双语对比和导航 -- 查阅 `.cn.txt` 获取中文翻译参考 -- 参考 `.full.txt` 获取最新的英文内容 - -**技术细节参考**: -- 查看 `files` 目录了解详细的变更信息 -- 包含官方diff、文本diff和历史版本 - -### 目录组织示例 - -``` -wiki_sync_output/ -├── 20251211_221150/ -│ ├── new_pages/ # 新创建的页面 -│ │ └── New_Page-20251211_220123-12345.full.txt -│ │ -│ └── changed_pages/ # 有变更的页面 -│ ├── To Do-20251211_124712-21941.full.txt -│ ├── To Do-20251211_124712-21941.cn.txt -│ ├── To Do-20251211_124712-21941.comparison.html -│ │ -│ └── files/ # 参考文件 -│ ├── To Do-20251211_124712-21941.diff.html -│ ├── To Do-20251211_124712-21941.diff.txt -│ └── To Do-20251211_124712-21941.old.txt +├── changed_pages/ # 有变更的页面 +│ ├── [页面名].full.txt # 英文最新版本 +│ ├── [页面名].cn.txt # 中文原文 +│ ├── [页面名].comparison.json # 结构化变更信息 +│ └── [页面名].old.txt # 英文历史版本 │ -└── 20251211_193512/ - └── ... +└── result_pages/ # 更新后的中文文档 + └── [页面名].cn.txt # 可直接复制到 Wiki ``` -## 许可证 +## 变更配对算法 -MIT License +工具使用**内容相似度算法**来判断变更是否应该配对: -## 贡献 +- 相似度阈值:0.5(50%) +- 只有内容相似度 ≥ 50% 的行才会被配对为 `replaced` +- 这避免了将完全不同内容的行错误配对 -欢迎提交 Issue 和 Pull Request。 \ No newline at end of file +## 目录结构 + +``` +sync-pd2-wiki/ +├── .claude/ +│ └── skills/ +│ └── wiki-sync-translate/ +│ ├── SKILL.md # Skill 定义 +│ └── scripts/ +│ └── wiki_sync.py # 同步脚本 +│ +├── wiki_sync_output/ # 同步输出 +├── references/ # 参考文件 +├── requirements.txt # Python 依赖 +├── CLAUDE.md # Claude Code 配置 +└── README.md # 本文件 +``` + +## License + +MIT diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f49b6b8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.28.0 +python-dotenv>=1.0.0 diff --git a/sync.py b/sync.py deleted file mode 100644 index 17504bc..0000000 --- a/sync.py +++ /dev/null @@ -1,1601 +0,0 @@ -# -*- coding: utf-8 -*- -""" -MediaWiki 最近变更同步工具 - 增强版 -支持: -1. 正常全量同步(无参数) -2. 手动指定时间起点:--since 2025-11-28T00:00:00Z -3. 只同步单个页面:--title "页面名称" -4. 单个页面时可选更新全局时间戳:--update-timestamp -5. 获取历史版本并生成diff -6. 同步中文翻译版本 -7. 生成双语对比网页 -""" - -import os -import argparse -from pathlib import Path -from datetime import datetime -import requests -from dotenv import load_dotenv -import difflib -import json -import re -from urllib.parse import quote - -# ==================== 配置区 ==================== -load_dotenv() -WIKI_API_URL_EN = os.getenv("WIKI_API_URL_EN", "https://wiki.projectdiablo2.com/w/api.php") -WIKI_API_URL_CN = os.getenv("WIKI_API_URL_CN", "https://wiki.projectdiablo2.cn/w/api.php") -OUTPUT_DIR = Path("wiki_sync_output") -OUTPUT_DIR.mkdir(exist_ok=True) - -# 全局变量,存储本次执行的输出目录 -CURRENT_OUTPUT_DIR = None - -LAST_TIMESTAMP_FILE = "last_sync_timestamp.txt" - -SESSION_EN = requests.Session() -SESSION_EN.headers.update({ - "User-Agent": "WikiSyncTool/4.0 (your-email@example.com; MediaWiki Sync Bot)" -}) - -SESSION_CN = requests.Session() -SESSION_CN.headers.update({ - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" -}) -SESSION_CN.trust_env = False # 禁用环境变量代理 -# ================================================ - -def load_last_timestamp(): - if not os.path.exists(LAST_TIMESTAMP_FILE): - return None - with open(LAST_TIMESTAMP_FILE, encoding="utf-8") as f: - return f.read().strip() - -def save_last_timestamp(ts): - with open(LAST_TIMESTAMP_FILE, "w", encoding="utf-8") as f: - f.write(ts) - -def get_recent_changes(since): - """获取自 since 时间后每个页面的最新 revid(自动去重)""" - params = { - "action": "query", - "list": "recentchanges", - "rcprop": "title|ids|timestamp", - "rctype": "edit|new", - "rcdir": "newer", - "rcstart": since, - "rclimit": 500, - "format": "json" - } - latest = {} - while True: - try: - r = SESSION_EN.get(WIKI_API_URL_EN, params=params) - r.raise_for_status() - response_data = r.json() - if "error" in response_data: - raise Exception(response_data["error"]) - for rc in response_data.get("query", {}).get("recentchanges", []): - latest[rc["title"]] = (rc["revid"], rc["timestamp"]) - if "continue" not in response_data: - break - params.update(response_data["continue"]) - except Exception as e: - print(f"获取最近更改时出错: {e}") - break - return latest - -def get_old_revid(title, end_time): - """获取 ≤ end_time 的最后一次修订的 revid(用于 fromrev)""" - params = { - "action": "query", - "prop": "revisions", - "titles": title, - "rvprop": "ids|timestamp", - "rvlimit": 1, - "rvdir": "older", - "rvstart": end_time, - "format": "json" - } - try: - r = SESSION_EN.get(WIKI_API_URL_EN, params=params).json() - pages = r["query"]["pages"] - page = next(iter(pages.values())) - if "revisions" not in page: - print(f" 页面 '{title}' 在指定时间前没有找到修订版本") - return None - - revisions = page["revisions"] - if len(revisions) >= 1: - return revisions[0]["revid"] - print(f" 页面 '{title}' 在指定时间前没有找到修订版本") - return None - except Exception as e: - print(f"获取旧版本ID时出错: {e}") - return None - -def get_page_content(wiki_url, session, title, revid=None): - """获取页面完整内容""" - params = { - "action": "query", - "prop": "revisions", - "titles": title, - "rvprop": "content|timestamp|ids", - "rvslots": "main", - "format": "json" - } - if revid: - params["rvstartid"] = revid - params["rvendid"] = revid - - try: - r = session.get(wiki_url, params=params).json() - pages = r["query"]["pages"] - page = next(iter(pages.values())) - - if "revisions" not in page: - return None, None, None - - rev = page["revisions"][0] - content = rev["slots"]["main"]["*"] - timestamp = rev["timestamp"] - rev_id = rev["revid"] - - return content, timestamp, rev_id - except Exception as e: - print(f"获取页面内容时出错: {e}") - return None, None, None - -def generate_text_diff(old_text, new_text): - """生成类似git diff的文本diff""" - if not old_text: - return "新创建页面" - - old_lines = old_text.splitlines(keepends=True) - new_lines = new_text.splitlines(keepends=True) - - differ = difflib.unified_diff( - old_lines, - new_lines, - lineterm='\n' - ) - - return ''.join(differ) - -def parse_diff_with_line_numbers(diff_text): - """解析diff文本,提取详细的行号信息""" - if not diff_text or diff_text.startswith("新创建页面"): - return [] - - parsed_lines = [] - current_old_line = 0 - current_new_line = 0 - in_hunk = False - - for line in diff_text.splitlines(): - if line.startswith('@@'): - # 解析hunk头部,格式如: @@ -start,count +start,count @@ - import re - match = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line) - if match: - old_start = int(match.group(1)) - old_count = int(match.group(2)) if match.group(2) else 1 - new_start = int(match.group(3)) - new_count = int(match.group(4)) if match.group(4) else 1 - - current_old_line = old_start - current_new_line = new_start - in_hunk = True - - parsed_lines.append({ - 'type': 'hunk', - 'content': line, - 'old_start': old_start, - 'old_count': old_count, - 'new_start': new_start, - 'new_count': new_count, - 'old_line': None, - 'new_line': None - }) - else: - parsed_lines.append({ - 'type': 'other', - 'content': line, - 'old_line': None, - 'new_line': None - }) - elif line.startswith('---') or line.startswith('+++'): - # 文件头信息 - continue - - #parsed_lines.append({ - # 'type': 'header', - # 'content': line, - # 'old_line': None, - # 'new_line': None - #}) - elif in_hunk: - if line.startswith('-'): - # 删除的行 - parsed_lines.append({ - 'type': 'removed', - 'content': line[1:], # 去掉开头的 '-' - 'old_line': current_old_line, - 'new_line': None - }) - current_old_line += 1 - elif line.startswith('+'): - # 新增的行 - parsed_lines.append({ - 'type': 'added', - 'content': line[1:], # 去掉开头的 '+' - 'old_line': None, - 'new_line': current_new_line - }) - current_new_line += 1 - elif line.startswith(' '): - # 未变更的行 - parsed_lines.append({ - 'type': 'context', - 'content': line[1:], # 去掉开头的 ' ' - 'old_line': current_old_line, - 'new_line': current_new_line - }) - current_old_line += 1 - current_new_line += 1 - else: - # 其他行(如空行) - parsed_lines.append({ - 'type': 'other', - 'content': line, - 'old_line': None, - 'new_line': None - }) - else: - # 不在任何hunk中的行 - parsed_lines.append({ - 'type': 'other', - 'content': line, - 'old_line': None, - 'new_line': None - }) - - return parsed_lines - -def search_chinese_page(title): - """在中文wiki中搜索对应的页面""" - # 首先尝试直接获取页面(因为中文wiki禁用了标题搜索) - params = { - "action": "query", - "prop": "revisions", - "titles": title, - "rvprop": "ids", - "format": "json" - } - - try: - r = SESSION_CN.get(WIKI_API_URL_CN, params=params, timeout=10) - data = r.json() - pages = data.get("query", {}).get("pages", {}) - - for page_id, page_info in pages.items(): - # page_id 为负数表示页面不存在 - if page_id != "-1" and "missing" not in page_info: - return page_info.get("title") - - except Exception as e: - print(f"搜索中文页面时出错: {e}") - - return None - -def create_diff_html(title, en_diff, en_old_lines, en_new_lines, cn_content=None): - """创建双语对比的HTML页面 - Word批注风格,英文变更直接显示在对应中文行右侧""" - # 准备中文内容行 - cn_lines = [] - if cn_content: - cn_lines = cn_content.splitlines() - - # 解析diff并获取行号信息 - parsed_diff = parse_diff_with_line_numbers(en_diff) if en_diff else [] - - # 构建行号到diff内容的映射 - 科学处理连续diff块 - en_changes_by_line = {} - blank_lines_to_insert = {} # 记录需要在某行前插入的空白行及其对应的新增内容 - - if parsed_diff: - i = 0 - while i < len(parsed_diff): - # 收集连续的diff块 - diff_block = [] - start_index = i - - # 收集连续的添加/删除操作(跳过hunk和header) - while i < len(parsed_diff): - item = parsed_diff[i] - if item['type'] in ['added', 'removed']: - diff_block.append(item) - elif item['type'] in ['hunk', 'header'] or item['type'] == 'context': - if diff_block: # 如果已经有diff块,就停止 - break - i += 1 - - # 处理连续的diff块 - 改进的连续匹配算法 - if diff_block: - # 使用新的匹配算法:连续的减号和加号应该按顺序匹配 - removed_items = [] - added_items = [] - - # 分离删除和新增项目 - for item in diff_block: - if item['type'] == 'removed': - removed_items.append(item) - elif item['type'] == 'added': - added_items.append(item) - - # 进行匹配:连续块里的每一个减都应该和后续的加形成匹配替换 - match_index = 0 - for removed_item in removed_items: - if match_index < len(added_items): - # 匹配成功:形成替换 - target_line = removed_item['old_line'] - if target_line not in en_changes_by_line: - en_changes_by_line[target_line] = [] - - en_changes_by_line[target_line].append({ - 'type': 'replaced', - 'old_content': removed_item['content'], - 'new_content': added_items[match_index]['content'] - }) - match_index += 1 - else: - # 没有匹配的加:这是删除 - target_line = removed_item['old_line'] - if target_line not in en_changes_by_line: - en_changes_by_line[target_line] = [] - - en_changes_by_line[target_line].append({ - 'type': 'removed', - 'content': removed_item['content'] - }) - - # 处理剩余的加(没有匹配的减):这是新增,应该在左侧空行 - if match_index < len(added_items): - # 找到基准行号(使用最后一个删除行的行号,如果没有则使用第一个新增的行号) - base_line = None - if removed_items: - base_line = removed_items[-1]['old_line'] - elif added_items: - base_line = added_items[match_index]['new_line'] - len(added_items) + match_index - - if base_line: - remaining_additions = added_items[match_index:] - blank_lines_to_insert[base_line + 1] = remaining_additions - - # 继续处理剩余项 - else: - i += 1 - - # HTML转义函数 - def html_escape(text): - if not text: - return "" - return (str(text) - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace('"', """) - .replace("'", "'")) - - def generate_inline_diff(old_text, new_text): - """生成GitHub风格的行内字符级diff""" - if not old_text or not new_text: - return html_escape(new_text or "") - - escaped_old = html_escape(old_text) - escaped_new = html_escape(new_text) - - # 使用difflib进行字符级别的比较 - differ = difflib.SequenceMatcher(None, escaped_old, escaped_new) - - result = [] - for tag, i1, i2, j1, j2 in differ.get_opcodes(): - if tag == 'equal': - # 相同的部分 - result.append(escaped_new[j1:j2]) - elif tag == 'replace': - # 替换的部分:删除的用红色背景,新增的用绿色背景 - deleted = escaped_old[i1:i2] - added = escaped_new[j1:j2] - result.append(f'{deleted}') - result.append(f'{added}') - elif tag == 'delete': - # 删除的部分用红色背景 - deleted = escaped_old[i1:i2] - result.append(f'{deleted}') - elif tag == 'insert': - # 新增的部分用绿色背景 - added = escaped_new[j1:j2] - result.append(f'{added}') - - return ''.join(result) - - def generate_clean_new_content(old_text, new_text): - """生成干净的新内容,只显示新增部分的高亮,不包含删除部分""" - if not old_text or not new_text: - return html_escape(new_text or "") - - escaped_old = html_escape(old_text) - escaped_new = html_escape(new_text) - - # 使用difflib进行字符级别的比较 - differ = difflib.SequenceMatcher(None, escaped_old, escaped_new) - - result = [] - for tag, i1, i2, j1, j2 in differ.get_opcodes(): - if tag == 'equal': - # 相同的部分 - result.append(escaped_new[j1:j2]) - elif tag == 'replace': - # 替换的部分:只显示新增的内容(绿色高亮),跳过删除的内容 - added = escaped_new[j1:j2] - result.append(f'{added}') - elif tag == 'delete': - # 删除的部分:跳过,不显示 - continue - elif tag == 'insert': - # 新增的部分用绿色背景 - added = escaped_new[j1:j2] - result.append(f'{added}') - - return ''.join(result) - - # 收集变更块信息用于导航 - change_blocks = [] - change_block_id = 0 - - # 生成HTML - html = f''' - - - - - Wiki Diff: {title} - - - -
-

{title}

-
- 英文Wiki: wiki.projectdiablo2.com - {f' | 中文Wiki: wiki.projectdiablo2.cn' if cn_content else ''} -
-
- - - -
-
中文翻译(含英文变更批注)
-
-''' - - # 添加中文内容和英文变更批注 - if cn_content: - for i, line in enumerate(cn_lines, 1): - # 检查是否需要在此行之前插入空白行 - if i in blank_lines_to_insert: - additions_list = blank_lines_to_insert[i] - change_block_id += 1 - - # 添加变更块到导航列表 - first_content = additions_list[0].get('content', '') if additions_list else '' - preview_text = first_content[:50] + "..." if len(first_content) > 50 else first_content - change_blocks.append({ - 'id': change_block_id, - 'line': i, - 'type': '新增', - 'preview': preview_text, - 'count': len(additions_list) - }) - - for idx, addition_item in enumerate(additions_list): - html += f'
' - html += '
' - html += ' ' # 不显示行号 - html += f'(新增英文内容占位)' - html += '
' - - # 为空白行添加对应的新增批注 - addition_content = addition_item.get('content', '') # 只取内容字段 - escaped_addition = html_escape(addition_content) - html += '
' - html += f'
' - html += f'
新增
' - html += f'
{escaped_addition}
' - html += '
' - html += '
' - - html += '
' - - escaped_line = html_escape(line) - has_changes = i in en_changes_by_line - changes = en_changes_by_line.get(i, []) - - # 判断是否为空行 - is_empty = not line.strip() - - # 如果有变更(除了新增),添加到导航列表 - if has_changes and any(change['type'] in ['replaced', 'removed'] for change in changes): - change_block_id += 1 - preview_text = line[:50] + "..." if len(line) > 50 else line - change_type = "替换" if any(change['type'] == 'replaced' for change in changes) else "删除" - - change_blocks.append({ - 'id': change_block_id, - 'line': i, - 'type': change_type, - 'preview': preview_text, - 'count': 1 - }) - - html += f'
' - html += f'
' - html += f'{i}' - html += f'{escaped_line if not is_empty else "(空行)"}' - html += '
' - - # 添加英文变更批注(只显示替换和删除操作,新增操作已经在空白行中显示) - if has_changes: - html += '
' - for change in changes: - if change['type'] == 'added': - # 新增内容已经在空白行中显示,这里跳过 - continue - elif change['type'] == 'removed': - escaped_change = html_escape(change['content']) - html += f'
' - html += f'
删除
' - html += f'
{escaped_change}
' - html += '
' - elif change['type'] == 'replaced': - # 生成干净的新内容(只显示新增部分的高亮,不包含删除部分) - clean_new_content = generate_clean_new_content(change['old_content'], change['new_content']) - html += f'
' - html += f'
替换
' - html += f'
{html_escape(change["old_content"])}
' - html += f'
{clean_new_content}
' - html += '
' - html += '
' - - html += '
' - else: - html += '
未找到对应的中文翻译页面
' - - # 调试日志:打印change_blocks信息 - print(f"DEBUG: Final change_blocks length = {len(change_blocks)}") - for i, block in enumerate(change_blocks): - print(f"DEBUG: Final block {i}: {block}") - - html += ''' -
-
- - - -''' - - return html - -def save_files(title, diff_text, full_text, timestamp, note="", revid=None, cn_content=None, old_full_text=None): - global CURRENT_OUTPUT_DIR - - # 确保本次执行的输出目录已经创建 - if CURRENT_OUTPUT_DIR is None: - current_time_str = datetime.now().strftime("%Y%m%d_%H%M%S") - CURRENT_OUTPUT_DIR = OUTPUT_DIR / current_time_str - CURRENT_OUTPUT_DIR.mkdir(exist_ok=True) - # 创建子目录 - (CURRENT_OUTPUT_DIR / "new_pages").mkdir(exist_ok=True) - (CURRENT_OUTPUT_DIR / "changed_pages").mkdir(exist_ok=True) - print(f"创建本次执行的输出目录: {CURRENT_OUTPUT_DIR}") - - safe_title = "".join(c if c.isalnum() or c in " -_." else "_" for c in title) - time_str = timestamp[:19].replace("-", "").replace(":", "").replace("T", "_") - base_filename = f"{safe_title}-{time_str}-{revid}" if revid else f"{safe_title}-{time_str}" - - # 判断是否为新页面 - is_new_page = diff_text and diff_text.startswith("新创建页面") - - if is_new_page: - # 新页面:只保存完整内容到 new_pages 目录 - target_dir = CURRENT_OUTPUT_DIR / "new_pages" - print(f" 检测到新页面,只保存完整内容") - - # 保存最新完整内容 - full_file = target_dir / f"{base_filename}.full.txt" - if full_text: - try: - with open(full_file, "w", encoding="utf-8") as f: - f.write(full_text) - print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)} (新页面完整内容)") - except Exception as e: - print(f" → 保存文件 {full_file} 时出错: {e}") - - else: - # 变更页面:保存主要文件到 changed_pages 目录,其他文件到 files 目录 - target_dir = CURRENT_OUTPUT_DIR / "changed_pages" - files_dir = target_dir / "files" - files_dir.mkdir(exist_ok=True) - - # 1. 保存最新完整内容(主文件) - full_file = target_dir / f"{base_filename}.full.txt" - if full_text: - try: - with open(full_file, "w", encoding="utf-8") as f: - f.write(full_text) - print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)} (完整内容)") - except Exception as e: - print(f" → 保存文件 {full_file} 时出错: {e}") - - # 2. 保存中文翻译内容(主文件) - if cn_content: - cn_file = target_dir / f"{base_filename}.cn.txt" - try: - with open(cn_file, "w", encoding="utf-8") as f: - f.write(cn_content) - print(f" → 已保存: {cn_file.relative_to(OUTPUT_DIR)} (中文翻译)") - except Exception as e: - print(f" → 保存文件 {cn_file} 时出错: {e}") - - # 3. 创建双语对比HTML页面(主文件) - en_new_lines = full_text.splitlines() if full_text else [] - en_old_lines = old_full_text.splitlines() if old_full_text else [] - - comparison_html = create_diff_html(title, diff_text, en_old_lines, en_new_lines, cn_content) - comparison_file = target_dir / f"{base_filename}.comparison.html" - try: - with open(comparison_file, "w", encoding="utf-8") as f: - f.write(comparison_html) - print(f" → 已保存: {comparison_file.relative_to(OUTPUT_DIR)} (双语对比页面)") - except Exception as e: - print(f" → 保存文件 {comparison_file} 时出错: {e}") - - - if diff_text: - text_diff_file = files_dir / f"{base_filename}.diff.txt" - try: - with open(text_diff_file, "w", encoding="utf-8") as f: - f.write(diff_text) - print(f" → 已保存: {text_diff_file.relative_to(OUTPUT_DIR)} (文本diff - 参考)") - except Exception as e: - print(f" → 保存文件 {text_diff_file} 时出错: {e}") - - if old_full_text: - old_full_file = files_dir / f"{base_filename}.old.txt" - try: - with open(old_full_file, "w", encoding="utf-8") as f: - f.write(old_full_text) - print(f" → 已保存: {old_full_file.relative_to(OUTPUT_DIR)} (历史版本 - 参考)") - except Exception as e: - print(f" → 保存文件 {old_full_file} 时出错: {e}") - -def process_single_page(title, since_time, update_timestamp=False): - """只处理单个页面""" - print(f"正在单独处理页面:{title}") - - # 获取当前最新 revid - try: - latest_content, latest_ts, latest_revid = get_page_content(WIKI_API_URL_EN, SESSION_EN, title) - if latest_content is None: - print("页面不存在或被删除") - return None - - # 获取旧 revid - old_revid = get_old_revid(title, since_time) - - # 初始化变量 - diff_text = None - old_content = None - cn_content = None - - if old_revid: - # 获取历史版本内容 - old_content, old_ts, _ = get_page_content(WIKI_API_URL_EN, SESSION_EN, title, old_revid) - - if old_content is not None: - # 生成文本diff - diff_text = generate_text_diff(old_content, latest_content) - print(f" 生成了文本diff ({len(diff_text)} 字符)") - else: - print(f" 无法获取历史版本内容") - diff_text = "新创建页面" - else: - # 新页面 - print(" 这是新创建的页面") - diff_text = "新创建页面" - - # 搜索对应的中文页面 - print(" 搜索中文翻译...") - cn_title = search_chinese_page(title) - if cn_title: - print(f" 找到中文页面: {cn_title}") - cn_content, cn_ts, cn_revid = get_page_content(WIKI_API_URL_CN, SESSION_CN, cn_title) - if cn_content: - print(f" 获取中文内容成功 ({len(cn_content)} 字符)") - else: - print(" 无法获取中文页面内容") - else: - print(" 未找到对应的中文翻译页面") - - # 保存所有文件 - save_files(title, diff_text, latest_content, latest_ts, "", latest_revid, cn_content, old_content) - - if update_timestamp: - save_last_timestamp(latest_ts) - print(f"已更新全局时间戳 → {latest_ts}") - - return latest_ts - except Exception as e: - print(f"处理页面 '{title}' 时出错: {e}") - return None - -def process_all_pages_since(since_time): - """处理自指定时间以来的所有页面变更""" - print("正在获取最近变更列表...") - changes = get_recent_changes(since_time) - if not changes: - print("没有发现任何变更") - return - - latest_global_ts = since_time - for title, (latest_revid, ts) in changes.items(): - print(f"\n处理:{title}") - # 复用单页处理逻辑 - page_latest_ts = process_single_page(title, since_time) - - if page_latest_ts and page_latest_ts > latest_global_ts: - latest_global_ts = page_latest_ts - - save_last_timestamp(latest_global_ts) - print(f"\n全量同步完成!本次最新时间戳已更新为:{latest_global_ts}") - print(f"文件保存在:{CURRENT_OUTPUT_DIR.resolve() if CURRENT_OUTPUT_DIR else OUTPUT_DIR.resolve()}") - -def main(): - parser = argparse.ArgumentParser(description="MediaWiki 同步工具 - 增强版支持双语对比") - parser.add_argument("--since", type=str, help="强制从指定时间开始同步,格式如 2025-11-28T00:00:00Z") - parser.add_argument("--title", type=str, help="只同步指定的单个页面标题") - parser.add_argument("--update-timestamp", action="store_true", - help="在单页模式下,完成后仍然更新全局 last_sync_timestamp.txt") - parser.add_argument("--run", action="store_true", - help="执行同步操作(必须提供此参数才能真正执行同步)") - - args = parser.parse_args() - - # 如果没有提供 --run 参数,则显示帮助信息并退出 - if not args.run: - parser.print_help() - return - - # 确定实际使用的 since 时间 - if args.since: - since_time = args.since - print(f"使用命令行指定的时间起点:{since_time}") - else: - since_time = load_last_timestamp() - if not since_time: - from datetime import timedelta - since_time = (datetime.utcnow() - timedelta(days=1)).isoformat(timespec='seconds') + "Z" - print(f"使用上次记录的时间起点:{since_time}") - - # 单页面模式 - if args.title: - process_single_page(args.title.strip(), since_time, args.update_timestamp) - return - - # 全量模式 - 使用复用的单页处理逻辑 - process_all_pages_since(since_time) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/target.txt b/target.txt deleted file mode 100644 index 10057a1..0000000 --- a/target.txt +++ /dev/null @@ -1,8 +0,0 @@ -根据README,sync.py中会获取wiki.projectdiablo2.com的变更并拉下原文的全量文件。现在需要增加以下功能: - -1. 获取英文wiki的最新页面full(已实现),获取其上个版本的全量full(用上一步中的old_revid拉取). -2. 如果该网页是新增(现有逻辑),则只保存最新文件full即可。 -3. 如果该wiki是变更,则用历史版本的full文件,和最新的文件进行diff,得到diff文件。此处用模仿git diff的Python或库进行。得到diff文件。 - -4. 对于该页面标题,去另一网站(wiki.projectdiablo2.cn)搜索并拉下原文,这是同步的翻译后的中文网站。需要注意的在两个网站的页面ID不会一致,但页面title是保持一致的,同时绝大部分页面经过了翻译。 -5. 保存一个网页,生成diff文件的网页展示,页面设计美观精致,使用现代化的CSS/JS。将页面竖向分成两栏,左边为英文源码的两个版本DIFF,右侧为同样行号的中文源码。 注意行号是保持一致的。绝大多数页面的中文的行号是完全一致的可以放心对比。diff的展示同样要有标准的红色、绿色等. \ No newline at end of file