前言
上个月分析有道云笔记的接口,通过charles
抓包工具,对有道云笔记网页版的各种接口进行抓包分析,确定其参数作用,接口包含:微信扫码登录
,创建文件夹或文件
,签到得流量
,更新文件夹/文件的 名字或内容
,获取资源内容,资源包括文件/图片/二进制文件
,删除文件夹或文件
,获取文件夹/文件详细信息
,列出根目录下面的文件
,根据fileId 列出对应目录下面文件夹或文件
,上传图片并返回URL
。
另外有道云笔记的markdown笔记中上传图片,只有会员才可以直接拖拽上传,免费用户是禁止上传的,而我分析接口发现,上传图片的接口是没有经过是否为会员的身份认证的(只有普通的身份认证),换句话说,即使你是普通用户利用接口也可以直接上传图片,从而实现普通用户具有一个需要会员才能拥有的功能。
接口
有道云笔记的大部分接口都是需要具有身份认证cookie才能使用,所以使用接口的第一步总是进行扫码登录,登录后cookie保存在本地的文本文件中,下次运行时,再次载入文本文件中的cookie,从而实现登录状态,所以要使用我写的接口,需要绑定微信。
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'])[0]
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,然后签到。
其它接口:
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'])[0]
# 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)[3:]
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方便很多。
另外我通过我分析的有道云笔记接口,做了俩个软件:有道云图床,有道云便签地址。后续我会把这俩软件放出来。