2025/12/14

logseq/ obsidian改日期格式檔名

 沒錯,你的邏輯非常清晰且安全。這正是處理大量資料遷移的標準 SOP:

  1. 內容清洗 (Content Refactoring): 先把筆記內部所有的 [[December 15th, 2025]] 連結改成 [[2025-12-15]]。這時候檔案還沒改名,所以連結會暫時失效(沒關係)。

  2. 檔案重命名 (File Renaming): 把檔名改成 2025-12-15.md。這時候剛才改好的連結就會自動生效。

  3. 衝突防護 (Collision Guard): 如果 2025-12-15.md 已經存在,則跳過該檔案的改名,留給你之後用 Obsidian 的 Note Composer 手動合併。

我為你寫了一個 Python Script,具備了 Dry Run (模擬執行) 功能。預設是「只顯示不執行」,確認沒問題後你可以把開關打開。

Python 自動遷移腳本

請將以下程式碼存成 migrate_roam_dates.py,並放在你的 Logseq pages 或 journals 資料夾同一層目錄中(或者你需要修改 TARGET_DIR 路徑)。

Python
import os
import re
import sys

# ================= 設定區 =================

# 指定你的筆記資料夾路徑 (例如: r"C:\Users\Name\Logseq\pages")
# 如果腳本放在筆記資料夾內,使用 "." 即可
TARGET_DIR = r"." 

# 安全開關:True = 只顯示會發生什麼事,不實際修改檔案
# 確認無誤後,請改成 False
DRY_RUN = True 

# =========================================

# 月份對照表
MONTH_MAP = {
    "January": "01", "February": "02", "March": "03", "April": "04",
    "May": "05", "June": "06", "July": "07", "August": "08",
    "September": "09", "October": "10", "November": "11", "December": "12"
}

# Regex 模式:匹配 Roam 格式日期 (例如 December 1st, 2025)
# 捕捉組: Group 1=Month, Group 2=Day, Group 3=Year
DATE_PATTERN = re.compile(
    r'(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})(?:st|nd|rd|th),\s+(\d{4})',
    re.IGNORECASE
)

def to_iso_format(match):
    """將匹配到的 Roam 日期轉換為 YYYY-MM-DD"""
    month_name = match.group(1).title() # 確保首字大寫以匹配字典
    day = int(match.group(2))
    year = match.group(3)
    
    month_num = MONTH_MAP.get(month_name)
    if not month_num:
        return match.group(0) # 找不到月份則不改
        
    return f"{year}-{month_num}-{day:02d}"

def process_files():
    print(f"🚀 開始執行... (Dry Run 模式: {DRY_RUN})")
    print(f"📂 目標資料夾: {os.path.abspath(TARGET_DIR)}\n")

    files = [f for f in os.listdir(TARGET_DIR) if f.endswith(".md")]
    
    # ---------------------------------------------------------
    # 第一階段:修改檔案內容 (Content Replacement)
    # ---------------------------------------------------------
    print("--- 階段 1: 修改檔案內容中的連結 ---")
    modified_count = 0
    
    for filename in files:
        file_path = os.path.join(TARGET_DIR, filename)
        
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # 尋找 [[December 15th, 2025]] 這種格式
            # 我們使用回調函數 to_iso_format 來動態替換
            new_content = DATE_PATTERN.sub(to_iso_format, content)
            
            if content != new_content:
                print(f"[修改內容] {filename}")
                modified_count += 1
                
                if not DRY_RUN:
                    with open(file_path, 'w', encoding='utf-8') as f:
                        f.write(new_content)
                        
        except Exception as e:
            print(f"❌ 讀取錯誤 {filename}: {e}")

    print(f"第一階段完成:共計畫修改 {modified_count} 個檔案的內容。\n")

    # ---------------------------------------------------------
    # 第二階段:修改檔名 (Rename Files)
    # ---------------------------------------------------------
    print("--- 階段 2: 修改實體檔名 ---")
    rename_count = 0
    skip_count = 0
    
    # 重新掃描一次檔案列表,確保處理的是當前狀態
    files = [f for f in os.listdir(TARGET_DIR) if f.endswith(".md")]

    for filename in files:
        # 檢查檔名是否符合 Roam 格式 (去掉 .md 後檢查)
        name_without_ext = filename[:-3]
        match = DATE_PATTERN.fullmatch(name_without_ext)
        
        if match:
            new_name_body = to_iso_format(match)
            new_filename = f"{new_name_body}.md"
            
            old_path = os.path.join(TARGET_DIR, filename)
            new_path = os.path.join(TARGET_DIR, new_filename)
            
            # 檢查新檔名是否已經存在 (衝突檢查)
            if os.path.exists(new_path) and filename != new_filename:
                print(f"⚠️ [跳過 - 檔名衝突] {filename} -> {new_filename} (目標檔案已存在)")
                skip_count += 1
            else:
                print(f"✅ [改名] {filename} -> {new_filename}")
                rename_count += 1
                
                if not DRY_RUN:
                    try:
                        os.rename(old_path, new_path)
                    except Exception as e:
                        print(f"❌ 改名失敗: {e}")

    print(f"\n第二階段完成:")
    print(f"預計改名: {rename_count}")
    print(f"預計跳過 (衝突): {skip_count}")
    
    if DRY_RUN:
        print("\n✨ 這是模擬執行,沒有檔案被修改。")
        print("請將程式碼中的 DRY_RUN = False 改為 True 以執行實際操作。")
    else:
        print("\n🎉 作業完成!請記得使用 Obsidian Note Composer 處理那些「跳過」的重複檔案。")

if __name__ == "__main__":
    # 簡單防呆:確認路徑存在
    if os.path.exists(TARGET_DIR):
        process_files()
    else:
        print("❌ 找不到目標資料夾,請修改 TARGET_DIR 設定。")

如何使用此腳本

  1. 備份!備份!備份!

    在執行任何腳本前,請將你的整個 Logseq/Obsidian 資料夾複製一份到其他地方。

  2. 設定路徑:

    打開腳本,修改 TARGET_DIR。

    • 如果你把腳本直接丟進 pages 資料夾執行,保持 TARGET_DIR = "." 即可。

    • 否則請填入完整路徑,例如:TARGET_DIR = r"D:\MyNotes\pages" (注意前面的 r)。

  3. 執行 Dry Run (模擬):

    直接執行腳本:python migrate_roam_dates.py。

    它會列出所有它「打算」修改的內容和檔名。

    • 檢查 [修改內容] 是否正確抓到了日期。

    • 檢查 [改名] 轉換的 ISO 格式是否正確。

    • 檢查 [跳過 - 檔名衝突] 是否有正確偵測到重複檔案。

  4. 正式執行:

    將程式碼第 13 行改成 DRY_RUN = False。

    再次執行腳本。

  5. 善後處理:

    執行完畢後,打開 Obsidian,你會發現大部分檔案都已經變成標準的 2025-12-15.md。

    剩下的就是那些在 Console 中顯示 [跳過] 的檔案(即舊格式檔名),請使用 Obsidian 的 Note Composer 手動將它們合併到新的日期檔案中。

沒有留言:

張貼留言

Buy me a coffee