wshuo 发表于 2021-5-27 16:29

有道云笔记接口分析

本帖最后由 wshuo 于 2021-5-27 19:02 编辑

### 前言
上个月分析有道云笔记的接口,通过`charles` 抓包工具,对有道云笔记网页版的各种接口进行抓包分析,确定其参数作用,接口包含:`微信扫码登录`,`创建文件夹或文件`,`签到得流量`,`更新文件夹/文件的 名字或内容`,`获取资源内容,资源包括文件/图片/二进制文件`,`删除文件夹或文件`,`获取文件夹/文件详细信息`,`列出根目录下面的文件`,`根据fileId 列出对应目录下面文件夹或文件`,`上传图片并返回URL`。

另外有道云笔记的**markdown**笔记中上传图片,只有会员才可以直接拖拽上传,免费用户是禁止上传的,而我分析接口发现,上传图片的接口是没有经过是否为会员的身份认证的(只有普通的身份认证),换句话说,即使你是普通用户利用接口也可以直接上传图片,从而实现普通用户具有一个需要会员才能拥有的功能。
### 接口
有道云笔记的大部分接口都是需要具有身份认证cookie才能使用,所以使用接口的第一步总是进行扫码登录,登录后cookie保存在本地的文本文件中,下次运行时,再次载入文本文件中的cookie,从而实现登录状态,所以要使用我写的接口,需要绑定微信。
```python
import requests
import re
from threading import Thread
import time
import requests
from io import BytesIO
import http.cookiejar as cookielib
from PIL import Image
import sys
import psutil
from base64 import b64decode
import os

requests.packages.urllib3.disable_warnings()

class show_code(Thread):
    def __init__(self,data):
      Thread.__init__(self)
      self.data = data

    def run(self):
      img = Image.open(BytesIO(self.data))# 打开图片,返回PIL image对象
      img.show()

def is_login(session):
    headers = {'User-Agent':"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"}
    url = "https://note.youdao.com/yws/api/self?method=get&keyfrom=web&cstk=6yZBz-Ma"
    try:
      pass
      session.cookies.load(ignore_discard=True,ignore_expires=True)
    except Exception as e:
      pass
    response = session.get(url,verify=False,headers=headers)
    if response.json().get("name",""):
      print(response.json().get("name",""))
      return session,True
    else:
      return session,False

def login():
    if not os.path.exists(".cookie"):
      os.makedirs('.cookie')
    if not os.path.exists('.cookie/ydy.txt'):
      with open(".cookie/ydy.txt",'w') as f:
            f.write("")

    session = requests.session()
    session.cookies = cookielib.LWPCookieJar(filename='.cookie/ydy.txt')
    session,status = is_login(session)
    if not status:
      url = "https://note.youdao.com/login/acc/login?app=web&product=YNOTE&tp=wxoa&cf=6&fr=1"
      response = session.get(url,verify=False,allow_redirects=False)
      state = re.findall("state=(.*?)\&",response.headers['Location'])
      url = "https://note.youdao.com/ywx/login/get"
      response = session.get(url,verify=False)
      data = response.json()
      # with open('qrcode.jpg','wb') as f:
            # f.write(b64decode(data['img']))
      img_url = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+data['ticket']
      t= show_code(session.get(img_url,verify=False).content)
      t.start()
      uuid = data['uuid']
      fromdata = dict(uuid=uuid,t=int(time.time()*1000))
      url = 'https://note.youdao.com/ywx/login/query'
      while 1:
            response = session.post(url,verify=False,data=fromdata)
            data = response.json()
            print(data)

            if data['login']:
    #             # for proc in psutil.process_iter():# 遍历当前process
    #               # try:
    #               #   if proc.name() == "Microsoft.Photos.exe":
    #               #         proc.kill()# 关闭该process
    #               # except Exception as e:
    #               #   print(e)
                break
            time.sleep(1)

      url = "https://note.youdao.com/login/acc/callback?code=" + data['code'] + "&state=" + state
      session.get(url)
      session.cookies.save(ignore_discard=True,ignore_expires=True)
    return session

if __name__ == '__main__':
    session = login()
    # 签到接口
    url = "https://note.youdao.com/yws/mapi/user?method=checkin"
    data = session.post(url).json()
    print(data)
```
   
上面代码是扫码登录接口。登录后保存**cookie**,然后签到。


其它接口:
```python
import random
import binascii
import time
import requests
import os
import http.cookiejar as cookielib
import re
from PyQt5.QtCore import *

requests.packages.urllib3.disable_warnings()

class WorkThread(QThread):
    """docstring for WorkThread"""
    genxin = pyqtSignal(dict)
    def __init__(self):
      super(WorkThread, self).__init__()
    def run(self):
      self.login()

    def is_login(self):
      headers = {'User-Agent':"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"}
      url = "https://note.youdao.com/yws/api/self?method=get&keyfrom=web&cstk=6yZBz-Ma"
      try:
            pass
            self.session.cookies.load(ignore_discard=True,ignore_expires=True)
      except Exception as e:
            pass
      response = self.session.get(url,verify=False)
      if response.json().get("name",""):
            signal = dict(
            code = "userName",
            data = response.json().get("name",""),
            )
            self.genxin.emit(signal)
            return True
      else:
            return False

    def login(self):
      if not os.path.exists(".cookie"):
            os.makedirs('.cookie')
      if not os.path.exists('.cookie/ydy.txt'):
            with open(".cookie/ydy.txt",'w') as f:
                f.write("")

      self.session = requests.session()
      self.session.cookies = cookielib.LWPCookieJar(filename='.cookie/ydy.txt')
      status = self.is_login()
      if not status:
            url = "https://note.youdao.com/login/acc/login?app=web&product=YNOTE&tp=wxoa&cf=6&fr=1"
            response = self.session.get(url,verify=False,allow_redirects=False)
            state = re.findall("state=(.*?)\&",response.headers['Location'])
            # print(response.headers['Location'])
            url = "https://note.youdao.com/ywx/login/get"
            response = self.session.get(url,verify=False)
            data = response.json()
            # with open('qrcode.jpg','wb') as f:
                # f.write(b64decode(data['img']))
            img_url = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+data['ticket']
            imgData = self.session.get(img_url,verify=False).content
            with open("qrcode.jpg","wb") as f:
                f.write(imgData)
            signal = dict(
            code = "未登录",
            data = "",
            )
            self.genxin.emit(signal)
            uuid = data['uuid']
            fromdata = dict(uuid=uuid,t=int(time.time()*1000))
            url = 'https://note.youdao.com/ywx/login/query'
            while 1:
                response = self.session.post(url,verify=False,data=fromdata)
                data = response.json()
      #         # code = re.findall("window.wx_code='(.*?)'",response.text)
      #         # sys.exit()
               
                if data['login']:
      #             # for proc in psutil.process_iter():# 遍历当前process
      #               # try:
      #               #   if proc.name() == "Microsoft.Photos.exe":
      #               #         proc.kill()# 关闭该process
      #               # except Exception as e:
      #               #   print(e)
                  break
                time.sleep(1)

            url = "https://note.youdao.com/login/acc/callback?code=" + data['code'] + "&state=" + state
            self.session.get(url)
            signal = dict(
            code = "登录成功",
            data = "",
            )
            self.genxin.emit(signal)
            self.session.cookies.save(ignore_discard=True,ignore_expires=True)
            self.is_login() # 再次判断,为了传递用户名,防止第一次登录没有响应用户名
      signal = dict(
            code = "已登录",
            data = "",
            )
      self.genxin.emit(signal)
      return self.session

    def checkin(self):
      """
      签到代码
      """
      url = "https://note.youdao.com/yws/mapi/user?method=checkin"
      data = self.session.post(url).json()
      return data   

    def createId(self,):
      """
      创建一个ID, ID可以作为文件夹/文件/资源的唯一标识符号
      """
      fileId = ""
      for i in range(8):
            num = random.randint(65536,65536*2-1)
            fileId += hex(num)
      return fileId

    def create(self,name,isDir,parentId="",fileId="",bodyString=""):
      """
      创建文件夹或文件
      :name 创建的文件夹或文件的名字
      :isDir 是否是文件夹
      :parentId 标识创建的位置,父FileID
      :fileId 创建文件夹或文件的唯一ID,如果为空,那么生成一个fileID,如果不为空说明是更新
      :bodyString 当创建文件的时候,默认为空,当更新文件内容的时候,填入文件内容
      """
      timeStamp = int(time.time())
      if not fileId:
            fileId = "WEB" + self.createId()
            createTime = timeStamp
      else:
            createTime = None
      if not name:
            name = fileId + '.md'
      url = "https://note.youdao.com/yws/api/personal/sync?method=push&keyfrom=web"

      if isDir:
            payload={
            "fileId":fileId,
            "name":name,
            "domain":1,
            "parentId":parentId,
            "rootVersion":-1,
            "sessionId":"",
            "dir":True,
            "createTime":createTime,
            "modifyTime":timeStamp,
            "editorVersion":1602642730000,
            }

      else:
            payload={
            "fileId":fileId,
            "name":name,
            "domain":1,
            "parentId":parentId,
            "rootVersion":-1,
            "sessionId":"",
            "dir":False,
            "createTime":createTime,
            "modifyTime":timeStamp,
            "editorVersion":1602642730000,

            "bodyString":bodyString,
            "transactionTime": timeStamp,
            "transactionId":fileId,
            }
      response = self.session.post(url,data=payload)
      return fileId

    def update(self,name,fileId,bodyString=""):
      """
      更新文件夹/文件的 名字或内容
      """
      if bodyString:
            self.create(name,False,fileId=fileId,bodyString=bodyString)
      else:
            self.create(name,True,fileId=fileId,bodyString=bodyString)

    def getFileContent(self,fileId):
      """
      获取资源内容,资源包括文件/图片/二进制文件
      """
      url = "https://note.youdao.com/yws/api/personal/sync?method=download&keyfrom=web"
      payload={
      "fileId": fileId,
      "version": -1,
      "read": True,
      }
      response = self.session.post(url,data=payload)
      return response.text
      # with open("1.jpg","wb") as f:
      #   f.write(response.content)

    def delete(self,fileId):
      """
      删除文件夹或文件
      """
      url = "https://note.youdao.com/yws/mapi/personal/sync"
      payload={
      "method": "delete",
      "fileId": fileId,
      "rootVersion": -1,
      "sessionId": "",
      "modifyTime": int(time.time()),
      "keyfrom": "web",
      }
      response = self.session.post(url, data=payload)
      # print(response.json())

    def getDetail(self,fileId):
      """
      获取文件夹/文件详细信息
      """
      url = f"https://note.youdao.com/yws/api/personal/file/{fileId}?method=getById&keyfrom=web"
      response = self.session.get(url,)
      print(response.json())

    def listRoot(self):
      """
      列出根目录下面的文件
      """
      url = "https://note.youdao.com/yws/api/personal/file?method=listEntireByParentPath&keyfrom=web&path=/"
      response = self.session.post(url,)
      return response.json()

    def listDir(self,fileId):
      """
      根据fileId 列出对应目录下面文件夹或文件
      """
      url = f"https://note.youdao.com/yws/api/personal/file/{fileId}?all=true&f=true&len=30&sort=1&isReverse=false&method=listPageByParentId&keyfrom=web&cstk=XSoyL7tI"
      # lastId: 4B94F19AF663408EA5CFD0A10B826798
      response = self.session.post(url)
      return response.json()

    def uploadImage(self,filePath,resourceName="",resourceId=""):
      """
      上传图片并返回URL
      :filePath 资源的文件的路径
      :resourceName 在服务器上的资源名字,一般无用
      :resourceId 资源唯一ID,如果为空,默认随机生成一个
      """
      with open(filePath,"rb") as f:
            data = f.read()
      timeStamp = int(time.time())
      if not resourceId:
            resourceId = "WEBRESOURCE" + createId()
      if not resourceName:
            dir_,resourceName = os.path.split(filePath)
      print(resourceName)
      headers = {
      "File-Size":str(len(data))
      }
      url = "https://note.youdao.com/yws/api/personal/sync/upload?cstk=ApF2T7G&keyfrom=web"
      response = self.session.post(url)
      transmitId= response.json()["transmitId"]

      url = f"https://note.youdao.com/yws/api/personal/sync/upload/{transmitId}"
      response = self.session.post(url,data=data)
      json_data = response.json()
      url = f"https://note.youdao.com/yws/api/personal/sync?method=putResource&resourceId={resourceId}&resourceName={resourceName}&rootVersion=-1&sessionId=&transmitId={transmitId}&genIcon=true&createTime={timeStamp}&modifyTime={timeStamp}&keyfrom=web"
      response = self.session.post(url)
      json_data = response.json()
      print(resourceId)
      print(response.headers["url"])

if __name__ == '__main__':
    workthread = WorkThread()
    workthread.run()

```
上面代码是我利用PyQt5与有道云笔记结合的接口,由于登录是需要消耗时间,所以我将其独立成一个QThread线程,这样防止阻塞界面线程。在登录过程中有几个状态,让其界面线程知道什么时候展示二维码,知道什么时候登录成功(关闭展示二维码)。

后续我想到的一些修改:登录的时候保存cookie用的是`cookiejar` 保存的cookie,说实话使用的时候不是特别方便,而我想到另一种保存cookie的方法。当登录成功后的session对象,将其用pickle.dump保存成一个本地文件(序列化),而下次登录的时候可以用pickle.load载入对象(反序列化),这样用来记录登录状态感觉比使用 **cookiejar**保存cookie方便很多。

另外我通过我分析的有道云笔记接口,做了俩个软件:有道云图床,[有道云便签地址](https://www.52pojie.cn/thread-1448436-1-1.html)。后续我会把这俩软件放出来。

wshuo 发表于 2021-5-27 17:00

forgives 发表于 2021-5-27 16:50
这里的`cstk=ApF2T7G` 参数应该是csrf token , 应该不能够重复长期使用的。

这个参数是没用的,即使你不加这个参数也是可以请求成功的,因为我这部分代码已经用一个月了(我做的一个有道云便签软件里面使用了这些接口)

forgives 发表于 2021-5-27 16:50

这里的`cstk=ApF2T7G` 参数应该是csrf token , 应该不能够重复长期使用的。

hjg2012 发表于 2021-5-27 17:09

厉害厉害

zhanglushuai 发表于 2021-5-27 17:21

太棒了!

探索知识 发表于 2021-5-27 17:26

我掉级了吗,有的不能回复了呢

guonetnet 发表于 2021-5-27 18:12

这个不错,希望能放出编译好的软件,谢谢!

地藏王菩萨 发表于 2021-5-27 18:36

这个不错,感谢谢谢!

Vonalier 发表于 2021-5-27 18:44

大佬牛逼啊,期待一波{:301_1001:}

BDBD168 发表于 2021-5-27 18:48

感谢楼主分享
页: [1] 2
查看完整版本: 有道云笔记接口分析