有道云笔记数据备份相关

PC客户端导出

最新版有导出全部文件的功能,但限制90天一次,除非开通会员(无限次)

开放平台API

开放平台地址

申请理由

我是一个个人开发者,API不是在APP或网站中使用,主要是作为个人的学习,测试及自己笔记的整理归档等。

主页为 https 协议,但 提交接口 为 http 协议,请求被浏览器阻止了,我们可以 F12 手动修改 html 元素,之后再提交

应用图片必须按照要求的格式和大小来上传,不然报错:图片格式不正确,可以使用 在线图片裁剪工具 来帮忙裁剪。

还是报错:未知错误,此功能可能有道云也不维护了。

使用接口模拟网页端调用

根据父目录查询子文件列表

https://note.youdao.com/yws/api/personal/file/F98AA8589CDA4035BEF23A271A295E46

根据文件ID下载文件内容

https://note.youdao.com/yws/api/personal/sync

具体代码

点击查看代码
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Feign;
import feign.FeignException;
import feign.Response;
import feign.Util;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.form.FormEncoder;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;

/**
 * 使用接口模拟网页版调用下载markdown类型笔记
 */
public class TestYoudaoyunNoteBackup2 {

    private static final String BASE_URL = "https://note.youdao.com/yws/api";
    private static final String CSTK = "lUwgyhL9"; // 从网页端请求中获取
    private static final String ROOT_FILEID = "WEBa042d0b14d7ac9f04727385b8d4c0811"; // 从网页端 url 中获取
    private static final StatisticsInfo STATISTICS_INFO = new StatisticsInfo();

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static {
        // 忽略未知属性
        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public static void main(String[] args) throws IOException {
        String cookie = getCookie();
        YoudaoyunNoteClient client = createClient();
        ;
        String rootFilePath = getRootFilePath();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        System.out.println(new Date());
        listEntryRecursiveAndFlatMap(client, ROOT_FILEID, cookie, rootFilePath, "");
        stopWatch.stop();
        System.out.println("花费时间:" + stopWatch.getTime() + "ms");
        System.out.println("统计信息:" + STATISTICS_INFO);
    }

    /**
     * 单线程备份,文件名为当前文件名称+上级目录名称
     */
    private static void listEntryRecursiveAndFlatMap(YoudaoyunNoteClient client, String parentFileId, String cookie,
                                                     String rootFilePath, String parentFileName) {
        ListByParentIdResponse response = client.listByParentId(parentFileId, true, true, 100, 1, false, "listPageByParentId", CSTK, cookie);
        for (FileInfo fileInfo : response.getEntries()) {
            FileEntry fileEntry = fileInfo.getFileEntry();
            String name = fileEntry.getName();
            Boolean dir = fileEntry.getDir();
            String fileId = fileEntry.getId();
            if (StringUtils.isNotBlank(parentFileName)) {
                name = parentFileName + "-" + name;
            }
            File file = new File(rootFilePath, name);
            if (dir) {
                boolean mkdirs = file.mkdirs();
                if (!mkdirs) {
                    throw new RuntimeException("创建文件夹失败");
                }
                STATISTICS_INFO.folderQuantityIncr();
                listEntryRecursiveAndFlatMap(client, fileId, cookie, file.getAbsolutePath(), name);
            } else {
                if (fileEntry.isMarkdown()) {
                    String fileContent = client.sync("download", fileId, -1, true, CSTK, cookie);
                    writeToFile(file, fileContent);
                    STATISTICS_INFO.fileQuantityIncr();
                }
            }
        }
    }

    private static String getRootFilePath() {
        String targetFilePath = "/Users/xxx/Desktop/files/youdaoyunnotebackup";
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss");
        String currentFormatTime = dateTimeFormatter.format(LocalDateTime.now());
        String rootFilePath = targetFilePath + "/" + currentFormatTime;
        File file = new File(rootFilePath);
        if (!file.exists()) {
            file.mkdirs();
        }
        return rootFilePath;
    }

    private static void writeToFile(File targetFile, String fileContent) {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(targetFile))) {
            bw.write(fileContent);
            bw.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("文件下载成功-> " + targetFile.getAbsolutePath() + "  " + new Date());
    }

    private static String getCookie() throws IOException {
        // 从网页端请求中获取
        InputStream inputStream = TestYoudaoyunNoteBackup2.class.getClassLoader().getResourceAsStream("youdaoyun/cookie.txt");
        return IOUtils.toString(inputStream, StandardCharsets.UTF_8.name());
    }

    private static YoudaoyunNoteClient createClient() {
        Feign.Builder builder = Feign.builder()
                .decoder(new MyDecoder())
                .contract(new SpringMvcContract())
                .encoder(new FormEncoder());
        return builder.target(YoudaoyunNoteClient.class, BASE_URL);
    }

    interface YoudaoyunNoteClient {
        /**
         * 根据父文件ID查询子文件列表
         *
         * @param parentFileId 父文件ID
         * @param all          是否查询全部 既包含文件夹也包含文件
         * @param f            未知
         * @param len          查询长度
         * @param sort         是否排序
         * @param isReverse    是否倒序
         * @param method       服务器端执行方法
         * @param cstk         未知 用户信息相关
         * @param cookie       用户信息
         * @return
         */
        @GetMapping("/personal/file/{parentFileId}")
        ListByParentIdResponse listByParentId(@PathVariable("parentFileId") String parentFileId,
                                              @RequestParam("all") Boolean all,
                                              @RequestParam("f") Boolean f,
                                              @RequestParam("len") Integer len,
                                              @RequestParam("sort") Integer sort,
                                              @RequestParam("salt") Boolean isReverse,
                                              @RequestParam("method") String method,
                                              @RequestParam("cstk") String cstk,
                                              @RequestHeader("Cookie") String cookie);

        @PostMapping(value = "/personal/sync", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        String sync(@RequestParam("method") String method,
                    @RequestPart("fileId") String fileId,
                    @RequestPart("version") Integer version,
                    @RequestPart("read") Boolean read,
                    @RequestPart("cstk") String cstk,
                    @RequestHeader("Cookie") String cookie);
    }

    static class MyDecoder implements Decoder {

        @Override
        public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
            if (String.class.equals(type)) {
                return Util.toString(response.body().asReader(Util.UTF_8));
            }
            InputStream inputStream = response.body().asInputStream();
            return OBJECT_MAPPER.readValue(inputStream, ResolvableType.forType(type).getRawClass());
        }
    }

    @Data
    static class ListByParentIdResponse {
        private List<FileInfo> entries;
    }

    @Data
    static class FileInfo {
        private FileEntry fileEntry;
    }

    @Data
    static class FileEntry {

        private String name;
        private Boolean dir;
        private String id;
        //0表示markdown 5表示word
        private String noteType;

        @JsonIgnore
        public boolean isMarkdown() {
            return "0".equals(noteType);
        }
    }

    @Data
    static class StatisticsInfo {
        private int folderQuantity;
        private int fileQuantity;

        private void folderQuantityIncr() {
            folderQuantity++;
        }

        private void fileQuantityIncr() {
            fileQuantity++;
        }
    }
}

使用别人封装的脚本

youdaonote-pull github

使用 Python 编写,底层也是对上述两个接口的封装,我们的 Java 实现也是参考了此脚本。

参考

有道云笔记 备份与导出
youdaonote-pull github

posted @ 2024-02-11 16:48  strongmore  阅读(113)  评论(0编辑  收藏  举报