Java实现一个解析CURL脚本小工具

该工具可以将CURL脚本中的Header解析为KV Map结构;获取URL路径、请求类型;解析URL参数列表;解析Body请求体:Form表单、Raw Body、KV Body、XML/JSON/TEXT结构体等。

使用示例

获取一个http curl脚本:

curl --location --request POST 'https://cainiao-inc.com?param_1=value_1&param_2=value_2' \
--header 'Cookie: USER_COOKIE' \
--header 'Content-Type: application/json' \
--data-raw '{
    "appName": "link",
    "apiId": "TEST_API",
    "content": {
        "address": "Cainiao Home",
        "city": "Hangzhou"
    }
}'

执行解析例子:

实现原理

实现原理很简单:基于Java正则 + 责任链设计模式,按照Curl脚本的常见语法去匹配、解析即可~

按照Curl语法结构,可以将其拆分为 5 个部分:

  • URL路径:http://cainiao.com

  • URL参数列表:?param_1=valie_1&param_2=valie_2

  • 请求方法类型: 例如 POST、GET、DELETE、PUT...... 需要正则匹配-X --request等标识符

  • Header请求头:例如 Cookie、Token、Content-Type...... 需要正则匹配-H --header等标识符

  • Body请求体:可以分为form-data/-formdata-rawdata-urlencode-d--datakvbody等。格式可能包含JSON、XML、文本、KV键值对,二进制流(暂不支持解析)等等。

具体实现

流程简图:

类关系图:

CurlParserUtil

Curl解析工具类:

public class CurlParserUtil {
    /**
     * 该方法是用来解析CURL的入口。
     *
     * @param curl 输入的CURL文本字符串
     * @return 返回解析后生成的CURL实体对象
     */
    public static CurlEntity parse(String curl) {
        CurlEntity entity = CurlEntity.builder().build();
        ICurlHandler<CurlEntity, String> handlerChain = CurlHandlerChain.init();

        // 如需扩展其他解析器,继续往链表中add即可
        handlerChain.next(new UrlPathHandler())
                .next(new UrlParamsHandler())
                .next(new HttpMethodHandler())
                .next(new HeaderHandler())
                .next(new HttpBodyHandler());

        handlerChain.handle(entity, curl);
        return entity;
    }
}

CurlEntity

解析后得到的Curl实体类(这里分了5个部分)

@Data
@Builder
public class CurlEntity {
    /**
     * URL路径
     */
    private String url;

    /**
     * 请求方法类型
     */
    private Method method;

    /**
     * URL参数
     */
    private Map<String, String> urlParams;

    /**
     * header参数
     */
    private Map<String, String> headers;

    /**
     * 请求体
     */
    private JSONObject body;

    public enum Method {
        GET,
        POST,
        PUT,
        DELETE
    }
}

ICurlHandler

责任链链表结构定义:

public interface ICurlHandler<R, S> {

    ICurlHandler<CurlEntity, String> next(ICurlHandler<CurlEntity, String> handler);

    void handle(CurlEntity entity, String curl);
}

CurlHandlerChain

责任链载体:

public abstract class CurlHandlerChain implements ICurlHandler<CurlEntity, String> {

    ICurlHandler<CurlEntity, String> next;

    @Override
    public ICurlHandler<CurlEntity, String> next(ICurlHandler<CurlEntity, String> handler) {
        this.next = handler;
        return this.next;
    }

    @Override
    public abstract void handle(CurlEntity entity, String curl);

    /**
     * for subclass call
     */
    protected void nextHandle(CurlEntity curlEntity, String curl) {
        if (next != null) {
            next.handle(curlEntity, curl);
        }
    }

    protected void validate(String curl) {
        if (StringUtils.isBlank(curl)) {
            throw new IllegalArgumentException("Curl script is empty");
        }

        Matcher matcher = CURL_BASIC_STRUCTURE_PATTERN.matcher(curl);
        if (!matcher.find()) {
            throw new IllegalArgumentException("Curl script is invalid");
        }
    }

    public static CurlHandlerChain init() {
        return new CurlHandlerChain() {
            @Override
            public void handle(CurlEntity entity, String curl) {
                this.validate(curl);

                // 替换掉可能存在的转译
                curl = curl.replace("\\", "");

                if (next != null) {
                    next.handle(entity, curl);
                }
            }
        };
    }

    public void log(Object... logParams) {
        // Write log for subclass extensions
    }
}

UrlPathHandler

URL路径解析:

public class UrlPathHandler extends CurlHandlerChain {

    @Override
    public void handle(CurlEntity entity, String curl) {
        String url = parseUrlPath(curl);
        entity.setUrl(url);

        this.log(url);
        super.nextHandle(entity, curl);
    }

    /**
     * 该方法用于解析URL路径。
     *
     * @param curl 需要解析的URL,以字符串形式给出
     * @return URL中的路径部分。如果找不到,将返回null
     */
    private String parseUrlPath(String curl) {
        Matcher matcher = CurlPatternConstants.URL_PATH_PATTERN.matcher(curl);
        if (matcher.find()) {
            return matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
        }
        return null;
    }

    @Override
    public void log(Object... logParams) {
        LogPrinter.info("UrlPathHandler execute: url={}", logParams);
    }
}

HttpMethodHandler

请求类型解析:

public class HttpMethodHandler extends CurlHandlerChain {

    @Override
    public void handle(CurlEntity entity, String curl) {
        CurlEntity.Method method = parseMethod(curl);
        entity.setMethod(method);

        this.log(method);
        super.nextHandle(entity, curl);
    }

    private CurlEntity.Method parseMethod(String curl) {
        Matcher matcher = CurlPatternConstants.HTTP_METHOD_PATTERN.matcher(curl);
        Matcher defaultMatcher = CurlPatternConstants.DEFAULT_HTTP_METHOD_PATTERN.matcher(curl);
        if (matcher.find()) {
            String method = matcher.group(1);
            return CurlEntity.Method.valueOf(method.toUpperCase());
        } else if (defaultMatcher.find()) {
            // 如果命令中包含 -d 或 --data,没有明确请求方法,默认为 POST
            return CurlEntity.Method.POST;
        } else {
            // 没有明确指定请求方法,默认为 GET
            return CurlEntity.Method.GET;
        }
    }

    @Override
    public void log(Object... logParams) {
        LogPrinter.info("HttpMethodHandler execute: method={}", logParams);
    }
}

UrlParamsHandler

URL参数列表解析:

public class UrlParamsHandler extends CurlHandlerChain {

    @Override
    public void handle(CurlEntity entity, String curl) {
        String url = extractUrl(curl);
        Map<String, String> urlParams = parseUrlParams(url);
        entity.setUrlParams(urlParams);

        this.log(urlParams);
        super.nextHandle(entity, curl);
    }

    private String extractUrl(String curl) {
        Matcher matcher = CurlPatternConstants.URL_PARAMS_PATTERN.matcher(curl);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    private Map<String, String> parseUrlParams(String url) {
        if (StringUtils.isBlank(url)) {
            return Collections.emptyMap();
        }

        Map<String, String> urlParams = new HashMap<>();
        // 提取URL的查询参数部分
        String[] urlParts = url.split("\\?");
        if (urlParts.length > 1) {
            // 只处理存在查询参数的情况
            String query = urlParts[1];
            // 解析查询参数到Map
            String[] pairs = query.split("&");
            for (String pair : pairs) {
                int idx = pair.indexOf("=");
                if (idx != -1 && idx < pair.length() - 1) {
                    String key = pair.substring(0, idx);
                    String value = pair.substring(idx + 1);
                    urlParams.put(key, value);
                } else {
                    // 存在无值的参数时
                    urlParams.put(pair, null);
                }
            }
        }
        return urlParams;
    }

    @Override
    public void log(Object... logParams) {
        LogPrinter.info("UrlParamsHandler execute: urlParams={}", logParams);
    }
}

HeaderHandler

Http Header解析:

public class HeaderHandler extends CurlHandlerChain{
    
    @Override
    public void handle(CurlEntity entity, String curl) {
        Map<String, String> headers = parseHeaders(curl);
        entity.setHeaders(headers);

        this.log(headers);
        super.nextHandle(entity, curl);
    }

    private Map<String, String> parseHeaders(String curl) {
        if (StringUtils.isBlank(curl)) {
            return Collections.emptyMap();
        }

        Matcher matcher = CurlPatternConstants.CURL_HEADERS_PATTERN.matcher(curl);
        Map<String, String> headers = new HashMap<>();
        while (matcher.find()) {
            String header = matcher.group(1);
            String[] headerKeyValue = header.split(":", 2);
            if (headerKeyValue.length == 2) {
                // 去除键和值的首尾空白字符
                headers.put(headerKeyValue[0].trim(), headerKeyValue[1].trim());
            }
        }

        return headers;
    }

    @Override
    public void log(Object... logParams) {
        LogPrinter.info("HeaderHandler execute: headers={}", logParams);
    }
}

HttpBodyHandler

Request Body请求体解析:

  • form-data/-form

  • data-urlencode

  • data-raw

  • default/-d/--data

格式可能包含JSON、XML、文本、KV键值对,二进制流(暂不支持解析)等等。

public class HttpBodyHandler extends CurlHandlerChain {
    
    @Override
    public void handle(CurlEntity entity, String curl) {
        JSONObject body = parseBody(curl);
        entity.setBody(body);

        this.log(body);
        super.nextHandle(entity, curl);
    }

    private JSONObject parseBody(String curl) {
        Matcher formMatcher = CurlPatternConstants.HTTP_FROM_BODY_PATTERN.matcher(curl);
        if (formMatcher.find()) {
            return parseFormBody(formMatcher);
        }

        Matcher urlencodeMatcher = CurlPatternConstants.HTTP_URLENCODE_BODY_PATTERN.matcher(curl);
        if (urlencodeMatcher.find()) {
            return parseUrlEncodeBody(urlencodeMatcher);
        }

        Matcher rawMatcher = CurlPatternConstants.HTTP_ROW_BODY_PATTERN.matcher(curl);
        if (rawMatcher.find()) {
            return parseRowBody(rawMatcher);
        }

        Matcher defaultMatcher = CurlPatternConstants.DEFAULT_HTTP_BODY_PATTERN.matcher(curl);
        if (defaultMatcher.find()) {
            return parseDefaultBody(defaultMatcher);
        }

        return new JSONObject();
    }

    private JSONObject parseDefaultBody(Matcher defaultMatcher) {
        String bodyStr = "";
        if (defaultMatcher.group(1) != null) {
            // 单引号包裹的数据
            bodyStr = defaultMatcher.group(1);
        } else if (defaultMatcher.group(2) != null) {
            // 双引号包裹的数据
            bodyStr = defaultMatcher.group(2);
        } else {
            // 无引号的数据
            bodyStr = defaultMatcher.group(3);
        }

        // 特殊Case: username=test&password=secret
        Matcher kvMatcher = CurlPatternConstants.DEFAULT_HTTP_BODY_PATTERN_KV.matcher(bodyStr);
        if (kvMatcher.find()) {
            return parseKVBody(bodyStr);
        }

        return JSONObject.parseObject(bodyStr);
    }

    private JSONObject parseKVBody(String kvBodyStr) {
        JSONObject json = new JSONObject();
        String[] pairs = kvBodyStr.split("&");
        for (String pair : pairs) {
            int idx = pair.indexOf("=");
            String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
            String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
            json.put(key, value);
        }
        return json;
    }

    private JSONObject parseFormBody(Matcher formMatcher) {
        JSONObject formData = new JSONObject();

        // 重置指针匹配的位置
        formMatcher.reset();
        while (formMatcher.find()) {
            // 提取表单项
            String formItem = formMatcher.group(1) != null ? formMatcher.group(1) : formMatcher.group(2);

            // 分割键和值
            String[] keyValue = formItem.split("=", 2);
            if (keyValue.length == 2) {
                String key = keyValue[0];
                String value = keyValue[1];

                // 检测文件字段标记
                // PS: 理论上文件标记字段不需要支持
                if (value.startsWith("@")) {
                    // 只提取文件名,不读取文件内容
                    formData.put(key, value.substring(1));
                } else {
                    // 放入表单数据
                    formData.put(key, value);
                }
            }
        }

        return formData;
    }

    private JSONObject parseUrlEncodeBody(Matcher urlencodeMatcher) {
        JSONObject urlEncodeData = new JSONObject();

        // 重置指针匹配的位置
        urlencodeMatcher.reset();
        while (urlencodeMatcher.find()) {
            // 提取键值对字符串
            String keyValueEncoded = urlencodeMatcher.group(1);

            // 分隔键和值
            String[] keyValue = keyValueEncoded.split("=", 2);
            if (keyValue.length == 2) {
                String key = keyValue[0];
                String value = keyValue[1];

                // 对值进行URL解码
                String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);

                // 存入数据到JSON对象
                urlEncodeData.put(key, decodedValue);
            }
        }

        return urlEncodeData;
    }

    private JSONObject parseRowBody(Matcher rowMatcher) {
        String rawData = rowMatcher.group(1);

        if (isXML(rawData)) {
            // throw new IllegalArgumentException("Curl --data-raw content cant' be XML");
            return xml2json(rawData);
        }

        try {
            return JSON.parseObject(rawData);
        } catch (Exception e) {
            throw new IllegalArgumentException("Curl --data-raw content is not a valid JSON");
        }
    }

    public static boolean isXML(String xmlStr) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            InputSource is = new InputSource(new StringReader(xmlStr));
            builder.parse(is);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private JSONObject xml2json(String xmlStr) {
        try {
            org.json.JSONObject orgJsonObj = XML.toJSONObject(xmlStr);
            String jsonString = orgJsonObj.toString();
            return JSON.parseObject(jsonString);
        } catch (JSONException e) {
            throw new LinkConsoleException("Curl --data-raw content xml2json error", e);
        }
    }

    @Override
    public void log(Object... logParams) {
        LogPrinter.info("HttpBodyHandler execute: body={}", logParams);
    }
}

CurlPatternConstants

正则匹配常量定义:

public interface CurlPatternConstants {

    /**
     * CURL基本结构校验
     */
    Pattern CURL_BASIC_STRUCTURE_PATTERN = Pattern.compile("^curl (\\S+)");

    /**
     * URL路径匹配
     */
    Pattern URL_PATH_PATTERN =
            Pattern.compile("(?:\\s|^)(?:'|\")?(https?://[^?\\s'\"]*)(?:\\?[^\\s'\"]*)?(?:'|\")?(?:\\s|$)");

    /**
     * 请求参数列表匹配
     */
    Pattern URL_PARAMS_PATTERN = Pattern.compile("(?:\\s|^)(?:'|\")?(https?://[^\\s'\"]+)(?:'|\")?(?:\\s|$)");

    /**
     * HTTP请求方法匹配
     */
    Pattern HTTP_METHOD_PATTERN = Pattern.compile("(?:-X|--request)\\s+(\\S+)");

    /**
     * 默认HTTP请求方法匹配
     */
    Pattern DEFAULT_HTTP_METHOD_PATTERN = Pattern.compile(".*\\s(-d|--data|--data-binary)\\s.*");

    /**
     * 请求头匹配
     */
    Pattern CURL_HEADERS_PATTERN = Pattern.compile("(?:-H|--header)\\s+'(.*?:.*?)'");

    /**
     * -d/--data 请求体匹配
     */
    Pattern DEFAULT_HTTP_BODY_PATTERN = Pattern.compile("(?:--data|-d)\\s+(?:'([^']*)'|\"([^\"]*)\"|(\\S+))");
    Pattern DEFAULT_HTTP_BODY_PATTERN_KV = Pattern.compile("^([^=&]+=[^=&]+)(?:&[^=&]+=[^=&]+)*$");

    /**
     * --data-raw 请求体匹配
     */
    Pattern HTTP_ROW_BODY_PATTERN = Pattern.compile("--data-raw '(.+?)'(?s)", Pattern.DOTALL);

    /**
     * --form 请求体匹配
     */
    Pattern HTTP_FROM_BODY_PATTERN = Pattern.compile("--form\\s+'(.*?)'|-F\\s+'(.*?)'");


    /**
     * --data-urlencode 请求体匹配
     */
    Pattern HTTP_URLENCODE_BODY_PATTERN = Pattern.compile("--data-urlencode\\s+'(.*?)'");

}

有问题可以留言讨论,刷流量评论定期删除!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

玩转OurBMC第八期:OpenBMC webui之通信交互

栏目介绍&#xff1a;“玩转OurBMC”是OurBMC社区开创的知识分享类栏目&#xff0c;主要聚焦于社区和BMC全栈技术相关基础知识的分享&#xff0c;全方位涵盖了从理论原理到实践操作的知识传递。OurBMC社区将通过“玩转OurBMC”栏目&#xff0c;帮助开发者们深入了解到社区文化、…

CentOS 7x 使用Docker 安装oracle11g完整方法

1.安装docker-ce 安装依赖的软件包 yum install -y yum-utils device-mapper-persistent-data lvm2添加Docker的阿里云yum源 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo更新软件包索引 yum makecache fast查看docker…

【日记】被客户一顿输出该怎么办(431 字)

正文 上午有个客户在电话里对着我一顿输出&#xff0c;说他们没有发票财务账务没法处理怎么怎么的。话里话外满满一股 “全是你们的错” 的味道。 当时我很想笑&#xff0c;大姐&#xff0c;你对我输出有啥用啊。票是上级行开的&#xff0c;我们又没有开票权限&#xff0c;对我…

给电脑bios主板设置密码

增强安全性&#xff1a;防止未经授权的人员更改 BIOS 中的重要设置&#xff0c;如启动顺序、硬件配置等。这有助于保护计算机系统的稳定性和数据的安全性。防止恶意篡改&#xff1a;阻止可能的攻击者或恶意软件通过修改 BIOS 设置来破坏系统或获取敏感信息。数据保护&#xff1…

Redis 主从同步

主从同步 很多企业没有使用Redis的集群&#xff0c;但是至少都做了主从。有了主从&#xff0c;当master挂掉的时候&#xff0c;运维让从库过来接管&#xff0c;服务就可以继续&#xff0c;否则master需要经过数据恢复和重启的过程&#xff0c;可能会拖很长时间&#xff0c;影响…

男士内裤品牌哪个好?2024公认好穿的五款男士内裤分享

男士内裤作为大家每天都要长时间穿着的贴身衣物&#xff0c;它的重要性不言而喻。为了确保健康与卫生&#xff0c;专家和医生建议您每三个月更换一次内裤&#xff0c;避免细菌滋生&#xff0c;让身体更加清爽自在。而一款优质的内裤&#xff0c;不仅要有舒适的弹性&#xff0c;…

TikTok电商带货指南:策略、技巧与流量获取全解析

随着短视频平台的迅猛发展&#xff0c;TikTok已成为品牌和个人进行带货营销的主要阵地之一。通过有创意的内容、有效的互动方式和精准的流量获取策略&#xff0c;品牌和个人都能在TikTok上取得显著的带货效果。本文Nox聚星将和大家探讨在TikTok上进行带货营销的有效策略和技巧&…

图形化编程:解锁数字创意的新钥匙

在这个日新月异的数字时代&#xff0c;编程已不再局限于专业人士的小圈子&#xff0c;它正逐渐成为一项基础技能&#xff0c;融入我们的日常生活与工作中。而对于那些对传统代码望而生畏的人来说&#xff0c;6547网认为图形化编程犹如一股清流&#xff0c;以其直观、易学的特点…

芝麻文件重命名 一键批量重命名 支持批量修改图片 文档 文件夹名称

芝麻文件重命名是一款专业的文件批量重命名软件&#xff0c;它提供了丰富的功能和灵活的命名规则&#xff0c;可以大大提高文件管理的效率。以下是关于芝麻文件重命名的详细介绍&#xff1a; 一、软件特点 支持批量重命名&#xff1a;芝麻文件重命名支持文件和文件夹的批量重命…

学生课程信息管理系统

摘 要 目前&#xff0c;随着科学经济的不断发展&#xff0c;高校规模不断扩大&#xff0c;所招收的学生人数越来越 多&#xff1b;所开设的课程也越来越多。随之而来的是高校需要管理更多的事务。对于日益增 长的学生相关专业的课程也在不断增多&#xff0c;高校对其管理具有一…

【机器学习】机器学习重要方法——无监督学习:理论、算法与实践

文章目录 引言第一章 无监督学习的基本概念1.1 什么是无监督学习1.2 无监督学习的主要任务 第二章 无监督学习的核心算法2.1 聚类算法2.1.1 K均值聚类2.1.2 层次聚类2.1.3 DBSCAN聚类 2.2 降维算法2.2.1 主成分分析&#xff08;PCA&#xff09;2.2.2 t-SNE 2.3 异常检测算法2.3…

扩散模型在时间序列预测中的兴起

摘要 本文探讨了扩散模型在时间序列预测中的应用。扩散模型在生成式人工智能的各个领域展示了最先进的成果。本文包括扩散模型的全面背景资料&#xff0c;详细说明了它们的调节方法&#xff0c;并回顾了它们在时间序列预测中的应用。分析涵盖了11个具体的时间序列实现&#xf…

C++11(下):线程库

线程库 1.线程1.1线程类介绍以及简单使用1.2线程函数参数1.3如何获取线程函数返回值 2.锁2.1锁的种类2.2 lock_guard与unique_lock 3.原子库3.1介绍与基本使用3.2CAS&#xff08;原子操作原理&#xff09; 4.条件变量 1.线程 1.1线程类介绍以及简单使用 在C11之前&#xff0c…

【操作系统】操作系统课后作业-聊天程序

无名管道与有名管道的区别 无名管道&#xff1a; 它是半双工的&#xff0c;具有固定的读端和写端。 只能用于具有亲缘关系的进程之间的通信&#xff08;也是父子进程或者兄弟进程之间&#xff09;。 不是普通的文件&#xff0c;不属于其他任何文件系统&#xff0c;并且只存…

实战|YOLOv10 自定义目标检测

引言 YOLOv10[1] 概述和使用自定义数据训练模型 概述 由清华大学的研究团队基于 Ultralytics Python 包研发的 YOLOv10&#xff0c;通过优化模型结构并去除非极大值抑制&#xff08;NMS&#xff09;环节&#xff0c;提出了一种创新的实时目标检测技术。这些改进不仅实现了行业领…

有关排序的算法

目录 选择法排序 冒泡法排序 qsort排序&#xff08;快速排序&#xff09; qsort排序整型 qsort排序结构体类型 排序是我们日常生活中比较常见的问题&#xff0c;这里我们来说叨几个排序的算法。 比如有一个一维数组 arr[8] {2,5,3,1,7,6,4,8},我们想要把它排成升序&#…

【MAVEN学习 | 第1篇】Maven介绍与安装

文章目录 前言 一. Maven主要作用1.1 依赖管理1.2 项目构建 二. Maven安装和配置2.1 安装2.2 配置环境变量2.3 命令测试2.4 配置文件&#xff08;1&#xff09;依赖本地缓存位置&#xff08;本地仓库位置&#xff09;&#xff08;2&#xff09;配置国内阿里镜像&#xff08;3&a…

logback-spring.xml 小记

为什么不用logback.xml 名字 加载顺序:logback.xml>application.yml>logback-spring.xml 使用xml中使用到配置文件属性时,就会报错 为什么logback中记录不到运行时报错 logback获取不到堆栈错误 解决办法:在全局错误出使用log.error()指定输出 为什么打印不出来myba…

【面试实战】# 并发编程之线程池配置实战

1.先了解线程池的几个参数含义 corePoolSize (核心线程池大小): 作用: 指定了线程池维护的核心线程数量&#xff0c;即使这些线程处于空闲状态&#xff0c;它们也不会被回收。用途: 核心线程用于处理长期的任务&#xff0c;保持最低的线程数量&#xff0c;以减少线程的创建和…

【html】爱心跳动动画:CSS魔法背后的故事

效果展示&#xff1a; 代码介绍&#xff1a; 爱心跳动动画&#xff1a;CSS魔法背后的故事 在前端开发中&#xff0c;CSS不仅仅是一种用于控制网页样式的工具&#xff0c;它也是一种表达创意和想象力的艺术手段。今天&#xff0c;我要为大家介绍一段使用CSS实现的爱心跳动动画…