项目实践 之 pdf简历的解析和填充(若依+vue3)

文章目录

  • 环境背景
  • 最终效果
  • 前端讲解
    • 左侧模块解析
    • 右侧上传模块解析
    • 前端步骤
  • 后端讲解
  • 代码
    • 前端

环境背景

  • 若依前后端分离框架 + vue
  • 最后边附有代码哦

最终效果

在这里插入图片描述

在这里插入图片描述

前端讲解

左侧模块解析

  • 1、左侧表单使用el-form
    在这里插入图片描述

    注意:
    1、prop出现的字段,需要保证是该类所具有的字段
    2、点击提交按钮后,调用的是handleSubmit方法

右侧上传模块解析

在这里插入图片描述

① v-if=“uploadedFileName” 如果对uploadedFileName不为空,该控件显示
② v-model=“upload.open” 在vue2中会写成 :visible.sync=“upload.open” ,在vue3中是不生效的,需要修改
③ 上传文件限制,只能上传1个
④ 前端限制,上传的文件只能是pdf

前端步骤

  • 1、在打开页面时,通过 created() 的 this.fetchResumeData()来获取数据
    在这里插入图片描述

  • 2、fetchResumeData通过await getResumeByUsername(username)来调用js的方法然后获得数据,然后通过this.myResume=response.data填充
    在这里插入图片描述

  • 3、当点击上传简历按钮时,会调用handleImport方法,然后更改upload的open属性为true,这样就显示了上传文件的对话框了
    在这里插入图片描述
    在这里插入图片描述

  • 4、文件上传完成后,会调用submitFileForm方法,开始上传,同时调用upload中的url进行文件解析
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 5、上传成功后,会调用handleFileSuccess方法,然后将内容填充
    在这里插入图片描述
    在这里插入图片描述

后端讲解

  • pom文件
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.26</version>
</dependency>
  • controller层
@RequestMapping("/parsepdf")
    public AjaxResult parsePdf(@RequestParam("file") MultipartFile file) {
        try {
            // 保存文件到本地
            String filePath = PdfUtil.save(file);
            // 获取 PDF 文件内容
            String content = PdfUtil.getContent(file.getInputStream());
            // 解析 PDF 内容并封装为简历信息
            Map<String, String> map = PdfUtil.setResume(content,file.getName(),filePath);

            // 返回解析后的数据
            return AjaxResult.success(map);
        } catch (Exception e) {
            System.err.println(e.getMessage());
            return AjaxResult.error("文件解析失败:" + e.getMessage());
        }
    }
  • pdf格式说明

    需按照如下的格式,因为正则匹配的解析是这么来的,可以结合后边的正则函数查看
    在这里插入图片描述

  • PdfUtil类

package com.ruoyi.utils;

import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.file.FileUploadUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PdfUtil {
    /**
     * 将上传的文档保存到本地
     * @param file
     * @return
     * @throws IOException
     */
    public static String save(MultipartFile file) throws IOException{
        String path = FileUploadUtils.upload(file);
        // 为什么是e?
        String realPath = path.substring(path.indexOf("e")+2);
        String baseDir = RuoYiConfig.getProfile();
        String filePath = baseDir + realPath;
        return filePath;
    }
    public static String getContent(InputStream inputStream) throws IOException {
        try (PDDocument document = PDDocument.load(inputStream)) {
            PDFTextStripper stripper = new PDFTextStripper();
            return stripper.getText(document);
        }
    }
    /**
     * 将内容按照字段存储进行匹配
     * @param content
     * @return
     */
    public static Map<String,String> setResume(String content,String fileName,String filePath){

        // map用来存储解析到的内容
        Map<String,String> map = new HashMap<>();
        map.put("file_name",fileName);
        map.put("file_path",filePath);
        String skillRegex ="专业技能\\s+(.*?)(?=工作经历|$)"; ;  // "专业技能\r?\n([\s\S]+)"
        String skill = regex(skillRegex,content);
        System.err.println("--------------专业技能-------------");
        System.err.println("skills:"+skill);
        map.put("skills",skill);

        String phoneRegex = "联系方式:(\\d+) 邮箱:(\\S+)";
        String phone = regex(phoneRegex,content);
        System.err.println("--------------联系电话-------------");
        System.err.println("phone"+phone);
        map.put("phone",phone);


        String titleRegex = "求职意向\\s+(.*?)(?=简介|$)";
        String title = regex(titleRegex,content);
        System.err.println("--------------求职意向-------------");
        System.err.println("title"+title);
        map.put("title",title);

        String summaryRegex =  "简介\\s+(.*?)(?=获奖及证书|$)";
        String summary = regex(summaryRegex,content);
        System.err.println("--------------简介即总结-------------");
        System.err.println("summary"+summary);
        map.put("summary",summary);

        String experienceRegex = "工作经历\\s+(.*?)(?=工作项目经历|$)";// "工作项目经历\\r?\\n([\\s\\S]+)"
        String experience = regex(experienceRegex,content);
        System.err.println("--------------工作项目经历-------------");
        System.err.println("experience"+experience);
        map.put("experience",experience);

        String projectRegex = "工作项目经历\\s+(.*)";// "工作项目经历\\r?\\n([\\s\\S]+)"
        String project = regex(projectRegex,content);
        System.err.println("--------------工作项目经历-------------");
        System.err.println("content"+project);
        map.put("content",project);


        String educationRegex = "教育经历\\s+(.*)"; // "< < < 个人信息\\s*(.*?)(?=< < < 教育背景)"
        String education = regex(educationRegex,content);
        System.err.println("--------------教育背景-------------");
        System.err.println("education"+education);
        map.put("education",education);

        String certificationRegex = "获奖及证书\\s+(.*?)(?=专业技能|$)";
        String certification = regex(certificationRegex,content);
        System.err.println("--------------获奖及证书-------------");
        System.err.println("certifications"+certification);
        map.put("certifications",certification);

        return map;
    }

    /**
     * 匹配规则
     * @param regex 匹配要求
     * @param content  需要匹配的内容
     * @return 匹配结果
     */
    public static String regex(String regex,String content){
        Pattern pattern=Pattern.compile(regex,Pattern.DOTALL);// 如果想要获取多行,这里一定添加的是Pattern.DOTALL
        Matcher matcher=pattern.matcher(content);
        if(matcher.find()){
            String data=matcher.group(1).trim();
            return data;
        }
        return null;
    }
}

代码

前端

<template>
  <div class="container">
    <div class="my-myResume">
      <!--简历编辑页面-->
      <el-form :model="myResume" ref="resumeForm" label-width="120px" class="myResume-form">
        <el-form-item label="求职意向" prop="title">
          <el-input v-model="myResume.title" placeholder="请输入简历标题"></el-input>
        </el-form-item>
        <el-form-item label="联系方式A" prop="summary">
          <el-input type="textarea" v-model="myResume.phone" placeholder="请输入您的手机号码"></el-input>
        </el-form-item>
        <el-form-item label="个人介绍" prop="summary">
          <el-input type="textarea" v-model="myResume.summary" placeholder="请输入个人介绍"></el-input>
        </el-form-item>
        <el-form-item label="工作经历" prop="experience">
          <el-input type="textarea" v-model="myResume.experience" placeholder="请输入工作经历"></el-input>
        </el-form-item>
        <el-form-item label="工作项目经历" prop="content">
          <el-input type="textarea" v-model="myResume.content" placeholder="请输入工作项目经历"></el-input>
        </el-form-item>
        <el-form-item label="教育经历" prop="education">
          <el-input type="textarea" v-model="myResume.education" placeholder="请输入教育经历"></el-input>
        </el-form-item>
        <el-form-item label="专业技能" prop="skills">
          <el-input type="textarea" v-model="myResume.skills" placeholder="请输入专业技能"></el-input>
        </el-form-item>
        <el-form-item label="获奖及证书" prop="certifications">
          <el-input type="textarea" v-model="myResume.certifications" placeholder="请输入获得的认证"></el-input>
        </el-form-item>
        <el-button type="primary" @click="handleSubmit">提交</el-button>
      </el-form>
    </div>
    <div class="pdfModule">
      <!--      上传PDF按钮-->
      <el-button type="primary" @click="handleImport">上传PDF简历</el-button>
      <div v-if="uploadedFileName" class="file-name">已上传文件:{{ uploadedFileName }}</div>
      <!--      上传对话框-->
      <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body="true">
        <el-upload
            ref="upload"
            :limit="1"
            accept=".pdf"
            :headers="upload.headers"
            :action="upload.url"
            :disabled="upload.isUploading"
            :on-progress="handleFileUploadProgress"
            :on-success="handleFileSuccess"
            :auto-upload="false"
        >
          <i class="el-icon-upload"></i>
          <div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div>
          <div class="el-upload__tip text-center" slot="tip">
            <span>仅允许导入pdf格式文件</span>
          </div>
        </el-upload>
        <div slot="footer" class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">确定</el-button>
          <el-button @click="upload.open = false">取消</el-button>
        </div>
      </el-dialog>
    </div>
  </div>
</template>

<script>
import {ElForm, ElFormItem, ElInput, ElButton, ElMessage} from 'element-plus';
import {updateResume, getResumeByUsername, uploadResume, getUserIdByUsername} from '@/api/myResume/myResume';  // 更新API路径
  import Cookies from 'js-cookie'
  import axios from 'axios';
  import {getToken} from "@/utils/auth.js";
  export default {
    name: 'MyResume',
    data() {
      return {
        // 初始化简历对象
        myResume: {
          resume_id: null,  //初始化为null,后续从当前用户获取
          title: '',
          phone:'',
          summary: '',
          experience: '',
          content:'',
          education: '',
          skills: '',
          certifications: '',
          file_name:'',
          file_path:'',
        },
        // 简历导入参数
        upload:{
          open:false,
          title:"上传PDF简历",
          isUploading:false,
          headers:{ Authorization:"Bearer "+getToken()},
          // url:process.env.VUE_APP_BASE_API+"/resumes/resume/import"
          url:"http://localhost:8088/student/myResume/parsepdf"
        },
        uploadedFileName:'', // 存储上传的文件名称
      };
    },
    methods: {
      // 导入按钮操作
      /**
       * 打开上传对话框
       */
      handleImport(){
        this.upload.title ="上传PDF简历";
        this.upload.open = true;
      },
      /**
       * 文件上传中处理
       */
      handleFileUploadProgress(event,file,fileList){
        this.upload.isUploading = true;
        console.log("文件上传中", event, file, fileList);
      },
      /**
       * 文件上传成功处理
       */
      async handleFileSuccess(response,file){
        this.upload.open = false;
        this.upload.isUploading = false;
        this.$refs.upload.clearFiles();

        if(response.code===200){
          this.fillFormWithPDFData(response.data);// 将解析的数据填充到表单中
          this.uploadedFileName = file.name;  //显示上传的文件名称
          ElMessage.success('文件上传成功');
        }else{
          ElMessage.error('文件解析失败');
        }
      },
      /**
       * 提交上传的文件
       */
      submitFileForm(){
        console.log("上传接口 URL:", this.upload.url); // 调试日志
        this.$refs.upload.submit();
      },
      // 将解析的PDF数据填充到表单中
      fillFormWithPDFData(data) {
        this.myResume.title = data.title || '';
        this.myResume.phone = data.phone || '';
        this.myResume.summary = data.summary || '';
        this.myResume.experience = data.experience || '';
        this.myResume.education = data.education || '';
        this.myResume.skills = data.skills || '';
        this.myResume.certifications = data.certifications || '';
        this.myResume.content = data.content || '';
        this.myResume.file_name = data.file_name || '';
        this.myResume.file_path = data.file_path || '';
      },

      // 提交表单更新简历
      async handleSubmit() {
        try {
          const username = this.getCurrentUsername(); // 获取当前用户的username
          console.log(username);
          if(!username){
            this.$message.error('未获取到用户信息');
            return;
          }
          const res = await updateResume(this.myResume);// 调用更新简历的API
          const userId = await getUserIdByUsername(username);
          console.log(userId);
          // const res = await  axios.post(url, this.myResume);
          if (res.code === 200) {
            ElMessage.success('简历更新成功');
          } else {
            ElMessage.success('简历更新失败');
          }
        } catch (error) {
          console.error('提交失败:', error);
          ElMessage.success('简历更新失败');
        }
      },
      // 获取简历数据 (初始加载)
      async fetchResumeData() {
        try {
          const username = await this.getCurrentUsername();
          const response = await getResumeByUsername(username);  // 调用获取简历数据的方法
          if (response.code === 200) {

            this.myResume = response.data;  // 使用返回的数据更新 myResume
            this.uploadedFileName = this.myResume.file_name;// 显示已上传的文件名称
            if(this.uploadedFileName){
              this.upload.open = true;
            }
          } else {
            console.error('获取简历数据失败:', response.msg);
          }
        } catch (error) {
          console.error('请求失败:', error);
        }
      },
      // 获取当前用户的username
      getCurrentUsername(){
        const name = Cookies.get('username');
        return name;
        // return this.$store.state.user.userId;
        // return localStorage.getItem('userId');
      }
    },
    created() {
      this.fetchResumeData(); // 页面加载时获取简历数据
    }
  };
</script>

<style scoped>

/* 容器布局 */
.container {
  display: flex;
  width: 100%;
  height: 100vh; /* 使容器占满整个视口高度 */
  background-color: #f9f9f9; /* 浅灰色背景 */
}

/* 简历编辑区域 */
.my-myResume {
  flex: 7; /* 占据 4 份 */
  padding: 20px;
  overflow-y: auto; /* 如果内容过多,允许滚动 */
}

/* PDF上传区域 */
.pdfModule {
  flex: 2; /* 占据 1 份 */
  padding: 20px;
  /*border-left: 1px solid #ddd; !* 添加左边框分隔 *!*/
}

/* 表单样式 */
.myResume-form {
  max-width: 800px; /* 限制表单最大宽度 */
  margin: 0 auto; /* 居中显示 */
}

.file-name{
  margin-top: 10px;
  font-size: 14px;
  color:#666;
}

.el-upload__text {
  font-size: 14px;
  color: #666;
}

.el-upload__tip {
  font-size: 12px;
  color: #999;
}

.dialog-footer {
  text-align: right;
}
</style>
  • js内容
// src/myResume/myResume.js

import request from '@/utils/request'
// 根据username获取userId
export function getUserIdByUsername(username){
  return request({
    url:`/student/myResume/getUserId/${username}`,
    method:'get'
  })
}
// 更新简历数据
export function updateResume(data) {
  return request({
    url: '/student/myResume/updateResume',
    method: 'put',
    data: data
  })
}

// 根据username获取简历数据
export function getResumeByUsername(username) {
  return request({
    url: `/student/myResume/getByUsername/${username}`,
    method: 'get',
  })
}

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

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

相关文章

Web自动化之Selenium控制已经打开的浏览器(Chrome,Edge)

在使用selenium进行web自动化或爬虫的时候,经常会面临登录的情况,对于这种情况,我们可以利用Selenium控制已经打开的浏览器&#xff0c;从而避免每次都需要重新打开浏览器并进行登录的繁琐步骤。 目录 说明 启动浏览器 注意 --user-data-dir说明 代码设定 代码 改进代…

千峰React:案例一

做这个案例捏 因为需要用到样式&#xff0c;所以创建一个样式文件&#xff1a; //29_实战.module.css .active{text-decoration:line-through } 然后创建jsx文件&#xff0c;修改main文件&#xff1a;导入Todos&#xff0c;写入Todos组件 import { StrictMode } from react …

自动驾驶FSD技术的核心算法与软件实现

引言&#xff1a;FSD技术的定义与发展背景 在当今快速发展的科技领域中&#xff0c;自动驾驶技术已经成为全球关注的焦点之一。其中&#xff0c;“FSD”&#xff08;Full Self-Driving&#xff0c;全自动驾驶&#xff09;代表了这一领域的最高目标——让车辆在无需人类干预的情…

Go红队开发—并发编程

文章目录 并发编程go协程chan通道无缓冲通道有缓冲通道创建⽆缓冲和缓冲通道 等协程sync.WaitGroup同步Runtime包Gosched()Goexit() 区别 同步变量sync.Mutex互斥锁atomic原子变量 SelectTicker定时器控制并发数量核心机制 并发编程阶段练习重要的细节端口扫描股票监控 并发编程…

【嵌入式原理设计】实验六:倒车控制设计

目录 一、实验目的 二、实验环境 三、实验内容 四、实验记录及处理 五、实验小结 六、成果文件提取链接 一、实验目的 熟悉和掌握各模块联合控制的工作方式 二、实验环境 Win10ESP32实验开发板 三、实验内容 1、用串口和OLED显示当前小车与障碍物的距离值&#xff1b…

探索浮点数在内存中的存储(附带快速计算补码转十进制)

目录 一、浮点数在内存中的存储 1、常见的浮点数&#xff1a; 2、浮点数存储规则&#xff1a; 3、内存中无法精确存储&#xff1a; 4、移码与指数位E&#xff1a; 5、指数E的三种情况&#xff1a; 二、快速计算补码转十进制 1、第一种方法讨论&#xff1a; 2、第二种方…

实体机器人识别虚拟环境中障碍物

之前的内容已经实现了虚拟机器人识别实体机器人的功能&#xff0c;接下来就是实体机器人如何识别虚拟环境中的障碍物&#xff08;包括虚拟环境中的障碍物和其他虚拟机器人&#xff09;。 我做的是基于雷达的&#xff0c;所以主要要处理的是雷达的scan话题 我的虚拟机器人命名…

湖北中医药大学谱度众合(武汉)生命科技有限公司研究生工作站揭牌

2025年2月11日&#xff0c;湖北中医药大学&谱度众合&#xff08;武汉&#xff09;生命科技有限公司研究生工作站揭牌仪式在武汉生物技术研究院一楼101会议室举行&#xff0c;湖北中医药大学研究生院院长刘娅教授、基础医学院院长孔明望教授、基础医学院赵敏教授、基础医学院…

ARM Coretex-M核心单片机(STM32)找到hardfault的原因,与hardfault解决方法

1. 前提基础知识&#xff08;ARM异常 压栈流程&#xff09;M核栈增长是地址逐渐减小的 **M3h ARM CM4核心带浮点处理器FPU的&#xff0c;压栈的东西还不一样 进入hardfult后看MSP或者SP的值&#xff0c;看下边第二章图如果hardfult里边啥都没有&#xff0c;就只有个while(1){}…

组件传递props校验

注意&#xff1a;prop是只读的&#xff01;不可以修改父组件的数据。 可以检验传过来的内容是否类型没问题。 App.vue <template><div><!-- <parentDemo/> --><componentA/></div></template> <script> import ComponentA …

机试刷题_NC52 有效括号序列【python】

NC52 有效括号序列 from operator import truediv # # 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可 # # # param s string字符串 # return bool布尔型 # class Solution:def isValid(self , s: str) -> bool:if not s…

threejs:document.createElement创建标签后css设置失效

vue3threejs&#xff0c;做一个给模型批量CSS2D标签的案例&#xff0c;在导入模型的js文件里&#xff0c;跟着课程写的代码如下&#xff1a; import * as THREE from three; // 引入gltf模型加载库GLTFLoader.js import { GLTFLoader } from three/addons/loaders/GLTFLoader.…

一文读懂西门子 PLC 串口转以太网系列模块

在工业自动化领域&#xff0c;随着智能化和信息化的不断发展&#xff0c;设备之间的高效通信变得至关重要。西门子 PLC 作为工业控制的核心设备&#xff0c;其通信方式的拓展需求日益凸显。西门子 PLC 串口转网口产品应运而生&#xff0c;它为实现串口设备与以太网网络的无缝连…

Linux | GRUB / bootloader 详解

注&#xff1a;本文为 “Linux | GRUB / bootloader” 相关文章合辑。 英文引文&#xff0c;机翻未校。 图片清晰度限于引文原状。 未整理去重。 What is Grub in Linux? What is it Used for? Linux 中的 Grub 是什么&#xff1f;它的用途是什么&#xff1f; Abhishek …

java高级(IO流多线程)

file 递归 字符集 编码 乱码gbk&#xff0c;a我m&#xff0c;utf-8 缓冲流 冒泡排序 //冒泡排序 public static void bubbleSort(int[] arr) {int n arr.length;for (int i 0; i < n - 1; i) { // 外层循环控制排序轮数for (int j 0; j < n -i - 1; j) { // 内层循环…

Dubbo RPC 原理

一、Dubbo 简介 Apache Dubbo 是一款高性能、轻量级的开源 RPC 框架&#xff0c;支持服务治理、协议扩展、负载均衡、容错机制等核心功能&#xff0c;广泛应用于微服务架构。其核心目标是解决分布式服务之间的高效通信与服务治理问题。 二、Dubbo 架构设计 1. 核心组件 Prov…

普中单片机-51TFT-LCD显示屏(1.8寸 STM32)

普中官方论坛&#xff1a; http://www.prechin.cn/gongsixinwen/208.html 普中科技-各型号开发板资料链接&#xff1a;https://www.bilibili.com/read/cv23681775/?spm_id_from333.999.0.0 27-TFTLCD显示实验_哔哩哔哩_bilibili 2.程序烧录 2.1设置彩屏驱动 3.实验效果

Starlink卫星动力学系统仿真建模第九讲-滑模(SMC)控制算法原理简介及卫星控制应用

滑模控制&#xff08;Sliding Mode Control&#xff09;算法详解 一、基本原理 滑模控制&#xff08;Sliding Mode Control, SMC&#xff09;是一种变结构控制方法&#xff0c;通过设计一个滑模面&#xff08;Sliding Surface&#xff09;&#xff0c;迫使系统状态在有限时间内…

nss刷题5(misc)

[HUBUCTF 2022 新生赛]最简单的misc 打开后是一张图片&#xff0c;没有其他东西&#xff0c;分离不出来&#xff0c;看看lsb&#xff0c;红绿蓝都是0&#xff0c;看到头是png&#xff0c;重新保存为png&#xff0c;得到一张二维码 扫码得到flag [羊城杯 2021]签到题 是个动图…

【C/C++】删除链表的倒数第 N 个结点(leetcode T19)

考点预览&#xff1a; 双指针法&#xff1a;通过维护两个指针来一次遍历链表&#xff0c;避免了多次遍历链表的低效方法。 边界条件&#xff1a;要特别处理删除头结点的情况。 题目描述&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回…