從 WizNote 為知筆記到 Joplin(下)

2021-03-02 曾嶸胡扯的地方

從 WizNote 為知筆記到 Joplin(上) 一文中講到了我為什麼要從為知筆記轉到 Joplin。本文講一講其中的技術細節。

wiz2joplin 項目是開源的,我在源碼中寫的注釋也很詳細,所以本文就不列舉所有實現,而是主要講一下設計思路和需要關注的問題。文中標註了報名和函數名稱,方便大家在 wiz2joplin 項目中尋找對應源碼查看。

要理解下面講述的細節,請先閱讀:WizNote 為知筆記 macOS 版本本地文件夾分析 。

讀取為知筆記

從為知筆記本地資料庫中讀取為知筆記。

讀取為知筆記的目錄信息,在為知筆記中稱為 location。

讀取為知筆記的 TAG 信息。

解壓縮為知筆記每個文檔的壓縮包到臨時文件夾: w2j.wiz.WizDocument._extract_zip

解析為知筆記文檔源碼中的內嵌的圖像資源、內鏈和附件: w2j.parser.parse_wiz_html

整理數據

為知筆記和 Joplin 中有一些相同的部分,也有一些不同的部分。我們在整理數據的時候,需要將它們進行一一對應。

1. 為知筆記的 document 有自己的 guid,Joplin 也使用同樣的 guid,兩者都是 32 個字符,但為知筆記採用了標準的 8-4-4-4-12 格式,而 Joplin 去掉了分隔符。只需要寫兩個簡單的函數進行轉換即可:

def towizid(id: str) -> str:    """ 從 joplin 的 id 格式轉為 wiz 的 guid 格式    """    one = id[:8]    two = id[8:12]    three = id[12:16]    four = id[16:20]    five = id[20:]    return '-'.join([one, two, three, four, five])

def tojoplinid(guid: str) -> str: """ 從 wiz 的 guid 格式轉為 joplin 的 id 格式 """    return ''.join(guid.split('-'))

2. 為知筆記的 TAG 和附件都擁有自己的 GUID,這與 Joplin 的 resource 的 GUID 可以進行一一對應。

3. 為知筆記的文檔中的內嵌圖像沒有 GUID,為知筆記的目錄也沒有 GUID,但 Joplin 中的內嵌圖像屬於標準資源,有自己的 GUID,Joplin 中的 notebook/folder 也擁有自己的 GUID。

4. 為知筆記的內鏈有附件內鏈和文檔內鏈兩種格式,使用正則表達式來提取其中的 GUID 部分:

RE_A_START = r'<a href="'RE_A_END = r'">([^<]+)</a>'
# 附件內鏈# 早期的連結沒有雙斜槓# wiz:open_attachment?guid=8337764c-f89d-4267-bdf2-2e26ff156098# 後期的連結有雙斜槓# wiz://open_attachment?guid=52935f17-c1bb-45b7-b443-b7ba1b6f854eRE_OPEN_ATTACHMENT_HREF = r'wiz:/{0,2}(open_\w+)\?guid=([a-z0-9\-]{36})'RE_OPEN_ATTACHMENT_OUTERHTML = RE_A_START + RE_OPEN_ATTACHMENT_HREF + RE_A_END
# 文檔內鏈,只需要提取 guid 後面的部分即可# wiz://open_document?guid=c6204f26-f966-4626-ad41-1b5fbdb6829e&amp;kbguid=&amp;private_kbguid=69899a48-dc52-11e0-892c-00237def97ccRE_OPEN_DOCUMENT_HREF = r'wiz:/{0,2}(open_\w+)\?guid=([a-z0-9\-]{36})&amp;kbguid=&amp;private_kbguid=([a-z0-9\-]{36})'RE_OPEN_DOCUMENT_OUTERHTML = RE_A_START + RE_OPEN_DOCUMENT_HREF + RE_A_END

在讀取為知筆記文檔源碼內容的時候還碰到一個問題,就是早期的為知筆記版本採用了 UTF16 編碼。如果使用默認的 UTF8 來讀取就會報錯。此時應該先檢測筆記源碼的編碼再讀取。這裡的檢測使用第三方庫 chardet 完成。

index_html = note_extract_dir.joinpath('index.html')if not index_html.is_file:    raise FileNotFoundError(f'主文檔文件不存在!{index_html} |{title}|')html_body_bytes = index_html.read_bytes()# 早期版本的 html 文件使用的是 UTF-16 LE(BOM) 編碼保存。最新的文件是使用 UTF-8(BOM) 編碼保存。要判斷編碼進行解析enc = chardet.detect(html_body_bytes)html_body = html_body_bytes.decode(encoding=enc['encoding'])
# 去掉換行符,早期版本的 html 文件使用了 \r\n 換行符,而且會切斷 html 標記。替換掉換行符方便正則html_body = html_body.replace('\r\n', '')html_body = html_body.replace('\n', '')

5. 為知筆記中的圖片在文檔源碼中使用的是 img 標籤,使用正則表達式提取:

# 圖像文件在 body 中存在的形式,即使是在 .md 文件中,也依然使用這種形式存在RE_IMAGE_OUTERHTML = r'<img .*?src="(index_files/[^"]+)"[^>]*>'

6. 上面解析出來的內鏈資源和附件資源,都會在 Joplin 中轉換成同一種形式: [Title](:/GUID),image 資源則會轉換成 ![Title](:/GUID) 形式。

臨時資料庫

由於部分的為知筆記資源在 Joplin 中沒有對應的 GUID,必須將這些資源上傳到 Joplin 才能取得 GUID,為了避免整個轉換過程的中斷導致重頭來過(畢竟有 3000 篇),我在轉換過程中建立了一個臨時資料庫,將轉換過程寫入到資料庫中,下次中斷的時候,就可以從資料庫中取得轉換狀態了。

下面是資料庫的定義:

CREATE_SQL: dict[str, str] = {    # 保存 Location 和 Folder 的關係    'l2f': """CREATE TABLE l2f (            location TEXT NOT NULL,            id TEXT,            title TEXT NOT NULL,            parent_location TEXT,            parent_id TEXT,            level INTEGER NOT NULL,            PRIMARY KEY (location)        );""",    # 處理過的文檔會保存在這裡,在這個表中能找到的文檔說明已經轉換成功了    'note': """CREATE TABLE note (            note_id TEXT not NULL,            title TEXT not NULL,            joplin_folder TEXT NOT NULL,            markup_language INTEGER NOT NULL,            wiz_location TEXT NOT NULL,            PRIMARY KEY (note_id)        );""",    # 處理過的資源保存在這裡,包括 image 和 attachment 資源    'resource': """CREATE TABLE resource (            resource_id TEXT not NULL,            title TEXT NOT NULL,            filename TEXT NOT NULL,            created_time INTEGER not NULL,            resource_type INTEGER NOT NULL,            PRIMARY KEY (resource_id)        );""",    # 保存為知筆記中的內鏈,也就是 resource 與 note 的關係,使用 文檔 guid 和 連接目標 guid 同時作為主鍵。連結目標 guid 為 joplin 格式    'internal_link': """        CREATE TABLE internal_link (            note_id TEXT not NULL,            resource_id TEXT not NULL,            title TEXT not NULL,            link_type TEXT NOT NULL,            PRIMARY KEY (note_id, resource_id)        );        CREATE INDEX idx_link_type ON internal_link (link_type);        CREATE INDEX idx_resource_id ON internal_link (resource_id);        """,    # 保存為知筆記中的 tag    'tag': """        CREATE TABLE tag (            tag_id TEXT not NULL,            title TEXT not NULL,            created_time INTEGER not NULL,            updated_time INTEGER not NULL,            PRIMARY KEY (tag_id)        );        CREATE UNIQUE INDEX idx_title ON tag (title);    """,    # 保存tag 與note 的關係    'note_tag': """CREATE TABLE note_tag (        note_id TEXT not NULL,        tag_id TEXT not NULL,        title TEXT not NULL,        created_time INTEGER not NULL,        PRIMARY KEY (note_id, tag_id)    );""",}

使用 Python 自帶的 sqlite3 來創建臨時資料庫。

上傳到 Jopin

1. 同步為知筆記的目錄到 Joplin: w2j.adapter.Adapter.sync_folders 以及 w2j.joplin.JoplinDataAPI.post_folder。

2. 同步為知筆記的附件和內嵌圖像: w2j.adapter.Adapter._upload_wiz_attachment 以及 w2j.adapter.Adapter._upload_wiz_image。

3. 同步筆記正文內容到 Joplin: w2j.adapter.Adapter.sync_all 以及 w2j.adapter.Adapter._sync_note。

為知筆記的文檔有兩種,一種標題以 .md 結尾的,為知筆記會將其作為 Markdown 格式來渲染,另一種不帶 .md 後綴的就作為 HTML 來渲染。

這裡說點題外話:

使用 .md 作為標題後綴,我不知道老魏是處於一個什麼樣的考量,但我肯定這不是一個優雅的解決方案。

儘管 Markdown 是在為知筆記出現之後才流行起來的,儘管為知筆記運行這麼多年可能有一些歷史包袱,但面對一個已經如此流行的技術,採用了這樣一種「近乎於無釐頭」的解決方案,反映出為知筆記團隊「懶於深入思考」的現狀。

在我分析為知筆記本地數據的時候,經常會碰到這種「無釐頭」的折衷方案。例如:

前後不一的筆記文本編碼,之前用 UTF16,後面改為 UTF8.

設計混亂的內鏈方式, wiz:open_attachment 和 wiz://open_attachment。

拼寫錯誤的資料庫列名。

其實只要多花一些思考的時間,這些問題都很容易被優雅地解決。

在同步到 Joplin 的時候,需要區分這兩種情況。為知筆記中保存的 .md 文章是一種很奇怪的格式:既不是純 Markdown,也不是純 HTML,而是使用 HTML 作為排版,包含純 Markdown 內容。

需要調用 HTML 渲染引擎來處理,將其中用于格式分隔(一般是 div/p/br)等等渲染成實際在 HTML 中的表現,但保持 Markdown 源碼不變。

我找到的最好的 Python 渲染引擎 :inscriptis 。

下面的 get_text 方法就是這套渲染引擎中提供的。

def gen_ilstr(is_markdown: bool, jil: JoplinInternalLink) -> str:    """ 返回被替換的內鏈    ilstr = internal link str    """    if is_markdown:        body = f'[{jil.title}](:/{jil.resource_id})'        if jil.link_type == 'image':            return '!' + body        return body    if jil.link_type == 'image':        return f'<img src=":/{jil.resource_id}" alt="{jil.title}">'    return f'<a href=":/{jil.resource_id}">{jil.title}</a>'

def gen_end_ilstr(is_markdown: bool, jils: list[JoplinInternalLink]): """ 返回 body 底部要加入的內容 ilstr = internal link str """ if is_markdown: return '\n\n# 附件連結\n\n' + '\n'.join([ '- ' + gen_ilstr(is_markdown, jil) for jil in jils]) body = ''.join([ f'<li>{gen_ilstr(is_markdown, jil)}</li>' for jil in jils]) return f'<br><br><h1>附件連結</h1><ul>{body}</ul>'
def convert_joplin_body(body: str, is_markdown: bool, internal_links: list[JoplinInternalLink]) -> str: """ 將為知筆記中的 body 轉換成 Joplin 內鏈 """ insert_to_end: list[JoplinInternalLink] = [] for jil in internal_links: if jil.outertext: body = body.replace(jil.outertext, gen_ilstr(is_markdown, jil)) if jil.link_type == 'open_attachment': insert_to_end.append(jil) if is_markdown: body = get_text(body) if insert_to_end: body += gen_end_ilstr(is_markdown, insert_to_end)    return body

最後,關於同步到 JoplinDataAPI 的正文內容,Joplin 文檔講解得並不詳細。我通過抓包 Joplin WebClipper 得到了隱藏的參數。

在將正文提交到 Joplin 的時候,通過這樣的參數配置,就能讓 Joplin 自動轉換 HTML 到 Markdown。效果還挺不錯的。

下面是更詳細的說明。

def post_note(self, id: str, title: str, body: str,     is_markdown: bool, parent_id: str, source_url: str) -> JoplinNote:    """ 創建一個新的 Note    隱藏的 Joplin 參數:通過抓包 Joplin WebClipper        complete Page Html    source_command    {        'name': 'completePageHtml',        'preProcessFor': 'html'    }    convert_to = html
simplified Page Html source_command { 'name': 'simplifiedPageHtml', } convert_to = markdown
complete page source_command = markdown { 'name': 'completePageHtml', 'preProcessFor': 'markdown' } convert_to = markdown """ kwargs = { 'id': id, 'title': title, 'parent_id': parent_id, 'markup_language': 1, } if source_url: kwargs['source_url'] = source_url if is_markdown: kwargs['body'] = body else: kwargs['body_html'] = body kwargs['convert_to'] = 'markdown' kwargs['source_command'] = { 'name': 'simplifiedPageHtml', }
query = self._build_query() logger.info(f'向 Joplin 增加 note {kwargs}') resp = self.client.post('/notes', params=query, json=kwargs) data = resp.json() if data.get('error'): logger.error(data['error']) raise ValueError(data['error'])    return JoplinNote(**data)

1

全部的重點就在這裡了,希望對你有所幫助。

更多細節在源碼中,歡迎訪問 wiz2joplin 項目以了解更多信息。

設置 Joplin 同步

下面兩篇文章詳細介紹了 Joplin 同步配置。有了同步功能,筆記軟體才完整。建議非程式設計師使用騰訊雲 COS 同步的方式,配置簡單,穩定性更有保證。

引用曾老師的 Python 課沒事找曾嶸胡扯一下

相關焦點

  • 為知筆記的 Wiz 究竟是什麼意思?
  • 印象筆記、有道雲筆記、傲遊筆記、為知筆記哪個好用的科學解讀
    網上有太多人在問印象筆記、有道雲筆記、傲遊筆記、為知筆記哪個好用的問題,筆者也大致瀏覽了一下答案發現都是完全從回答者自我出發,而非從問者出發。因此基本導致答案都是自說自話。而並沒有解決問者的問題。
  • 【用戶案例】用為知筆記督促學英語
    作者:陳曉嬋
  • 如何將微信文章的內容收藏整理,為知筆記可以實現
    為知筆記是一款不錯的軟體,可以使用該軟體進行網頁的保存,記錄自己生活瑣事,保存記錄重要的文檔,記錄照片,使用便籤,如果在微信中閱讀到好的文章可以使用為知的一鍵功能事項微信文章的收藏。下面介紹如何通過設置實現為知筆記收藏微信文章。
  • 印象筆記大幅漲價,為知筆記不再免費,雲筆記產品還能生存多久?
    12月9日為知筆記發布公告,宣布自試用期結束後全面收費,如果免費用戶不升級成VIP(付費)便只能使用本地筆記功能,無法同步雲端。印象筆記創始人兼CEO Phil Libin曾承認,Evernote存在「5% problem」——「很多用戶都只用到了Evernote 5%的功能,問題在於每個用戶用到的5%功能都不一樣。」為了滿足不同人的5%,Evernote變得越來越龐大,越來越笨重。可Phil Libin或許沒想到的是,也許有些人並不需要那麼多5%。
  • 筆記app在中國
    有道雲筆記功能全面,覆蓋雲協作、語音識別、網頁簡報等功能,採用「三備份儲存」技術以防資料丟失,解決了個人資料和信息跨平臺跨地點的管理問題。印象筆記移動端應用便捷,可以手寫、錄音、拍照、截屏,還能在筆記內對文字、圖片進行標註,付費版還支持對pdf和office文件的搜索。為知筆記勝在筆記功能多樣性和代碼管理性。
  • 課堂筆記的重要性知多少
    課堂筆記是學生和老師耳熟能詳的,然而課堂筆記的重要性我們了解多少呢?課堂筆記顧名思義就是學生在課堂上把老師的講課內容和板書的重點抄記下來。看起來十分簡單,所以學生們大多是能抄記下來一二,但怎麼樣去記課堂筆記呢?就應該從課堂筆記的重要性說起。
  • 還在為記筆記犯愁嗎?2019最好用的雲筆記軟體都在這裡
    如果說有什麼軟體適合所有人的,那麼雲筆記一定算一個,不管你是用來分享看法、記錄靈感、總結知識等,雲筆記都能做到,在本文中,我們將為大家帶來最好用的雲筆記軟體。>一鍵博客,想把自己的筆記發布到網絡分享給他人,你不需要再單獨建一個網站,使用螞蟻筆記的博客功能一鍵變博客專業數學公式,雖然對於大多數人來說用不到,但是很強大歷史記錄,隨時查看歷史版本,防止每一個失誤對於技術人員來說尤其是網際網路技術人員,螞蟻筆記是一個非常合適的選擇
  • 印象筆記免費版限制2臺設備 筆記要不要遷移? - 軟體和應用...
    筆者從記錄、同步、分享等幾個雲筆記基礎功能,對比了國內用戶比較多的幾款雲筆記產品:印象筆記、有道雲筆記、為知筆記,也許可以給你一些參考。1. 終端覆蓋程度來看有道雲筆記和為知筆記覆蓋較全且實現同步功能免費,有道的Mac端體驗被人詬病很久,近期看到更新了2.0版本,體驗和視覺都改善了非常多,其他端的整體質量也是不錯的。
  • 線上筆記分享會,好的筆記曬出來!
    b.題材不限:課程筆記、讀書筆記、自學記錄、學習心得和文獻閱讀筆記等。c.筆記要求:紙質筆記要求書寫工整,記錄清晰;電子筆記要求邏輯鮮明,條理清晰有重點。參賽方式參賽者需將筆記於(2020年6月3日24:00點前)前發送到QQ郵箱:1946585407@qq.com。
  • 最適合設計師的筆記軟體 Notion
    我的雲筆記歷史之前,我使用過印象筆記、網易雲筆記、為知筆記和 Vnote,卻始終沒有滿足我的需求。為知筆記2016年初,剛開始使用雲筆記。那個時候唯一的需求就是看教程記筆記。之前使用紙質筆記很不方便,所以就開始接觸雲筆記,對比的當時市面上的產品,最後選擇了為知筆記,原因很簡單,因為它免費功能多。通過寫筆記,慢慢的我開始了解到了Markdown 一種標記語法,就像發現了新大陸一樣,原先看著Word 那種編輯區眾多的按鈕讓我倍感壓力,使用後瞬間輕鬆了很多。從此我對筆記有了多一個要求,一定要有Markdown功能。
  • 知與行(讀書筆記)
    知:曉得、明了。(理論) 行:行動,活動。
  • 騰訊教育牽手印象筆記將推出「印象筆記教育版APP」
    編輯:書林目前,市場上的網絡文檔筆記類產品大致有:有道雲筆記、OneNote、印象筆記、為知筆記、金山文檔、石墨文檔、騰訊文檔。作為雲筆記的鼻祖,印象筆記脫胎於Evernote,旨在幫助用戶便捷高效收集信息,並把信息智能整理為知識、轉化為行動。昨日,印象筆記與騰訊教育應用平臺宣布達成戰略合作協議,雙方將在遠程會議、文檔協作、在線課堂、雲端筆記、智慧作業等方面深入合作,為中國師生提供包含工具、內容和服務的教學一體化方案。
  • 有道筆記、印象筆記、OneNote到底哪個更好?
    有道筆記、印象筆記和office的OneNote是現在市面上用的比較多的三款筆記軟體。之前還有一個比較火的為知筆記,但是現在基本是比較廢了,也是一個收費軟體,暫且不提。印象筆記其實印象筆記算是筆記軟體裡邊的元老了。除了它這個多平臺登錄問題,其他的功能我覺得都無可挑剔,非常符合使用習慣。這也是我一直在使用有道和印象之間糾結的原因。
  • 印象筆記七周年:從矽谷公司到本土企業 「大象」如何瘦身?
    印象筆記CEO唐毅在現場分享發展路徑和戰略從矽谷公司到本土企業印象筆記七周年線下活動綠色背景下的灰色大象Evernote來自美國,2012年進入中國後,使用了「印象筆記」在華品牌名。有人曾將矽谷公司進入中國市場的發展歷程劃分為三個階段:第一階段是谷歌退出中國之前,以惠普、IBM等IT巨頭為代表,中國市場是它們重要的銷售增量;第二階段是以Evernote、Airbnb、Linkedln等公司為代表,它們在中國市場啟動了獨立的品牌和運營團隊,並在一定程度上引入了中方資本,但並無獨立決策權;最後一個階段是以印象筆記等公司的獨立為標誌,代表著矽谷公司正視市場差異和中國市場的重要性
  • 想要做出好看的筆記,2個筆記模板幫到你
    隨著移動網際網路發展,生活節奏變得越來越快,工作時間也越來越長,而且工作信息及各種微信、QQ等社交媒體上的信息宛如洪水猛獸一般進入到我們的生活中,面對這麼多信息,單單依靠大腦的記憶,明顯就不太可取,所以說我們就需要藉助輔助設備進行記錄,比如說印象筆記、石墨文檔以及有道雲筆記等記錄型app,或者筆記本、手帳等傳統的記錄工具都是我們記錄信息的不錯選擇。
  • 如何利用OneNote把手機kindle APP看書的筆記整理到電腦上?
    文 | 史記沒讀完 微信公眾號 | 知無涯不逾矩 如需轉載請在公眾號後臺聯繫授權 kindle電子書的生詞或者筆記導到電腦上比較簡單,直接連數據線,使用kindlemate一鍵導入,大家網上搜索一下即可。
  • 知新共學外刊群(2021年全年)
    知新共學外刊群自2017年11月上線以來,已經走過了近三年的時光。
  • 知之為知之,不知為不知,是知也
        對一個事物要知其然,更要知其所以然,司法辦案更是這樣。法學家與普通人的區別之一是能把一個詞寫成一篇論文,比如記得朋友大靜的碩士畢業論文就是《論自首》,這樣的一個刑法概念,能寫三萬來字,也著實要下一番功夫。
  • 還在為課堂筆記頭疼?推薦 2 種高效、美觀的方法
    我們在課堂上要對重要內容有概括了解而非過度注意細節,這類內容完全可以在課下的自主學習中掌握。圖 | 清華大學醫學院楊晨李曉安做的神經解剖學的讀書筆記3.名字、數量、日期。雖然這些也屬於細節信息,但是應當記錄下來,因為記錄這些信息花費的時間比起之後在書籍中查閱要少得多。4.重要的概念、專業表述。