有道云笔记快速迁移到语雀云笔记

该文章已生成可运行项目,

最近在切换工具,先是把 火狐浏览器 多平台更换了,换成夸克浏览器,有一说一,阿里的产品真的非常棒,尤其在运存管理和CPU 资源管理上,原来用火狐,风扇猛转,CPU 与运存高居不下,现在换成 夸克,感觉换了辆车一样,夸克除了书签同步迅猛外,扩展插件更是兼容谷歌与微软,估计其他浏览器插件也大部分兼容吧,我没测试,只是觉得这点非常 nice ,自定义的扩展插件也非常容易就能安装上去。最重要的就是书签同步,无论是 PC端,Phone 端,无论什么系统,书签同步就是即时的,没有之前火狐那样,换一个系统,书签同步缓冲半天都反应不过来。

其次,有道云笔记也是一个吃运存与资源的工具,我现在用的是17年 Intel芯片的 Mac Pro开发本,硬件资源有限,火狐跟有道云对这些老家伙硬件不友好,也是没得办法,换吧。

我的系统是 MacOS10.15.7 , Python3.11 的环境

第一阶段:从有道云笔记导出数据

1.1 准备导出环境

首先,需要在Mac上获取 youdaonote-pull工具,直接网络搜索资源,在 Git 上下载,解压后如下图 ,

如果你是 Mac 系统,pull.py 里边有个代码需要修改一下,可以直接拷贝覆盖原来的代码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import logging
import os
import platform
import re
import sys
import time
import traceback
import xml.etree.ElementTree as ET
from enum import Enum
from typing import Tuple

import requests
# from win32_setctime import setctime

from core import log
from core.api import YoudaoNoteApi
from core.common import get_script_directory
from core.covert import YoudaoNoteConvert
from core.image import ImagePull

__author__ = "Depp Wang (deppwxq@gmail.com)"
__github__ = "https//github.com/DeppWang/youdaonote-pull"

REGEX_SYMBOL = re.compile(r'[\\/:\*\?"<>\|]')  # 符号:\ / : * ? " < > |
MARKDOWN_SUFFIX = ".md"


class FileType(Enum):
    OTHER = 0
    MARKDOWN = 1
    XML = 2
    JSON = 3


class FileActionEnum(Enum):
    CONTINUE = "跳过"
    ADD = "新增"
    UPDATE = "更新"


class YoudaoNotePull(object):
    """
    有道云笔记 Pull 封装
    """

    def __init__(self):
        self.root_local_dir = None  # 本地文件根目录
        self.youdaonote_api = None
        self.smms_secret_token = None
        self.is_relative_path = None  # 是否使用相对路径

    def _covert_config(self, config_path=None) -> Tuple[dict, str]:
        """
        转换配置文件为 dict
        :param config_path: config 文件路径
        :return: (config_dict, error_msg)
        """

        config_path = (
            config_path
            if config_path
            else os.path.join(get_script_directory(), "config.json")
        )
        with open(config_path, "rb") as f:
            config_str = f.read().decode("utf-8")

        try:
            config_dict = json.loads(config_str)
        except:
            return (
                {},
                "请检查「config.json」格式是否为 utf-8 格式的 json!建议使用 Sublime 编辑「config.json」",
            )

        key_list = ["local_dir", "ydnote_dir", "smms_secret_token", "is_relative_path"]
        if key_list != list(config_dict.keys()):
            return (
                {},
                "请检查「config.json」的 key 是否分别为 local_dir, ydnote_dir, smms_secret_token, is_relative_path",
            )
        return config_dict, ""

    def _check_local_dir(self, local_dir, test_default_dir=None) -> Tuple[str, str]:
        """
        检查本地文件夹
        :param local_dir: 本地文件夹名(绝对路径)
        :return: local_dir, error_msg
        """
        # 如果没有指定本地文件夹,当前目录新增 youdaonote 目录
        if not local_dir:
            add_dir = test_default_dir if test_default_dir else "youdaonote"
            # 兼容 Windows 系统,将路径分隔符(\\)替换为 /
            local_dir = os.path.join(get_script_directory(), add_dir).replace("\\", "/")

        # 如果指定的本地文件夹不存在,创建文件夹
        if not os.path.exists(local_dir):
            try:
                os.mkdir(local_dir)
            except:
                return "", "请检查「{}」上层文件夹是否存在,并使用绝对路径!".format(local_dir)
        return local_dir, ""

    def _get_ydnote_dir_id(self, ydnote_dir) -> Tuple[str, str]:
        """
        获取指定有道云笔记指定目录 ID
        :param ydnote_dir: 指定有道云笔记指定目录
        :return: dir_id, error_msg
        """
        root_dir_info = self.youdaonote_api.get_root_dir_info_id()
        root_dir_id = root_dir_info["fileEntry"]["id"]

        # 如果不指定文件夹,取根目录 ID
        if not ydnote_dir:
            return root_dir_id, ""

        dir_info = self.youdaonote_api.get_dir_info_by_id(root_dir_id)
        for entry in dir_info["entries"]:
            file_entry = entry["fileEntry"]
            if file_entry["name"] == ydnote_dir:
                return file_entry["id"], ""

        return "", "有道云笔记指定顶层目录不存在"

    def get_ydnote_dir_id(self) -> Tuple[str, str]:
        """
        获取有道云笔记根目录或指定目录 ID
        :return:
        """
        config_dict, error_msg = self._covert_config()
        if error_msg:
            return "", error_msg
        local_dir, error_msg = self._check_local_dir(local_dir=config_dict["local_dir"])
        if error_msg:
            return "", error_msg
        self.root_local_dir = local_dir
        self.youdaonote_api = YoudaoNoteApi()
        error_msg = self.youdaonote_api.login_by_cookies()
        logging.info("本次使用 Cookies 登录")
        if error_msg:
            return "", error_msg
        self.smms_secret_token = config_dict["smms_secret_token"]
        self.is_relative_path = config_dict["is_relative_path"]
        return self._get_ydnote_dir_id(ydnote_dir=config_dict["ydnote_dir"])

    def _judge_type(self, file_id, youdao_file_suffix) -> Enum:
        """
        判断笔记类型
        :param file_id:
        :param youdao_file_suffix:
        :return:
        """
        file_type = FileType.OTHER
        # 1、如果文件是 .md 类型
        if youdao_file_suffix == MARKDOWN_SUFFIX:
            file_type = FileType.MARKDOWN
            return file_type
        elif (
            youdao_file_suffix == ".note"
            or youdao_file_suffix == ".clip"
            or youdao_file_suffix == ""
        ):
            response = self.youdaonote_api.get_file_by_id(file_id)
            # 2、如果文件以 `<?xml` 开头
            if response.content[:5] == b"<?xml":
                file_type = FileType.XML
            # 3、如果文件以 `{` 开头
            elif response.content.startswith(b'{"'):
                file_type = FileType.JSON
        return file_type

    def _get_file_action(self, local_file_path, modify_time) -> Enum:
        """
        获取文件操作行为
        :param local_file_path:
        :param modify_time:
        :return: FileActionEnum
        """
        # 如果不存在,则下载
        if not os.path.exists(local_file_path):
            return FileActionEnum.ADD

        # 如果已经存在,判断是否需要更新
        # 如果有道云笔记文件更新时间小于本地文件时间,说明没有更新,则不下载,跳过
        if modify_time <= os.path.getmtime(local_file_path):
            logging.info("此文件「%s」不更新,跳过", local_file_path)
            return FileActionEnum.CONTINUE
        # 同一目录存在同名 md 和 note 文件时,后更新文件将覆盖另一个
        return FileActionEnum.UPDATE

    def _optimize_file_name(self, name) -> str:
        """
        优化文件名
        :param name:
        :return:
        """
        # 替换下划线
        regex_symbol = re.compile(r"[<]")  # 符号: <
        # 删除特殊字符
        del_regex_symbol = re.compile(r'[\\/":\|\*\?#>]')  # 符号:\ / " : | * ? # >
        # 首尾的空格
        name = name.replace("\n", "")
        # 去除换行符
        name = name.strip()
        # 替换一些特殊符号
        name = regex_symbol.sub("_", name)
        name = del_regex_symbol.sub("", name)
        return name

    def pull_dir_by_id_recursively(self, dir_id, local_dir):
        """
        根据目录 ID 循环遍历下载目录下所有文件
        :param dir_id:
        :param local_dir: 本地目录
        :return: error_msg
        """
        dir_info = self.youdaonote_api.get_dir_info_by_id(dir_id)
        try:
            entries = dir_info["entries"]
        except KeyError:
            raise KeyError("有道云笔记修改了接口地址,此脚本暂时不能使用!请提 issue")
        for entry in entries:
            file_entry = entry["fileEntry"]
            id = file_entry["id"]
            name = file_entry["name"]
            if file_entry["dir"]:
                sub_dir = os.path.join(local_dir, name).replace("\\", "/")
                if not os.path.exists(sub_dir):
                    os.mkdir(sub_dir)
                self.pull_dir_by_id_recursively(id, sub_dir)
            else:
                modify_time = file_entry["modifyTimeForSort"]
                create_time = file_entry["createTimeForSort"]
                self._add_or_update_file(id, name, local_dir, modify_time, create_time)

    def _add_or_update_file(
        self, file_id, file_name, local_dir, modify_time, create_time
    ):
        """
        新增或更新文件
        :param file_id:
        :param file_name:
        :param local_dir:
        :param modify_time:
        :param create_time:
        :return:
        """
        file_name = self._optimize_file_name(file_name)
        youdao_file_suffix = os.path.splitext(file_name)[1]  # 笔记后缀
        original_file_path = os.path.join(local_dir, file_name).replace(
            "\\", "/"
        )  # 原后缀路径

        # 所有类型文件均下载,不做处理
        file_type = self._judge_type(file_id, youdao_file_suffix)

        # 「文档」类型本地文件均已 .md 结尾
        local_file_path = (
            os.path.join(
                local_dir, "".join([os.path.splitext(file_name)[0], MARKDOWN_SUFFIX])
            ).replace("\\", "/")
            if file_type != FileType.OTHER
            else original_file_path
        )

        # 如果有有道云笔记是「文档」类型,则提示类型
        tip = (
            ",云笔记原格式为 {}".format(file_type.name) if file_type != FileType.OTHER else ""
        )

        file_action = self._get_file_action(local_file_path, modify_time)
        if file_action == FileActionEnum.CONTINUE:
            return
        if file_action == FileActionEnum.UPDATE:
            # 考虑到使用 f.write() 直接覆盖原文件,在 Windows 下报错(WinError 183),先将其删除
            os.remove(local_file_path)
        try:
            self._pull_file(
                file_id,
                original_file_path,
                local_file_path,
                file_type,
                youdao_file_suffix,
            )
            if file_action == FileActionEnum.CONTINUE:
                logging.debug(
                    "{}「{}」{}".format(file_action.value, local_file_path, tip)
                )
            else:
                logging.info("{}「{}」{}".format(file_action.value, local_file_path, tip))

            # 本地文件时间设置为有道云笔记的时间
            if platform.system() == "Windows":
                try:
                    setctime(local_file_path, create_time)
                except:
                    os.utime(local_file_path, (create_time, modify_time))
            else:
                # Mac 和 Linux 使用 utime 设置时间
                os.utime(local_file_path, (create_time, modify_time))
            

        except Exception as error:
            logging.info(
                "{}「{}」可能失败!请检查文件!错误提示:{}".format(
                    file_action.value, original_file_path, format(error)
                )
            )

    def _pull_file(
        self, file_id, file_path, local_file_path, file_type, youdao_file_suffix
    ):
        """
        下载文件
        :param file_id:
        :param file_path:
        :param local_file_path: 本地
        :param file_type:
        :param youdao_file_suffix:
        :return:
        """
        # 1、所有的都先下载
        response = self.youdaonote_api.get_file_by_id(file_id)
        with open(file_path, "wb") as f:
            f.write(response.content)  # response.content 本身就是字节类型

        # 2、如果文件是 note 类型,将其转换为 MarkDown 类型
        if file_type == FileType.XML:
            try:
                YoudaoNoteConvert.covert_xml_to_markdown(file_path)
            except ET.ParseError:
                logging.info("此 note 笔记应该为 17 年以前新建,格式为 html,将转换为 Markdown ...")
                YoudaoNoteConvert.covert_html_to_markdown(file_path)
            except Exception as e:
                logging.info("note 笔记转换 MarkDown 失败,将跳过", repr(e))
        elif file_type == FileType.JSON:
            YoudaoNoteConvert.covert_json_to_markdown(file_path)

        # 3、迁移文本文件里面的有道云笔记图片(链接)
        if file_type != FileType.OTHER or youdao_file_suffix == MARKDOWN_SUFFIX:
            imagePull = ImagePull(
                self.youdaonote_api, self.smms_secret_token, self.is_relative_path
            )
            imagePull.migration_ydnote_url(local_file_path)


if __name__ == "__main__":
    log.init_logging()

    start_time = int(time.time())

    try:
        youdaonote_pull = YoudaoNotePull()
        ydnote_dir_id, error_msg = youdaonote_pull.get_ydnote_dir_id()
        if error_msg:
            logging.info(error_msg)
            sys.exit(1)
        logging.info("正在 pull,请稍后 ...")
        youdaonote_pull.pull_dir_by_id_recursively(
            ydnote_dir_id, youdaonote_pull.root_local_dir
        )
    except requests.exceptions.ProxyError:
        logging.info(
            "请检查网络代理设置;也有可能是调用有道云笔记接口次数达到限制,请等待一段时间后重新运行脚本,若一直失败,可删除「cookies.json」后重试"
        )
        traceback.print_exc()
        logging.info("已终止执行")
        sys.exit(1)
    except requests.exceptions.ConnectionError:
        logging.info("网络错误,请检查网络是否正常连接。若突然执行中断,可忽略此错误,重新运行脚本")
        traceback.print_exc()
        logging.info("已终止执行")
        sys.exit(1)
    # 链接错误等异常
    except Exception as err:
        logging.info("Cookies 可能已过期!其他错误:", format(err))
        traceback.print_exc()
        logging.info("已终止执行")
        sys.exit(1)

    end_time = int(time.time())
    logging.info("运行完成!耗时 {} 秒".format(str(end_time - start_time)))

1.2 获取并配置Cookie

由于有道云笔记的登录验证,工具无法直接通过账号密码登录,需要通过Cookie进行身份验证 

        1.2.1.获取Cookie:在Chrome或Edge浏览器中安装名为“Cookie-Editor”的插件。通过账户密码登录有道云笔记网页版后,通过此插件分别搜索框输入三个Cookie 的 key :YNOTE_CSTKYNOTE_LOGIN 和 YNOTE_SESS , 把对应的 值 记下来

        1.2.2.配置cookies.json:在 youdaonote-pull-master 目录下,打开 cookies.json文件(((如果没有这个 json 文件,则自己新建一个),将复制的三个Cookie值按指定格式填入 

{

  "cookies":

  [

     ["YNOTE_CSTK", "Cookie-Editor插件记录的值", ".note.youdao.com", "/"],        ["YNOTE_LOGIN", "Cookie-Editor插件记录的值", ".note.youdao.com","/"],       ["YNOTE_SESS", "Cookie-Editor插件记录的值", ".note.youdao.com", "/"]

  ]

}

        1.2.3 配置 config.json: 在 youdaonote-pull-master 目录下, config.json 文件修改以下信息,如果没有该文件,则新建一个config.json ,主要修改 local_dir 这个 key ,这个路径是保存 有道云笔记 的所有笔记文件,包括文件中的图片,我是保存在Mac 电脑的桌面,/Users/inn_4/Desktop/youdaoBeckUp/ , youdaoBeckUp是我自己创建的文件夹

{

         "local_dir": "/Users/inn_4/Desktop/youdaoBeckUp/",

        "ydnote_dir": "",

        "smms_secret_token": "",

        "is_relative_path": true

}

1.3 获取并配置Cookie 与执行导出

在终端中切换到脚本目录,运行命令 python pull.py

结果确认:工具运行成功后,您的笔记(通常会被转换为Markdown格式)以及关联的图片文件将会被下载到指定的本地目录中,下图是我下载好的 有道笔记 资源包路径 youdaoBeckUp -->  。

第二阶段:推送内容到语雀

2.1 导入内容至语雀

        2.1.1  我以 AI 模型为例:先登入 语雀 APP 或者网页版,新建一个知识库 【AI 模型】

        2.1.2  回到 youdaoBeckUp 路径下的 AI 模型文件夹下,全选所有内容,包括 images 文件夹,同时压缩成一个 .zip 文件

生成 一个 归档.zip 

回到语雀 APP 或者 网页版,进入 自定义的知识模块 ------- AI 模型 中,点击鼠标右键,导入

导入格式选择 Markdown

进一步选择 Markdown 打包后的压缩文件

从这里导入 归档.zip 压缩文件 , 至此,回到语雀刷新,即可看到新的数据。

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值