顯示具有 Logseq 標籤的文章。 顯示所有文章
顯示具有 Logseq 標籤的文章。 顯示所有文章

2025/08/08

Python | 調整 dropbox中 obsidian & logseq共用journal folder中 檔名規則出入

 #!/usr/bin/env python3

# -*- coding: utf-8 -*-


import re

import sys

import argparse

from pathlib import Path

from datetime import date


# ===== 工具:月份與序數 =====

MONTHS_FULL = [

    "January","February","March","April","May","June",

    "July","August","September","October","November","December"

]

MONTHS_ABBR = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]

MONTH_TO_NUM = {m:i+1 for i,m in enumerate(MONTHS_FULL)}

ABBR_TO_NUM  = {m:i+1 for i,m in enumerate(MONTHS_ABBR)}

NUM_TO_MONTH = {i+1:m for i,m in enumerate(MONTHS_FULL)}


def ordinal(n: int) -> str:

    if 11 <= (n % 100) <= 13:

        suffix = "th"

    else:

        suffix = {1:"st",2:"nd",3:"rd"}.get(n % 10, "th")

    return f"{n}{suffix}"


def canonical_stem(d: date) -> str:

    # 目標:MMMM do, yyyy(注意 do 小寫)

    return f"{NUM_TO_MONTH[d.month]} {ordinal(d.day)}, {d.year}"


# ===== 解析各種日期字串(給 [[...]] 正規化用) =====

def parse_date_token(s: str):

    s = s.strip()


    m = re.match(rf"^\s*({'|'.join(MONTHS_FULL)})\s+(\d{{1,2}})(?:st|nd|rd|th)?,\s*(\d{{4}})\s*$", s, flags=re.IGNORECASE)

    if m:

        month = MONTH_TO_NUM[m.group(1).capitalize()]

        day = int(m.group(2)); year = int(m.group(3))

        try: return date(year, month, day)

        except ValueError: return None


    m = re.match(rf"^\s*({'|'.join(MONTHS_ABBR)})\.?\s+(\d{{1,2}})(?:st|nd|rd|th)?,\s*(\d{{4}})\s*$", s, flags=re.IGNORECASE)

    if m:

        month = ABBR_TO_NUM[m.group(1).capitalize()]

        day = int(m.group(2)); year = int(m.group(3))

        try: return date(year, month, day)

        except ValueError: return None


    m = re.match(r"^\s*(\d{4})[._/\-](\d{1,2})[._/\-](\d{1,2})\s*$", s)

    if m:

        y, mo, d = map(int, m.groups())

        try: return date(y, mo, d)

        except ValueError: return None


    return None


# ===== 連結正規化 [[日期]] → [[MMMM do, yyyy]] =====

REF_PATTERN = re.compile(r"\[\[\s*([^\]\|#]+?)\s*\]\]")


def rewrite_date_page_refs(text: str):

    replaced = 0

    def _repl(m):

        nonlocal replaced

        inner = m.group(1)

        d = parse_date_token(inner)

        if d:

            replaced += 1

            return f"[[{canonical_stem(d)}]]"

        return m.group(0)

    new_text = REF_PATTERN.sub(_repl, text)

    return new_text, replaced


# ===== 安全重新命名(加 merged 前綴時避免撞名) =====

def prefixed_unique_path(p: Path, prefix="merged ") -> Path:

    target = p.with_name(prefix + p.name)

    if not target.exists():

        return target

    stem, suf = target.stem, target.suffix

    i = 1

    while True:

        c = target.with_name(f"{stem} ({i}){suf}")

        if not c.exists():

            return c

        i += 1


# ===== 解析檔名取得日期 =====

def parse_date_from_stem(stem: str):

    s = stem.strip()


    m = re.match(rf"^({'|'.join(MONTHS_FULL)})\s+(\d{{1,2}})(?:st|nd|rd|th)?,\s*(\d{{4}})$", s)

    if m:

        month = MONTH_TO_NUM[m.group(1)]

        day = int(m.group(2)); year = int(m.group(3))

        try:

            d = date(year, month, day)

            style = "canonical" if re.search(r"(st|nd|rd|th),", s) else "long_month"

            return d, style

        except ValueError:

            return None


    m = re.match(rf"^({'|'.join(MONTHS_ABBR)})\.?\s+(\d{{1,2}})(?:st|nd|rd|th)?,\s*(\d{{4}})$", s, flags=re.IGNORECASE)

    if m:

        month = ABBR_TO_NUM[m.group(1).capitalize()]

        day = int(m.group(2)); year = int(m.group(3))

        try: return date(year, month, day), "abbr_month"

        except ValueError: return None


    m = re.match(r"^(\d{4})[._-](\d{1,2})[._-](\d{1,2})$", s)

    if m:

        y, mo, d = map(int, m.groups())

        try: return date(y, mo, d), "iso"

        except ValueError: return None


    return None


# ===== 檔案讀寫 =====

def read_text(path: Path) -> str:

    return path.read_text(encoding="utf-8", errors="ignore")


def write_text(path: Path, text: str):

    path.write_text(text, encoding="utf-8")


def append_transformed(src: Path, dest: Path) -> tuple[int,int]:

    raw = read_text(src)

    fixed, replaced = rewrite_date_page_refs(raw)

    with dest.open("a", encoding="utf-8") as f:

        if dest.exists() and dest.stat().st_size > 0:

            f.write("\n")

        f.write(fixed)

        if not fixed.endswith("\n"):

            f.write("\n")

    return len(fixed), replaced


def rewrite_links_in_file(path: Path) -> int:

    raw = read_text(path)

    fixed, replaced = rewrite_date_page_refs(raw)

    if replaced > 0:

        write_text(path, fixed)

    return replaced


# ===== 掃描候選 =====

def find_candidates(root: Path):

    """

    掃描 root 下的 .md(不遞迴,跳過 'merged ' 開頭)

    回傳 { date: { 'canonical': Path|None, 'sources': [Path,...] } }

    """

    mapping = {}

    for p in root.glob("*.md"):

        if p.name.lower().startswith("merged "):

            continue

        parsed = parse_date_from_stem(p.stem)

        if not parsed:

            continue

        d, style = parsed

        bucket = mapping.setdefault(d, {"canonical": None, "sources": []})

        if style == "canonical":

            bucket["canonical"] = p

        else:

            bucket["sources"].append(p)

    # 保留所有有關聯的日期(包含只有 canonical 的,稍後用邏輯過濾「不需處理」的)

    return mapping


def process_dates(root: Path, mapping: dict, limit: int, dry_run: bool):

    if not mapping:

        print("沒有可處理的日期檔。")

        return


    dates_sorted = sorted(mapping.keys(), reverse=True)


    # 僅選擇「需要處理」的日期:

    # - 有 sources(需要合併),或

    # - 沒有 canonical(只有 1 個或多個來源 → 需要改名或合併)

    # 會跳過:已是 canonical 且沒有 sources(代表已正確,屬 no-op)

    work_dates = [d for d in dates_sorted if not (mapping[d]["canonical"] and not mapping[d]["sources"])]


    if not work_dates:

        print("找不到需要處理的日期(最新的都已是 MMMM do, yyyy 且無重複)。")

        return


    selected = work_dates[:limit]


    print("日期狀態(由新到舊):")

    for d in dates_sorted:

        info = mapping[d]

        if info["canonical"] and not info["sources"]:

            status = "OK 已是 MMMM do, yyyy(跳過)"

        elif info["canonical"] and info["sources"]:

            status = f"合併 {len(info['sources'])} 來源"

        elif not info["canonical"] and len(info["sources"]) == 1:

            status = "改名為 MMMM do, yyyy"

        else:

            status = f"合併 {len(info['sources'])} 來源(並建立目標)"

        tag = " ← 將處理" if d in selected else ""

        print(f"- {d.isoformat()}:{status}{tag}")


    for d in selected:

        info = mapping[d]

        target = info["canonical"] if info["canonical"] else root / f"{canonical_stem(d)}.md"

        will_create = (info["canonical"] is None) and (not target.exists())

        sources_sorted = sorted(info["sources"], key=lambda p: p.name.lower())


        print(f"\n=== {d.isoformat()} ===")


        # 單一來源且尚未存在目標 → 直接改名(不加 merged)

        if info["canonical"] is None and len(sources_sorted) == 1 and will_create:

            src = sources_sorted[0]

            print(f"[SINGLE] 僅有來源:{src.name}")

            if dry_run:

                print(f"  [REWRITE] 將在檔內正規化 [[日期]] → [[{canonical_stem(d)}]]")

                print(f"  [RENAME]  '{src.name}'  →  '{target.name}'")

            else:

                replaced = rewrite_links_in_file(src)

                if replaced:

                    print(f"  [REWRITE] 已正規化 {replaced} 處 [[日期]]")

                src.rename(target)

                print(f"  [RENAMED] '{src.name}'  →  '{target.name}'")

                # 保險:對新檔再跑一次正規化

                replaced_t = rewrite_links_in_file(target)

                if replaced_t:

                    print(f"  [TARGET REWRITE] {target.name} 內正規化 {replaced_t} 處 [[日期]]")

            continue


        # 其他情境:存在目標或有多來源 → 合併 + 來源加 merged 前綴

        if will_create:

            print(f"[TARGET] {target.name}(將建立)")

        else:

            print(f"[TARGET] {target.name}")


        planned = []

        for src in sources_sorted:

            new_path = prefixed_unique_path(src)  # merged ...

            planned.append((src, new_path))


        if dry_run:

            print(f"  [TARGET REWRITE] 將正規化 {target.name} 內的 [[日期]](若存在)")

            for src, new_path in planned:

                print(f"  [COPY]   '{src.name}'  →  '{target.name}'(先正規化連結)")

                print(f"  [RENAME] '{src.name}'  →  '{new_path.name}'")

            continue


        if will_create:

            target.write_text("", encoding="utf-8")

            print(f"[CREATE TARGET] 已建立:{target.name}")


        replaced_t = rewrite_links_in_file(target)

        if replaced_t:

            print(f"  [TARGET REWRITE] {target.name} 內正規化 {replaced_t} 處 [[日期]]")


        total_chars = 0

        total_refs  = 0

        for src, new_path in planned:

            appended_len, replaced_refs = append_transformed(src, target)

            total_chars += appended_len

            total_refs  += replaced_refs

            print(f"  [COPIED]  '{src.name}'  →  '{target.name}'  (+{appended_len} chars, fixed {replaced_refs} refs)")

            src.rename(new_path)

            print(f"  [RENAMED] '{src.name}'  →  '{new_path.name}'")


        print(f"[SUMMARY] 合併 {len(planned)} 個來源,總附加 ~{total_chars} 字元、正規化 {total_refs} 個 [[日期]] → {target.name}")


    print(f"\n完成!此次處理日期數量:{len(selected)}。")


def main():

    ap = argparse.ArgumentParser(description="合併/正規化日記到 MMMM do, yyyy;跳過已是正確格式且無重複的最新日期。")

    ap.add_argument("root", nargs="?", default=".", help="要處理的資料夾(預設:目前資料夾)")

    ap.add_argument("-n","--limit", type=int, default=1000, help="一次處理的『日期數量』(預設:1;可改 10 或 1000)")

    ap.add_argument("--dry-run", action="store_true", help="僅列出將進行的動作,不修改檔案")

    args = ap.parse_args()


    root = Path(args.root).expanduser().resolve()

    if not root.exists():

        print(f"找不到資料夾:{root}")

        sys.exit(1)


    mapping = find_candidates(root)

    process_dates(root, mapping, limit=args.limit, dry_run=args.dry_run)


if __name__ == "__main__":

    main()


2025/08/04

如何同時使用 Logseq 與 Obsidian?

🎯 核心觀念

先釐清你的需求

在開始使用 Logseq 或 Obsidian 前,應先問自己:「我到底想用這些工具達成什麼目標?」
如果還沒釐清需求,很可能會陷入「有了解決方案,卻沒找到問題」的情境。

🔄 可以雙軌並行,互補使用

你可以根據各自的優勢與不足來決定使用場景:

  • 主觀使用法:用你喜歡的那一套工具去做那件事。

  • 客觀使用法:用一個工具來補足另一個的不足之處。


💡 使用情境與分工建議

使用工具 建議用途
Logseq 區塊式筆記、任務管理、每日筆記、間隔重複學習、Zotero / PDF 整合、以 Journal 為主導的「每日輸入流」
Obsidian 跨檔案筆記整理、大型專案或研究、視覺化連結地圖、更強大的自訂排版與插件、Readwise 整合、跨平台同步

⚠️ 注意事項:兩者之間的相容性

Logseq 與 Obsidian 都使用 Markdown,但:

  • Markdown 格式略有差異,某些語法、屬性或資料夾結構在兩邊可能顯示不一樣。

  • 使用相同資料夾時,可能導致顯示異常或格式錯亂

  • 解法:依照規範進行設定與資料夾規劃


🔧 讓 Obsidian 和 Logseq 協同工作的建議設定

📁 檔案與資料夾結構建議

功能 建議設定
筆記存放位置 /pages 資料夾(兩邊通用)
附件存放位置 /assets 資料夾
每日筆記格式 Logseq 和 Obsidian 都設定為 MMMM do, YYYY(如:August 4th, 2025)
每日筆記位置 /journals 資料夾
連結格式 在 Obsidian 中:關閉 Wikilinks、使用相對路徑
任務管理 建立一頁 [[logseq tasks]],可用查詢語法過濾 DOING 等狀態

🔌 推薦插件(Obsidian 專用)

插件 功能
obsidian-plugin-logseq 讓 Obsidian 更好地預覽 Logseq 的 Markdown
obsseq 協助從 Logseq 遷移至 Obsidian
obsidian-logseq-compat 增進 Obsidian 與 Logseq 的視覺相容性
Template.Obs 一個模板庫,適合整合兩者使用

✅ 實作技巧與小撇步

  • /assets 建立附件資料夾,快速儲存與引用圖片和檔案

  • 在 Obsidian 中使用「Outliner」、「Zoom」等插件強化大綱結構(但手機版可能需停用)

  • 用相同資料夾作為 Obsidian Vault 與 Logseq 的工作空間,便於雙向同步


🎓 結論建議

  • 雙軌並行是可行的,但你需要設定好一致的命名規則與檔案結構。

  • 開始使用後,實際操作比看影片或建議更能幫你釐清偏好。

  • 最終選擇不在於哪個工具比較好,而是你在哪個環境中思考與寫作最順手。


2022/05/16

tagging, bi directional linking & markdown

 

在GTD還流行的年代,當時的概念是每個itme都要寫上一些metadata

- status: in progress, deferred, someday maybe

- location: @office, @home, @commute

- time: @morning, @phone, @writing

- person: w/Amy, w/Kevin...

當這些不夠用的時候,還要另外加上tag,當時David Allen說某個週末做一次review,可以更有效率,但我一直都有兩個疑問

1. 我覺得為了維護這些資料反而需要花費更多時間

2. 這個系統讓我壓力更大

3. 我不認為GTD可以Scale,當有更多成員,複雜性更高的時候,應該就會爆炸

當時有許多有趣的工具應該都是為了維護越來越複雜的metadata而衍生出來

- theBrain https://www.thebrain.com/ 可以連結文件彼此之間的概念,到現在還是很優秀,但費用對我來說依舊沒有動力投資

- XMind 跟iThought我後來選了iThought,因為買斷對我來說感覺還是比較舒服,iThoughs作者英國光頭的形象也讓我比較有親切感。MindMap畫到一個程度,總是不容易再回去連結到其他的概念,Roam Research這些後來的工具讓使用者在觀念之間跳來跳去容易多了。Mindoomo, MindMeister, MindNode, Coggle, SimpleMind, MindManager, Visual Understanding Environment, Mind42, bubbl.us, 

- Tinderbox 另外一個想學沒學好的工具,我像是松鼠一樣蒐集了許多想要閱讀的清單,閱讀的速度跟不上蒐集的速度。還好現在有Speechify可以幫助我多讀一點東西。

不論如何,GTD在過去二十年依舊幫助我許多,改變了我安排決定事情的優先順序的習慣,到現在甚至改變了我手寫筆記的習慣,我也這樣請小孩子寫自己的筆記&日記。

  • ? 問題 >> ?該怎麼做
  • [ ]   該做的事情 >>[ ] 閱讀第二課
  • 想法 >> 可以用在肉桂麵包
  • -> 流程順序 >> 清洗 -> 烹飪 -> 上菜
  • # 標籤 >> #猴子 #自然課
  • @ context & location
  • - 條列項目 >>
    • - 物品一
    • -事件二 

另外一個有趣的事情是,為了要釐清這些metadata之間的關聯性,有很長一段時間我想要建立一個立體的tag關聯性整理界面,一個3D的網狀架構,讓概念之間彼此連結,並且用節點之間的距離,來換算關聯性跟權重,我想參考星狀資料結構,搭配Google Earth的界面來讓使用者(其實是我自己)來整理腦子的想法。這個概念太模糊,我就沒有能力可以實現。

後來的WikiNode, WikiLink都有了knowledge graph的技術,Roam Research, Obsidian, LogSeq等都有自動視覺化顯示概念關係的工具,我之前想要達到的目的,使用bi directional linking甚至比視覺抓取方式更有效率。

感謝世界上這麼多傑出聰明的人們,許多困難我還在掙扎的時候,他們就實做出比我能想像到更好的工具出來給我們用了:)

Buy me a coffee