1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
| import os import re import difflib import xml.etree.ElementTree as ET from datetime import datetime
XML_PATH = "cnblogs_backup.xml" MD_SOURCE_DIR = "md_original" OUTPUT_DIR = "hexo_posts"
def is_valid_xml_char(c): code = ord(c) return ( code == 0x9 or code == 0xA or code == 0xD or 0x20 <= code <= 0xD7FF or 0xE000 <= code <= 0xFFFD or 0x10000 <= code <= 0x10FFFF )
def clean_xml_text(raw_text): """清洗XML非法字符和字符实体""" def replace_dec_entity(match): code = int(match.group(1)) try: char = chr(code) return char if is_valid_xml_char(char) else "" except: return "" raw_text = re.sub(r'&#(\d+);', replace_dec_entity, raw_text)
def replace_hex_entity(match): code = int(match.group(1), 16) try: char = chr(code) return char if is_valid_xml_char(char) else "" except: return "" raw_text = re.sub(r'&#x([0-9a-fA-F]+);', replace_hex_entity, raw_text)
return "".join([c for c in raw_text if is_valid_xml_char(c)])
def clean_title(title): """统一标题清洗:去除博客园后缀、特殊字符,用于匹配""" title = re.sub(r'-zhangtingxi\s*$', '', title, flags=re.IGNORECASE) title = re.sub(r'^\[[^\]]+\]\s*', '', title) title = title.lower() return re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', title)
def parse_pub_date(date_str): """兼容Atom/RSS多种时间格式""" formats = [ "%a, %d %b %Y %H:%M:%S %Z", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S+08:00", "%Y-%m-%d %H:%M:%S" ] for fmt in formats: try: dt = datetime.strptime(date_str, fmt) return dt.strftime("%Y-%m-%d %H:%M:%S") except: continue return date_str
def parse_xml_meta(xml_path): """解析Atom格式XML,提取元数据""" with open(xml_path, "r", encoding="utf-8") as f: raw_text = f.read() clean_text = clean_xml_text(raw_text) root = ET.fromstring(clean_text)
items = [] for node in root.iter(): tag = node.tag.split('}')[-1] if tag in ("item", "entry"): items.append(node)
if not items: raise Exception("未找到任何文章节点")
meta_map = {} for item in items: def get_text(tag_name): for child in item: if child.tag.split('}')[-1] == tag_name: return (child.text or "").strip() return ""
def get_attr(tag_name, attr_name): for child in item: if child.tag.split('}')[-1] == tag_name: return child.get(attr_name, "").strip() return ""
raw_title = get_text("title") link = get_attr("link", "href") or get_text("link") pub_date = get_text("published") or get_text("updated") or get_text("pubDate") date = parse_pub_date(pub_date)
categories_raw = [] for child in item: if child.tag.split('}')[-1] == "category": term = child.get("term", "").strip() if term: categories_raw.append(term)
cat_paths = [c for c in categories_raw if "/" in c] tags = [c for c in categories_raw if "/" not in c]
if cat_paths: categories = [p.strip() for p in cat_paths[-1].split("/") if p.strip()] else: categories = [categories_raw[0]] if categories_raw else ["未分类"]
key = clean_title(raw_title) final_title = re.sub(r'-zhangtingxi\s*$', '', raw_title, flags=re.IGNORECASE)
meta_map[key] = { "title": final_title, "date": date, "link": link, "tags": tags, "categories": categories }
print(f"✅ 从XML中解析到 {len(meta_map)} 篇文章元数据") print("📌 XML清洗后标题样例:") for i, k in enumerate(list(meta_map.keys())[:3]): print(f" {i+1}. {meta_map[k]['title']} → 匹配键: {k}") return meta_map
def generate_hexo_post(md_path, meta, output_dir): """生成Hexo标准文章""" with open(md_path, "r", encoding="utf-8") as f: content = f.read()
fm = ["---"] fm.append(f'title: "{meta["title"]}"') fm.append(f"date: {meta['date']}")
fm.append("tags:") if meta["tags"]: for tag in meta["tags"]: fm.append(f" - {tag}") else: fm.append(" - []")
fm.append("categories:") for cat in meta["categories"]: fm.append(f" - {cat}")
fm.append("---") fm.append("")
notice = f"**本文搬运自本人博客园博客,若图片加载不出来,可到原文查看:[{meta['link']}]({meta['link']})**\n\n" final_content = "\n".join(fm) + notice + content
filename = os.path.basename(md_path) out_path = os.path.join(output_dir, filename) os.makedirs(output_dir, exist_ok=True) with open(out_path, "w", encoding="utf-8") as f: f.write(final_content)
def main(): meta_map = parse_xml_meta(XML_PATH)
md_files = [] for root, dirs, files in os.walk(MD_SOURCE_DIR): for file in files: if file.endswith(".md"): md_files.append(os.path.join(root, file))
print(f"\n🔍 找到 {len(md_files)} 个本地MD文件") print("📌 本地文件名清洗后样例:") for i, path in enumerate(md_files[:3]): name = os.path.splitext(os.path.basename(path))[0] print(f" {i+1}. {name} → 匹配键: {clean_title(name)}")
success_count = 0 failed_files = [] all_meta_keys = list(meta_map.keys())
for md_path in md_files: filename = os.path.basename(md_path) title_raw = os.path.splitext(filename)[0] title_key = clean_title(title_raw)
meta = meta_map.get(title_key) if not meta: matches = difflib.get_close_matches(title_key, all_meta_keys, n=1, cutoff=0.85) if matches: meta = meta_map[matches[0]]
if not meta: failed_files.append(filename) continue
generate_hexo_post(md_path, meta, OUTPUT_DIR) success_count += 1
print(f"\n🎉 转换完成!成功 {success_count} 篇,匹配失败 {len(failed_files)} 篇") print(f"输出目录:{os.path.abspath(OUTPUT_DIR)}")
if failed_files: with open("匹配失败清单.txt", "w", encoding="utf-8") as f: f.write("以下文件未匹配到元数据,请手动核对:\n") for name in failed_files: f.write(name + "\n") print("⚠️ 匹配失败的文件已写入「匹配失败清单.txt」")
if __name__ == "__main__": main()
|