C++ - 仿 RabbitMQ 实现消息队列--服务端核心模块实现(三)

目录

队列数据管理

代码实现

测试代码

绑定信息(交换机-队列)管理

代码实现

测试代码


队列数据管理

当前队列数据的管理,本质上是队列描述信息的管理,描述当前服务器上有哪些队列。

  • 定义队列描述数据类
  1. 队列名称
  2. 是否持久化标志
  3. 是否独占标志
  4. 是否自动删除标志
  5. 其他参数
  • 定义队列数据持久化类(数据持久化的 sqlite3 数据库中)
  1. 创建/删除队列数据表
  2. 新增队列数据
  3. 移除队列数据
  4. 查询所有队列数据
  • 定义队列数据管理类
  1. 创建队列,并添加管理(存在则 OK,不存在则创建)
  2. 删除队列
  3. 获取指定队列
  4. 获取所有队列
  5. 判断指定队列是否存在
  6. 获取队列数量
  7. 销毁所有队列数据

代码实现

与交换机数据管理的实现非常相似,只需要修改表结构即可

#pragma once
#include "../mqcommon/helper.hpp"
#include "../mqcommon/log.hpp"
#include "../mqcommon/msg.pb.h"
#include <unordered_map>
#include <memory>namespace jiuqi
{struct MsgQueue{using ptr = std::shared_ptr<MsgQueue>;std::string name;bool durable;bool exclusive;bool auto_delete;std::unordered_map<std::string, std::string> args;MsgQueue() {}MsgQueue(const std::string &qname,bool qdurable,bool qexclusive,bool qauto_delete,const std::unordered_map<std::string, std::string> qargs): name(qname), durable(qdurable), exclusive(qexclusive), auto_delete(qauto_delete), args(qargs) {}void setArgs(const std::string &str_args){// key=val&key=val.....std::vector<std::string> sub_args;StrHelper::split(str_args, "&", sub_args);for (auto &arg : sub_args){size_t pos = arg.find("=");std::string key = arg.substr(0, pos);std::string val = arg.substr(pos + 1);args.insert(std::make_pair(key, val));}}std::string getArgs(){if (args.empty())return "";std::string result;for (auto &arg : args){result += arg.first + "=" + arg.second + "&";}result.pop_back();return result;}};using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;class QueueMapper{public:QueueMapper(const std::string &dbfile) : _sql_helper(dbfile){std::string path = FileHelper::parentDirectory(dbfile);FileHelper::createDirectory(path);            assert(_sql_helper.open());createTable();}void createTable(){std::stringstream ss;ss << "create table if not exists queue_table("<< "name varchar(32) primary key, "<< "durable int, "<< "exclusive int, "<< "auto_delete int, "<< "args varchar(128));";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);if (!ret){ERROR("创建队列数据库表失败");abort();}}void removeTable(){std::string sql = "drop table if exists queue_table;";bool ret = _sql_helper.exec(sql, nullptr, nullptr);if (!ret){ERROR("删除交换机数据库表失败");abort();}}bool insert(MsgQueue::ptr &queue){std::stringstream ss;ss << "insert into queue_table values('"<< queue->name << "', "<< queue->durable << ", "<< queue->exclusive << ", "<< queue->auto_delete << ", '"<< queue->getArgs() << "');";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);return ret;}bool remove(const std::string &name){std::stringstream ss;ss << "delete from queue_table where "<< "name = " << "'" << name << "';";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);return ret;}QueueMap recovery(){QueueMap result;std::string sql = "select name, durable, exclusive, auto_delete, args from queue_table";_sql_helper.exec(sql, selectCallback, &result);return result;}private:static int selectCallback(void *arg, int numcol, char **row, char **fields){QueueMap *result = (QueueMap *)arg;MsgQueue::ptr mqp = std::make_shared<MsgQueue>();mqp->name = row[0];mqp->durable = (bool)std::stoi(row[1]);mqp->exclusive = (bool)std::stoi(row[2]);mqp->auto_delete = (bool)std::stoi(row[3]);if (row[4])mqp->setArgs(row[4]);result->insert(std::make_pair(mqp->name, mqp));return 0;}private:SqliteHelper _sql_helper;};class QueueManager{public:using ptr = std::shared_ptr<QueueManager>;QueueManager(const std::string &dbfile) : _mapper(dbfile){_queues = _mapper.recovery();}void declareQueue(const std::string &name,bool durable,bool exclusive,bool auto_delete,std::unordered_map<std::string, std::string> &args){std::unique_lock<std::mutex> lock(_mutex);auto it = _queues.find(name);if (it != _queues.end())return;auto queue = std::make_shared<MsgQueue>(name, durable, exclusive, auto_delete, args);_queues.insert(std::make_pair(name, queue));if (durable)_mapper.insert(queue);}void deleteQueue(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _queues.find(name);if (it == _queues.end())return;if (it->second->durable)_mapper.remove(name);_queues.erase(name);}MsgQueue::ptr selectQueue(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _queues.find(name);if (it == _queues.end())return nullptr;return it->second;}QueueMap allQueue(){std::unique_lock<std::mutex> lock(_mutex);return _queues;}bool exists(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it = _queues.find(name);if (it == _queues.end())return false;return true;            }size_t size(){std::unique_lock<std::mutex> lock(_mutex);return _queues.size();}void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeTable();_queues.clear();}private:std::mutex _mutex;QueueMapper _mapper;QueueMap _queues;};
}

测试代码

#include "../mqserver/queue.hpp"
#include <gtest/gtest.h>jiuqi::QueueManager::ptr qmp;class ExchangeTest : public testing::Environment
{
public:virtual void SetUp() override{qmp = std::make_shared<jiuqi::QueueManager>("./data/queue.db");}virtual void TearDown() override{qmp->clear();}
};TEST(ExchangeTest, insert_test)
{std::unordered_map<std::string, std::string> map = {{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}};std::unordered_map<std::string, std::string> map_empty;qmp->declareQueue("queue1", true, false, false, map);qmp->declareQueue("queue2", true, false, false, map);qmp->declareQueue("queue3", true, false, false, map);qmp->declareQueue("queue4", true, false, false,map_empty);qmp->declareQueue("queue5", true, false, false,map_empty);qmp->declareQueue("queue6", true, false, false,map_empty);ASSERT_EQ(qmp->size(), 6);
}TEST(ExchangeTest, select_test)
{jiuqi::MsgQueue::ptr mqp = qmp->selectQueue("queue3");ASSERT_EQ(mqp->name, "queue3");ASSERT_EQ(mqp->durable, true);ASSERT_EQ(mqp->exclusive, false);ASSERT_EQ(mqp->auto_delete, false);ASSERT_EQ(mqp->getArgs(), std::string("k1=v1&k2=v2&k3=v3"));
}TEST(ExchangeTest, delete_test)
{qmp->deleteQueue("queue1");jiuqi::MsgQueue::ptr mqp = qmp->selectQueue("queue1");ASSERT_EQ(mqp.get(), nullptr);ASSERT_EQ(qmp->exists("queue1"), false);
}int main(int argc, char *argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new ExchangeTest);return RUN_ALL_TESTS();
}

绑定信息(交换机-队列)管理

绑定信息,本质上就是一个交换机关联了哪些队列的描述。

  • 定义绑定信息类
  1. 交换机名称
  2. 队列名称
  3. binding_key(分发匹配规则-决定了哪些数据能被交换机放入队列)
  • 定义绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)
  1. 创建/删除绑定信息数据表
  2. 新增绑定信息数据
  3. 移除指定绑定信息数据
  4. 移除指定交换机相关绑定信息数据:移除交换机的时候会被调用
  5. 移除指定队列相关绑定信息数据:移除队列的时候会被调用f. 查询所有绑定信息数据:用于重启服务器时进行历史数据恢复
  • 定义绑定信息数据管理类
  1. 创建绑定信息,并添加管理(存在则 OK,不存在则创建)
  2. 解除指定的绑定信息
  3. 删除指定队列的所有绑定信息
  4. 删除交换机相关的所有绑定信息
  5. 获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给自己关联的队列
  6. 判断指定绑定信息是否存在
  7. 获取当前绑定信息数量
  8. 销毁所有绑定信息数据

代码实现

同样与上述类的实现类似

 

#pragma once
#include "../mqcommon/helper.hpp"
#include "../mqcommon/log.hpp"
#include "../mqcommon/msg.pb.h"
#include <unordered_map>
#include <memory>namespace jiuqi
{struct Binding{using ptr = std::shared_ptr<Binding>;std::string exchangeName;std::string queueName;std::string bindingKey;Binding() {}Binding(const std::string &ename, const std::string &qname, const std::string &key): exchangeName(ename), queueName(qname), bindingKey(key){}};using QueueBindingMap = std::unordered_map<std::string, Binding::ptr>;using BindingMap = std::unordered_map<std::string, QueueBindingMap>;class BindingMapper{public:BindingMapper(const std::string &dbfile) : _sql_helper(dbfile){std::string path = FileHelper::parentDirectory(dbfile);FileHelper::createDirectory(path);_sql_helper.open();createTable();}void createTable(){std::string sql = "create table if not exists binding_table(exchangeName varchar(32), queueName varchar(32), bindingKey varchar(128), PRIMARY KEY (exchangeName, queueName));";bool ret = _sql_helper.exec(sql, nullptr, nullptr);if (!ret){ERROR("创建绑定信息数据库表失败");abort();}}void removeTable(){std::string sql = "drop table if exists binding_table;";bool ret = _sql_helper.exec(sql, nullptr, nullptr);if (!ret){ERROR("删除绑定信息数据库表失败");abort();}}bool insert(Binding::ptr &binding){std::stringstream ss;ss << "insert into binding_table values('"<< binding->exchangeName << "', '"<< binding->queueName << "', '"<< binding->bindingKey << "');";std::string sql = ss.str();if (!_sql_helper.exec(sql, nullptr, nullptr)){ERROR("插入绑定记录失败");return false;}return true;}bool remove(const std::string &ename, const std::string &qname){std::stringstream ss;ss << "delete from binding_table where "<< "exchangeName = '" << ename << "' "<< "and queueName = '" << qname << "';";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);return ret;}bool removeExchangeBindings(const std::string &ename){std::stringstream ss;ss << "delete from binding_table where "<< "exchangeName = '" << ename << "';";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);return ret;}bool removeQueueBindings(const std::string &qname){std::stringstream ss;ss << "delete from binding_table where "<< "queueName = '" << qname << "';";std::string sql = ss.str();bool ret = _sql_helper.exec(sql, nullptr, nullptr);return ret;}BindingMap recovery(){BindingMap result;std::string sql = "select exchangeName, queueName, bindingKey from binding_table;";_sql_helper.exec(sql, selectCallback, &result);return result;}private:static int selectCallback(void *arg, int numcol, char **row, char **fields){BindingMap *result = (BindingMap *)arg;Binding::ptr bp = std::make_shared<Binding>(row[0], row[1], row[2]);// 为了防止交换机相关绑定信息已经存在,不能直接创建队列映射// 因此要先获取交换机对应的映射对象// 使用引用的好处, 如果不存在就会创建QueueBindingMap &qbm = (*result)[bp->exchangeName];qbm.insert(std::make_pair(bp->queueName, bp));return 0;}private:SqliteHelper _sql_helper;};class BindingManager{public:using ptr = std::shared_ptr<BindingManager>;BindingManager(const std::string &dbfile) : _mapper(dbfile){_bindings = _mapper.recovery();}bool bind(const std::string &ename, const std::string &qname, const std::string &key, bool durable){std::unique_lock<std::mutex> lock(_mutex);auto eit = _bindings.find(ename);if (eit != _bindings.end() && eit->second.find(qname) != eit->second.end())return true;Binding::ptr bp = std::make_shared<Binding>(ename, qname, key);if (durable){if(!_mapper.insert(bp))return false;}QueueBindingMap &qbm = _bindings[ename];qbm.insert(std::make_pair(qname, bp));return true;}void unbind(const std::string &ename, const std::string &qname){std::unique_lock<std::mutex> lock(_mutex);auto eit = _bindings.find(ename);if (eit == _bindings.end())return;auto qit = eit->second.find(qname);if (qit == eit->second.end())return;_bindings[ename].erase(qname);if (eit->second.empty())_bindings.erase(ename);_mapper.remove(ename, qname);}void removeExchangeBinding(const std::string &ename){std::unique_lock<std::mutex> lock(_mutex);auto eit = _bindings.find(ename);if (eit == _bindings.end())return;_mapper.removeExchangeBindings(ename);_bindings.erase(ename);}void removeQueueBinding(const std::string &qname){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeQueueBindings(qname);// 遍历所有交换机的绑定for (auto it = _bindings.begin(); it != _bindings.end();){// 删除该队列在当前交换机的绑定it->second.erase(qname);// 如果当前交换机的绑定为空,则删除该交换机的条目if (it->second.empty()){it = _bindings.erase(it); // erase返回下一个有效的迭代器}else{++it;}}}QueueBindingMap getExchangeBindings(const std::string &ename){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it == _bindings.end())return QueueBindingMap();return _bindings[ename];}Binding::ptr getBinding(const std::string &ename, const std::string &qname){std::unique_lock<std::mutex> lock(_mutex);auto eit = _bindings.find(ename);if (eit == _bindings.end())return nullptr;auto qit = eit->second.find(qname);if (qit == eit->second.end())return nullptr;return qit->second;}bool exists(const std::string &ename, const std::string &qname){std::unique_lock<std::mutex> lock(_mutex);auto eit = _bindings.find(ename);if (eit == _bindings.end())return false;auto qit = eit->second.find(qname);if (qit == eit->second.end())return false;return true;}size_t size(){std::unique_lock<std::mutex> lock(_mutex);size_t size = 0;for (auto start = _bindings.begin(); start != _bindings.end(); start++)size += start->second.size();return size;}void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeTable();_bindings.clear();}private:std::mutex _mutex;BindingMapper _mapper;BindingMap _bindings;};
}

        值得注意的是:在使用unordered_map保存绑定信息的时候,插入和删除的方式与之前不同,具体在注释中给出了解释。 

测试代码

#include "../mqserver/binding.hpp"
#include <gtest/gtest.h>jiuqi::BindingManager::ptr bmp;class BindingTest : public testing::Environment
{
public:virtual void SetUp() override{bmp = std::make_shared<jiuqi::BindingManager>("./data/binding.db");}virtual void TearDown() override{bmp->clear();}
};TEST(ExchangeTest, insert_test)
{bmp->bind("exchange1", "queue1", "651", 1);bmp->bind("exchange1", "queue2", "651", 1);bmp->bind("exchange1", "queue3", "651", 1);bmp->bind("exchange2", "queue1", "651", 1);bmp->bind("exchange2", "queue2", "651", 1);bmp->bind("exchange2", "queue3", "651", 1);ASSERT_EQ(bmp->size(), 6);
}TEST(ExchangeTest, select_test)
{jiuqi::Binding::ptr bp = bmp->getBinding("exchange2", "queue1");ASSERT_EQ(bp->exchangeName, "exchange2");ASSERT_EQ(bp->queueName, "queue1");ASSERT_EQ(bp->bindingKey, "651");
}TEST(ExchangeTest, delete_test)
{bmp->unbind("exchange1", "queue3");jiuqi::Binding::ptr bp = bmp->getBinding("exchange1", "queue3");ASSERT_EQ(bp.get(), nullptr);ASSERT_EQ(bmp->exists("exchange1", "queue3"), false);
}int main(int argc, char *argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new BindingTest);return RUN_ALL_TESTS();
}

        在测试的过程中,发现了一种错误,就是创建数据库表时发生了out of memory错误,开始还以为是系统内存不足,后来发现在构造mapper时忘记了打开数据库,所以得知如果没有打开数据库就创建表就会发生out of memory错误。

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

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

相关文章

51c自动驾驶~合集9

自己的原文哦~ https://blog.51cto.com/whaosoft/11627386 #端到端1 说起端到端&#xff0c;每个从业者可能都觉得会是下一代自动驾驶量产方案绕不开的点&#xff01;特斯拉率先吹响了方案更新的号角&#xff0c;无论是完全端到端&#xff0c;还是专注于planner的模…

时间长了忘记jupyter的环境是哪个了

有这些但是忘记是哪个了jupyter kernelspec list查看内核路径&#xff0c;这个内核是用来告诉jupyter 去哪找内核配置的到这个路径下打开json文件查看使用的python环境从而确定是哪个conda环境为jupyter使用的python环境jupyter的工作原理&#xff1a;在创建conda环境后会安装j…

PYTHON从入门到实践-15数据可视化

数据可视化是数据分析中不可或缺的一环&#xff0c;它能够将抽象的数据转化为直观的图形&#xff0c;帮助我们更好地理解数据特征和发现潜在规律。本文将介绍如何使用Python中的Matplotlib和Plotly库进行数据可视化&#xff0c;并通过掷骰子的概率模拟案例展示可视化的实际应用…

Spring IOC 容器 **默认注册 Bean** 的 8 条规则

Spring IOC 容器 默认注册 Bean 的 8 条规则 &#xff08;Spring Framework 6.x 源码级总结&#xff09;阅读提示&#xff1a;把下面 8 条规则背下来&#xff0c;再读 Spring 源码时&#xff0c;你会在任何一行代码里立刻知道「这个 BeanDefinition 是从哪儿来的」。1️⃣ 环境…

29.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--用户配置服务

用户配置服务是孢子记账中最简单的部分。简单说&#xff0c;用户配置服务就是用户自定义的配置项存储服务&#xff0c;用于我们的APP根据用户的配置实现指定的功能。它提供了一个简单的接口&#xff0c;允许用户存储和检索他们的配置数据。就目前来说&#xff0c;用户配置只有一…

Python实现PDF按页分割:灵活拆分文档的技术指南

Python实现PDF按页分割&#xff1a;灵活拆分文档的技术指南 PDF文件处理是日常工作中的常见需求&#xff0c;特别是当我们需要将大型PDF文档拆分为多个部分时。本文将介绍如何使用Python创建一个灵活的PDF分割工具&#xff0c;能够根据用户指定的页数范围任意分割文档。 需求分…

「iOS」——GCD其他方法详解

GCD学习GCD其他方法dispatch_semaphore &#xff08;信号量&#xff09;**什么是信号量**dispatch_semaphore主要作用dispatch_semaphore主要作用异步转同步设置一个最大开辟的线程数加锁机制dispatch_time_t 两种形式GCD一次性代码(只执行一次)dispatch_barrier_async/sync栅栏…

【图像处理基石】如何实现一个车辆检测算法?

基于AI的车牌检测和识别算法 问题描述、应用场景与难点 问题描述 车牌检测和识别是计算机视觉领域的一个特定任务&#xff0c;主要包含两个核心步骤&#xff1a; 车牌检测&#xff1a;从图像中准确定位车牌的位置和区域车牌识别&#xff1a;对检测到的车牌区域进行字符识别&…

计算机学报 2025年 区块链论文 录用汇总 附pdf下载

计算机学报 Year&#xff1a;2025 2024请看 1 Title: 基于区块链的动态多云多副本数据完整性审计方法研究 Authors: Key words: 区块链&#xff1b;云存储&#xff1b;多云多副本存储&#xff1b;数据完整性审计 Abstract: 随着云计算技术的快速发展和云存储服务的日益…

计算机网络-UDP协议

UDP&#xff08;用户数据报协议&#xff09;是传输层的一种无连接、不可靠、轻量级的协议&#xff0c;适用于对实时性要求高、能容忍少量数据丢失的场景&#xff08;如视频流、DNS查询等&#xff09;。以下是UDP的详细解析&#xff1a;1. UDP的核心特点特性说明无连接通信前无需…

子域名收集和c段查询

子域名收集方法一、sitesite&#xff1a; 要查询的域名可以查到相关网站二、oneforall &#xff08;子域名查找工具&#xff09;下载后解压的文件夹在当前文件夹打开终端然后运行命令 python oneforall.py --target xxxxxxxx&#xff08;这里放你要查的网址&#xff09; run最…

计网-TCP拥塞控制

TCP的拥塞控制&#xff08;Congestion Control&#xff09;是核心机制之一&#xff0c;用于动态调整发送方的数据传输速率&#xff0c;避免网络因过载而出现性能急剧下降&#xff08;如丢包、延迟激增&#xff09;。其核心思想是探测网络可用带宽&#xff0c;并在拥塞发生时主动…

依赖倒置原则 Dependency Inversion Principle - DIP

基本知识 1.依赖倒置原则&#xff08;DIP&#xff09;是面向对象设计&#xff08;OOD&#xff09;中的五个基本原则之一&#xff0c;通常被称为 SOLID 原则中的 D 2.核心思想&#xff1a; 高层模块不应该依赖低层模块&#xff0c;两者都应该依赖抽象。 (High-level modules sho…

原生input添加删除图标类似vue里面移入显示删除[jquery]

<input type"text" id"servicer-search" class"form-control" autocomplete"off" />上面是刚开始的input <div class"servicer-search-box"><input type"text" id"servicer-search" cla…

整理分享 | Photoshop 2025 (v26.5) 安装记录

导语&#xff1a; 最近整理资源时&#xff0c;发现有朋友在找新版 Photoshop。正好手边有 Photoshop 2025年7月的版本&#xff08;v26.5&#xff09;&#xff0c;就记录下来分享给大家&#xff0c;供有需要的朋友参考。关于这个版本&#xff1a;这个 Photoshop v26.5 安装包&am…

【Redis】Redis 数据存储原理和结构

一、Redis 存储结构 1.1 KV结构 Redis 本质上是一个 Key-Value&#xff08;键值对&#xff0c;KV&#xff09;数据库&#xff0c;在它丰富多样的数据结构底层&#xff0c;都基于一种统一的键值对存储结构来进行数据的管理和操作 Redis 使用一个全局的哈希表来管理所有的键值对…

【RAG优化】深度剖析OCR错误,从根源修复RAG应用的识别问题

1. 引言:OCR——RAG系统中的关键问题 当我们将一个包含扫描页面的PDF或一张报告截图扔给RAG系统时,我们期望它能“读懂”里面的内容。这个“读懂”的第一步,就是OCR。然而,OCR过程并非100%准确,它受到图像质量、文字布局、字体、语言等多种因素的影响。 一个看似微不足道…

【第六节】方法与事件处理器

方法与事件处理器 方法处理器 可以用 v-on 指令监听 DOM 事件: <div id="example"> <button v-on:click="greet">Greet</button></div>绑定一个单击事件处理器到一个方法 greet 。下面在 Vue 实例中定义这个方法 var vm=new V…

大语言模型Claude 4简介

Anthropic公司成立于2021年&#xff0c;由一群OpenAI前员工组成。他们最新发布的大语言模型(Large Language Model, LLM) Claude 4系列包括两个版本&#xff1a;Claude Opus 4和Claude Sonnet 4&#xff1a;(1).Claude Sonnet 4&#xff1a;是Claude Sonnet 3.7的升级&#xff…

国产化PDF处理控件Spire.PDF教程:Python 将 PDF 转换为 Markdown (含批量转换示例)

PDF 是数字文档管理的普遍格式&#xff0c;但其固定布局特性限制了在需要灵活编辑、更新或现代工作流集成场景下的应用。相比之下&#xff0c;Markdown&#xff08;.md&#xff09;语法轻量、易读&#xff0c;非常适合网页发布、文档编写和版本控制。 E-iceblue旗下Spire系列产…