有道云笔记数据备份相关
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++;
}
}
}
使用别人封装的脚本
使用 Python 编写,底层也是对上述两个接口的封装,我们的 Java 实现也是参考了此脚本。