深度学习系列71:表格检测和识别

news2024/11/16 6:50:56

1. pdf处理

如果是可编辑的pdf格式,那么可以直接用pdfplumber进行处理:

import pdfplumber
import pandas as pd

with pdfplumber.open("中新科技:2015年年度报告摘要.PDF") as pdf:
    page = pdf.pages[1]   # 第一页的信息
    text = page.extract_text()
    print(text)
    table = page.extract_tables()
    for t in table:
        # 得到的table是嵌套list类型,转化成DataFrame更加方便查看和分析
        df = pd.DataFrame(t[1:], columns=t[0])
        print(df)

如果是图片格式的pdf,可以使用pdf2image库将pdf转为图片后再继续后面的流程:

from pdf2image import convert_from_path
img = np.array(convert_from_path(path, dpi=800, use_cropbox=True)[0])

2. 表格位置检测

2.1 使用ppstructure

使用paddleocr库中的ppstructure可以方便获取表格位置,参考代码:

from paddleocr import PPStructure
structure = table_engine(source_img)

2.2 使用tabledetector

import tabledetector as td
result = td.detect(pdf_path="pdf_path", type="bordered", rotation=False, method='detect')

2.3 使用cv2的图形学方法

调试简单,具体代码如下:

  1. 二值化去除水印
    在这里插入图片描述
  2. 使用getStructuringElement获取纵线和横线
    在这里插入图片描述
  3. 两者合并,使用findContours获取表格外边框和内部单元格 在这里插入图片描述

3. 位置确认

获取所有单元格后,使用下面的函数获取单元格的相对位置关系:

from typing import Dict, List, Tuple
import numpy as np

class TableRecover:
    def __init__(
        self,
    ):
        pass

    def __call__(self, polygons: np.ndarray) -> Dict[int, Dict]:
        rows = self.get_rows(polygons)
        longest_col, each_col_widths, col_nums = self.get_benchmark_cols(rows, polygons)
        each_row_heights, row_nums = self.get_benchmark_rows(rows, polygons)
        table_res = self.get_merge_cells(
            polygons,
            rows,
            row_nums,
            col_nums,
            longest_col,
            each_col_widths,
            each_row_heights,
        )
        return table_res

    @staticmethod
    def get_rows(polygons: np.array) -> Dict[int, List[int]]:
        """对每个框进行行分类,框定哪个是一行的"""
        y_axis = polygons[:, 0, 1]
        if y_axis.size == 1:
            return {0: [0]}

        concat_y = np.array(list(zip(y_axis, y_axis[1:])))
        minus_res = concat_y[:, 1] - concat_y[:, 0]

        result = {}
        thresh = 5.0
        split_idxs = np.argwhere(minus_res > thresh).squeeze()
        if split_idxs.ndim == 0:
            split_idxs = split_idxs[None, ...]

        if max(split_idxs) != len(minus_res):
            split_idxs = np.append(split_idxs, len(minus_res))

        start_idx = 0
        for row_num, idx in enumerate(split_idxs):
            if row_num != 0:
                start_idx = split_idxs[row_num - 1] + 1
            result.setdefault(row_num, []).extend(range(start_idx, idx + 1))

        # 计算每一行相邻cell的iou,如果大于0.2,则合并为同一个cell
        return result

    def get_benchmark_cols(
        self, rows: Dict[int, List], polygons: np.ndarray
    ) -> Tuple[np.ndarray, List[float], int]:
        longest_col = max(rows.values(), key=lambda x: len(x))
        longest_col_points = polygons[longest_col]
        longest_x = longest_col_points[:, 0, 0]

        theta = 10
        for row_value in rows.values():
            cur_row = polygons[row_value][:, 0, 0]

            range_res = {}
            for idx, cur_v in enumerate(cur_row):
                start_idx, end_idx = None, None
                for i, v in enumerate(longest_x):
                    if cur_v - theta <= v <= cur_v + theta:
                        break

                    if cur_v > v:
                        start_idx = i
                        continue

                    if cur_v < v:
                        end_idx = i
                        break

                range_res[idx] = [start_idx, end_idx]

            sorted_res = dict(
                sorted(range_res.items(), key=lambda x: x[0], reverse=True)
            )
            for k, v in sorted_res.items():
                if v[0]==None or v[1]==None:
                    continue

                longest_x = np.insert(longest_x, v[1], cur_row[k])
                longest_col_points = np.insert(
                    longest_col_points, v[1], polygons[row_value[k]], axis=0
                )

        # 求出最右侧所有cell的宽,其中最小的作为最后一列宽度
        rightmost_idxs = [v[-1] for v in rows.values()]
        rightmost_boxes = polygons[rightmost_idxs]
        min_width = min([self.compute_L2(v[3, :], v[0, :]) for v in rightmost_boxes])

        each_col_widths = (longest_x[1:] - longest_x[:-1]).tolist()
        each_col_widths.append(min_width)

        col_nums = longest_x.shape[0]
        return longest_col_points, each_col_widths, col_nums

    def get_benchmark_rows(
        self, rows: Dict[int, List], polygons: np.ndarray
    ) -> Tuple[np.ndarray, List[float], int]:
        leftmost_cell_idxs = [v[0] for v in rows.values()]
        benchmark_x = polygons[leftmost_cell_idxs][:, 0, 1]

        theta = 10
        # 遍历其他所有的框,按照y轴进行区间划分
        range_res = {}
        for cur_idx, cur_box in enumerate(polygons):
            if cur_idx in benchmark_x:
                continue

            cur_y = cur_box[0, 1]

            start_idx, end_idx = None, None
            for i, v in enumerate(benchmark_x):
                if cur_y - theta <= v <= cur_y + theta:
                    break

                if cur_y > v:
                    start_idx = i
                    continue

                if cur_y < v:
                    end_idx = i
                    break

            range_res[cur_idx] = [start_idx, end_idx]

        sorted_res = dict(sorted(range_res.items(), key=lambda x: x[0], reverse=True))
        for k, v in sorted_res.items():
            if v[0]==None or v[1]==None:
                continue

            benchmark_x = np.insert(benchmark_x, v[1], polygons[k][0, 1])

        each_row_widths = (benchmark_x[1:] - benchmark_x[:-1]).tolist()

        # 求出最后一行cell中,最大的高度作为最后一行的高度
        bottommost_idxs = list(rows.values())[-1]
        bottommost_boxes = polygons[bottommost_idxs]
        max_height = max([self.compute_L2(v[3, :], v[0, :]) for v in bottommost_boxes])
        each_row_widths.append(max_height)

        row_nums = benchmark_x.shape[0]
        return each_row_widths, row_nums

    @staticmethod
    def compute_L2(a1: np.ndarray, a2: np.ndarray) -> float:
        return np.linalg.norm(a2 - a1)

    def get_merge_cells(
        self,
        polygons: np.ndarray,
        rows: Dict,
        row_nums: int,
        col_nums: int,
        longest_col: np.ndarray,
        each_col_widths: List[float],
        each_row_heights: List[float],
    ) -> Dict[int, Dict[int, int]]:
        col_res_merge, row_res_merge = {}, {}
        merge_thresh = 20
        for cur_row, col_list in rows.items():
            one_col_result, one_row_result = {}, {}
            for one_col in col_list:
                box = polygons[one_col]
                box_width = self.compute_L2(box[3, :], box[0, :])

                # 不一定是从0开始的,应该综合已有值和x坐标位置来确定起始位置
                loc_col_idx = np.argmin(np.abs(longest_col[:, 0, 0] - box[0, 0]))
                merge_col_cell = max(sum(one_col_result.values()), loc_col_idx)

                # 计算合并多少个列方向单元格
                for i in range(merge_col_cell, col_nums):
                    col_cum_sum = sum(each_col_widths[merge_col_cell : i + 1])
                    if i == merge_col_cell and col_cum_sum > box_width:
                        one_col_result[one_col] = 1
                        break
                    elif abs(col_cum_sum - box_width) <= merge_thresh:
                        one_col_result[one_col] = i + 1 - merge_col_cell
                        break
                else:
                    one_col_result[one_col] = i + 1 - merge_col_cell + 1

                box_height = self.compute_L2(box[1, :], box[0, :])
                merge_row_cell = cur_row
                for j in range(merge_row_cell, row_nums):
                    row_cum_sum = sum(each_row_heights[merge_row_cell : j + 1])
                    # box_height 不确定是几行的高度,所以要逐个试验,找一个最近的几行的高
                    # 如果第一次row_cum_sum就比box_height大,那么意味着?丢失了一行
                    if j == merge_row_cell and row_cum_sum > box_height:
                        one_row_result[one_col] = 1
                        break

                    elif abs(box_height - row_cum_sum) <= merge_thresh:
                        one_row_result[one_col] = j + 1 - merge_row_cell
                        break
                else:
                    one_row_result[one_col] = j + 1 - merge_row_cell + 1

            col_res_merge[cur_row] = one_col_result
            row_res_merge[cur_row] = one_row_result

        res = {}
        for i, (c, r) in enumerate(zip(col_res_merge.values(), row_res_merge.values())):
            res[i] = {k: [cc, r[k]] for k, cc in c.items()}
        return res

调用代码如下:

h_min = 10
h_max = 5000

def sortContours(cnts, method='left-to-right'):
    reverse = False
    i = 0
    if method == "right-to-left" or method == "bottom-to-top":
        reverse = True
    if method == "top-to-bottom" or method == "bottom-to-top":
        i = 1
    boundingBoxes = [cv2.boundingRect(c) for c in cnts]
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i], reverse=reverse))
    return (cnts, boundingBoxes)

def sorted_boxes(dt_boxes):
    num_boxes = dt_boxes.shape[0]
    dt_boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0]))
    _boxes = list(dt_boxes)
    for i in range(num_boxes - 1):
        for j in range(i, -1, -1):
            if (
                abs(_boxes[j + 1][0][1] - _boxes[j][0][1]) < 10
                and _boxes[j + 1][0][0] < _boxes[j][0][0]
            ):
                _boxes[j], _boxes[j + 1] = _boxes[j + 1], _boxes[j]
            else:
                break
    return _boxes

def getBboxDtls(raw):
    ######### 1. 获得表格的边框,确保merge正确展示了图中的表格边框
    gray = cv2.cvtColor(raw, cv2.COLOR_BGR2GRAY)
    binary = 255-cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)[1]
    rows, cols = binary.shape
    scale = 30
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (cols // scale, 1))
    eroded = cv2.erode(binary, kernel, iterations=1)
    dilated_col = cv2.dilate(eroded, kernel, iterations=1)
    scale = 20
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, rows // scale))
    eroded = cv2.erode(binary, kernel, iterations=1)
    dilated_row = cv2.dilate(eroded, kernel, iterations=1)
    merge = cv2.add(dilated_col, dilated_row)
    kernel = np.ones((3,3),np.uint8)
    merge = cv2.erode(cv2.dilate(merge, kernel, iterations=3), kernel, iterations=3)
    plt.figure(figsize=(60,30))
    io.imshow(merge[1500:2500])
    
    ########## 2. 获取表格坐标
    tableData = []
    contours = cv2.findContours(merge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = contours[0] if len(contours) == 2 else contours[1]
    contours, boundingBoxes = sortContours(contours, method='top-to-bottom')
    # 获取表格外边框
    for c in contours:
        x, y, w, h = cv2.boundingRect(c)
        if (h>h_min):
            tableData.append((x, y, w, h))        
    # 获取表格内部的单元格
    contours, hierarchy = cv2.findContours(merge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    contours, boundingBoxes = sortContours(contours, method="top-to-bottom")
    boxes = []
    for c in contours:
        x, y, w, h = cv2.boundingRect(c)
        if (h>h_min) and (h<h_max):
            boxes.append([x, y, w, h])

    ########## 3. 计算表格单元格位置关系
    bboxDtls = {}
    for tableBox1 in tableData:
        key = tableBox1
        values = []
        for tableBox2 in boxes:
            x2, y2, w2, h2 = tableBox2
            if tableBox1[0] <= x2 <= tableBox1[0] + tableBox1[2] and tableBox1[1] <= y2 <= tableBox1[1] + tableBox1[3]:
                values.append(tableBox2)
        bboxDtls[key] = values
    for key, values in bboxDtls.items():
        x_tab, y_tab, w_tab, h_tab = key
        for box in values:
            x_box, y_box, w_box, h_box = box
    return bboxDtls

4. 表格文字识别

4.1 wired_table_rec

可以尝试使用wired_table_rec进行识别:

from wired_table_rec import WiredTableRecognition
table_rec = WiredTableRecognition()
table_str = table_rec(cv2.imread(img_path))[0]
HTML(table_str)

4.2 rapidocr_onnxruntime或者pytessect

或者可以使用更原子化的ocr服务,逐个单元格进行ocr识别,完整代码如下:

"""
首先安装pdf2image和rapidocr_onnxruntime两个库。
图像处理部分的参数和代码可以自行调整:
1. pad参数用于去除图片的边框
2. 转pdf时,有时候800dpi会失败,因此需要加入try except
3. 图片太小时ocr效果不好,因此做了resize。这里的3000可以自行调整。
4. 大图片做了二值化处理,目的是去除水印的干扰。这里的180也可以尝试自行调整。
5. 只处理第一个单元格总数大于50的表格。如果要识别图片中所有表格,可修改代码。
6. 返回的是html格式的表格,可以用pd.read_html函数转为dataframe
"""
rocr = RapidOCR()
rocr.text_det.preprocess_op = DetPreProcess(736, 'max')
def getResult(path,pad = 20, resize_thresh=3000, binary_thresh=180):
    if 'pdf' in path:
        try:
            source_img = np.array(convert_from_path(path, dpi=800, use_cropbox=True)[0])[pad:-pad,pad:-pad]
        except:
            source_img = np.array(convert_from_path(path, dpi=300, use_cropbox=True)[0])[pad:-pad,pad:-pad]
    else:
        source_img = cv2.imread(path)[pad:-pad,pad:-pad]
    if source_img.shape[1] < resize_thresh:
        source_img =cv2.resize(source_img,(resize_thresh,int(source_img.shape[0]/source_img.shape[1]*resize_thresh)))
    img = cv2.threshold(cv2.cvtColor(source_img, cv2.COLOR_BGR2GRAY), binary_thresh, 255, cv2.THRESH_BINARY)[1] 
    bboxDtls = getBboxDtls(source_img)
    boxes = []
    table = None
    # 寻找到第一个单元格数大于50的表后停止
    for k,v in bboxDtls.items():
        if len(v)>50:
            table = k
            for r in tqdm(v[1:]):
                res = rocr(img[r[1]: r[1]+r[3],r[0]:r[2]+r[0]])[0]
                if res!=None:
                    res.sort(key = lambda x:(x[0][0][1]//(img.shape[1]//20),x[0][0][0]//(img.shape[0]//20)))
                    boxes.append([[r[0], r[1]], [r[0], r[1]+r[3]],[r[0]+r[2], r[1]+r[3]], [r[0]+r[2], r[1]], ''.join([t[1].replace('\n','').replace(' ','') for t in res])])
                else:
                    boxes.append([[r[0], r[1]], [r[0], r[1]+r[3]],[r[0]+r[2], r[1]+r[3]], [r[0]+r[2], r[1]], ''])  
            break
    polygons = sorted_boxes(np.array(boxes))
    texts = [p[4] for p in polygons]
    tr = TableRecover()
    table_res = tr(np.array([[np.array(p[0]),np.array(p[1]),np.array(p[2]),np.array(p[3])] for p in polygons]))
    table_html = """<table border="1" cellspacing="0">"""
    for vs in table_res.values():
        table_html+="<tr>"
        for i,v in vs.items():
            table_html+=f"""<td colspan="{v[0]}" rowspan="{v[1]}">{texts[i]}</td>"""
        table_html+="</tr>"
    table_html+="""</table>"""
    return table_html

原图为:https://www.95598.cn/omg-static/99107281818076039603801539578309.jpg
最终识别出来的结果如下:
在这里插入图片描述

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

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

相关文章

深度学习项目实践——qq聊天机器人(transformer)(一)原理介绍

文章目录 首先第一步——QQ是如何实现实时聊天数据传输过程1. 用户发送消息的开始2. 数据封装与加密3. 建立连接&#xff1a;WebSocket协议的应用4. 消息的传输过程5. 接收者获取消息6. 双向通信与实时性保障7. 保持连接与断线重连 第二步——聊天机器人是如何来接管QQ账号的组…

什么牌子超声波清洗机好?家用超声波清洗机推荐

随着民众生活品质与幸福感的不断提升&#xff0c;诸如珠宝饰品、眼镜等精细物品成为了许多家庭中的常备之物。然而&#xff0c;这些小巧物件容易积累微尘并潜藏细菌&#xff0c;悄然威胁我们的健康安全。超声波清洗机应运而生&#xff0c;成为了解决这一隐患的理想方案&#xf…

快速了解Rust 的数据分析库Polars

【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 (jd.com) 17.1.1 什么是Polars Polars是一个基于 Rust 的数据分析库&#xff0c;它的目标是提供一个高性能的数据分析工具&#xff0c;同时也…

自定义审批字段

一. 新增特性 1.路径&#xff1a;SPRO->物料管理->采购->采购订单->采购订单的下达过程->编辑特性 2.输入特征名Z_USRC2_PO点新建 二. 将特性分配给类 1.路径&#xff1a;SPRO->物料管理->采购->采购订单->采购订单的下达过程->编辑类 2.输入…

Windows上MSYS2的安装和使用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、下载二、安装三、使用1.打开命令行2.搜索软件3.安装软件4.卸载软件5.更新环境6.其他四、MSYS2和Cygwin的差别总结前言 MSYS2这个工具我是越用越喜欢,很多东西放在Linux上如鱼得水但是放在…

ClkLog针对神策不支持全埋点的客户端实现用户访问基础统计分析

本文将介绍&#xff0c;ClkLog针对神策不支持全埋点的客户端实现用户访问基础统计分析 1。 客户遇到的问题 ClkLog的用户访问基础统计分析功能是基于神策SDK的全埋点来实现的。 我们遇到有些客户是使用C、C#等语言来开发的客户端&#xff0c;然而神策此类SDK&#xff08;如C, C…

什么是代理IP_如何建立代理IP池?

什么是代理IP_如何建立代理IP池&#xff1f; 1. 概述1.1 什么是代理IP&#xff1f;1.2 代理IP的工作原理1.3 爬虫的应用场景1.3.1 搜索引擎&#xff0c;最大的爬虫1.3.2 数据采集&#xff0c;市场分析利器1.3.3 舆情监控&#xff0c;品牌营销手段1.3.4 价格监测&#xff0c;全网…

jetsonNano烧录Ubuntu20.04镜像使用ROS2

本来想要参考Jetson nano升级Ubuntu20.04来进行升级。 但是此过程也有大坑&#xff0c;我的目的是&#xff0c;除了升级Ubuntu20.04&#xff0c;在上面使用ROS2&#xff0c;我还希望我写的代码可以使用上Pytorch。 方式一&#xff08;未成功&#xff09; 按照上面的教程可以正…

Spring Bean加载耗时采集工具

功能介绍 Target&#xff1a;针对启动慢的 Spring 应用&#xff0c;找出 IOC 容器启动过程中&#xff0c;加载耗时较长的 Bean 对象进行治理。 &#xfeff; 实现原理 主要用到Spring本身提供的两个扩展接口&#xff1a;BeanPostProcessor ApplicationListener 这两个接口…

202408830测试RK3588的rockit/VI的编译

202408830测试RK3588的rockit/VI的编译 2024/8/30 14:58 前言 环境介绍&#xff1a; 1.编译环境 Ubuntu 20.04.6 LTS rootrootrootroot-desktop:~$ rootrootrootroot-desktop:~$ cat /etc/issue Ubuntu 20.04.6 LTS \n \l rootrootrootroot-desktop:~$ 2.SDK版本&#xff1a…

8,sql查询条件查询语句

查询员工表结构&#xff0c;并分析 DESC 表名;DESC t_employee; 查询出生日期在 1990-01-01 和 1995-01-01 之间的员工信息。between 区间比较这句话的意思就是查看所有员工的生日在1990-01-01和1995-01-01之间的SELECT * FROM 表名 WHERE 生日 between 1990-01-01 AND 1995-0…

共绘国际智图:Elvy与图为科技携手探索边缘计算新境界

近日&#xff0c;巴西知名企业Elvy到访深圳图为科技&#xff0c;共议“合作开发边缘计算机及联合开拓海外市场”事宜。 在全球化日益加深的今天&#xff0c;技术的跨界合作正成为推动行业进步的重要力量。8月23日&#xff0c;一场旨在深化国际合作、共推边缘计算技术发展的会议…

云 VS 边缘计算,关系与区别是什么?

云计算和边缘计算的区别是什么&#xff1f; 云是一种 IT 环境&#xff0c;可以抽象、汇集和共享整个网络中的 IT 资源。边缘是网络边缘的计算位置&#xff0c;以及这些物理位置上的硬件和软件。是在云中运行工作负载&#xff0c;而边缘计算是在边缘设备上运行工作负载。 边缘…

马丁格尔交易策略Anzo Capital指出问题核心,那就是保证金

使用马丁格尔交易策略进行外汇交易时&#xff0c;Anzo Capital 强调了保证金管理的重要性。通过精准计算和策略规划&#xff0c;Anzo Capital 帮助交易者在波动的市场中保持资金安全&#xff0c;并最大化投资回报。 Anzo Capital 提醒交易者&#xff0c;了解波动回弹至关重要&…

渗透测试中最常见的安全漏洞有哪些

目录 常见的安全漏洞 拓展 渗透测试中如何检测SQL注入漏洞&#xff1f; 如何防范跨站脚本攻击(XSS)&#xff1f; 文件上传漏洞通常是如何被利用的&#xff1f; 思维导图 常见的安全漏洞 在渗透测试中&#xff0c;以下是一些最常见的安全漏洞&#xff1a; SQL注入&#x…

计算机毕设推荐-基于python的超市数据处理可视化分析

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于python的超市数据处理可…

AI编码公司Magic获得近5亿美元巨额投资

Magic&#xff0c;一家专注于生成式人工智能AI编码的初创公司&#xff0c;最近在AI领域取得了显著的成就。该公司通过创建模型来生成代码并自动执行软件开发任务&#xff0c;成功吸引了包括前谷歌CEO埃里克施密特在内的一系列知名投资者的关注&#xff0c;并完成了一轮3.2亿美元…

【MySQL 12】事务管理 (带思维导图)

文章目录 &#x1f308; 一、事务的基本概念⭐ 1. 事务是什么⭐ 2. 事务的特性 &#x1f308; 二、事务的版本支持&#x1f308; 三、事务的提交方式⭐ 1. 查看事务的提交方式⭐ 2. 设置事务的提交方式 &#x1f308; 四、事务的特性证明⭐ 1. 事务的常规操作⭐ 2. 证明事务的原…

mapbox-gl 常用Expressions表达式

文章目录 一、前言1.1 概念1.2 Mapbox gl提供的表达式计算器 二、所有支持的运算符2.1 颜色运算符2.1.1 rgb2.1.2 rgba2.1.3 hsl2.1.4 hsla2.1.5 to-rgba 2.2 Math 数学计算运算符2.2.1 , -, *, /, %, ^2.2.2 abs, ceil, floor, round2.2.3 sin, cos, tan, asin, acos, atan2.2…

Hbuilder创建的项目(uniApp + Vue3)中引入UnoCSS原子css引擎

这里是UnoCSS的官网介绍 UnoCS通过简化和优化CSS的编写过程来提高Web开发的效率和可维护性。好处是&#xff1a; 提升开发效率提升开发效率提高一致性增强灵活性易于维护方便的集成与配置 同时还支持预设变量和规则。这些可参看官网进行配置。Unocss通过其原子化方法、高度的…