EasyExcel读文件详解和源码分析

news2025/1/4 18:49:32

读取文件导入的话,我们经常看到下面这些方法。

//同步的返回,不推荐使用
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum)..doReadSync(); 

//异步的,通过监听器处理读到的数据。
EasyExcel.read(file).sheet(sheetNo).head(Class.class).headRowNumber(headRowNum).registerReadListener(监听器)doRead(); 

首先建议大家可以看一下 EasyExcel为我们提供的 EasyExcelFactory工厂类相关的源代码,看懂源代码的话,对我们自己编写适合自己项目的方法很用用处。

引入依赖:

        <!--   easyexcel 3.1.0+版本不需要poi依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.1</version>
        </dependency>

一、EasyExcel读取文件源码

1、EasyExcelFactory工厂类

方法:EasyExcel.read(file)

查看 read()方法:

在这里插入图片描述

EasyExcel类调用 read方法,实际调用的是 EasyExcelFactory类的方法。

查看 EasyExcelFactory类方法:

在这里插入图片描述

EasyExcelFactory工厂类定义了许多读和写的重载方法。主要看读方法,分为两类:

  • 读取Excel文件,返回 ExcelWriterBuilder类
  • 读取Excel文件中的sheet,返回 ExcelReaderSheetBuilder类

从这里我们可以看出,EasyExcelFactory工厂类创建了 XxxBuilder类,并返回了 XxxBuilder类,那是不是我们也可以直接使用 XxxBuilder类操作读文件。答案是肯定的。

2、ExcelWriterBuilder类

查看 ExcelReaderBuilder类:

在这里插入图片描述

ExcelReaderBuilder类实例化时,创建了 ReadWorkbook对象。

查看 ExcelReaderBuilder类方法:

在这里插入图片描述

ExcelReaderBuilder类主要处理 Excel文件和相关文件属性信息,比如设置 字符编码、文件加密的密码,忽略处理哪行数据等。

3、ExcelReaderSheetBuilder类

查看 ExcelReaderSheetBuilder类:

在这里插入图片描述

ExcelReaderSheetBuilder类实例化时,创建了 ReadSheet对象和 ExcelReader对象。

查看 ExcelReaderSheetBuilder类方法:

在这里插入图片描述

ExcelReaderSheetBuilder类主要处理 Excel文件中的每一个 sheet信息,比如设置要读取的sheet名,索引。

调用 doRead()方法其实底层通过 ExcelReader对象读取每一个 sheet信息。

4、ExcelReader类

查看 ExcelReader类:

在这里插入图片描述

查看 ExcelReader类方法:

在这里插入图片描述
这里主要看一下 read方法。

在这里插入图片描述

可以看出 ExcelReader类包含了 ReadWorkbook对象和 ReadSheet对象。从而处理 Excel文件中的每一个 sheet信息。

思考:

(1)ExcelReader类是如何初始化的?

在 ExcelReaderBuilder类调用 sheet()方法时,初始化了 ExcelReader对象。

在这里插入图片描述

在 ExcelReaderSheetBuilder类调用 doRead()方法时,底层就通过 ExcelReader对象遍历读取每一个 sheet信息。

在这里插入图片描述

(2)ReadWorkbook对象和 ReadSheet对象是如何赋值给 ExcelReader类的?

在各自的XxxBuilder类中通过构造方法初始化时,创建的,然后分别在调用 sheet()方法和 doRead()方法各自的 build()方法中完成赋值的。

有了上面的知识,接下来通过 EasyExcel操作读 Excel文件就比较简单了。

二、读一个sheet

一般情况,我们创建一个对象来和 Excel文件sheet的列名建立映射关系。

指定列的下标或者列名:

  • 在字段上添加 @ExcelProperty注解,不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配。

自定义格式转换:

  • 可以自定义格式转换器,也可以使用自带的日期、数字格式转换。

下面我们创建一个Excel文件。

在这里插入图片描述

根据 Sheet信息创建一个映射类:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {

    @ExcelProperty("标题")
    private String string;

    @ExcelProperty("日期")
    private Date date;

    @ExcelProperty("浮点数据")
    private Double doubleData;

}

1、同步读

同步读并返回数据,不推荐使用。

    public static void main(String[] args) {
        syncRead();
    }

    private static void syncRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        List<DemoData> demoDataList = EasyExcel.read(fileName)
                .sheet()
                .head(DemoData.class)
                .headRowNumber(1)
                .doReadSync(); //同步读
        log.info("同步解析到所有数据为:{}", JSON.toJSONString(demoDataList));
    }

2、异步读

注册一个自带的匿名监听器。

    public static void asyncRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        EasyExcel.read(fileName)// 读取Excel文件
                .sheet(0) // 读取哪个sheet,索引从0开始
                .head(DemoData.class) // 设置映射对象
                .headRowNumber(1) // 设置1,因为头值占了一行。如果多行头,就设置几行。索引从1开始
                .registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
                        log.info("解析到一条数据为:{}", JSON.toJSONString(demoData));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("全部解析完成");
                    }
                })
                .doRead();
    }

在这里插入图片描述

三、读多个sheet

监听器可以理解为是对读取的数据进行校验(空校验,类型校验)和处理的逻辑部分,用于异步读取。

在上面异步读中的代码,使用了一个参数就是 AnalysisEventListener<T> excelListener的监听器。

AnalysisEventListener类实现了 ReadListener接口,ReadListener中有下面几个方法:

public interface ReadListener<T> extends Listener {
 
    // 在转换异常获取其他异常下会调用本接口。
    default void onException(Exception exception, AnalysisContext context) throws Exception {
        throw exception;
    }
 
    //读取表头数据存在headMap中
    default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
 
    //读取一行一行数据到data
    void invoke(T data, AnalysisContext context);
 
    void extra(CellExtra var1, AnalysisContext context);
 
    //在完成所有数据解析后进行的操作。AOP思想。
    void doAfterAllAnalysed(AnalysisContext context);
 
    default boolean hasNext(AnalysisContext context) {
        return true;
    }
}

我们可以根据进行自己需求的扩展 AnalysisEventListener这个类或者ReadListener接口,对那几个方法进行扩展。

在上面的Excel文件中我们再创建一个Sheet。

在这里插入图片描述

对应再创建一个映射类:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData2 {

    @ExcelProperty("标题")
    private String string;

    @ExcelProperty("日期")
    private Date date;

    @ExcelProperty("浮点数据")
    private Double doubleData;

    @ExcelProperty("整数")
    private Integer integerData;

    /**
     * Java String类型会丢失精度,建议定义为 BigDecimal|Double类型
     */
    @ExcelProperty("经度")
    private Double longitude;

    @ExcelProperty("纬度")
    private Double latitude;

}

1、使用匿名监听器

    public static void manySheetRead() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        ExcelReader excelReader = EasyExcel.read(fileName).build();

        ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
                .registerReadListener(new AnalysisEventListener<DemoData>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData demoData, AnalysisContext analysisContext) {
                        log.info("readSheet1 解析到一条数据为:{}", JSON.toJSONString(demoData));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("readSheet1 全部解析完成");
                    }
                }).build();

        ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
                .headRowNumber(2) // 注意Sheet2表头占了两行
                .registerReadListener(new AnalysisEventListener<DemoData2>() { //注册读的监听器
                    /**
                     * 每解析一行excel数据,就会被调用一次
                     * @param demoData2
                     * @param analysisContext
                     */
                    @Override
                    public void invoke(DemoData2 demoData2, AnalysisContext analysisContext) {
                        log.info("readSheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
                    }

                    /**
                     * 全部解析完被调用
                     * @param analysisContext
                     */
                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        log.info("readSheet2 全部解析完成");
                    }
                }).build();

        excelReader.read(readSheet1, readSheet2);
    }

在这里插入图片描述

2、自定义读监听器

2.1 自定义读监听器

自定义一个通用的读监听器,继承 AnalysisEventListener类。

@Slf4j
public class CustomEasyExcelReadListener<T> extends AnalysisEventListener<T> {
    // 保存读取的对象
    private final List<T> rows = new ArrayList<>();

    // Sheet对应的名字
    private String sheetName = "";

    // 获取对应类
    private Class headClazz;

    // 此集合用来存储错误信息
    private final List<String> errorMessage = new ArrayList<>();

    public CustomEasyExcelReadListener(Class headClazz) {
        this.headClazz = headClazz;
    }


    /**
     * 通过Class获取类字段信息
     *
     * @param headClazz
     * @return
     * @throws NoSuchFieldException
     */
    public Map<Integer, String> getIndexNameMap(Class headClazz) throws NoSuchFieldException {
        Map<Integer, String> result = new HashMap<>();
        Field field;
        Field[] fields = headClazz.getDeclaredFields();     //获取类中所有的属性
        for (int i = 0; i < fields.length; i++) {
            field = headClazz.getDeclaredField(fields[i].getName());
            //log.info(String.valueOf(field));
            field.setAccessible(true);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);//获取根据注解的方式获取ExcelProperty修饰的字段
            if (excelProperty != null) {
                int index = excelProperty.index();         //索引值
                String[] values = excelProperty.value();   //字段值
                StringBuilder value = new StringBuilder();
                for (String v : values) {
                    value.append(v);
                }
                result.put(index, value.toString());
            }
        }
        return result;
    }

    /**
     * 读取表头数据存在headMap中。如果你校验表头格式时可以使用。
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        log.info("解析到一条表头数据:{}", JSON.toJSONString(headMap));
        Map<Integer, String> head = new HashMap<>();
        try {
            //通过Class获取到使用@ExcelProperty注解配置的字段
            head = getIndexNameMap(headClazz);
            log.info(String.valueOf(head));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        //解析到的excel表头和实体配置的进行比对
        Set<Integer> keySet = head.keySet();
        for (Integer key : keySet) {
            if (StringUtils.isEmpty(headMap.get(key))) {
                errorMessage.add("您上传的文件第" + (key + 1) + "列表头为空,请按照模板检查后重新上传");
            }
            if (!headMap.get(key).equals(head.get(key))) {
                errorMessage.add("您上传的文件第" + (key + 1) + "列表头与模板表头不一致,请检查后重新上传");
            }
        }
    }

    /**
     * 读取一行一行数据到object
     *
     * @param object
     * @param context
     */
    @Override
    public void invoke(T object, AnalysisContext context) {
        // 实际数据量比较大时,rows里的数据可以存到一定量之后进行批量处理(比如存到数据库),
        // 然后清空列表,以防止内存占用过多造成OOM
        rows.add(object);
    }

    /**
     * 在完成数据解析后进行的操作。AOP思想。
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 当前sheet的名称 编码获取类似
        sheetName = context.readSheetHolder().getSheetName();
        log.info("sheetName = {} -> 所有数据解析完成, read {} rows", sheetName, rows.size());
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     *
     * @param exception 抛出异常
     * @param context   解析内容
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            errorMessage.add("第" + excelDataConvertException.getRowIndex() + "行,第" + (excelDataConvertException.getColumnIndex() + 1) +
                    "列数据类型解析异常,数据为:" + excelDataConvertException.getCellData());
            log.error("第{}行,第{}列数据类型解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex() + 1,
                    excelDataConvertException.getCellData());
        }
    }

    public List<T> getRows() {
        return rows;
    }

    public Class getHeadClazz() {
        return headClazz;
    }

    public List<String> getErrorMessage() {
        return errorMessage;
    }

    public String getSheetName() {
        return sheetName;
    }
}

2.2 实例测试

    /**
     * 自定义 ReadListener监听器测试方法
     */
    private static void manySheetReadWthCustomReadListener() {
        String fileName = "D:\\TempFiles\\表格.xlsx";
        ExcelReader excelReader = EasyExcel.read(fileName).build();

        CustomEasyExcelReadListener mySheet1Listener = new CustomEasyExcelReadListener(DemoData.class);
        CustomEasyExcelReadListener mySheet2Listener = new CustomEasyExcelReadListener(DemoData2.class);

        List<ReadSheet> readSheetList = new ArrayList<>();
        ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).headRowNumber(1)
                .registerReadListener(mySheet1Listener).build();

        ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData2.class)
                .headRowNumber(2) // 注意Sheet2表头占了两行
                .registerReadListener(mySheet2Listener).build();

        readSheetList.add(readSheet1);
        readSheetList.add(readSheet2);
        excelReader.read(readSheetList);

        System.out.println("============Sheet1 解析解析完成,数据如下================");
        //获取Sheet1监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
        List rows = mySheet1Listener.getRows();
        for (Object row : rows) {
            log.info("Sheet1 解析到一条数据为:{}", JSON.toJSONString(row));
        }
        //获取解决出的错误信息
        List<String> errorMessage = mySheet1Listener.getErrorMessage();
        log.info("Sheet1 解析错误信息为:{}", JSON.toJSONString(errorMessage));


        System.out.println("============Sheet2 解析解析完成,数据如下================");
        //获取Sheet2监听器读到的数据,拿到的数据大家可以根据需求进行数据库操作
        List<DemoData2> demoData2List = mySheet2Listener.getRows();
        for (DemoData2 demoData2 : demoData2List) {
            log.info("Sheet2 解析到一条数据为:{}", JSON.toJSONString(demoData2));
        }
        //获取解决出的错误信息
        List<String> errorMessage2 = mySheet2Listener.getErrorMessage();
        log.info("Sheet2 解析错误信息为:{}", JSON.toJSONString(errorMessage2));
    }

在这里插入图片描述

通用的自定义读监听器中 invokeHeadMap方法校验有点不合适,大家可以创建针对性的读监听器,分别处理。一般 invokeHeadMap方法根据需要使用。

更多操作查看官方文档:https://easyexcel.opensource.alibaba.com/

– 求知若饥,虚心若愚。

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

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

相关文章

VMware Tools安装“保熟“技巧

网上关于如何安装VMware Tools也有很多帖子,但是基本很难对症下药。下面笔者给出两种情况&#xff0c;读者可根据自己概况定位自己的问题&#xff0c;从而进行解决。 如果读者安装操作系统时是如笔者如下截图 那么读者可参考这个解决方案 安装VMware Tools选项显示灰色的正确解…

【算法与数据结构】151、LeetCode反转字符串中的单词

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;本题的主要思路是先处理多余的空格&#xff0c;这个处理方法可以参考博主的这篇文章【算法与数据结构】…

PNP三极管开关电路注意事项

这个是一个NPN三极管的开关电路。 有时候我们也会用PNP的三极管搭建三极管开关电路。 当MCU输出低电平时&#xff0c;三极管BE间的电压小于开启电压&#xff0c;这时三极管饱和导通。 当MCU输出高电平&#xff0c;很多同学认为三极管BE间的电压大于开启&#xff0c;这时三极管截…

在IDEA中使用groovy脚本生成POJO

步骤1&#xff1a;打开Database窗格&#xff0c;新建数据库连接 数据库连接默认只是当前工程使用&#xff0c;想要所有IDEA窗口共享 步骤2&#xff1a;编辑groovy脚本 步骤3&#xff1a;选择一张或多张表&#xff0c;生成代码 生成效果 附&#xff1a;groovy脚本 import com.i…

POI合并单元格设置单元格样式

文章目录 设置居中设置背景颜色设置边框设置字体合并单元格实际使用运行效果 设置居中 CellStyle centerStyle wb.createCellStyle();centerStyle.setAlignment(HorizontalAlignment.CENTER); // 居中centerStyle.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中设…

桐庐旅行|桐庐团建全攻略「两天一夜行程」

桐庐被《国家地理》评选为全球25个最佳旅行目的地之一 获评「中国最美县城」&#xff0c;热门综艺《向往的生活》拍摄地 浙江的山水精华尽在「桐庐」 今夏绝对不能错过的避暑胜地 交通信息 车程&#xff1a;杭州1.5h 、上海3h、宁波2.5H、南京3.5H 尖峰推荐目的地 深澳古村 始…

原来,这就是铁路隧道R型变压器的工作真相!

铁路作为我们日常交通的重要出行设备&#xff0c;其安全稳定性极为重要。高速铁路具有行车速度快、行车密度高、负荷分布密集、自动化程度高、要求安全、正点运行的特点。铁路隧道对电力系统的供电可靠性也有非常严格的要求。铁路隧道R型变压器在铁路隧道供电系统中的主要功能是…

【线程池】史上最全的ThreadPoolExecutor源码详解

目录 一、线程池框架 1.1 第一层结构 1.2 接口简介 1.3 核心实现类 1.4 辅助类 1.5 完成服务 二、ThreadPoolExecutor的成员属性和内部类 2.1 主要成员属性以及工具方法 2.2 五种内部类 2.2.1 拒绝策略内部类&#xff08;Policy&#xff09; 2.2.2 工作线程内部类&a…

八数码、解华容道(bfs,全局择先,A*搜索)

【问题描述】 题目6&#xff1a;数阵问题 每个局面是三行三列的数字方阵&#xff0c;每个位置为0-8的一个数码且互不相同&#xff0c;求从初始局面&#xff08;自己设定&#xff09;如何“最快”移动到终止局面&#xff08;自己设定&#xff09;。 移动规则&#xff1a;每次只…

【实战】 四、JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求1.login2.middleware of json-server3.jira-dev-tool&#xff08;imooc-jira-tool&#xff09;安装问…

Onlyoffice安装步骤

使用docker安装Onlyoffice社区版 第X章 占位… 文章目录 使用docker安装Onlyoffice社区版说明一、系统需求二、安装步骤1.下载2.安装3.测试4. 升级为HTTPS协议4.1生成私钥4.2 生成CSR,即&#xff1a;证书签名请求文件4.3 使用私钥和CSR签署证书 未完&#xff0c;待续总结 说明…

【程序员面试金典】面试题 17.21. 直方图的水量

【程序员面试金典】面试题 17.21. 直方图的水量 题目描述解题思路 题目描述 描述&#xff1a;给定一个直方图(也称柱状图)&#xff0c;假设有人从上面源源不断地倒水&#xff0c;最后直方图能存多少水量?直方图的宽度为 1。 上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直…

从数据取地址运算符 从地址取数据运算符* 数组函数传送三种方法

概念 a表示数据本身。 &a表示a对应的地址。 *a表示存a数据的地址。 #include<stdio.h> int main() { int a 10; int *b &a; int* d &a; int c *b; …

Spring Boot 中的 WebSocketSession 是什么,原理,如何使用

Spring Boot 中的 WebSocketSession 是什么&#xff0c;原理&#xff0c;如何使用 介绍 在现代 Web 应用程序中&#xff0c;实时通信是一个非常常见的需求。传统的 HTTP 协议是无法支持实时通信的&#xff0c;因为它是一种无状态协议&#xff0c;每次请求都是独立的&#xff0…

实现数据库版本的留言墙(表白墙)练习

目录 目标 1.创建工程 2.构建目录工程结构 3.设置编码格式 4.查看Maven的配置 6.导入HTML和JS&#xff0c;配置tomcat 7.测试网站是否可以正常访问 8.编写业务代码 目标&#xff1a; 1.熟练掌握前端向后端提交数据2.后端接收数据并校验3.通过JAVA代码进行数据库操作4.返回…

8、Redis哨兵(sentinel)

是什么? 吹哨人巡查监控后台master主机是否故障&#xff0c;如果故障了根据投票数 \textcolor{red}{投票数}投票数自动将某一个从库转换为新主库&#xff0c;继续对外服务 作用&#xff1a;俗称无人值守运维 能干嘛? 主从监控&#xff1a;监控主从redis库运行是否正常 消息…

云原生|kubernetes|离线化部署kubesphere(从网络插件开始记录)

前言&#xff1a; kubesphere的离线化部署指的是通过自己搭建的harbor私有仓库拉取镜像&#xff0c;完全不依赖于外部网络的方式部署。 我的kubernetes集群是一个单master节点&#xff0c;双工作节点&#xff0c;总计三个节点的版本为1.22.16的集群。 该集群只是初始化完成了…

华为、阿里巴巴、字节跳动 100+ Python 面试问题总结(一)

系列文章目录 个人简介&#xff1a;机电专业在读研究生&#xff0c;CSDN内容合伙人&#xff0c;博主个人首页 Python面试专栏&#xff1a;《Python面试》此专栏面向准备面试的2024届毕业生。欢迎阅读&#xff0c;一起进步&#xff01;&#x1f31f;&#x1f31f;&#x1f31f; …

如何在VirtualBox安装CentOS 7

一、简介VirtualBox VirtualBox 是一款开源虚拟机软件。VirtualBox 是由德国 Innotek 公司开发&#xff0c;由Sun Microsystems公司出品的软件&#xff0c;使用Qt编写&#xff0c;在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。Innotek 以 GNU General Public Lice…

6、多层感知机:数值稳定性和模型初始化

1、数值稳定性 考虑一个具有 L L L层、输入 x \mathbf{x} x和输出 o \mathbf{o} o的深层网络。每一层 l l l由变换 f l f_l fl​定义&#xff0c;该变换的参数为权重 W ( l ) \mathbf{W}^{(l)} W(l)&#xff0c;其隐藏变量是 h ( l ) \mathbf{h}^{(l)} h(l)&#xff08;令 h …