大文件上传服务-后端V1V2

news2025/1/20 6:06:40

文章目录

      • 大文件上传概述:
      • minio分布式文件存储
        • 使用的一些技术
        • 校验MD5的逻辑
      • uploadV1 版本 1
      • uploadv2 版本 2

大文件上传概述:

之前项目做了一个文件上传的功能,最近看到有面试会具体的问这个上传功能的细节,把之前做的项目拿过来总结一下,自己写的一个文件上传服务,如果是单体项目直接就是 file-model,微服务可以给文件单独搞一个服务。

对于大的文件上传由于网络带宽的限制非常的耗时间,可以将文件进行切分,类似于IP数据包重组和分片。

类似于百度网盘一秒上传文件,这个如何实现的呢? 相同的文件上传过一次之后使用了MD5加密的算法,下次就不用等待上传了。

对于海量的文件,如何去存储呢? 这些都是需要去考虑的内容。可以使用轻量级的分布式存储minio来存储文件。复杂一点可以使用 fastDFS 来存储文件。

文件存储 sql 表:

create table file_url_table
(
    id          int auto_increment
        primary key,
    file_name   varchar(255)                        not null,
    file_type   varchar(30)                         not null,
    file_md5    varchar(32)                         not null,
    file_url    varchar(512)                        not null,
    endpoint    varchar(255)                        not null,
    buck_name   varchar(255)                        not null,
    object_name varchar(255)                        not null,
    created_at  timestamp default CURRENT_TIMESTAMP null,
    updated_at  timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP
);

minio分布式文件存储

后端文件的存储使用分布式文件存储系统minio

一个minio的地址包括:

endpoint:节点的信息 http:ip地址+:端口号

buckName: 桶的基础信息

objectName: 文件夹的信息+文件的名称。

http://xxxxx:9000/powerproject/file/2024/12/01/bbb5ba51d7b65cf4495de91ca55bfa23.mp3

项目中使用 minio

导入minio maven依赖:

<dependency>
     <groupId>io.minio</groupId>
     <artifactId>minio</artifactId>
     <version>8.5.7</version>
</dependency>

配置minoConfig,使用Configuration注解,spring在启动的时候会扫描这个类。@bean会创建一个MinioClient,交给Spring容器去管理。


@Configuration

public class MinioConfig {
      
   @Value("${minio.accessKey}")
    private String accessKey;
    @Value("${minio.secretKey}")
    private String secretKey;
    @Value("${minio.endpoint}")
    private String endPoint;

     
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder().endpoint(endPoint)
                .credentials(accessKey, secretKey).build();
    }
}
  • **@Bean** 注解定义了一个 Spring Bean 方法,用来初始化并提供配置好的 MinioClient
  • 通过自动注入,Spring Boot 会将配置文件中的 minio.accessKey, minio.secretKey, minio.endPoint 自动注入到类的字段中,并将配置好的 MinioClient 实例作为 Bean 提供给整个应用使用。
  • spring启动的时候会扫描到configuration这个包。

断点续传:

通常视频文件都比较大,所以对于媒资系统上传文件的需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。

什么是断点续传:

引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性

使用的一些技术

因为上传的结构传进来的都是MultipartFile,但是文件合并完成之后是File类型的,需要将File转为MultipartFile。

  //将木桶的文件 转换成 MultipartFile
        MultipartFile multipartFile = new MockMultipartFile(
                "file",                           // 表单中文件参数的名字
                fileName,             // 文件名
                fileType,                // 文件类型
                new FileInputStream(outputFile)   // 文件输入流
        );

MockMultipartFile 的构造函数有四个参数:

  • 第一个参数"file"
    • 这是表单中用于接收文件的字段名。通常是前端表单中的 <input type="file" name="file"> 中的 name 属性值。
    • 例如,在 HTML 表单中,字段可能长这样:
<input type="file" name="file">
- 后端通过 `"file"` 来访问上传的文件。
  • 第二个参数fileName
    • 这是文件的名字,即上传文件时的文件名。它将作为文件在服务器上存储时的名称,或者用作处理文件时的标识符。
    • 例如,fileName 可能是 "document.pdf""image.jpg" 等。
  • 第三个参数fileType
    • 这是文件的 MIME 类型(文件类型)。常见的文件类型如 "application/pdf", "image/jpeg", "text/plain" 等。
    • 在这里,它指定了文件的格式/类型,告诉服务器如何处理该文件。
  • 第四个参数new FileInputStream(outputFile)
    • 这是文件内容的输入流。**FileInputStream**** 用于读取文件内容并将其传递给 **MockMultipartFile**。**
    • outputFile 是一个 File 对象,表示要上传的实际文件。通过 new FileInputStream(outputFile),你可以将文件内容转换为字节流,使得它可以被传递到 MockMultipartFile 中。

MultipartFile 提供了几个常用方法,比如:

  • getName(): 获取文件的字段名。
  • getOriginalFilename(): 获取文件的原始文件名。
  • getBytes(): 获取文件的字节内容。
  • getInputStream(): 获取文件的输入流。
校验MD5的逻辑

对于已经存在的文件,文件的 md5 值都是相同的。文件上传前端解析 md5,查询数据库中是否有数据,如果没有再实现文件上传。

//去数据库中查询这个文件的哈希值是否存在,如果存在就不用上传了,直接拿到这个文件的地址
@GetMapping("/md5")
public AjaxResult getMd5(@RequestParam("md5") String md5) {
    Map<String, Object> map=fileService.getMd5(md5);
    return AjaxResult.success(map);
}
@SneakyThrows
@Override
public Map<String, Object> getMd5(String md5) {
// 从数据库查询文件信息
FileEntity file = fileMapper.getMd5(md5);

// 如果文件不存在,直接返回 null
if (file == null) {
    return null;   
}

// 获取文件相关信息
String bucketName = file.getBuckName();
String objectName = file.getObjectName();
String fileUrl = file.getFileUrl();
String fileType = file.getFileType();
String fileName = file.getFileName();

try {
    // 检查文件是否存在于 MinIO
    if (!minioTemplate.isExist(objectName)) {
        // 文件不存在
        return null;
    }
} catch (MinioException e) {
    // 如果 MinIO 抛出异常,记录异常信息
    return null;
}

// 文件存在,构建返回的 Map
Map<String, Object> result = new HashMap<>();
result.put("url", fileUrl);
result.put("fileType", fileType);
result.put("fileName", fileName);

return result;
}

判断文件是否存在

   public boolean isExist(String objectName) throws Exception {
        StatObjectResponse statObjectResponse = minioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
        return statObjectResponse == null;
    }

uploadV1 版本 1

画板

上传整个文件。

 @GetMapping("/uploadV1")
    public AjaxResult uploadFileV1(MultipartFile file) {
        Map<String, Object> map = fileService.uploadV1(file);
        return AjaxResult.success(map);
    }

前置校验,检查文件的大小。 文件不能过大。 限制了一些文件上传的大小 FileType 枚举类

// 通过枚举类校验文件的大小
private void preCheck(MultipartFile file) {
String fileName = file.getOriginalFilename();  //得到原来的名称
String fileSuffix = fileName.substring(fileName.lastIndexOf(".")); // 拿到后缀
FileType fileType = FileType.getFileTypeByExtension(fileSuffix);
if (fileType == null) {
    throw new ServiceException("不支持的文件格式");
}

// 校验文件大小
if (file.getSize() > fileType.getMaxSize()) {
    throw new ServiceException("文件大小超过" + (fileType.getMaxSize() / (1024 * 1024)) + "MB");
}
}
import java.util.Arrays;
import java.util.List;

public enum FileType {
    //两个属性 一个是文件的扩展名  一个是文件大小
    IMAGE(Arrays.asList(".jpg", ".png", ".jpeg"), 1024 * 1024 * 50),  // 50MB
    DOCUMENT(Arrays.asList(".doc", ".xls", ".ppt", ".txt", ".pdf", ".mp3", ".wav", ".ply"), 1024 * 1024 * 100),  // 100MB
    VIDEO(Arrays.asList(".mp4", ".avi", ".mov"), 1024 * 1024 * 200);  // 200MB

    private List<String> extensions;  // 支持的文件扩展名
    private long maxSize;  // 最大文件大小

    // 构造函数
    FileType(List<String> extensions, long maxSize) {
        this.extensions = extensions;
        this.maxSize = maxSize;
    }

    public List<String> getExtensions() {
        return extensions;
    }

    public long getMaxSize() {
        return maxSize;
    }

    // 根据文件扩展名获取文件类型
    public static FileType getFileTypeByExtension(String fileSuffix) {
        for (FileType type : FileType.values()) {
            if (type.getExtensions().contains(fileSuffix)) {
                return type;
            }
        }
        return null;  // 如果没有匹配的文件类型,返回 null
    }
}

不同的文件放在不同的文件夹下面。

image开头的 存储一些 .jpg、.jped,.png文件。

file 开头的存放 .doc,.xls文件。 使用枚举类可以来实现动态的管理。 设置文件存储路径,使用日期的存储不同,不用的文件类型存储不同的路径

	String fileBucket = getFileBucket(fileSuffix);
	if (fileBucket == null) {
		return null;
	}
	// 获取当前日期
	Date now = new Date();
	// 使用 SimpleDateFormat 来格式化日期为 "yyyy/MM/dd" 格式
	DateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
	String date = sdf.format(now);
	// 组合最终的文件夹路径:如 "image/2024/11/30/"
	return fileBucket + date + "/";
	}
	
	private static String getFileBucket(String fileSuffix) {
	return FileCategory.getBucketPrefixByExtension(fileSuffix);
	}
package com.njitzx.fileupload.enu;

import java.util.Arrays;
import java.util.List;

public enum FileCategory {
    IMAGE(Arrays.asList(".jpg", ".jpeg", ".png", ".gif"), "image/"),
    DOCUMENT(Arrays.asList(".doc", ".xls", ".ppt", ".txt", ".pdf", ".mp3", ".wav", ".ply"), "file/"),
    VIDEO(Arrays.asList(".mp4", ".avi", ".mov"), "video/"),
    AUDIO(Arrays.asList(".mp3", ".wav"), "audio/");

    private List<String> extensions;  // 支持的文件扩展名
    private String bucketPrefix;      // 存储路径前缀

    // 构造函数
    FileCategory(List<String> extensions, String bucketPrefix) {
        this.extensions = extensions;
        this.bucketPrefix = bucketPrefix;
    }

    // 获取支持的文件扩展名列表
    public List<String> getExtensions() {
        return extensions;
    }

    // 获取对应的存储路径
    public String getBucketPrefix() {
        return bucketPrefix;
    }

    // 根据文件扩展名获取对应的存储路径
    public static String getBucketPrefixByExtension(String fileSuffix) {
        for (FileCategory category : FileCategory.values()) {
            if (category.getExtensions().contains(fileSuffix)) {
                return category.getBucketPrefix();
            }
        }
        return null;  // 如果没有找到对应的类型,返回 null
    }
}

上传的Service代码。

文件上传成功后需要将信息插入到文件表中去。 文件存储 service 方法:

public Map<String, Object> uploadV1(MultipartFile file) {
//文件的名称
String md5Name = MD5Util.getFileMD5(file);

String fileName = file.getOriginalFilename();

String fileSuffix = fileName.substring(fileName.lastIndexOf("."));

String folderName = generateFolderName(fileSuffix); //文件夹名称
//拼接objectName
String objectName = folderName + md5Name + fileSuffix;  //objectName

//拼接文件的全文路径
String fileUrl = endPoint + "/" + bucketName + "/" + objectName; //完整的URL

//放入到minio中去
minioTemplate.pubObject(objectName, file);  //存入到minio中去

//将文件上传信息插入到数据库中
FileEntity file1 = FileEntity.builder().fileName(fileName).fileType(fileSuffix.substring(1))
.fileUrl(fileUrl).endpoint(endPoint).buckName(bucketName).objectName(objectName).createdAt(new Date()).updatedAt(new Date()).fileMd5(md5Name).build();
//插入文件信息
HashMap<String, Object> map = new HashMap<>();
fileMapper.insertFile(file1);

//存储map中返回给前端  文件url、name、文件类型
map.put("fileUrl", fileUrl);
map.put("fileName", fileName);
map.put("fileType", fileSuffix.substring(1));

return map;
}

mapper的插入语句

  <!-- 插入文件记录 -->
    <insert id="insertFile" parameterType="com.njitzx.fileupload.pojo.FileEntity">
        INSERT INTO file_url_table (file_name, file_type, file_md5, file_url, endpoint, buck_name, object_name,
                                    created_at, updated_at)
        VALUES (#{fileName}, #{fileType}, #{fileMd5}, #{fileUrl}, #{endpoint}, #{buckName}, #{objectName}, #{createdAt},
                #{updatedAt})
    </insert>

测试接口:

uploadv2 版本 2

前端上传的大文件 在前端进行切片传输,后端接收切片的文件,整合组合成一个新的文件。

涉及到切片,类似于IP数据包重组,标识一个文件的信息。

fileId  文件信息的唯一标识
currentChunk 当前的块数
totalChunk 总的块数
File 文件的数据

类似于 IP 数据包,切片的那个字段需要有当前第一块,后面是否还有数据包信息。

    @PostMapping("/uploadV2")
    public AjaxResult uploadFileV2(
            @RequestParam("fileID") Long fileID,           // 接收文件ID
            @RequestParam("currentChunk") Integer currentChunk, // 接收当前块
            @RequestParam("totalChunk") Integer totalChunk,     // 接收总块数
            @RequestParam("file") MultipartFile file           // 接收文件数据
    ) throws IOException {
        fileService.uploadV2(fileID, currentChunk, totalChunk, file);
        return AjaxResult.success();
    }

通过System.getProperty(“user.dir”) 拿到当前的项目目录。

在当前目录下面创建一个upload文件夹,通过fIleId来区分不同的文件。

文件的名称就是当前的 chunk_+当前的块名称。 将传过来的小文件保存起来。

@SneakyThrows
@Override
public void uploadV2(Long fileID, Integer currentChunk, Integer totalChunk, MultipartFile file) {
    //当前的文件夹
    String currentDir = System.getProperty("user.dir");

    String tempDir = currentDir + "/upload/" + fileID;

    File dir = new File(tempDir);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    // 保存当前分片
    String chunkPath = tempDir + "/chunk_" + currentChunk;
    File chunkFile = new File(chunkPath);
    file.transferTo(chunkFile);
    // 记录当前上传分片,等待后续合并
    System.out.println("保存分片:" + chunkPath);
    // 如果所有分片上传完毕,返回合并请求
}

前端在所有的分片上传成功之后进行合并请求。

通过get请求 传过来 文件的id、文件名称、文件类型等。

@GetMapping("/merge")
public AjaxResult mergeFile(@RequestParam("fileID") Long fileID, @RequestParam("fileType") String fileType,
                            @RequestParam("fileName") String fileName) throws IOException {

    Map<String, Object> map = fileService.merge(fileID, fileType, fileName);
    return AjaxResult.success();
}

合并的步骤是这样的,首先获取到所有的分片文件。按照分片编号进行排序。将所有的小的文件按照顺序组成一个大的文件,这就又回到了第一步,使用uploadv1将文件重组上传。

@SneakyThrows
@Override
public Map<String, Object> merge(Long fileID, String fileType, String fileName) {
    String currentDir = System.getProperty("user.dir"); //拿到当前项目的目录
    String type = fileType.substring(fileType.lastIndexOf("/") + 1);
    String tempDir = currentDir + "/upload/" + fileID;
    File dir = new File(tempDir);

    if (!dir.exists()) {
        throw new ServiceException("分片文件不存在");
    }

    // 获取所有的分片文件
    List<File> chunkFiles = Arrays.asList(Objects.requireNonNull(dir.listFiles()));

    // 按照分片编号排序
    chunkFiles.sort(Comparator.comparingInt(file -> Integer.parseInt(file.getName().split("_")[1])));

    // 合并所有分片文件
    File outputFile = new File(currentDir + "/upload/" + fileID + "." + type);
    //true标识不覆盖文件,在原有的文件后面添加。
    try (FileOutputStream outputStream = new FileOutputStream(outputFile, true)) {
        //遍历所有的小的文件
        for (File chunk : chunkFiles) {
            try (FileInputStream inputStream = new FileInputStream(chunk)) {
                byte[] buffer = new byte[1024];
                int len;
                while ((len = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, len);
                }
            }
        }
    }
    //将木桶的文件 转换成 MultipartFile
    MultipartFile multipartFile = new MockMultipartFile(
        "file",                           // 表单中文件参数的名字
        fileName,             // 文件名
        fileType,                // 文件类型
        new FileInputStream(outputFile)   // 文件输入流
    );
    // 调用 fileService.uploadV1 方法上传文件
    Map<String, Object> map = uploadV1(multipartFile);
    // 返回合并后的文件URL
    return map;
}

上面的内容还是不够完整,因为文件临时存储在本地上,最后合并起来再放入到mino中,还是将大的文件存放到服务器上,如果网络中断并且时间也是较长的。应该将切片的文件存放到minio中去,最后在minio中进行文件的合并。

文件切片上传成功:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2279221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【k8s面试题2025】1、练气期

主要通过呼吸吐纳等方法&#xff0c;将外界的天地灵气吸入体内&#xff0c;初步改造身体&#xff0c;使身体素质远超常人。 文章目录 docker 和虚拟机的不同Kubernetes 和 docker 的关系Kube-proxy IPVS 和 iptables 的异同蓝绿发布Kubernetes中常见的数据持久化方式关于 Docke…

快速入门:如何注册并使用GPT

文章目录 ProtonMail邮箱步骤 1&#xff1a;访问Proton官网步骤 2&#xff1a;创建ProtonMail账户步骤 3&#xff1a;选择注册免费账户步骤 4&#xff1a;填写邮箱地址和手机号&#xff08;可选&#xff09;步骤 5&#xff1a;邮箱验证&#xff08;必须进行验证&#xff09;步骤…

浅谈云计算22 | Kubernetes容器编排引擎

Kubernetes容器编排引擎 一、Kubernetes管理对象1.1 Kubernetes组件和架构1.2 主要管理对象类型 二、Kubernetes 服务2.1 服务的作用与原理2.2 服务类型 三、Kubernetes网络管理3.1 网络模型与目标3.2 网络组件3.2.1 kube-proxy3.2.2 网络插件 3.3 网络通信流程 四、Kubernetes…

Vulnhub DC-8靶机攻击实战(一)

导语   Vulnhub DC-8靶机教程来了,好久没有更新打靶的教程了,这次我们在来更新一期关于Vulnhub DC-8的打靶训练,如下所示。 安装并且启动靶机 安装并且启动靶机,如下所示。 开始信息采集 进入到Kali中,通过如下的命令来查找到靶机的IP地址。 arp-scan -l根据上面的结…

自学SpringBoot笔记

概念 什么是SpringBoot&#xff1f; Spring Boot 是基于 Spring Framework 的一款开源框架&#xff0c;主要用于简化 Spring 应用程序的开发。它通过提供一系列的 开箱即用的功能 和 自动配置&#xff0c;让开发者可以快速构建生产级别的独立应用程序&#xff0c;而无需手动配…

通过学习更多样化的生成数据进行更广泛的数据分发来改进实例分割

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 本次使用的英文整理的一些记录&#xff0c;练习一下为后续SCI发表论文打好基础 Improving Instance Segmentation by Learning Wider Data Distribution with More Diverse Generative Data Abstract In…

USB3020任意波形发生器4路16位同步模拟量输出卡1MS/s频率 阿尔泰科技

信息社会的发展&#xff0c;在很大程度上取决于信息与信号处理技术的先进性。数字信号处理技术的出现改变了信息 与信号处理技术的整个面貌&#xff0c;而数据采集作为数字信号处理的必不可少的前期工作在整个数字系统中起到关键 性、乃至决定性的作用&#xff0c;其应用已经深…

提示词的艺术----AI Prompt撰写指南(个人用)

提示词的艺术 写在前面 制定提示词就像是和朋友聊天一样&#xff0c;要求我们能够清楚地表达问题。通过这个过程&#xff0c;一方面要不断练习提高自己地表达能力&#xff0c;另一方面还要锻炼自己使用更准确精炼的语言提出问题的能力。 什么样的提示词有用&#xff1f; 有…

自定义提示确认弹窗-vue

最初可运行代码 弹窗组件代码&#xff1a; &#xff08;后来发现以下代码可运行&#xff0c;但打包 typescript 类型检查出错&#xff0c;可打包的代码在文末&#xff09; <template><div v-if"isVisible" class"dialog"><div class&quo…

【JavaEE】Spring(1)

一、什么是Spring和SpringBoot Spring是Java应用程序的开发框架&#xff0c;其目的就是为了简化Java开发&#xff1b;SpringBoot是在spring框架的基础上构建的一个快速开发框架&#xff0c;其作用是进一步简化Spring程序开发 二、SpringBoot项目 2.1 创建项目 1. 设置jdk版本…

【Rust自学】13.4. 闭包 Pt.4:使用闭包捕获环境

13.4.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

浅谈操作系统与初识Linux

一、Linux操作系统的出现 1.1操作系统的出现以及相关的四个要素 1.2最早出现的操作系统及其创始人 起初&#xff0c;IBM为了让计算机可以以更低技术成本进行使用&#xff0c;以此来售卖计算机&#xff1b; 为计算机搭载上了Unix操作系统&#xff0c;Unix由肯汤普森用汇编语…

K8S开启/关闭审计日志

K8S默认禁用审计 开启/关闭 k8s 审计日志 默认 Kubernetes 集群不会输出审计日志信息。通过以下配置&#xff0c;可以开启 Kubernetes 的审计日志功能。 准备审计日志的 Policy 文件配置 API 服务器&#xff0c;开启审计日志重启并验证 准备审计日志 Policy 文件 apiVersio…

深入探讨DICOM医学影像中的MPPS服务及其具体实现

深入探讨DICOM医学影像中的MPPS服务及其具体实现 1. 引言 在医疗影像的管理和传输过程中&#xff0c;DICOM&#xff08;数字影像和通信医学&#xff09;标准发挥着至关重要的作用。除了DICOM影像的存储和传输&#xff08;如影像存储SCP和影像传输SCP&#xff09;&#xff0c;…

VGG (Visual Geometry Group) :深度卷积神经网络的图像识别利器

前言 在深度学习的蓬勃发展历程中&#xff0c;卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;为图像识别领域带来了革命性的突破。而 VGG&#xff08;Visual Geometry Group&#xff09;作为其中的杰出代表&#xff0c;凭借其简洁而高效的…

【机器学习实战中阶】音乐流派分类-自动化分类不同音乐风格

音乐流派分类 – 自动化分类不同音乐风格 在本教程中,我们将开发一个深度学习项目,用于自动化地从音频文件中分类不同的音乐流派。我们将使用音频文件的频率域和时间域低级特征来分类这些音频文件。 对于这个项目,我们需要一个具有相似大小和相似频率范围的音频曲目数据集…

HTML基础与实践

目录 HTML 结构 认识 HTML 标签 HTML 文件基本结构 标签层次结构 快速生成代码框架 HTML 常见标签 注释标签 标题标签: h1-h6 段落标签: p 换行标签: br 格式化标签 图片标签: img 超链接标签: a ​编辑链接的几种形式: 表格标签 基本使用 合并单元格 …

差分(前缀和的逆运算)

作用&#xff1a; 在 [ l ,r ] 数组中&#xff0c;对全部数字c 思路 原数组a 构造差分数组b使得a[i]b1b2b3...bi; a数组是b数组的前缀和,b1b2b3...bnan b[i] a[i]-a[i-1]; 在d21,那在前缀和时&#xff0c;这些a都1 在数组中&#xff0c;要l~r这段数c 在l处c后&#xff0c…

学习记录1

[SUCTF 2019]EasyWeb 直接给了源代码&#xff0c;分析一下 <?php function get_the_flag(){// webadmin will remove your upload file every 20 min!!!! $userdir "upload/tmp_".md5($_SERVER[REMOTE_ADDR]);if(!file_exists($userdir)){mkdir($userdir);}if…

走出实验室的人形机器人,将复刻ChatGPT之路?

1月7日&#xff0c;在2025年CES电子展现场&#xff0c;黄仁勋不仅展示了他全新的皮衣和采用Blackwell架构的RTX 50系列显卡&#xff0c;更进一步展现了他对于机器人技术领域&#xff0c;特别是人形机器人和通用机器人技术的笃信。黄仁勋认为机器人即将迎来ChatGPT般的突破&…