qt学习:arm摄像头+c调用v412框架驱动+qt调用v412框架驱动 显示摄像头画面

目录

跟内核进行数据通信的函数

编程步骤

c代码

头文件

打开摄像头文件 /dev/videox

获取当前主机上(开发板)摄像头列表信息

设置当前摄像头的画面格式  比如说 设置 采集图像的宽度为640  高度 480

在内核空间中,申请一个缓冲区队列(队列中有4块缓冲区)

将申请好的缓冲区队列 映射到 用户空间中

开启摄像头 

采集数据 yuyv

将采集出来的yuyv格式的数据---转换成 rgb

将c代码移植到qt代码中,在qt的界面显示摄像头画面

MCamera摄像头类添加头文件,成员,函数接口

实现MCamera摄像头类函数接口

在主ui界面类加入头文件,成员,函数接口

在构造函数中初始化摄像头类和定时器类,并关联定时器的槽函数

开启摄像头按钮点击事件

关闭摄像头按钮点击事件

拍照按钮点击事件

定时器槽函数实现


qt由于在arm qt版本下,没有多媒体库,所以,arm qt程序要访问摄像头,那么必须要使用linux操作系统v412(video for linux 2)机制

简单来说就是要在arm上安装v412框架就可以使用摄像头

跟内核进行数据通信的函数

#include <stropts.h>

int ioctl(int fildes,int request,.../* arg */);

  • 第一个参数:fd文件描述符
  • 第二个参数:要发送的命令
    • VIDIOC_REQBUFS:分配内存
    • VIDIOC_QUERYBUF:把 VIDIOC_REQBUFS 中分配的数据缓存转换成物理地址
    • VIDIOC_QUERYCAP:查询驱动功能
    • VIDIOC_ENUM_FMT:获取当前驱动支持的视频格式
    • VIDIOC_S_FMT:设置当前驱动的频捕获格式
    • VIDIOC_G_FMT:读取当前驱动的频捕获格式
    • VIDIOC_TRY_FMT:验证当前驱动的显示格式
    • VIDIOC_CROPCAP:查询驱动的修剪能力
    • VIDIOC_S_CROP:设置视频信号的边框
    • VIDIOC_G_CROP:读取视频信号的边框
    • VIDIOC _QBUF:把数据放回缓存队列
    • VIDIOC_DQBUF:把数据从缓存中读取出来
    • VIDIOC_$TREAMON:开始视频显示函数
    • VIDIOC_STREAMOFF:结束视频显示函数
    • VIDIOC_QUERYSTD:检查当前视频设备支持的标准,例如 PAL或NTSC。
  • ....:变参接口,根据第二个参数来决定

编程步骤

  • 打开摄像头文件 /dev/videox
  • 获取当前主机上的摄像头列表信息
    • #define VIDIOC_ENUM_FMT _IOWR('V',2,struct v412_fmtdesc)
      • struct v412_fmtdesc fmt;
      • ioctl(fd,VIDIOC_ENUM_FMT,&fmt);
  • 设置当前视频捕捉格式
    • 也就是设置摄像头采集画面的宽度、高度、格式
    • VIDIOC_S_FMT:设置当前驱动的视频捕捉格式
    • #define VIDIOC_S_FMT _IOWR('V',5,struct v412_format)
      • struct v412_format format;
      • format.宽度 = 640;
      • format.高度 = 400;
      • ioctl(fd,VIDIOC_S_FMT,&format);
  • 申请内核缓冲区队列VIDIOC_REQBUFS
    • 一般会将摄像头获取到的数据放到4块内存空间---一次性存储4帧画面数据
  • 将申请的内核缓冲区队列映射到用户空间VIDIOC_QUERYBUF
  • 开启摄像头VIDIOC_STREAMON  
    • 将开启摄像头的命令从应用层发送到内核层中,内核层就会驱动这个摄像头开始工作
  • while(1)
    • 采集数据  --yuyv格式
      • 注意;摄像头采集出来的图像数据格式组成是yuyv
    • 将采集出来的一帧画面yuyv格式的数据转为rgb格式
    • 将转换好rgb格式数据显示到ui控件上

c代码

头文件

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/videodev2.h> //v4l2视频开发框架
#include <sys/mman.h>

打开摄像头文件 /dev/videox

    //打开摄像头文件返回文件描述符
    int fd = open("/dev/video7",O_RDWR);
    if(fd == -1)
    {
        perror("open camera error");
        return -1;
    }

获取当前主机上(开发板)摄像头列表信息

    //2、获取当前主机上(开发板)摄像头列表信息
    //#define VIDIOC_ENUM_FMT         _IOWR('V',  2, struct v4l2_fmtdesc)
    struct v4l2_fmtdesc v4fmt; //定义一个结构体
    v4fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //视频捕捉类型
    int i = 0;
    while(1)
    {
        v4fmt.index = i++;
        int ret = ioctl(fd,VIDIOC_ENUM_FMT,&v4fmt);
        if(ret < 0)
        {
            //perror("获取失败");
            break;
        }
        printf("index = %d\n",v4fmt.index);
        printf("flags = %d\n",v4fmt.flags);
        printf("description = %s\n",v4fmt.description);
        unsigned char*p =  (unsigned char*)&v4fmt.pixelformat;
        printf("pixelformat = %c%c%c%c\n",p[0],p[1],p[2],p[3]);
        printf("reserved = %d\n",v4fmt.reserved[0]);
    }

设置当前摄像头的画面格式  比如说 设置 采集图像的宽度为640  高度 480

    //3、设置当前摄像头的画面格式  比如说 设置 采集图像的宽度为640  高度 480
    //#define VIDIOC_S_FMT		_IOWR('V',  5, struct v4l2_format)
    //发送VIDIOC_S_FMT命令需要定义struct v4l2_format结构体,
    //该结构体存放摄像头的画面格式,将摄像头设置为视频捕捉模式需要用到struct v4l2_format结构体
    struct v4l2_format vfmt;
    //设置为视频捕捉模式
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vfmt.fmt.pix.width = 640;// 设置宽(因为底层驱动已固定大小,不能任意改)
    vfmt.fmt.pix.height = 480;//设置高度
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //设置视频采集格式
    //将设置好的格式发送到摄像头
    ioctl(fd,VIDIOC_S_FMT,&vfmt);
    //设置完通过VIDIOC_G_FMT这个命令来查看格式有没有设置成功
    //#define VIDIOC_G_FMT        _IOWR('V',  4, struct v4l2_format) //获取格式
    memset(&vfmt,0,sizeof(vfmt)); //清空结构体
    //设置为视频捕捉模式
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    //获取的格式信息存放在该结构体中
    int ret = ioctl(fd,VIDIOC_G_FMT,&vfmt);
    if(ret < 0)
    {
        perror("获取格式失败");
        return -1;
    }
    //获取成功判断它的宽和高和模式
    if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV )
    {
        printf("设置成功\n");
    }else{
        printf("设置失败\n");
    }

在内核空间中,申请一个缓冲区队列(队列中有4块缓冲区)

    //4、在内核空间中,申请一个缓冲区队列(队列中有4块缓冲区)
    //发送VIDIOC_REQBUFS命令需要struct v4l2_requestbuffers结构体
    struct v4l2_requestbuffers reqbuffers;
    reqbuffers.count = 4; //申请4个缓冲区
    reqbuffers.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 摄像头采集模式
    reqbuffers.memory = V4L2_MEMORY_MMAP; // mmap内存映射
    //向内核空间打一个报告,向你申请4个缓冲区队列,申请的方式为内存映射
    ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffers); //获取的信息存放在该结构体
    if(ret < 0)
    {
        perror("申请队列空间失败");
        return -1;
    }

将申请好的缓冲区队列 映射到 用户空间中

    //5、将申请好的缓冲区队列映射到用户空间中
    //#define VIDIOC_QUERYBUF _IOWR('v',9,struct v4l2_buffer)
    //这个数组有4个元素,每个元素存储的是地址,存储的地址就是映射成功之后的地址
    unsigned char* mptr[4];
    unsigned int size[4];
    //发送VIDIOC_QUERYBUF命令需要struct v4l2_buffer结构体
    struct v4l2_buffer mapbuffer;
    mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 摄像头采集模式

    for(int i=0;i<4;i++){
        mapbuffer.index = i; //申请的缓冲区编号
        //发送VIDIOC_QUERYBUF命令来查询内核空间队列
        ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer);    
        if(ret < 0) {
            perror("查询内核空间队列失败\n");
            return -1;
        }    
        //mmap真正来实现内存映射 ,该函数的返回值是一个内存的首地址
        mptr[i] = (unsigned char*)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);    
        size[i] = mapbuffer.length;
        
        //通知使用完毕,----放回内核
        //#define VIDIOC_QBUF        _IOWR('V', 15, struct v4l2_buffer)
        ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer);
        if(ret < 0){
            perror("放回失败\n");
            return -1;
        }
    }

开启摄像头 

    //6、开启摄像头 
    //#define VIDIOC_STREAMON		 _IOW('V', 18, int)
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 摄像头采集模式
    //发送VIDIOC_QUERYBUF命令来开启摄像头
    ioctl(fd,VIDIOC_STREAMON,&type);

采集数据 yuyv

        unsigned char buffer[640*480*3] = {0};//采集后的数据
        int size;
        //发送命令采集数据函数
        get_frame(buffer,&size);

//获取图片
void get_frame(unsigned char*buffer,int *size)
{ 
    //1、先查询 当前帧数据 到底 在哪个 缓冲区中
    //采集数据 --VIDIOC_DQBUF:把数据从缓存中读取出来
    //#define VIDIOC_DQBUF        _IOWR('V', 17, struct v4l2_buffer)
    //发送VIDIOC_DQBUF需要struct v4l2_buffer结构体
    struct v4l2_buffer readbuffer;
    readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//采集模式
    //发送VIDIOC_DQBUF采集数据放到结构体里
    int ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer);
    if(ret < 0)
    {
        perror("提取数据失败");
        return -1 ;
    }
    //使用内存拷贝 -- void *memcpy(void *dest, const void *src, size_t n);
    memcpy(buffer,mptr[readbuffer.index],readbuffer.length);
    *size = readbuffer.length;

    //VIDIOC_QBUF:把数据放回缓存队列
    //通知内核已使用完毕
    //#define VIDIOC_QBUF        _IOWR('V', 15, struct v4l2_buffer)
    ret = ioctl(fd,VIDIOC_QBUF,&readbuffer);
    if(ret < 0)
    {
        perror("放回队列失败");
        return -1 ;
    }
}

将采集出来的yuyv格式的数据---转换成 rgb

        unsigned char rgb[640*480*3] = {0};//转换后的数据
        //8、将采集出来的yuyv格式的数据---转换成 rgb
        yuyv2rgb0(buffer, rgb, 640, 480);
//将yuyv格式转为rgb格式
int yuyv2rgb0(unsigned char *yuv, unsigned char *rgb, unsigned int width, unsigned int height)
{
     unsigned int in, out;
     int y0, u, y1, v;
     unsigned int pixel24;
     unsigned char *pixel = (unsigned char *)&pixel24;
     unsigned int size = width*height*2;

     for(in = 0, out = 0; in < size; in += 4, out += 6)
     {
          y0 = yuv[in+0];
          u  = yuv[in+1];
          y1 = yuv[in+2];
          v  = yuv[in+3];

          sign3 = 1;
          pixel24 = yuyv2rgb(y0, u, v);
          rgb[out+0] = pixel[0];
          rgb[out+1] = pixel[1];
          rgb[out+2] = pixel[2];

          pixel24 = yuyv2rgb(y1, u, v);
          rgb[out+3] = pixel[0];
          rgb[out+4] = pixel[1];
          rgb[out+5] = pixel[2];

     }
     return 0;
}

将c代码移植到qt代码中,在qt的界面显示摄像头画面

新建一个ui界面,布局一个两个按钮用于开启,停止,一个QLabel显示画面

通过定时器来调整帧数

新建一个MCamera摄像头类,将c代码移植进去

MCamera摄像头类添加头文件,成员,函数接口

extern "C"{
    #include<stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/ioctl.h>
    #include <string.h>
    #include <linux/videodev2.h> //v4l2视频开发框架
    #include <sys/mman.h>
    #include <unistd.h>
}
class MCamera
{
public:
    MCamera(const char*deviceName = "/dev/video7");

    void start();//启动摄像头
    void stop();//关闭摄像头
    void get_frame(unsigned char*buffer,int *size);//获取图像数据
    int yuyv2rgb0(unsigned char *buffer, unsigned char *rgbdata, int w, int h);//yuyv转为rgb
private:
    int fd;//摄像头文件描述符
    unsigned char* mptr[4];//这个数组有4个元素,每个元素存储的是地址,存储的地址就是映射成功之后的地址
    unsigned int size[4];//图像的大小
    bool captrueFlag;//拍照标志位
};

实现MCamera摄像头类函数接口

MCamera::MCamera(const char*deviceName )
{
    //1、打开摄像头文件
    //打开摄像头文件返回文件描述符
     fd = open(deviceName,O_RDWR);
     if(fd == -1)
     {
         perror("open camera error");
         return ;
     }
     //2、获取当前主机上(开发板)摄像头列表信息
     //#define VIDIOC_ENUM_FMT         _IOWR('V',  2, struct v4l2_fmtdesc)
     struct v4l2_fmtdesc v4fmt; //定义一个结构体
     v4fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //视频捕捉类型
     int i = 0;
     while(1)
     {
         v4fmt.index = i++;
         int ret = ioctl(fd,VIDIOC_ENUM_FMT,&v4fmt);
         if(ret < 0)
         {
             //perror("获取失败");
             break;
         }
         printf("index = %d\n",v4fmt.index);
         printf("flags = %d\n",v4fmt.flags);
         printf("description = %s\n",v4fmt.description);
         unsigned char*p =  (unsigned char*)&v4fmt.pixelformat;
         printf("pixelformat = %c%c%c%c\n",p[0],p[1],p[2],p[3]);
         printf("reserved = %d\n",v4fmt.reserved[0]);
     }

     //3、设置当前摄像头的画面格式  比如说 设置 采集图像的宽度为640  高度 480
     //#define VIDIOC_S_FMT		_IOWR('V',  5, struct v4l2_format)
     struct v4l2_format vfmt;
     vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     vfmt.fmt.pix.width = 640;// 设置宽(因为底层驱动已固定大小,不能任意改)
     vfmt.fmt.pix.height = 480;//设置高度
     vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; //设置视频采集格式

     ioctl(fd,VIDIOC_S_FMT,&vfmt);

     //#define VIDIOC_G_FMT        _IOWR('V',  4, struct v4l2_format) //获取格式
     memset(&vfmt,0,sizeof(vfmt)); //清 零结构体

     vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
     int ret = ioctl(fd,VIDIOC_G_FMT,&vfmt); //获取的    信息存 放在该结构体
     if(ret < 0)
     {
         perror("获取格式失败");
         return ;
     }
     if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_YUYV )
     {
         printf("设置成功\n");
     }else{
         printf("设置失败\n");
     }
     //4、在内核空间中,申请一个缓冲区队列(队列中有4块缓冲区)
     struct v4l2_requestbuffers reqbuffers;
     reqbuffers.count = 4; //申请4个缓冲区
     reqbuffers.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;// 摄像头采集
     reqbuffers.memory = V4L2_MEMORY_MMAP; // mmap
     //向内核空间打一个报告,向你申请4个缓冲区队列,申请的方式为内存映射
     ret = ioctl(fd,VIDIOC_REQBUFS,&reqbuffers); //获取的    信息存 放在该结构体
     if(ret < 0)
     {
         perror("申请队列空间失败");
         return ;
     }
     //5、将申请好的缓冲区队列 映射到 用户空间中


     struct v4l2_buffer mapbuffer;
     mapbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

     for(int i=0;i<4;i++){
         mapbuffer.index = i; //申请的缓冲区编号
         ret = ioctl(fd,VIDIOC_QUERYBUF,&mapbuffer);
         if(ret < 0) {
             perror("查询内核空间队列失败\n");
             return ;
         }
         //真正来实现内存映射 ,该函数的返回值是一个内存的首地址
         mptr[i] = (unsigned char*)mmap(NULL,mapbuffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,mapbuffer.m.offset);
         size[i] = mapbuffer.length;

         //通知使用完毕,----放回内核
         //#define VIDIOC_QBUF        _IOWR('V', 15, struct v4l2_buffer)
         ret = ioctl(fd,VIDIOC_QBUF,&mapbuffer);
         if(ret < 0){
             perror("放回失败\n");
             return ;
         }
     }
}

void MCamera::start()
{
    //6、开启摄像头
    //#define VIDIOC_STREAMON		 _IOW('V', 18, int)
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd,VIDIOC_STREAMON,&type);
}

void MCamera::stop()
{
    //[8] 停止采集
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
   int ret = ioctl(fd,VIDIOC_STREAMOFF,&type);
   //释放内存 int munmap(void *addr, size_t length);
   for(int i=0;i<4;i++)
   {
       munmap(mptr[i],size[i]);
   }
   //[9] 关闭设备
   ::close(fd);
}

void MCamera::get_frame(unsigned char *buffer, int *size)
{
    //采集数据 --VIDIOC_DQBUF:把数据从缓存中读取出来
    //#define VIDIOC_DQBUF        _IOWR('V', 17, struct v4l2_buffer)
    //1、先查询 当前帧数据 到底 在哪个 缓冲区中
    struct v4l2_buffer readbuffer;
    readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    int ret = ioctl(fd,VIDIOC_DQBUF,&readbuffer);
    if(ret < 0)
    {
        perror("提取数据失败");
        return  ;
    }
    //使用内存拷贝 -- void *memcpy(void *dest, const void *src, size_t n);
    memcpy(buffer,mptr[readbuffer.index],readbuffer.length);
    *size = readbuffer.length;

    //VIDIOC_QBUF:把数据放回缓存队列
    //通知内核已使用完毕
    //#define VIDIOC_QBUF        _IOWR('V', 15, struct v4l2_buffer)
    ret = ioctl(fd,VIDIOC_QBUF,&readbuffer);
    if(ret < 0)
    {
        perror("放回队列失败");
        return  ;
    }
}

int MCamera::yuyv2rgb0(unsigned char *buffer, unsigned char *rgbdata, int w, int h)
{
    int r1, g1, b1;
    int r2, g2, b2;

    for(int i=0; i<w*h/2; i++)
    {
        char data[4];
        memcpy(data, buffer+i*4, 4);
        //Y0U0Y1V1  -->[Y0 U0 V1] [Y1 U0 V1]
        unsigned char Y0=data[0];
        unsigned char U0=data[1];
        unsigned char Y1=data[2];
        unsigned char V1=data[3];

        r1 = Y0+1.4075*(V1-128); if(r1>255)r1=255; if(r1<0)r1=0;
        g1 =Y0- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g1>255)g1=255; if(g1<0)g1=0;
        b1 = Y0 + 1.779 * (U0-128);  if(b1>255)b1=255; if(b1<0)b1=0;
        r2 = Y1+1.4075*(V1-128);if(r2>255)r2=255; if(r2<0)r2=0;
        g2 = Y1- 0.3455 * (U0-128) - 0.7169*(V1-128); if(g2>255)g2=255; if(g2<0)g2=0;
        b2 = Y1 + 1.779 * (U0-128);  if(b2>255)b2=255; if(b2<0)b2=0;

        rgbdata[i*6+0]=r1;
        rgbdata[i*6+1]=g1;
        rgbdata[i*6+2]=b1;
        rgbdata[i*6+3]=r2;
        rgbdata[i*6+4]=g2;
        rgbdata[i*6+5]=b2;
    }
}

在主ui界面类加入头文件,成员,函数接口

#include <QTimer>
#include "mcamera.h"

private slots:
    void onUpdateCameraUi();

private:
    MCamera *m_camera;//自定义摄像头类
    QTimer *m_timer;//定时器类

在构造函数中初始化摄像头类和定时器类,并关联定时器的槽函数

    m_camera = NULL;

    m_timer = new QTimer;
    connect(m_timer,&QTimer::timeout,this,&Widget::onUpdateCameraUi);

开启摄像头按钮点击事件

    if(m_camera == NULL)
    {
        m_camera = new MCamera;
        m_camera->start();
        //启动定时器,间隔时间采集图像数据
        m_timer->start(1); //1秒钟采集 一张画面(一帧画面)
    }

关闭摄像头按钮点击事件

    if(m_camera != NULL)
    {
       m_timer->stop();
       m_camera->stop();

       delete  m_camera;

       m_camera = NULL;
    }

拍照按钮点击事件

captrueFlag=true;

定时器槽函数实现

    //  采集数据
    unsigned char buffer[640*480*3] = {0};
    unsigned char rgb[640*480*3] = {0};
    int size;
    m_camera->get_frame(buffer,&size);

    //yuyv转换rgb
    m_camera->yuyv2rgb0(buffer,rgb,640,480);

    //显示   源图像rgb --->pic对象
    QImage img = QImage(rgb,640,480,QImage::Format_RGB888);
    //保存为图片
    if(captrueFlag)
    {
        img.save("1.bmp");
        captrueFlag = false;
    }
    QPixmap pic = QPixmap::fromImage(img);
    ui->label->setPixmap(pic);

在linux中交叉编译放到开发板上运行

相关推荐

  1. qt学习:多媒体Multimedia摄像头

    2024-02-07 20:18:02       35 阅读
  2. ubuntu opencv c++调用摄像头 unsuccessiful

    2024-02-07 20:18:02       34 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-02-07 20:18:02       14 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-07 20:18:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-07 20:18:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-07 20:18:02       18 阅读

热门阅读

  1. IDEA2023SpingBoot只能勾选17和21

    2024-02-07 20:18:02       35 阅读
  2. Python 字符串追加

    2024-02-07 20:18:02       29 阅读
  3. Elasticsearch单个索引数据量过大的优化

    2024-02-07 20:18:02       38 阅读
  4. C Primer Plus(第六版)15.9 编程练习 第5题

    2024-02-07 20:18:02       30 阅读
  5. [Usaco2008 Feb]Line连线游戏

    2024-02-07 20:18:02       32 阅读
  6. MySQL基础查询篇(10)-正则表达式的基础使用

    2024-02-07 20:18:02       28 阅读