# 物实换封面脚本

**URL:** <http://physicslab.turtlesim.com/t/topic/625>\
**Category:** 使用教程\
**Tags:** zh\_CN\
**Created:** [2024 年7 月 26 日 17:53 UTC](http://physicslab.turtlesim.com/t/topic/625 "2024-07-26T17:53:25Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

作者： ![AMDYES](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/amdyes/32/492_2.png) [@AMDYES](http://physicslab.turtlesim.com/u/AMDYES)\
发布日期： [2024 年7 月 26 日 17:53 UTC](http://physicslab.turtlesim.com/t/topic/625/1 "2024-07-26T17:53:25Z")

</div>

这玩意也是鸽了老长时间，一开始我发现怎么做的时候就随便写了个雏形，然后也没用几次，后来又去鼓捣别的玩意，就这样给搁置了  
今天晚上把它随便完善了一下  
使用Python编写，需要requests库  
首先要用这个脚本获取一下登录参数（token和authcode）  
**请务必保管好自己的token和authcode，一旦泄露，任何得到它的人都可以通过直接调用API来使用你的账号！**

```Python
import gzip
import json
import random
import requests
from requests import HTTPError

def random_device_id() -> str:
    """
    生成随机的设备ID（Device.Identifier），用于登录

    设备ID是40个十六进制数字（字母小写，没有0x前缀）
    :return: 随机设备ID字符串
    """
    return hex(random.randint(0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))[2:].rjust(40, '0')

headers = {
    'x-API-Version': '2411',
    'Accept-Encoding': 'gzip',
    'Content-Type': 'gzipped/json'
}

def login(email: str, password: str) -> requests.Response:
    data = {
        'Login': email,
        'Password': password,
        'Version': 2411,

        'Device': {
            'Identifier': random_device_id(),
            'Timezone': 'China Standard Time',
            'Language': 'Chinese'
        }
    }

    g = gzip.compress(json.dumps(data).encode('utf-8'))
    return requests.post('https://physics-api-cn.turtlesim.com/Users/Authenticate', headers=headers, data=g)

def main():
    email = input('请输入邮箱：')
    password = input('请输入密码：')
    resp = login(email, password)
    resp.raise_for_status()
    data = resp.json()
    if data['Status'] == 403:
        raise RuntimeError('邮箱或密码错误')
    elif data['Status'] != 200:
        raise RuntimeError(f'登录失败，错误{data["Status"]}，信息：{data["Message"]}')

    token = data['Token']
    authcode = data['AuthCode']
    print(f'token：{token}')
    print(f'authcode：{authcode}')

if __name__ == ' __main__':
    try:
        main()
    except HTTPError as e:
        print('网络或服务端错误：', e)
    except RuntimeError as e:
        print(e)

```

* * *

下面这个才是换封面脚本，登录成功得到token和authcode之后，把它以字符串的形式填到下面这个脚本的headers字典中，按提示操作即可

```Python
import gzip
import json
import os
import re

import requests
from requests import HTTPError

headers = {
    'x-API-Token': token, # token,
    'x-API-AuthCode': authcode, # authcode,
    'x-API-Version': '2411',
    'Accept-Encoding': 'gzip',
    'Content-Type': 'gzipped/json'
}

def json_gzip_encode(data: dict) -> bytes:
    """
    字典转JSON并用GZIP压缩
    :param data: 要转换的字典
    :return: 转换后的bytes
    """
    return gzip.compress(json.dumps(data).encode('utf-8'))

def get_summary(content_id: str, category: str) -> requests.Response:
    """
    获取实验介绍（没有电路）
    :param content_id: 实验序号
    :param category: 实验分区，实验区 = Experiment，黑洞区 = Discussion
    :return: 响应对象
    """
    data = {
        'ContentID': content_id,
        'Category': category
    }

    return requests.post('https://physics-api-cn.turtlesim.com/Contents/GetSummary',
                         headers=headers,
                         data=json_gzip_encode(data))

def submit_experiment(data: dict) -> requests.Response:
    """
    更新实验
    :param data: 实验的字典
    :return: 响应对象
    """
    return requests.post('https://physics-api-cn.turtlesim.com/Contents/SubmitExperiment',
                         headers=headers,
                         data=json_gzip_encode(data))

def upload(filename: str, policy: str, authorization: str) -> requests.Response:
    """
    上传实验图片
    :param filename: 图片文件名
    :param policy: 由更新实验请求得到的policy字段
    :param authorization: 由更新实验请求得到的authorization字段
    :return: 响应对象
    """
    with open(filename, 'rb') as f:
        data = {
            'policy': (None, policy, None),
            'authorization': (None, authorization, None),
            'file': ('temp.jpg', f, None)
        }
        return requests.post('http://v0.api.upyun.com/qphysics', files=data)

def confirm_experiment(summary_id: str, category: str, image: int) -> requests.Response:
    """
    实验图片上传完成后的确认
    :param summary_id: 实验序号
    :param category: 实验分区，实验区 = Experiment，黑洞区 = Discussion
    :param image: 图片序号
    :return: 响应对象
    """
    data = {
        'Category': category,
        'SummaryID': summary_id,
        'Image': image,
        'Extension': '.jpg'
    }
    return requests.post('https://physics-api-cn.turtlesim.com/Contents/ConfirmExperiment',
                         headers=headers,
                         data=json_gzip_encode(data))

def main():
    content_id = input('请输入实验序号：').strip()
    category_input = input('请输入实验分区，1=实验，2=黑洞：').strip()
    filename = input('请输入图片文件名：').strip('"')

    if not re.match('^[a-fA-F0-9]{24}$', content_id):
        raise RuntimeError('实验序号无效')
    categories = {'1': 'Experiment', '2': 'Discussion'}
    if category_input not in categories:
        raise RuntimeError('实验分区无效')
    category = categories[category_input]

    # ---------------------
    # 获取实验简介
    summary_resp = get_summary(content_id, category)
    summary_resp.raise_for_status()
    summary_json = summary_resp.json()

    status = summary_json['Status']
    message = summary_json['Message']

    match status:
        case 403:
            raise RuntimeError(f'登录失效，请重新获取token和authcode（{message}）')
        case 404:
            raise RuntimeError(f'实验不存在，也可能是分区选错了！（{message}）')
        case _ if status != 200:
            raise RuntimeError(f'获取实验信息失败，错误{status}，信息：{message}')

    summary = summary_json['Data']
    if '$type' in summary:
        del summary['$type']

    print('获取实验信息成功')

    # ---------------------
    # 更新实验
    file_size = os.path.getsize(filename)
    submit = {
        'Request': {
            'FileSize': file_size,
            'Extension': '.jpg'
        },
        'Summary': summary
    }
    submit_resp = submit_experiment(submit)
    submit_resp.raise_for_status()
    submit_json = submit_resp.json()

    status = submit_json['Status']
    message = submit_json['Message']

    if status == 403:
        raise RuntimeError(f'这不是你的实验，无法更改封面（{message}）')
    elif status != 200:
        raise RuntimeError(f'更新实验（请求上传）失败，错误{status}，信息：{message}')

    print('更新实验（请求上传）成功')

    # 上传
    policy = submit_json['Data']['Token']['Policy']
    authorization = submit_json['Data']['Token']['Authorization']
    upload(filename, policy, authorization)
    print('图片上传完成')

    # 确认
    confirm_experiment(summary_id=summary['ID'],
                       category=category,
                       image=summary['Image'] + 1)
    print('更新图片序号完成')
    print('更换封面成功')

if __name__ == ' __main__':
    try:
        main()
    except HTTPError as e:
        print('网络或服务端错误：', e)
    except OSError as e:
        print('图片文件不存在', e)
    except RuntimeError as e:
        print(e)

```

---

<div class="post-metadata">

作者： ![Electrolysis](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/electrolysis/32/142_2.png) [@Electrolysis](http://physicslab.turtlesim.com/u/Electrolysis)\
发布日期： [2024 年7 月 28 日 08:45 UTC](http://physicslab.turtlesim.com/t/topic/625/2 "2024-07-28T08:45:36Z")

</div>

有意思，以后我试试C++也搞一个…

---

<div class="post-metadata">

作者： ![AMDYES](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/amdyes/32/492_2.png) [@AMDYES](http://physicslab.turtlesim.com/u/AMDYES)\
发布日期： [2024 年7 月 28 日 11:22 UTC](http://physicslab.turtlesim.com/t/topic/625/3 "2024-07-28T11:22:23Z")

</div>

过了一天就修改不了吗？  
这玩意也是鸽了老长时间，一开始我发现怎么做的时候就随便写了个雏形，然后也没用几次，后来又去鼓捣别的玩意，就这样给搁置了  
今天晚上把它随便完善了一下  
使用Python编写，需要requests库（pip install requests）  
作用是可以把你的实验封面换成任何图片，格式支持jpg、png、gif（不能动）、webp

# 1.登录脚本

首先要用这个脚本获取一下登录参数（token和authcode）  
**请务必保管好自己的token和authcode，一旦泄露，任何得到它的人都可以通过直接调用API来使用你的账号！**

```Python
import gzip
import json
import random
import requests
from requests import HTTPError

def random_device_id() -> str:
    """
    生成随机的设备ID（Device.Identifier），用于登录

    设备ID是40个十六进制数字（字母小写，没有0x前缀）
    :return: 随机设备ID字符串
    """
    return hex(random.randint(0, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))[2:].rjust(40, '0')

headers = {
    'x-API-Version': '2411',
    'Accept-Encoding': 'gzip',
    'Content-Type': 'gzipped/json'
}

def login(email: str, password: str) -> requests.Response:
    data = {
        'Login': email,
        'Password': password,
        'Version': 2411,

        'Device': {
            'Identifier': random_device_id(),
            'Timezone': 'China Standard Time',
            'Language': 'Chinese'
        }
    }

    g = gzip.compress(json.dumps(data).encode('utf-8'))
    return requests.post('https://physics-api-cn.turtlesim.com/Users/Authenticate', headers=headers, data=g)

def main():
    email = input('请输入邮箱：')
    password = input('请输入密码：')
    resp = login(email, password)
    resp.raise_for_status()
    data = resp.json()
    if data['Status'] == 403:
        raise RuntimeError('邮箱或密码错误')
    elif data['Status'] != 200:
        raise RuntimeError(f'登录失败，错误{data["Status"]}，信息：{data["Message"]}')

    token = data['Token']
    authcode = data['AuthCode']
    print(f'token：{token}')
    print(f'authcode：{authcode}')

if __name__ == ' __main__':
    try:
        main()
    except HTTPError as e:
        print('网络或服务端错误：', e)
    except RuntimeError as e:
        print(e)

```

# 2.换封面脚本

下面这个是换封面脚本，登录成功得到token和authcode之后，把它以字符串的形式填到下面这个脚本的headers字典中，按提示操作即可  
小窍门：windows系统按住shift右键点击文件，会有“复制文件地址”的选项，在要求输入图片文件名时粘贴进来就可以  
也可以直接把文件拖进控制台（部分IDE或控制台不支持）

```Python
import gzip
import json
import os
import re

import requests
from requests import HTTPError

from token_authcode import token, authcode

headers = {
    'x-API-Token': token, # token,
    'x-API-AuthCode': authcode, # authcode,
    'x-API-Version': '2411',
    'Accept-Encoding': 'gzip',
    'Content-Type': 'gzipped/json'
}

def json_gzip_encode(data: dict) -> bytes:
    """
    字典转JSON并用GZIP压缩
    :param data: 要转换的字典
    :return: 转换后的bytes
    """
    return gzip.compress(json.dumps(data).encode('utf-8'))

def get_summary(content_id: str, category: str) -> requests.Response:
    """
    获取实验介绍（没有电路）
    :param content_id: 实验序号
    :param category: 实验分区，实验区 = Experiment，黑洞区 = Discussion
    :return: 响应对象
    """
    data = {
        'ContentID': content_id,
        'Category': category
    }

    return requests.post('https://physics-api-cn.turtlesim.com/Contents/GetSummary',
                         headers=headers,
                         data=json_gzip_encode(data))

def submit_experiment(data: dict) -> requests.Response:
    """
    更新实验
    :param data: 实验的字典
    :return: 响应对象
    """
    return requests.post('https://physics-api-cn.turtlesim.com/Contents/SubmitExperiment',
                         headers=headers,
                         data=json_gzip_encode(data))

def upload(filename: str, policy: str, authorization: str) -> requests.Response:
    """
    上传实验图片
    :param filename: 图片文件名
    :param policy: 由更新实验请求得到的policy字段
    :param authorization: 由更新实验请求得到的authorization字段
    :return: 响应对象
    """
    with open(filename, 'rb') as f:
        data = {
            'policy': (None, policy, None),
            'authorization': (None, authorization, None),
            'file': ('temp.jpg', f, None)
        }
        return requests.post('http://v0.api.upyun.com/qphysics', files=data)

def main():
    content_id = input('请输入实验序号：').strip()
    category_input = input('请输入实验分区，1=实验，2=黑洞：').strip()
    filename = input('请输入图片文件名：').strip('"')

    # 实验序号是24位16进制字符串
    if not re.match('^[a-fA-F0-9]{24}$', content_id):
        raise RuntimeError('实验序号无效')
    categories = {'1': 'Experiment', '2': 'Discussion'}
    if category_input not in categories:
        raise RuntimeError('实验分区无效')
    category = categories[category_input]

    # ---------------------
    # 获取实验简介
    summary_resp = get_summary(content_id, category)
    summary_resp.raise_for_status()
    summary_json = summary_resp.json()

    status = summary_json['Status']
    message = summary_json['Message']

    match status:
        case 403:
            raise RuntimeError(f'登录失效，请重新获取token和authcode（{message}）')
        case 404:
            raise RuntimeError(f'实验不存在，也可能是分区选错了！（{message}）')
        case _ if status != 200:
            raise RuntimeError(f'获取实验信息失败，错误{status}，信息：{message}')

    summary = summary_json['Data']
    if '$type' in summary:
        del summary['$type']

    print('获取实验信息成功')

    # ---------------------
    # 更新实验
    file_size = os.path.getsize(filename)
    submit = {
        'Request': {
            'FileSize': file_size,
            'Extension': '.jpg'
        },
        'Summary': summary
    }
    submit_resp = submit_experiment(submit)
    submit_resp.raise_for_status()
    submit_json = submit_resp.json()

    status = submit_json['Status']
    message = submit_json['Message']

    if status == 403:
        raise RuntimeError(f'这不是你的实验，无法更改封面（{message}）')
    elif status != 200:
        raise RuntimeError(f'更新实验（请求上传）失败，错误{status}，信息：{message}')

    print('更新实验（请求上传）成功')

    # ---------------------
    # 上传
    policy = submit_json['Data']['Token']['Policy']
    authorization = submit_json['Data']['Token']['Authorization']
    upload(filename, policy, authorization).raise_for_status()
    print('图片上传完成')

    # ---------------------
    # 更新图片序号
    # 改用/Contents/SubmitExperiment
    # 放弃/Contents/ConfirmExperiment是因为如果你是合作者就会报403
    summary['Image'] += 1
    update_image_resp = submit_experiment({'Summary': summary})
    update_image_resp.raise_for_status()
    update_image_json = update_image_resp.json()

    status = update_image_json['Status']
    message = update_image_json['Message']

    if status != 200:
        raise RuntimeError(f'更新图片序号失败，错误{status}，信息：{message}')

    print('更新图片序号完成')
    print('更换封面成功')

if __name__ == ' __main__':
    try:
        main()
    except HTTPError as e:
        print('网络或服务端错误：', e)
    except OSError as e:
        print('图片文件不存在', e)
    except RuntimeError as e:
        print(e)

```

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年7 月 29 日 13:59 UTC](http://physicslab.turtlesim.com/t/topic/625/4 "2024-07-29T13:59:34Z")

</div>

两次SubmitExperiment会在发布实验的时候出问题

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年7 月 29 日 14:00 UTC](http://physicslab.turtlesim.com/t/topic/625/5 "2024-07-29T14:00:26Z")

</div>

以及requests.post的json参数可以在很多时候代替你那个手动gzip

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年7 月 29 日 14:01 UTC](http://physicslab.turtlesim.com/t/topic/625/6 "2024-07-29T14:01:48Z")

</div>

> <https://github.com/GoodenoughPhysicsLab/physicsLab/blob/master/physicsLab/web.py>

~~厚颜无耻贴个广告()~~

---

<div class="post-metadata">

作者： ![AMDYES](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/amdyes/32/492_2.png) [@AMDYES](http://physicslab.turtlesim.com/u/AMDYES)\
发布日期： [2024 年7 月 29 日 15:31 UTC](http://physicslab.turtlesim.com/t/topic/625/7 "2024-07-29T15:31:22Z")

</div>

这个我一开始就没想让它能发布实验，只能修改，避免有人用来刷屏  
还有好像不用gzip也能提交……  
请求头Content-Type: application/json  
Accept-Encoding去掉

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年7 月 30 日 01:16 UTC](http://physicslab.turtlesim.com/t/topic/625/8 "2024-07-30T01:16:02Z")

</div>

刚才发现紫应该更新了api, 现在拿到token与auto\_code也会爆Login.Required

---

<div class="post-metadata">

作者： ![Electrolysis](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/electrolysis/32/142_2.png) [@Electrolysis](http://physicslab.turtlesim.com/u/Electrolysis)\
发布日期： [2024 年7 月 30 日 13:43 UTC](http://physicslab.turtlesim.com/t/topic/625/9 "2024-07-30T13:43:19Z")

</div>

紫：怕到时候又有一百多个水实验刷出来（

---

<div class="post-metadata">

作者： ![John-Chen](http://physicslab.turtlesim.com/letter_avatar_proxy/v4/letter/j/4491bb/32.png) [@John-Chen](http://physicslab.turtlesim.com/u/John-Chen)\
发布日期： [2024 年8 月 1 日 02:55 UTC](http://physicslab.turtlesim.com/t/topic/625/10 "2024-08-01T02:55:34Z")

</div>

会过时的

---

<div class="post-metadata">

作者： ![故事里的人](http://physicslab.turtlesim.com/letter_avatar_proxy/v4/letter/%E6%95%85/b2d939/32.png) [@故事里的人](http://physicslab.turtlesim.com/u/%E6%95%85%E4%BA%8B%E9%87%8C%E7%9A%84%E4%BA%BA)\
发布日期： [2024 年8 月 1 日 13:13 UTC](http://physicslab.turtlesim.com/t/topic/625/11 "2024-08-01T13:13:42Z")

</div>

实验发布频率是有限制的（应该不会在客户端限制

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年8 月 3 日 06:31 UTC](http://physicslab.turtlesim.com/t/topic/625/12 "2024-08-03T06:31:04Z")

</div>

作者大大，你现在的脚本会炸掉  
Traceback (most recent call last):  
File “d:\projects\physicsLab\code\myTest.test.py”, line 155, in   
main()  
File “d:\projects\physicsLab\code\myTest.test.py”, line 129, in main  
policy = submit\_json[‘Data’][‘Token’][‘Policy’]  
TypeError: ‘NoneType’ object is not subscriptable

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年8 月 3 日 09:13 UTC](http://physicslab.turtlesim.com/t/topic/625/13 "2024-08-03T09:13:00Z")

</div>

我好像找到原因了，有些图片不行而有些图片可以，我怀疑和图片格式有关

---

<div class="post-metadata">

作者： ![AMDYES](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/amdyes/32/492_2.png) [@AMDYES](http://physicslab.turtlesim.com/u/AMDYES)\
发布日期： [2024 年8 月 3 日 09:46 UTC](http://physicslab.turtlesim.com/t/topic/625/14 "2024-08-03T09:46:59Z")

</div>

我测试的结论是文件过大，有1MB（FileSize不能超过1048576）的限制  
我手动把FileSize设置成1048576没问题，再加1就返回的Data.Token就是null了  
这个问题在物实就有人提出过，而我当时无法复现，就是因为我用的都是一些截图，或者是随便下载的图片，本身就不是很大  
我认为和格式没有关系，在调用/Contents/SubmitExperiment的时候，也仅仅只是写了个扩展名，实际图片还没传呢  
顺便一提，这里的FileSize和实际图片大小必须一致，不然上传图床的时候就会403（message: file too large/small），不过把FileSize写成图片文件大小的相反数可以绕过这个限制

OK，我成功把一个13MB（13558055字节，FileSize:-13558055）的图片传上去了233

---

<div class="post-metadata">

作者： ![某屑依宁](http://physicslab.turtlesim.com/letter_avatar_proxy/v4/letter/%E6%9F%90/b2d939/32.png) [@某屑依宁](http://physicslab.turtlesim.com/u/%E6%9F%90%E5%B1%91%E4%BE%9D%E5%AE%81)\
发布日期： [2024 年8 月 3 日 09:50 UTC](http://physicslab.turtlesim.com/t/topic/625/15 "2024-08-03T09:50:32Z")

</div>

这是py？

---

<div class="post-metadata">

作者： ![Arendelle](http://physicslab.turtlesim.com/user_avatar/physicslab.turtlesim.com/arendelle/32/512_2.png) [@Arendelle](http://physicslab.turtlesim.com/u/Arendelle)\
发布日期： [2024 年8 月 3 日 10:29 UTC](http://physicslab.turtlesim.com/t/topic/625/17 "2024-08-03T10:29:27Z")

</div>

有道理，我等会儿试一下（我目前碰到的部分情况也符合你的描述）
