save a version

This commit is contained in:
2026-03-31 16:10:20 +08:00
parent 40b55b3c32
commit e3efa5dbc1

View File

@@ -10,15 +10,15 @@ import zipfile
import json import json
import argparse import argparse
from pathlib import Path from pathlib import Path
from typing import Optional, List, Dict, Tuple from typing import Optional, List, Dict, Tuple, Union
from datetime import datetime from datetime import datetime
import time import time
import re import re
def extract_all_zips( def extract_all_zips(
source_dir: str | Path = "data", source_dir: Union[str, Path] = "data",
output_dir: Optional[str | Path] = None, output_dir: Optional[Union[str, Path]] = None,
delete_original: bool = True delete_original: bool = True
) -> List[Path]: ) -> List[Path]:
""" """
@@ -89,99 +89,112 @@ def read_docx_content(doc_path: Path) -> List[str]:
def read_doc_content(doc_path: Path) -> List[str]: def read_doc_content(doc_path: Path) -> List[str]:
"""读取 .doc 格式 Word 文档内容""" """读取 .doc 格式 Word 文档内容(使用 LibreOffice"""
lines = [] import subprocess
import tempfile
# 方法 1: 尝试使用 antiword soffice_paths = [
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
'/Applications/LibreOffice.app/Contents/MacOS/libreoffice',
'soffice',
]
print(f"🔍 正在查找 LibreOffice...")
soffice_cmd = None
for path in soffice_paths:
print(f" 检查:{path}")
try:
import os
if os.path.isfile(path):
print(f" ✓ 文件存在")
result = subprocess.run([path, '--version'], capture_output=True, timeout=10)
print(f" ✓ 版本检查返回码:{result.returncode}")
print(f" 输出:{result.stdout.decode()[:100]}")
if result.returncode == 0:
soffice_cmd = path
print(f"✓ 使用 LibreOffice: {path}")
break
except Exception as e:
print(f" ✗ 错误:{e}")
continue
if not soffice_cmd:
print("❌ LibreOffice 未安装或无法访问")
return []
print(f"📄 开始转换文件:{doc_path}")
try: try:
import subprocess with tempfile.TemporaryDirectory() as tmpdir:
result = subprocess.run( print(f"📁 临时目录:{tmpdir}")
['antiword', str(doc_path)], result = subprocess.run(
capture_output=True, [soffice_cmd, '--headless', '--convert-to', 'txt', '--outdir', tmpdir, str(doc_path)],
text=True, capture_output=True,
timeout=30 timeout=60
) )
if result.returncode == 0: print(f"🔄 转换返回码:{result.returncode}")
lines = [line.strip() for line in result.stdout.split('\n') if line.strip()] if result.returncode != 0:
return lines print(f"✗ 转换失败:{result.stderr.decode()[:300]}")
except FileNotFoundError: return []
pass txt_file = Path(tmpdir) / (doc_path.stem + '.txt')
if txt_file.exists():
with open(txt_file, 'r', encoding='utf-8', errors='ignore') as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
print(f"✓ 成功读取 {len(lines)} 行内容")
return lines
else:
print(f"✗ 转换后的文本文件不存在")
except Exception as e: except Exception as e:
print(f"antiword 读取失败:{e}") print(f"✗ LibreOffice 读取失败:{e}")
# 方法 2: 尝试使用 textract print(f"⚠️ 无法读取 .doc 文件 ({doc_path.name})")
try: return []
import subprocess
result = subprocess.run(
['textract', str(doc_path), '--output', '/dev/stdout'],
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
lines = [line.strip() for line in result.stdout.split('\n') if line.strip()]
return lines
except FileNotFoundError:
pass
except Exception as e:
print(f"textract 读取失败:{e}")
# 方法 3: 尝试使用 libreoffice
try: try:
import subprocess
import tempfile import tempfile
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
result = subprocess.run( result = subprocess.run(
['libreoffice', '--headless', '--convert-to', 'txt', '--outdir', tmpdir, str(doc_path)], [soffice_cmd, '--headless', '--convert-to', 'txt', '--outdir', tmpdir, str(doc_path)],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=30 timeout=60
) )
if result.returncode == 0: if result.returncode != 0:
txt_file = Path(tmpdir) / (doc_path.stem + '.txt') print(f"LibreOffice 转换失败:{result.stderr}")
if txt_file.exists(): return None
with open(txt_file, 'r', encoding='utf-8', errors='ignore') as f: txt_file = Path(tmpdir) / (doc_path.stem + '.txt')
lines = [line.strip() for line in f.readlines() if line.strip()] if txt_file.exists():
return lines with open(txt_file, 'r', encoding='utf-8', errors='ignore') as f:
except FileNotFoundError: lines = [line.strip() for line in f.readlines() if line.strip()]
pass return lines
except Exception as e: except Exception as e:
print(f"libreoffice 读取失败:{e}") print(f"LibreOffice 读取失败:{e}")
print(f"⚠️ 无法读取 .doc 文件 ({doc_path.name}),请安装以下工具之一:") print(f"⚠️ 无法读取 .doc 文件 ({doc_path.name})")
print(f" - antiword: brew install antiword")
print(f" - textract: pip install textract")
print(f" - LibreOffice: brew install --cask libreoffice")
return [] return []
def find_word_doc_recursive(base_dir: Path) -> Optional[Path]: def find_word_doc_recursive(base_dir: Path) -> Optional[Path]:
"""递归查找 base_dir 下任意层级的 Word 文档 (.docx 优先)""" """递归查找 base_dir 下符合目标的 Word 文档
# 先找 .docx 文件
for item in base_dir.rglob("*.docx"):
return item
# 再找 .doc 文件
for item in base_dir.rglob("*.doc"):
return item
return None
def create_config_file(word_content: List[str], config_file: Optional[Path] = None) -> Path: 目标目录条件:包含一个 Word 文件 (.docx 或 .doc) 以及两个子目录
"""将 Word 内容保存为配置文件""" 如果当前目录只有一个子目录且无 Word 文件,则继续深入查找
if config_file is None: """
config_file = Path("data") / "word_config.json" def find_target_dir(current_dir: Path) -> Optional[Path]:
items = list(current_dir.iterdir())
subdirs = [item for item in items if item.is_dir()]
word_files = [item for item in items if item.suffix.lower() in ('.docx', '.doc')]
config = {"word_content": word_content} # 目标条件:恰好一个 Word 文件且至少两个子目录
with open(config_file, 'w', encoding='utf-8') as f: if len(word_files) == 1 and len(subdirs) >= 2:
json.dump(config, f, ensure_ascii=False, indent=2) return word_files[0]
return config_file
# 只有一个子目录且没有 Word 文件,继续深入
if len(subdirs) == 1 and len(word_files) == 0:
return find_target_dir(subdirs[0])
def save_word_config(config: Dict, config_file: Optional[Path] = None) -> None: return None
"""保存 Word 配置"""
if config_file is None: return find_target_dir(base_dir)
config_file = Path("word_config.json")
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
def get_files_with_times(folder: Path) -> List[Tuple[Path, datetime]]: def get_files_with_times(folder: Path) -> List[Tuple[Path, datetime]]:
@@ -192,129 +205,69 @@ def get_files_with_times(folder: Path) -> List[Tuple[Path, datetime]]:
ctime = datetime.fromtimestamp(f.stat().st_ctime) ctime = datetime.fromtimestamp(f.stat().st_ctime)
files.append((f, ctime)) files.append((f, ctime))
files.sort(key=lambda x: x[1]) files.sort(key=lambda x: x[1])
print(files)
return files return files
def normalize_filename(filename: str, url_domain: str) -> str:
"""标准化文件名"""
name = Path(filename).stem
# 移除扩展名括号中的内容,如 (共 27 张 PPT)
name = re.sub(r'\s*\(共 \d+ 张 [^\)]+\)', '', name)
# 学科网文件名可能有 + 前缀
if url_domain == 'www.zxxk.com':
name = re.sub(r'^\+?(\d+\.\d+)', r'\1', name)
name = re.sub(r'\+([^.]+)\.', r'\1.', name)
return name.strip()
def extract_datetime_from_text(text: str) -> Optional[datetime]:
"""从文本中提取日期时间"""
patterns = [
r'(\d{4})/(\d{1,2})/(\d{1,2})\s+(\d{1,2}):(\d{2})',
r'(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})',
]
for pattern in patterns:
match = re.search(pattern, text)
if match:
try:
return datetime(
int(match.group(1)),
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
int(match.group(5))
)
except:
pass
return None
def match_content_to_file(
content: str,
all_files: List[Tuple[Path, datetime]],
url_domain: str,
threshold: float = 0.6
) -> Optional[Tuple[Path, datetime, float]]:
"""根据内容和名称匹配文件"""
best_match = None
best_score = 0
content_normalized = content.lower().strip()
content_clean = re.sub(r'https?://\S+', '', content_normalized)
content_clean = re.sub(r'\d{4}[/\-]\d{1,2}[/\-]\d{1,2}\s+\d{1,2}:\d{2}', '', content_clean)
for file_path, ctime in all_files:
filename = file_path.name
filename_normalized = normalize_filename(filename, url_domain)
filename_clean = filename_normalized.lower()
common_words = set(filename_clean.split()) & set(content_clean.split())
name_score = len(common_words) / (len(filename_clean.split()) + len(content_clean.split()) - len(common_words) + 1)
contained_score = 0
if filename_normalized in content_clean or content_clean in filename_normalized:
contained_score = 0.5
elif filename_normalized[:30] in content_clean[:100] or content_clean[:100] in filename_normalized[:30]:
contained_score = 0.3
score = name_score * 0.5 + contained_score
if score > threshold and score > best_score:
best_score = score
best_match = (file_path, ctime, score)
return best_match
def build_file_associations( def build_file_associations(
word_content: List[str], word_content: List[str],
twole_folder: Path, twole_folder: Path,
zxxk_folder: Path, zxxk_folder: Path,
) -> Dict: ) -> Dict:
"""构建文件关联关系""" """构建文件关联关系"""
result = { result = []
"21世纪教育网": [],
"学科网": [],
"word_items": []
}
twole_files = get_files_with_times(twole_folder) twole_files = get_files_with_times(twole_folder)
zxxk_files = get_files_with_times(zxxk_folder) zxxk_files = get_files_with_times(zxxk_folder)
for i, content in enumerate(word_content): for i, content in enumerate(word_content):
content_dt = extract_datetime_from_text(content) # 按 \t 分割成段
parts = content.split('\t')
# 匹配 21世纪教育网 if len(parts) < 6:
match = match_content_to_file(content, twole_files, 'www.21cnjy.com') continue
if match:
result["21世纪教育网"].append({ # 提取两组数据:标题、网址、时间
"index": i, title1, url1, time1 = parts[0].strip(), parts[1].strip(), parts[2].strip()
"file": match[0].name, title2, url2, time2 = parts[3].strip(), parts[4].strip(), parts[5].strip()
"ctime": match[1].isoformat(),
"similarity": round(match[2], 2), # 第一组:根据网址匹配文件
"word_content": content if 'www.21cnjy.com' in url1 and twole_files:
file_path, ctime = twole_files.pop(0)
result.append({
"filename": file_path.name,
"title": title1,
"url": url1,
"time": time1
}) })
twole_files.remove((match[0], match[1])) elif 'www.zxxk.com' in url1 and zxxk_files:
file_path, ctime = zxxk_files.pop(0)
# 匹配学科网 result.append({
match = match_content_to_file(content, zxxk_files, 'www.zxxk.com') "filename": file_path.name,
if match: "title": title1,
result["学科网"].append({ "url": url1,
"index": i, "time": time1
"file": match[0].name,
"ctime": match[1].isoformat(),
"similarity": round(match[2], 2),
"word_content": content
}) })
zxxk_files.remove((match[0], match[1]))
# 添加 Word 条目 # 第二组:根据网址匹配文件
result["word_items"].append({ if 'www.21cnjy.com' in url2 and twole_files:
"index": i, file_path, ctime = twole_files.pop(0)
"content": content, result.append({
"time": content_dt.isoformat() if content_dt else None "filename": file_path.name,
}) "title": title2,
"url": url2,
"time": time2
})
elif 'www.zxxk.com' in url2 and zxxk_files:
file_path, ctime = zxxk_files.pop(0)
result.append({
"filename": file_path.name,
"title": title2,
"url": url2,
"time": time2
})
return result return {"items": result}
def print_associations(associations: Dict, max_items: int = 10) -> None: def print_associations(associations: Dict, max_items: int = 10) -> None:
@@ -323,47 +276,42 @@ def print_associations(associations: Dict, max_items: int = 10) -> None:
print("文件关联关系预览") print("文件关联关系预览")
print("=" * 80) print("=" * 80)
print("\n【21世纪教育网】") print(f"\n{len(associations.get('items', []))} 条记录")
for item in associations["21世纪教育网"][:max_items]: for item in associations.get('items', [])[:max_items]:
print(f" {item['ctime'][:19]} | {item['file']}") print(f"\n {item.get('filename', '')}")
print(f" {item['word_content'][:60]}...") print(f" 标题:{item.get('title', '')[:50]}...")
print(f" URL: {item.get('url', '')}")
print("\n【学科网】") print(f" 时间:{item.get('time', '')}")
for item in associations["学科网"][:max_items]:
print(f" {item['ctime'][:19]} | {item['file']}")
print(f"{item['word_content'][:60]}...")
def main() -> None: def main():
"""主函数""" parser = argparse.ArgumentParser(description='数据文件关联处理程序')
parser = argparse.ArgumentParser(description='文件关联处理程序') parser.add_argument('--extract', action='store_true', help='解压压缩文件')
parser.add_argument('--unpack', action='store_true', help='解压文件')
parser.add_argument('--associate', action='store_true', help='建立文件关联') parser.add_argument('--associate', action='store_true', help='建立文件关联')
parser.add_argument('--base-dir', type=str, help='基础目录(包含 Word 文档的目录)') parser.add_argument('--base-dir', type=str, help='基础目录')
parser.add_argument('--delete-original', action='store_true', help='解压后删除原文件')
args = parser.parse_args() args = parser.parse_args()
# 解压文件 # 解压压缩文件
if args.unpack: if args.extract:
extract_all_zips("data") source_dir = Path(args.base_dir) if args.base_dir else Path("data")
extract_all_zips(source_dir, delete_original=args.delete_original)
return return
# 建立文件关联 # 建立文件关联
if args.associate: if args.associate:
base_dir = Path(args.base_dir) if args.base_dir else Path("data") base_dir = Path(args.base_dir) if args.base_dir else Path("data")
# 递归查找 Word 文档
word_doc = find_word_doc_recursive(base_dir) word_doc = find_word_doc_recursive(base_dir)
if not word_doc: if not word_doc:
print("⚠️ 未找到 Word 文档 (.doc 或 .docx),请先解压文件") print("⚠️ 未找到 Word 文档 (.doc 或 .docx),请先解压文件")
return return
# 读取 Word 内容
word_content = read_word_content(word_doc) word_content = read_word_content(word_doc)
if not word_content: if not word_content:
print("⚠️ 无法读取 Word 文档内容") print("⚠️ 无法读取 Word 文档内容")
return return
# 确定关联目录Word 文档的父目录,包含学科网和 21 世纪教育网)
base_dir = word_doc.parent base_dir = word_doc.parent
print(f"✓ 找到 Word 文档:{word_doc}") print(f"✓ 找到 Word 文档:{word_doc}")
print(f" 关联目录:{base_dir}") print(f" 关联目录:{base_dir}")
@@ -381,10 +329,9 @@ def main() -> None:
with open(output_file, 'w', encoding='utf-8') as f: with open(output_file, 'w', encoding='utf-8') as f:
json.dump(associations, f, ensure_ascii=False, indent=2) json.dump(associations, f, ensure_ascii=False, indent=2)
print(f"\n✓ 关联关系已保存到:{output_file}") print(f"\n✓ 关联关系已保存到:{output_file}")
print_associations(associations) print_associations(associations["items"])
return return
# 默认打印帮助
parser.print_help() parser.print_help()