吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 329|回复: 0
收起左侧

[Windows] 【python】文件CRC32C多线程校验工具(v1.0)

[复制链接]
不谙世事的雨滴 发表于 2024-5-19 19:20
本帖最后由 不谙世事的雨滴 于 2024-5-19 19:29 编辑

这是一款由我用python编写,通过在分享前获取文件CRC32C校验值并记录在文件中,和在接受文件之后根据记录文件比对文件CRC32C的方式,方便文件分享者和接收者确认数据完整性、正确性的工具。


注意!!
软件为了利用CPU中SSE4.2指令集的CRC32指令提升计算效率,降低CPU占用,因此用到的python库crc32c及其使用的CRC32C标准,和常用的CRC32(B)标准相比,也就是大多数压缩软件和校验程序提供的CRC32,有很大的区别,两者不能通用,也就是不能用常规的计算CRC32的方法来校验本程序生成的CRC32C记录,所以建议无论是分享前记录CRC的过程,还是接收到文件之后的比对,都不要离开本软件


关于crc32的CPU指令的详细介绍,参见下方这篇文章:
【基于Intel SSE4.2指令集crc32指令进行快速crc32c校验(使用Delphi/Fpc内联汇编实现)】
https://www.52pojie.cn/thread-1584310-1-1.html
(出处: 吾爱破解论坛)


软件(和源码)链接:
①百度云:(拥有完整资料,包括软件自身、说明文本、操作演示视频背景音乐欣赏,以及视频中出现的音乐文件分享https://pan.baidu.com/s/1iBlKi5hV_kdkkzCgVjblYQ?pwd=0000
②蓝奏云:(因文件大小100M限制,只有软件自身、源码和一个说明用的txt文本文件,以及导航用的、存有百度云链接的文本文件)https://wwm.lanzouq.com/b0ny5lvuf
没有百度云VIP账号,想要看操作演示的(或者只是想随便听听音乐打发时间的),请访问下面这个链接,到B站收看:https://www.bilibili.com/video/BV18rupeWEt7


工具基于python的wmi模块获取文件路径所在磁盘的数字编号,然后基于磁盘SMART工具“smartctl.exe"(pySMART模块调用的exe程序)根据前面获取到的磁盘数字编号,获取对应磁盘的旋转速度、接口类型、是否为SSD的SMART属性,以此判断磁盘硬件类型(固态/机械),并合理分配同时读取的文件数(线程数)。
①在机械硬盘(HDD)上以单线程读取文件(逐个读取文件),降低机械盘寻道压力的同时,读取、计算校验的速度也能赶上机械盘自身的连续读取大文件的速度,这种情况下和7-zip读取校验CRC的速度不相上下;
②而在nvme和SSD上以4线程读取文件(同时读取4个文件),在获取一个文件夹下的多个文件(递归)CRC值的场景下,相较于7-zip和已有的大部分校验软件,速度上应该会有显著提升,可以加快校验速度,节省时间。在我自己电脑的nvme固态盘上测试,读取速度相较原来翻了一倍。4个线程也是我多次测试所得最优线程数。
③在不能读取识别明确硬件类型的磁盘上(U盘、虚拟磁盘……),以2线程读取文件(同时读取2个文件);
④识别磁盘硬件类型出错的情况下,以单线程运行。




软件运行截图:

运行截图

运行截图





OK,重要的交代完了。接下来,愿意听我继续哔哔的请留下
===================================================


故事要从一周前开始说起,那天我心情好,决定限一天内,分享些懂的都懂的资源。资源的大小很大,超过100G,我用了7-zip分卷并加密,然后上传网盘。为了预防下载时数据出错,我特意把每个分卷压缩包的CRC32都跑了一遍,并记录在一个txt里面。果然有两三人解压失败了,不过当我让他们跑一遍CRC32比对一下时,出乎意料的是他们都对这个基本操作一无所知,无奈只能亲自给他们发操作步骤。想不到都2024年了,还有人不会校验文件,那就给他们弄个傻瓜式的自动化校验工具吧,这样只要运行一个exe就能自动校验文件夹下(包含递归)的所有文件,并显示出有问题的那几个。于是脑子一热,说干就干。 本以为用python写个计算CRC32的函数会很容易,毕竟之前搞的“Peazip文件密钥加密模式 - 转 - 明文密码(v1.0)”就用到了sha512的计算,谁知hashlib里面居然没有专门计算crc32的函数,还得跑到zlib这个库那边去。一搜zlib使用crc32函数的资料,当场崩溃,搜到的几乎全是类似“crc32(f.read())”这样一股脑把文件整个吞进去的,丝毫没考虑到电脑内存的大小,如果直接copy下来的话,遇到大文件那不得分分钟报错?还好之后我加了“特大文件”这个关键词,搜出一个靠谱的分批缓冲读取的,计算出的CRC32和7-zip一样,总算是能用了。搜到的原本的代码中的循环好像记得是用“for line in file:”写的,其中file似乎是以“rb”模式打开的文件的句柄,也有可能我记错了,当时不知道这个line有什么特别的意思,网上搜了搜,只找到一个说能自适应读取单元和缓冲什么的,搜到的结果最多两三条,要么是文不对题,要么和这条解释相似,个人感觉似乎不太行,于是按照自己的理解改了下,顺便作为模板放在下面,也好让后面的人少踩点坑。


[Python] 纯文本查看 复制代码
# -*- coding: UTF-8 -*-
import os,sys
from zlib import crc32

# 传入文件路径,返回CRC32字符串
def calc(task_file: str) -> str:
    # 初始化结果变量为0
    result = 0
    try:
        file_handle = open(task_file, "rb")
        # 只要文件没读完,就不断迭代结果自身。
        #   一次read的字节大小理论上可以随便选,因为大部分文件的大小不是一次读取的大小的整数倍,总会多出或少掉几个字节,
        #   而且看网上资料说,这个函数还可以传入字符串,字符串的长度就更随意了,所以可以放心一次读取的长度应该不会影响到最终结果。
        #   
        #   取“1048576(1 MiB)”是出于性能考虑,我使用ATTO测试过,我的nvme盘在传输大小(块大小)为1 MiB到2 MiB时,
        #   未勾选“直接传输”的读取速率差不多达到最大,估计其他固态和机械盘应该也差不多
        #   “直接传输”据我估计是带上系统自身调度和缓冲特性的操作,和勾上后用到的底层操作,也就是硬件层面类似磁盘碎片整理软件里看到的“directwrite”操作正好相对。
        #   python读取文件的函数到不了底层,所以得去掉这个勾。
        #   恰好这个数字和1024关系很直接,感觉非常不错,所以就定为了“1048576(1 MiB)”
        #   
        #   突然想到固态盘里,闪存单元的块大小是不是也是这么大?不过感觉应该要比1 MiB小吧。
        #
        while buf := file_handle.read(1048576): # 1024 × 1024 = 1048576(1 MiB)
            result = crc32(buf,result)
        file_handle.close()
    except:
        try:
            file_handle.close()
        except:
            pass

    # “&”这部分的意思是:取结果低位的4个byte(0xFF)转换成hex字符串,以8个“0-F”的方式显示(“:08X”那部分代表的意思)
    result = f'{result & 0xFFFFFFFF:08X}'
    
    return result

# 将要计算CRC32值的文件路径替换掉下面这个样本
file = "[drive]:/path/to/file"
print(f"{file}的CRC32:{calc(file)}\n")
os.system("@pause>nul")
sys.exit(0)



解决完了怎么计算的问题,我又想到现在的校验软件差不多都是逐个读取和校验文件的,要不我试试多线程?要是能加快总体读取速度就好了。果然这一步走对了,在我的nvme固态上实现了整体读取速度翻番的效果,这下能省差不多一半的时间,有成就感了。不过多线程带来好处的同时,CPU的占用和发热多少有点大了,而且也为后面制作进度条埋了雷。


我原本的设想是多个文件的进度条同时显示出来,不过借鉴了大半天,结果测试时显示的进度条全混在一起分都分不清,而且还报错,大概意思是不能同时什么什么的……罢了,还是自己写一个显示进度条的函数吧,就用基础的print()和清屏的循环,好歹能自己有点控制能力,人家的库虽然美观、显示效果不错,不过一到了我这个场景下就歇菜了,当然如果深挖的话问题还是有可能解决的,不过我已经累了。总之,换成自己写的进度条后,总算能正常显示了,不过有个小问题,就是经常不定期闪烁,本来我想忽略不管的,但我一个程序员大佬朋友觉得这太晃眼了,受不了,于是我又去研究了下。

其实在问题出现的时候,我已经猜到了“os.system("cls")”这条里面多少有点猫腻,在我的理解里,这个过程的耗时会比Python自己的命令长,毕竟涉及到调用系统命令,而且自己以前运行批处理时也似乎记得cls再echo会出现闪烁,不过似乎较难察觉,又或许是我记错了。总之得想办法找到效果相同的方法替换掉它。首先搜到的是打印ANSI控制字符“\r”,不过只能回退一行,清掉整个屏幕的控制字符就难找了,毕竟这些控制字符估计都属于DOS时代的东西了(比如让蜂鸣器响的控制字符),除了换行什么的一直在用,其他特殊的找起来比较难。好在搜了半天,总算找到一个用“\033c”的:https://blog.csdn.net/Qiuml0703/article/details/131778995,果然一用上这个“\033c”,问题迎刃而解,甚至不用单独写一行清屏,只要把这个字符放在所有字符的前面,上一轮打印的字符就会自动清掉。在找资料的过程中,还认识了“sys.stdout.flush()”和“sys.stdout.write()”,前者用来刷新输出缓冲区,把还暂时停留在控制台缓冲区里的东西立刻打印出来,后者则可以说完全可以取代print(),据网上资料所说,print()就是调用的“sys.stdout.write()”顺便加了点自己的特性进去,比如自动换行。凭我的感觉和以前学C语言的记忆,觉得sys.stdout.write()是更底层的东西,而且刚好和“sys.stdout.flush()”是一对的,于是把打印进度条函数里的print()改成了这个,这样一来应该能提高一点“强迫症”性能吧,哈哈。现在看来,虽然90%以上的情况下,人们都是用的“os.system("cls")”来清屏,不过在某些特定场景下,比如我这个,就得做出调整了,“\033c”无疑就是个很好的方法和例子

又经过一段时间的优化,进度条的显示也让我满意了,想来应该可以分享出去了。不过我又冷不丁想到,万一是在机械硬盘上运行,多线程还能提高速度吗?搜索之前我心里已经有答案了。果然搜过之后和我想的一样,因为只有一个磁头,多线程在机械硬盘上完全是拖后腿的操作,不仅如此,我觉得还是给机械硬盘折寿的存在,毕竟迅雷的多线程写入毁掉多少盘(当然大部分是叠瓦的,不过垂直的也危险,只有企业级的可能还比较抗造点)是有目共睹的,虽然我这个还只是读取,不过脑海里已经能预想到我的多线程读取程序在机械盘上启动,然后不停地“炒豆子”,用户心中顿时跑过一万匹xxx,想要把我削了的场景了

于是又马不停蹄地投入了磁盘硬件类型识别的研究中。我的思路是:首先要通过文件路径获得所在的分区的盘符,然后通过盘符确定物理磁盘的编号(系统磁盘管理里面显示的磁盘号),最后根据物理磁盘的编号,通过某种识别软件或库识别出硬盘的硬件类型。第一步“通过文件路径获得所在的分区的盘符”简单,直接现成的“section= os.path.splitdrive(os.getcwd())[0]”一步到位;第二步难度就直接快升天了,费了老半天才找到用wmi库获得电脑磁盘号和分区列表的代码,总算接近了些,接下来就是在idle里各种print(),找出自己想要的,可算是完成了第二步的从分区到磁盘号的映射;到了第三步,能找到的可用的方法就快几乎没有了,一开始不管怎么搜都搜不到怎么用python判断是否为固态或者机械,只找到一个用powershell的方法,命令是“Get-PhysicalDisk| Select-Object FriendlyName, MediaType”,不过当我把“FriendlyName”改为单独用“Get-PhysicalDisk”命令显示出的第一列的“Number”,也就是我想要的内容,具体命令是“Get-PhysicalDisk| Select-Object Number, MediaType”,再敲回车时,Number那一列居然诡异地消失了:

powershell截图

powershell截图



我心里顿时“Oh GodPlease No! No!! No!!……”,怎会出现如此离谱的bug,算了,还是弃用powershell吧,powershell光是启动完成就要老半天,虽然Python有popen()和subprocess()以及各种传输管道的支持,理论上是能捕获到,不过看上面这样子,变数太大了,传输管道什么的还得查资料,自己根本不会,到最后可能还是一场空。可没有了powershell,其他的方法似乎更离谱了,打开磁盘碎片整理程序查看这个虽然有道理,可是程序运行过程中还要跳一个磁盘碎片整理的窗口出来捕获文本,多少有点更离谱了……

直接的办法不太行,试试间接的如何?不少磁盘都支持SMART……诶,我怎么没想到用获取SMART这个方法,这个灵感来得太及时了!换了方向之后,一切就豁然开朗了,找到了一个pySMART的python模块,虽然说明文件少得可怜,而且还依赖软件“smartmontools”以及解压后环境变量里设置的smartctl.exe的路径,不过好歹没之前用powershell测试时那么迷茫了,经过一顿寻找和尝试,找到了“handle.rotation_rate, handle.interface , handle.is_ssd”这三个能间接判断磁盘硬件类型的关键方法,于是就得到了上述三步一并实现的函数的代码,如下:
[Python] 纯文本查看 复制代码
import os,sys,time,wmi
from pySMART import Device

# 线程数:同时最多有几个文件在读取和计算CRC32
#  固态
thread_num_for_ssd = 4
#  机械
thread_num_for_hdd = 1
#  其他未知(可能为U盘)
thread_num_for_other = 2
#  获取失败时的线程数
thread_num_for_error = 1

def allocate_thread_num_by_disk_hardware_type() -> tuple:
    # 获取硬盘分区和磁盘号的关联
    c = wmi.WMI()
    section_to_physical_disk_dict = {}
    for physical_disk in c.Win32_DiskDrive():
        for partition in physical_disk.associators("Win32_DiskDriveToDiskPartition"):
            for logical_disk in partition.associators("Win32_LogicalDiskToPartition"):
                #print("|".join([physical_disk.Caption, partition.Caption, logical_disk.Caption]))
                section_to_physical_disk_dict[logical_disk.DeviceID] = physical_disk.Index
    
    # 获取程序所在目录的盘符,寻找对应的磁盘号
    section = os.path.splitdrive(os.getcwd())[0]
    print(f"所在盘符为:【{section}】")

    # 获取所在的磁盘号
    disk_index = section_to_physical_disk_dict.get(section)
    print(f"磁盘号为:【{disk_index}】")
    print("\n")

    if disk_index != None:
        handle = Device(f'/dev/pd{disk_index}')
        spin , slot , is_SSD = handle.rotation_rate , handle.interface , handle.is_ssd
        print(f"旋转速度:{spin}")
        print(f"接口:【{slot}】")
        print(f"是否为SSD:{is_SSD}")
        print("\n")
        if spin:
            print("硬件类型:机械硬盘")
            disk_type = "HDD"
            allocated_thread_num = thread_num_for_hdd
        elif (slot == "nvme") or is_SSD :
            print("硬件类型:固态硬盘")
            disk_type = "SSD"
            allocated_thread_num = thread_num_for_ssd
        else:
            print("硬件类型:未知,无旋转速度,可能为U盘")
            disk_type = "other"
            allocated_thread_num = thread_num_for_other
    else:
        print("获取磁盘号失败")
        disk_type = "error"
        allocated_thread_num = thread_num_for_error
    
    return (allocated_thread_num , disk_type)

thread_num , disk_hardware_type = allocate_thread_num_by_disk_hardware_type()
os.system("pause")
sys.exit(0)



本以为有了代码之后,接下来只要运行就能得出结果了,不过我还是疏忽了。就在我朋友的电脑上测试的时候,发现原本应该得出的是机械盘的结果,但“handle.rotation_rate, handle.interface , handle.is_ssd”这3个值都是None或者False,一开始我以为是软件也有没囊括到的硬盘型号,但无意间想到“smartctl.exe”的工具说明里提到了要获得管理员权限,才能保证获得准确的信息。于是我让朋友又用右键的“管理员权限”运行了一下,果然该获取到的信息又都获取到了,也被正确识别为机械硬盘了。于是赶紧做了个新的C语言启动器替换了上去,代码如下:

[C++] 纯文本查看 复制代码
#pragma comment(linker, "/subsystem:windows /entry:mainCRTStartup")
//编译不弹黑窗的选项

#include <string.h>
#include <direct.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <winuser.h>
#include <tchar.h>


int main(void)
{
    //进入到当前路径下的“bin”文件夹 
        _chdir("bin");//一定要先进入到文件夹(设定程序的工作文件夹)再启动exe,不然窗口左上角的图标显示不出来,还有可能会有其他意想不到的问题
    
        SHELLEXECUTEINFO sei;
    ZeroMemory(&sei, sizeof(SHELLEXECUTEINFO));//使用前最好清空
    sei.cbSize = sizeof(SHELLEXECUTEINFO);//管理员权限执行,最基本的使用与 ShellExecute 类似
    sei.lpVerb = _T("runas");
    sei.lpFile = _T("校验CRC32.exe");
    sei.nShow = SW_SHOWNORMAL;
    sei.fMask = SEE_MASK_FLAG_NO_UI;//出现错误,不在函数执行中显示错误消息框,比如不会弹出找不到文件之类的窗口,直接返回失败。防止在后面重复弹出错误消息。 
    
        if (!ShellExecuteEx(&sei))
    {
        DWORD dwStatus = GetLastError();
        
                if (dwStatus == ERROR_CANCELLED)
        {
            //printf("提升权限被用户拒绝\n");
            MessageBox(
                                NULL,
                                (LPCTSTR)"提升权限被用户拒绝",//文本
                                (LPCTSTR)"starter",//标题
                                MB_OK | MB_ICONERROR | MB_TOPMOST | MB_SETFOREGROUND //“确定”按钮、“错误”图标、置顶、前台
                        );
        }
        else if (dwStatus == ERROR_FILE_NOT_FOUND)
        {
            //printf("所要执行的文件没有找到\n");
            MessageBox(
                                NULL,
                                (LPCTSTR)"所要执行的文件没有找到",//文本
                                (LPCTSTR)"starter",//标题
                                MB_OK | MB_ICONERROR | MB_TOPMOST | MB_SETFOREGROUND //“确定”按钮、“错误”图标、置顶、前台
                        );
        }
        
    }

    return 0;
}





之所以单独做个启动器,不仅是因为要提升为管理员权限运行以确保获取准确的硬件信息,而且也是为了用户能少进一层文件夹,毕竟“smartctl.exe”等“smartmontools”的软件组件文件,需要和打包好的python程序放在同一个文件夹下面,才能确保程序启动时pySMART能调用到,才能获取到磁盘SMART信息。


能做到上述几点已是不易,该想到的问题差不多都想到了,故事照理来说应该就快结束了吧?不过后面又多出了一段相当重要的部分,而且是在我录好演示视频,准备传到论坛上去的时候。在我准备这条帖子,搜索有没有类似的帖子时,看到了下面这条帖子:
【基于Intel SSE4.2指令集crc32指令进行快速crc32c校验(使用Delphi/Fpc内联汇编实现)】
https://www.52pojie.cn/thread-1584310-1-1.html
(出处: 吾爱破解论坛)


当时看完后心想,这也太符合我现在的需要了吧,要是能套上汇编或者C的话,计算的效率不得蹭蹭地往上去啊!可惜自己只学过51单片机的汇编,C语言也才入门,而且也不懂怎么在python里嵌入汇编和C,虽说Python理论上好像确实有这个功能,但是如果嵌入的话,以自己现在的水平,怕是一行汇编也看不懂,更别提上手了,毕竟多少得做些修改,自己又不懂,这套操作还是太高端了。当时时间不早了,于是就去睡了。 第二天醒来时又想到这个,看来自己还真是念念不忘啊,那就去pypi看看有没有现成的“宝藏”库吧。不得不说运气真就是好,找到个叫“crc32c”的库:https://pypi.org/project/crc32c/。一看里面的介绍,就是用这个处理器指令加速的,如果处理器不支持指令加速的话,库还自动换为软件算法打底。那还等什么,赶紧安排一波测试。测试结果让人喜出望外,在nvme上以设计的4线程并行读取计算的速度比原来似乎稍快的同时,CPU的占用也从原来软媒雷达显示的40%左右,任务管理器显示的75%直接降到了——现在软媒雷达显示的20%左右,和任务管理器显示的37%~40%左右(i7-9750H,6核心12线程),不过就是输出的CRC32C和通用的CRC32B不一样罢了,反正都是在软件里“闭环”操作,软件自身直接双击启动自动记录 + 自动校验的设计也很人性化,应该不会有人傻到再用别的软件根据文本记录一个个自己校验吧?所以也就不考虑输出的CRC32的兼容性了,毕竟用上硬件加速后,得到的提升非常诱人。

理论上只要支持SSE4.2就能使用CRC32硬件加速,网上说AMD的3、5、7代Ryzen都支持SSE4.2,但因为库的介绍里只写了intel,我有点不确定amd的处理器是否也支持,于是找了身边的同学帮忙测试了下,发现R7 6800H也支持,看来兼容性应该不用太担心,只要不是老的处理器,估计都支持这个硬件加速。软件启动后,也会在标题那里显示是否开启了硬件加速。下面是现在使用的,CRC32_C的计算函数的代码模板:
[Python] 纯文本查看 复制代码
# -*- coding: UTF-8 -*-
import os,sys
import crc32c

# 传入文件路径,返回10位CRC32_C数字字符串
def calc(task_file: str) -> str:
    # 初始化结果变量为0
    result = 0
    try:
        file_handle = open(task_file, "rb")
        # 只要文件没读完,就不断迭代结果自身。
        #   一次read的字节大小理论上可以随便选,因为大部分文件的大小不是一次读取的大小的整数倍,总会多出或少掉几个字节,
        #   而且看网上资料说,这个函数还可以传入字符串,字符串的长度就更随意了,所以可以放心一次读取的长度应该不会影响到最终结果。
        #   
        #   取“1048576(1 MiB)”是出于性能考虑,我使用ATTO测试过,我的nvme盘在传输大小(块大小)为1 MiB到2 MiB时,
        #   未勾选“直接传输”的读取速率差不多达到最大,估计其他固态和机械盘应该也差不多
        #   “直接传输”据我估计是带上系统自身调度和缓冲特性的操作,和勾上后用到的底层操作,也就是硬件层面类似磁盘碎片整理软件里看到的“directwrite”操作正好相对。
        #   python读取文件的函数到不了底层,所以得去掉这个勾。
        #   恰好这个数字和1024关系很直接,感觉非常不错,所以就定为了“1048576(1 MiB)”
        #   
        #   
        #
        while buf := file_handle.read(1048576): # 1024 × 1024 = 1048576(1 MiB)
            result = crc32c.crc32c(buf,result)
        file_handle.close()
    except:
        try:
            file_handle.close()
        except:
            pass

    
    result = str(result).zfill(10)
    
    return result

# 将要计算CRC32_C值的文件路径替换掉下面这个样本
file = "[drive]:/path/to/file"
print(f"{file}的CRC32_C:{calc(file)}\n")
os.system("pause")
sys.exit(0)



由于这个新的计算函数的改动之前根本没预料到,所以演示视频里用来演示的还是之前那一版通用CRC32(B)的版本,截图也是从那个演示视频里截来的,不过操作上没什么差别,我也不打算改视频和截图了。


现在回过头来,发现自己长篇大论似乎有近千字了,感谢各位能看到现在


也感谢自己的坚持,完成了这个成就感满满的工具软件。 软件的源码就包括在网盘链接里,后面有建议、发现bug的欢迎随时来找我

免费评分

参与人数 3吾爱币 +5 热心值 +2 收起 理由
ZengHugh + 1 + 1 我很赞同!
gknight + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
onlyclxy + 3 鼓励转贴优秀软件安全工具和文档!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 提醒:禁止复制他人回复等『恶意灌水』行为,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-6-17 09:19

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表