【从零开发Mybatis】引入MapperConfig.xml和Mapper映射配置

news2025/4/17 3:23:54

引言

学习MyBatis源码之前,了解它是如何通过JDBC查询数据库数据的基础知识是非常有用的。

上一篇我们编写了一个最简单的示例,通过JDBC查询数据库数据,从本文开始,我们将正式开始Mybatis框架的开发。

通过JDBC查询数据库数据存在的问题及处理方案

问题1:数据源写死在代码中
处理方案:引入MapperConfig.xml全局配置文件,配置数据源等信息

问题2:SQL语句写死在代码中
处理方案:引入Mapper映射文件,配置SQL脚本等信息

引入MapperConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
    <dataSource >
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;serverTimezone=UTC"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </dataSource>

    <mappers>
        <mapper resource="AuthorMapper.xml"/>
    </mappers>

</configuration>

以上XML 配置文件是一个典型的 MyBatis 配置文件的一部分,用于定义数据源(dataSource)和映射器(mapper)。这个配置文件定义了数据库连接的信息,并指定了一个映射器文件 AuthorMapper.xml。

引入Mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper">
    
    <select id="selectAuthor">
        select id, username, email from author where id = ?
    </select>

</mapper>

  • mapper 标签:定义一个 MyBatis 映射器,其中 namespace 属性指定了映射器的全限定类名。
  • namespace 属性:org.apache.ibatis.domain.blog.mappers.AuthorMapper,这是映射器的唯一标识符,用于区分不同的映射器。
  • select 标签:定义了一个 SQL 查询语句。
  • id 属性:selectAuthor,这是 SQL 语句的唯一标识符,用于在代码中引用此 SQL 语句。
  • SQL 语句:select id, username, email from author where id = ?,这是一个简单的 SQL 查询语句,用于从 author 表中选择特定记录。
  • ? 占位符:表示一个参数占位符,将在执行 SQL 语句时替换为实际值。

解析MapperConfig.xml和Mapper映射文件

我们对SqlSession类进行改造,数据源及SQL脚本通过读取MapperConfig.xml和Mapper映射文件获取,代码如下:

package org.apache.ibatis.session;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.*;
import java.util.*;

/**
 * Sql会话
 *
 * @author crazy coder
 * @since 2024/9/27
 **/
public class SqlSession {
    public String selectOne(String statement, Integer param) throws ParserConfigurationException, XPathExpressionException, IOException, SAXException {

        final String configResource = "MapperConfig.xml";
        InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(configResource);
        Reader reader = new InputStreamReader(in);

        // 读取XML配置文件
        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
        Document document = docBuilder.parse(new InputSource(reader));

        XPathFactory xPathFactory = XPathFactory.newInstance();
        XPath xpath = xPathFactory.newXPath();
        Node configNode = (Node) xpath.evaluate("/configuration", document, XPathConstants.NODE);

        // 解析XML配置信息 - 数据源
        // 驱动
        String driver = null;
        // 数据库连接 URL
        String url = null;
        // 数据库用户名
        String username = null;
        // 数据库密码
        String password = null;
        Node envNode = (Node) xpath.evaluate("dataSource", configNode, XPathConstants.NODE);
        NodeList nodeList = envNode.getChildNodes();
        for (int i = 0, n = nodeList.getLength(); i < n; i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Properties attributes = new Properties();
                NamedNodeMap attributeNodes = node.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        attributes.put(attribute.getNodeName(), value);
                    }
                }
                if ("driver".equals(attributes.get("name"))) {
                    driver = (String) attributes.get("value");
                } else if ("url".equals(attributes.get("name"))) {
                    url = (String) attributes.get("value");
                } else if ("username".equals(attributes.get("name"))) {
                    username = (String) attributes.get("value");
                } else if ("password".equals(attributes.get("name"))) {
                    password = (String) attributes.get("value");
                }
            }
        }

        // 读取Mapper配置文件
        List<String> resourceMapperList = new ArrayList<>();
        Node mappersNode = (Node) xpath.evaluate("mappers", configNode, XPathConstants.NODE);
        NodeList mapperList = mappersNode.getChildNodes();
        for (int i = 0, n = mapperList.getLength(); i < n; i++) {
            Node node = mapperList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                NamedNodeMap attributeNodes = node.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("resource".equals(attribute.getNodeName())) {
                            resourceMapperList.add(value);
                        }
                    }
                }
            }
        }
        Map<String, String> statementMap = new HashMap<>();
        for (String mapperResource : resourceMapperList) {
            try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) {
                Reader mapperReader = new InputStreamReader(inputStream);
                Document mapperDocument = docBuilder.parse(new InputSource(mapperReader));
                Node mapperNode = (Node) xpath.evaluate("/mapper", mapperDocument, XPathConstants.NODE);

                String namespace = "";
                NamedNodeMap mapperAttributeNodes = mapperNode.getAttributes();
                if (mapperAttributeNodes != null) {
                    for (int j = 0; j < mapperAttributeNodes.getLength(); j++) {
                        Node attribute = mapperAttributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("namespace".equals(attribute.getNodeName())) {
                            namespace = value;
                        }
                    }
                }

                Node selectNode = (Node) xpath.evaluate("select", mapperNode, XPathConstants.NODE);
                String mapperId = "";
                NamedNodeMap attributeNodes = selectNode.getAttributes();
                if (attributeNodes != null) {
                    for (int j = 0; j < attributeNodes.getLength(); j++) {
                        Node attribute = attributeNodes.item(j);
                        String value = attribute.getNodeValue();
                        if ("id".equals(attribute.getNodeName())) {
                            mapperId = value;
                        }
                    }
                }
                String sql = selectNode.getTextContent();
                statementMap.put(namespace + "." + mapperId, sql);
            }
        }


        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            // 加载 MySQL JDBC 驱动
            Class.forName(driver);

            // 获取数据库连接
            conn = DriverManager.getConnection(url, username, password);

            // 准备 SQL 语句
            String sql = statementMap.get(statement);

            // 创建预编译语句
            pstmt = conn.prepareStatement(sql);

            // 设置参数
            pstmt.setLong(1, param);

            // 执行 SQL 查询操作
            rs = pstmt.executeQuery();

            // 处理结果集
            StringBuilder result = new StringBuilder();
            while (rs.next()) {
                result.append("id: ").append(rs.getInt("id"))
                        .append(", username: ").append(rs.getString("username"))
                        .append(", email: ").append(rs.getString("email"));

            }
            return result.toString();

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            in.close();
        }

        return "";
    }
}

这段代码实现了通过 MyBatis 的配置文件读取数据源信息,并执行 SQL 查询的功能。下面是对这段代码的详细解析:

  • 方法定义 selectOne:
public String selectOne(String statement, Integer param)
    throws ParserConfigurationException, XPathExpressionException, IOException, SAXException {

这个方法接受两个参数:statement 和 param。statement 是一个字符串,表示 SQL 语句的唯一标识符;param 是一个整数类型的参数,用于 SQL 语句中的占位符。

  • 读取MapperConfig.xml配置文件
final String configResource = "MapperConfig.xml";
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(configResource);
Reader reader = new InputStreamReader(in);

// 读取XML配置文件
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document document = docBuilder.parse(new InputSource(reader));

这里使用了 DOM 解析器来读取配置文件 MapperConfig.xml,并将文件内容解析成 Document 对象。

  • 解析数据源信息
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
Node configNode = (Node) xpath.evaluate("/configuration", document, XPathConstants.NODE);

// 解析XML配置信息 - 数据源
// 驱动
String driver = null;
// 数据库连接 URL
String url = null;
// 数据库用户名
String username = null;
// 数据库密码
String password = null;
Node envNode = (Node) xpath.evaluate("dataSource", configNode, XPathConstants.NODE);
NodeList nodeList = envNode.getChildNodes();
for (int i = 0, n = nodeList.getLength(); i < n; i++) {
    Node node = nodeList.item(i);
    if (node.getNodeType() == Node.ELEMENT_NODE) {
        Properties attributes = new Properties();
        NamedNodeMap attributeNodes = node.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                attributes.put(attribute.getNodeName(), value);
            }
        }
        if ("driver".equals(attributes.get("name"))) {
            driver = (String) attributes.get("value");
        } else if ("url".equals(attributes.get("name"))) {
            url = (String) attributes.get("value");
        } else if ("username".equals(attributes.get("name"))) {
            username = (String) attributes.get("value");
        } else if ("password".equals(attributes.get("name"))) {
            password = (String) attributes.get("value");
        }
    }
}

这部分代码解析了配置文件中的 节点,并从中提取出数据库连接所需的各项信息:驱动、URL、用户名和密码。

  • 读取 Mapper 文件
List<String> resourceMapperList = new ArrayList<>();
Node mappersNode = (Node) xpath.evaluate("mappers", configNode, XPathConstants.NODE);
NodeList mapperList = mappersNode.getChildNodes();
for (int i = 0, n = mapperList.getLength(); i < n; i++) {
    Node node = mapperList.item(i);
    if (node.getNodeType() == Node.ELEMENT_NODE) {
        NamedNodeMap attributeNodes = node.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("resource".equals(attribute.getNodeName())) {
                    resourceMapperList.add(value);
                }
            }
        }
    }
}

这部分代码解析了配置文件中的 节点,并从中提取出映射器文件的路径列表。

  • 解析 SQL 语句
Map<String, String> statementMap = new HashMap<>();
for (String mapperResource : resourceMapperList) {
    try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(mapperResource)) {
        Reader mapperReader = new InputStreamReader(inputStream);
        Document mapperDocument = docBuilder.parse(new InputSource(mapperReader));
        Node mapperNode = (Node) xpath.evaluate("/mapper", mapperDocument, XPathConstants.NODE);

        String namespace = "";
        NamedNodeMap mapperAttributeNodes = mapperNode.getAttributes();
        if (mapperAttributeNodes != null) {
            for (int j = 0; j < mapperAttributeNodes.getLength(); j++) {
                Node attribute = mapperAttributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("namespace".equals(attribute.getNodeName())) {
                    namespace = value;
                }
            }
        }

        Node selectNode = (Node) xpath.evaluate("select", mapperNode, XPathConstants.NODE);
        String mapperId = "";
        NamedNodeMap attributeNodes = selectNode.getAttributes();
        if (attributeNodes != null) {
            for (int j = 0; j < attributeNodes.getLength(); j++) {
                Node attribute = attributeNodes.item(j);
                String value = attribute.getNodeValue();
                if ("id".equals(attribute.getNodeName())) {
                    mapperId = value;
                }
            }
        }
        String sql = selectNode.getTextContent();
        statementMap.put(namespace + "." + mapperId, sql);
    }
}

这部分代码解析了每个映射器文件中的 节点,并从中提取出 SQL 语句的 namespace 和 id,并将它们组合成键值对存储在 statementMap 中。

  • 执行 SQL 查询
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

try {
    // 加载 MySQL JDBC 驱动
    Class.forName(driver);

    // 获取数据库连接
    conn = DriverManager.getConnection(url, username, password);

    // 准备 SQL 语句
    String sql = statementMap.get(statement);

    // 创建预编译语句
    pstmt = conn.prepareStatement(sql);

    // 设置参数
    pstmt.setLong(1, param);

    // 执行 SQL 查询操作
    rs = pstmt.executeQuery();

    // 处理结果集
    StringBuilder result = new StringBuilder();
    while (rs.next()) {
        result.append("id: ").append(rs.getInt("id"))
              .append(", username: ").append(rs.getString("username"))
              .append(", email: ").append(rs.getString("email"));
    }
    return result.toString();

} catch (ClassNotFoundException | SQLException e) {
    e.printStackTrace();
} finally {
    // 关闭资源
    if (pstmt != null) {
        try {
            pstmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    in.close();
}

return "";

这部分代码实现了通过 JDBC 连接到数据库,并执行 SQL 查询的操作。它使用预编译语句来提高安全性,并正确处理了结果集。

测试用例

package org.apache.ibatis.session;

import org.junit.jupiter.api.Test;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import java.io.IOException;

class SqlSessionTest {

    @Test
    void selectOne() throws ParserConfigurationException, SAXException, XPathExpressionException, IOException {
        SqlSession sqlSession = new SqlSession();
        String statement = "org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor";
        System.out.println(sqlSession.selectOne(statement, 101));
    }
}

测试类 SqlSessionTest 包含了一个 JUnit 测试用例 selectOne,用于测试 SqlSession 类中的 selectOne 方法。

整体项目结构

在这里插入图片描述

总结

本文我们实现了以下功能:

  • 加载配置文件:从类路径中加载 MapperConfig.xml 文件。
  • 解析数据源信息:提取MapperConfig.xml 文件中的数据库连接信息(驱动、URL、用户名和密码)。
  • 读取 Mapper 文件:读取MapperConfig.xml 文件中指定的 Mapper 文件。解析 Mapper 文件中的 SQL 语句,并将 SQL 语句及其标识符存储在 Map 中。

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

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

相关文章

计算机网络:数据链路层 —— 共享式以太网

文章目录 共享式以太网CSMA/CD 协议CSMA/CD 协议 的基本原理 共享式以太网的争用期共享式以太网的最小帧长共享式以太网的最大帧长共享式以太网的退避算法截断二进制指数退避算法 共享二进制以太网的信道利用率使用集线器的共享式以太网10BASE-T 共享式以太网 共享式以太网是当…

微分几何-曲线论(曲线)

文章目录 曲线定义常见曲线直线圆圆柱螺线维维安尼曲线 连续曲线/光滑曲线正则曲线切向量切线方程法平面&#xff08;法面&#xff09; 弧长定理1&#xff1a;弧长公式弧长参数化定理2&#xff1a;任何一条正则曲线都可以使用弧长作参数.&#xff08;也称弧长参数为**自然参数*…

[Linux] 逐层深入理解文件系统 (1)—— 进程操作文件

标题&#xff1a;[Linux] 文件系统 &#xff08;1&#xff09;—— 进程操作文件 个人主页水墨不写bug &#xff08;图片来源于网络&#xff09; 目录 一、进程与打开的文件 二、文件的系统调用与库函数的关系 1.系统调用open() 三、内存中的文件描述符表 四、缓冲区…

【Java SE 】类和对象详解

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 目录 1&#xff0c; 面向对象认识 1.1 什么时面向对象 1.2 面向对象和面向过程 1.2.1 一个例子理解对象和过程 1. 对于电脑来说 2. 对于我们人来说 2. 类的定…

还在为调用大模型API接口返回数据的处理问题烦恼???来看看这篇文章,解决你的烦恼!!!

大家好&#xff01;我是学徒小z&#xff0c;今天给大家分享一下我做项目过程中遇到的一个问题。 文章目录 问题大模型接口解决方案流式返回和回调函数另一个问题1. 使用web组件2. 使用第三方库 问题 不知道大家有没有遇到这样一个问题。在调用大模型接口的时&#xff0c;返回…

Lnmp(mysql分离)(nginx 1.13.6+mysql5.5+php5.3)环境一键搭建

Lnmp&#xff08;mysql分离&#xff09;&#xff08;nginx 1.13.6mysql5.5php5.3&#xff09;环境一键搭建 如果对运维课程感兴趣&#xff0c;可以在b站上、csdn或微信视频号 上搜索我的账号&#xff1a; 运维实战课程&#xff0c;可以关注我&#xff0c;学习更多免费的运维实…

深度学习之残差网络ResNet

文章目录 1. 残差网络定义2. 数学基础函数类3. 残差块4.ResNet模型5.训练模型6.小结 1. 残差网络定义 随着我们设计的网络越来越深&#xff0c;深刻理解“新添加的层如何提升神经网络的性能”变得至关重要。更重要的是设计网络的能力。在这种网络中&#xff0c;添加层会使得网…

单例模式:为何继承无法保证子类的单例特性

这里写目录标题 一、引言二、背景描述三、单例模式的规范边界全局访问点与静态工厂方法代码示例与注意事项 四、单例实现继承遇到的问题五、结论与替代方案结论替代方案特殊的想法&#x1f338;&#x1f338;源码阶段验证编译阶段验证运行阶段验证总结 一、引言 在软件设计中&a…

实时语音转文字(基于NAudio+Whisper+VOSP+Websocket)

今天花了大半天时间研究一个实时语音转文字的程序&#xff0c;目的还包括能够唤醒服务&#xff0c;并把命令提供给第三方。 由于这方面的材料已经很多&#xff0c;我就只把过程中遇到的和解决方案简单说下。源代码开源在AudioWhisper: 实时语音转文字(基于NAudioWhisperVOSPWe…

基于SSM的个性化商铺系统【附源码】

基于SSM的个性化商铺系统 效果如下&#xff1a; 用户登录界面 app首页界面 商品信息界面 店铺信息界面 用户功能界面 我的订单界面 后台登录界面 管理员功能界面 用户管理界面 商家管理界面 店铺信息管理界面 商家功能界面 个人中心界面 研究背景 研究背景 科学技术日新月异…

Leetcode 每日温度

class Solution {public int[] dailyTemperatures(int[] temperatures) {int n temperatures.length;Stack<Integer> stack new Stack<>();//默认将数组中的所有元素初始化为 0int[] results new int[n];for(int i 0; i < n; i) {while(!stack.isEmpty() &a…

leaflet前端JS实现高德地图POI兴趣点批量分类下载(附源码下载)

前言 leaflet 入门开发系列环境知识点了解&#xff1a; leaflet api文档介绍&#xff0c;详细介绍 leaflet 每个类的函数以及属性等等leaflet 在线例子leaflet 插件&#xff0c;leaflet 的插件库&#xff0c;非常有用 内容概览 leaflet前端JS实现高德地图POI兴趣点批量分类下载…

小猿口算炸鱼脚本

目录 写在前面&#xff1a; 一、关于小猿口算&#xff1a; 二、代码逻辑 1.数字识别 2.答题部分 三、代码分享&#xff1a; 补充&#xff1a;软件包下载 写在前面&#xff1a; 最近小猿口算已经被不少大学生攻占&#xff0c;小学生直呼有挂。原本是以为大学生都打着本…

【Python爬虫】看电影还在用VIP?一个python代码让你实现电影自由!附源码

今日主题 如何用Python解析vip电影。 什么是vip电影&#xff1f; 这些vip电影啊&#xff0c;想要观看的话&#xff0c;必须充值会员&#xff0c;否则没法看。 比如这个&#xff1a; 这些vip电影解析后呢&#xff1f; 不需要会员&#xff0c;不需要登录&#xff0c;可以直接…

Java-类与对象

一、面向对象 在了解类与对象前&#xff0c;我们需要先知道"面向对象"这个词的概念&#xff1a; 在Java语言中&#xff0c;我们的主要思想就是"面向对象"&#xff0c;而在之前我们所学习的C语言中大部分时候的思想是"面向过程"。 那么什么是&…

MySQL-10.DML-添加数据insert

一.DML(INSERT) -- DDL&#xff1a;数据操作语言 -- DML&#xff1a;插入数据 - insert -- 1.为tb_emp表的username&#xff0c;name&#xff0c;gender字段插入值 insert into tb_emp (username,name,gender) values (wuji,无忌,1); -- 这样会报错&#xff0c;因为create_ti…

DS堆的实际应用(10)

文章目录 前言一、堆排序建堆排序 二、TopK问题原理实战创建一个有一万个数的文件读取文件并将前k个数据创建小堆用剩余的N-K个元素依次与堆顶元素来比较将前k个数据打印出来并关闭文件 测试 三、堆的相关习题总结 前言 学完了堆这个数据结构的概念和特性后&#xff0c;我们来看…

限时设计ui

ctrl-------放大缩小 空格-----画面移动 alt------复制 页面<画板<图层 添加交互事件 原型 点击蓝色的圆&#xff0c;从1跳转到2 点击绿色的圆&#xff0c;从2跳转到1

基于SSM+Vue+MySQL的健身房管理系统

系统展示 系统背景 随着人们生活水平的提高和健康意识的增强&#xff0c;越来越多的人选择去健身房锻炼。传统的健身房管理方式往往依赖于纸质记录和人工操作&#xff0c;这种方式不仅效率低下&#xff0c;而且容易出错。为了提高健身房的管理效率和服务质量&#xff0c;开发一…

python项目实战——下载美女图片

python项目实战——下载美女图片 文章目录 python项目实战——下载美女图片完整代码思路整理实现过程使用xpath语法找图片的链接检查链接是否正确下载图片创建文件夹获取一组图片的链接获取页数 获取目录页的链接 完善代码注意事项 完整代码 import requests import re import…