跳到主要内容
Redis 核心数据结构:String 类型深度解析与 C++ 实战 | 极客日志
C++ 算法
Redis 核心数据结构:String 类型深度解析与 C++ 实战 综述由AI生成 基于 redis-plus-plus 库深入解析 Redis String 类型。涵盖基础读写(SET/GET)、过期策略设置、条件更新(NX/XX)、批量操作(MSET/MGET)、子串处理(GETRANGE/SETRANGE)及原子计数器(INCR/DECR)。通过 C++ 代码示例展示了如何利用现代 C++ 特性(如 optional、chrono)实现安全高效的 Redis 交互,并探讨了分布式锁等应用场景。
极光 发布于 2026/3/30 更新于 2026/5/25 34 浏览Redis 核心数据结构:String 类型深度解析与 C++ 实战
前言
在当今数据驱动的世界里,Redis 以其卓越的性能和丰富的数据结构,已成为内存数据库领域的翘楚。无论是作为高速缓存、消息队列,还是分布式锁的实现方案,Redis 的身影无处不在。而在 Redis 提供的所有数据结构中,String 类型无疑是基石中的基石。它不仅是构建其他复杂结构的基础,其自身强大的命令集也足以应对各种复杂的业务场景。
本文将以广受欢迎的 C++ Redis 客户端库 redis-plus-plus 为实战工具,系统性地、由浅入深地剖析 Redis String 类型的核心命令。我们将从最基础的 SET 和 GET 操作讲起,逐步探索包括过期时间设置、条件更新、批量操作、子字符串处理以及原子计数器在内的各种高级用法。
本文旨在为您提供一份不仅包含'如何做',更解释'为什么这么做'的详尽指南。我们将深入探讨 redis-plus-plus 如何利用现代 C++ 的特性(如 std::optional、std::chrono、迭代器等)来提供一个既安全又高效的编程接口,并结合完整的代码示例、逐行分析以及最佳实践,帮助您在 C++ 项目中将 Redis String 的威力发挥到极致。
第一章:基础奠基 —— SET 与 GET
SET 和 GET 是 Redis 世界的 'Hello, World!'。SET 用于将一个字符串值关联到一个键上,而 GET 则用于根据键获取其关联的字符串值。如果键已经存在,SET 会无条件地覆盖旧值。
1.1 基本读写操作
让我们从一个简单的 C++ 函数开始,它演示了最基本的设置、读取和更新操作。
#include <iostream>
#include <sw/redis++/redis.h>
using namespace std::chrono_literals;
void test1_basic_set_get (sw::redis::Redis& redis) {
std::cout << ">> 1. 演示 SET 和 GET 基础操作" << std::endl;
redis.flushall ();
std::cout << "设置 'key' 的值为 '111'" << std::endl;
redis.set ("key" , "111" );
value = redis. ( );
(value) {
std::cout << << value. () << std::endl;
} {
std::cout << << std::endl;
}
std::cout << << std::endl;
std::cout << << std::endl;
redis. ( , );
value = redis. ( );
(value) {
std::cout << << value. () << std::endl;
}
}
auto
get
"key"
if
"获取到 'key' 的值:"
value
else
"'key' 不存在"
"\n--------------------------\n"
"更新 'key' 的值为 '222'"
set
"key"
"222"
get
"key"
if
"更新后获取到 'key' 的值:"
value
代码深度解析
**sw::redis::Redis& redis**: 函数通过引用的方式接收一个 Redis 连接对象。这是一种高效的做法,避免了不必要的对象拷贝,确保所有操作都在同一个 TCP 连接上进行。
**redis.flushall()**: 此命令对应 Redis 的 FLUSHALL,会清空整个 Redis 实例中所有数据库的数据。在编写测试用例时,它能确保每次测试都在一个可预测的、干净的环境中开始。再次强调,此命令在生产环境中具有极高的风险,必须谨慎使用。
**redis.set("key", "111")**: 这是对 Redis SET 命令的直接封装。它是一个原子操作,当此命令执行时,Redis 会确保在设置值的过程中不会被其他命令打断。
**auto value = redis.get("key")**: redis-plus-plus 的 get 方法并未直接返回 std::string,而是返回一个 std::optional<std::string>。这是一个非常精妙和安全的现代 C++ 设计。
核心概念:std::optional 的威力 std::optional (C++17) 是一个模板类,用于表示一个'可能存在,也可能不存在'的值。这完美地映射了 GET 命令的行为:
如果键存在 ,Redis 返回对应的值,std::optional 对象内部会包含这个 std::string 值。
如果键不存在 ,Redis 返回 nil,std::optional 对象则处于'空'状态。
这种设计的巨大优势在于类型安全 。它在编译时就强制你处理'值可能不存在'的情况,从而避免了传统 C 语言风格(如返回空指针或特殊字符串)可能导致的运行时错误(如空指针解引用)。
检查是否存在 : if (value) 或 if (value.has_value()) 是检查 optional 是否包含值的标准方法。
安全获取值 : value.value() 是获取内部值的推荐方式。但请注意 :如果在一个空的 optional 对象上调用 .value(),程序会抛出 std::bad_optional_access 异常。因此,必须先检查再访问,正如示例代码所做的那样。
带默认值的获取 : value.value_or("default_string") 是一个更便捷的方法,如果值存在则返回它,否则返回提供的默认值。
1.2 错误处理最佳实践 在实际应用中,与 Redis 的交互可能会因为网络中断、服务器宕机或配置错误而失败。redis-plus-plus 在遇到这类问题时会抛出 sw::redis::Error 异常。因此,健壮的代码应该将所有 Redis 操作包裹在 try-catch 块中。
void robust_redis_operations (sw::redis::Redis& redis) {
try {
redis.set ("key" , "some_value" );
auto val = redis.get ("key" );
if (val) {
std::cout << "Success: " << val.value () << std::endl;
}
} catch (const sw::redis::Error &e) {
std::cerr << "Redis command failed: " << e.what () << std::endl;
}
}
第二章:掌控时间 —— 为键设置过期策略 在许多场景下,我们存入 Redis 的数据并不需要永久保存,例如用户会话信息、页面缓存、验证码等。为这些'临时'数据设置一个自动过期时间(Time-To-Live, TTL),是 Redis 作为缓存服务器的核心功能之一。
redis-plus-plus 利用 C++11 引入的 <chrono> 库,提供了一种类型安全且语义清晰的方式来设置过期时间。
2.1 SET 的过期时间选项 SET 命令本身就支持在设置键值对的同时原子性地指定过期时间。
void test2_set_with_expiration (sw::redis::Redis& redis) {
std::cout << "\n>> 2. 演示 SET 带有过期时间的操作" << std::endl;
redis.flushall ();
std::cout << "设置 'key',10 秒后过期" << std::endl;
redis.set ("key" , "111" , std::chrono::seconds (10 ));
long long ttl = redis.ttl ("key" );
std::cout << "设置后,'key' 的剩余过期时间 (TTL): " << ttl << " 秒" << std::endl;
std::cout << "程序休眠 5 秒..." << std::endl;
std::this_thread::sleep_for (std::chrono::seconds (5 ));
ttl = redis.ttl ("key" );
std::cout << "5 秒后,'key' 的剩余过期时间 (TTL): " << ttl << " 秒" << std::endl;
if (redis.exists ("key" )) {
std::cout << "此时 'key' 仍然存在" << std::endl;
}
std::cout << "程序再休眠 6 秒..." << std::endl;
std::this_thread::sleep_for (std::chrono::seconds (6 ));
if (!redis.exists ("key" )) {
std::cout << "总共 11 秒后,'key' 已按预期自动删除" << std::endl;
}
}
代码深度解析
**redis.set("key", "111", std::chrono::seconds(10))**: 这是 set 方法的一个重载版本。第三个参数接收一个 std::chrono::duration 对象。
std::chrono 的优势 : 直接传入整数 10 可能会有歧义(是秒、毫秒还是分钟?)。而 std::chrono::seconds(10) 或 10s 这样的写法,在编译时就明确了时间单位,大大提高了代码的可读性和健壮性。你也可以使用 std::chrono::milliseconds, std::chrono::minutes 等。
**long long time = redis.ttl("key")**: 此调用对应 Redis 的 TTL 命令,用于获取一个键的剩余过期时间(以秒为单位)。
返回值 > 0: 键存在,且返回值是剩余的秒数。
返回值为 -1: 键存在,但没有设置过期时间(永久有效)。
返回值为 -2: 键不存在。
应用场景
缓存 : Web 页面的片段、数据库查询结果等可以缓存几分钟,到期自动失效,强制应用重新从数据源获取最新数据。
会话管理 : 网站用户的登录状态可以存储在 Redis 中,并设置一个合理的过期时间(如 30 分钟)。用户长时间无操作,会话自动过期,需要重新登录。
分布式锁 : 为锁设置一个过期时间是一种重要的安全机制,可以防止因持有锁的客户端崩溃而导致死锁。
第三章:精细控制 —— 条件化 SET 在某些分布式场景下,我们不希望 SET 操作总是无条件地覆盖旧值。我们可能需要实现'仅当键不存在时才设置'或'仅当键存在时才更新'的逻辑。这正是 SET 命令的 NX 和 XX 选项的用武之地。
NX (if N ot eX ists): 只在键不存在时,才对键进行设置操作。
XX (if eX ists): 只在键已经存在时,才对键进行设置操作。
redis-plus-plus 通过 sw::redis::UpdateType 枚举来支持这些选项。
void test3_conditional_set (sw::redis::Redis& redis) {
std::cout << "\n>> 3. 演示 SET 的 NX 和 XX 选项" << std::endl;
redis.flushall ();
std::cout << "尝试使用 NX (NOT_EXIST) 设置 'key'" << std::endl;
bool success = redis.set ("key" , "val_nx" , 0 s, sw::redis::UpdateType::NOT_EXIST);
if (success) {
std::cout << "成功:'key' 原本不存在,已设置为 'val_nx'" << std::endl;
} else {
std::cout << "失败:'key' 已存在,设置操作被忽略" << std::endl;
}
std::cout << "当前 'key' 的值:" << redis.get ("key" ).value_or ("N/A" ) << std::endl;
std::cout << "\n再次尝试使用 NX 设置 'key'" << std::endl;
success = redis.set ("key" , "another_val" , 0 s, sw::redis::UpdateType::NOT_EXIST);
if (success) {
std::cout << "成功:'key' 原本不存在,已设置为 'another_val'" << std::endl;
} else {
std::cout << "失败:'key' 已存在,设置操作被忽略" << std::endl;
}
std::cout << "当前 'key' 的值:" << redis.get ("key" ).value_or ("N/A" ) << std::endl;
std::cout << "\n尝试使用 XX (EXIST) 更新 'key'" << std::endl;
success = redis.set ("key" , "val_xx" , 0 s, sw::redis::UpdateType::EXIST);
if (success) {
std::cout << "成功:'key' 存在,已更新为 'val_xx'" << std::endl;
} else {
std::cout << "失败:'key' 不存在,更新操作被忽略" << std::endl;
}
std::cout << "当前 'key' 的值:" << redis.get ("key" ).value_or ("N/A" ) << std::endl;
redis.del ("key" );
std::cout << "\n删除 'key' 后,尝试使用 XX 更新" << std::endl;
success = redis.set ("key" , "another_val_xx" , 0 s, sw::redis::UpdateType::EXIST);
if (success) {
std::cout << "成功:'key' 存在,已更新为 'another_val_xx'" << std::endl;
} else {
std::cout << "失败:'key' 不存在,更新操作被忽略" << std::endl;
}
std::cout << "当前 'key' 的值:" << redis.get ("key" ).value_or ("N/A" ) << std::endl;
}
代码深度解析
**redis.set(..., sw::redis::UpdateType::NOT_EXIST)**: 这行代码的意图非常明确——仅当 key 不存在时,才将其值设为 val_nx。set 命令的这个重载版本会返回一个 bool 值,true 表示设置成功,false 表示因不满足条件而未执行。
**sw::redis::UpdateType::EXIST**: 逻辑与 NOT_EXIST 相反,要求 key 必须已存在,set 操作才会执行。
核心应用:实现分布式锁 SET key value NX EX seconds 是 Redis 实现分布式锁的经典模式。
获取锁 : 一个客户端尝试执行 redis.set("my_lock", "unique_id", 30s, sw::redis::UpdateType::NOT_EXIST)。
如果返回 true,代表该客户端成功获取了锁。unique_id 是一个随机字符串,用于标识锁的持有者。30s 的过期时间是为了防止客户端崩溃导致锁无法释放。
如果返回 false,代表锁已被其他客户端持有,获取失败。
释放锁 : 锁的持有者在完成任务后,需要通过一个脚本(确保原子性)来判断锁的 unique_id 是否与自己持有的一致,如果一致才执行 DEL 命令,防止误删他人的锁。
第四章:效率为王 —— MSET 与 MGET 批量操作 假设你需要一次性设置或获取 100 个键值对。如果使用循环调用 100 次 SET 或 GET,将会产生 100 次独立的网络往返(Round-Trip Time, RTT)。在网络延迟较高的环境中,这会极大地影响程序性能。
Redis 提供了 MSET 和 MGET 命令,允许你在一次请求中处理多个键,将网络开销从 O(N) 降为 O(1)。
4.1 MSET:一次设置多个键值对 MSET 是一个原子性操作,它会一次性设置所有提供的键值对,要么全部成功,要么全部失败(例如在事务中)。
redis-plus-plus 提供了多种便捷的方式来调用 MSET。
方法一:使用初始化列表 对于固定的、少量的键值对,使用 C++11 的初始化列表(initializer list)最为直观。
void test4_mset_initializer_list (sw::redis::Redis& redis) {
std::cout << "\n>> 4.1 演示 MSET (使用初始化列表)" << std::endl;
redis.flushall ();
std::cout << "一次性设置 key1, key2, key3" << std::endl;
redis.mset ({
std::make_pair ("key1" , "111" ),
std::make_pair ("key2" , "222" ),
{"key3" , "333" }
});
auto value1 = redis.get ("key1" );
auto value2 = redis.get ("key2" );
auto value3 = redis.get ("key3" );
std::cout << "获取 'key1': " << value1. value_or ("N/A" ) << std::endl;
std::cout << "获取 'key2': " << value2. value_or ("N/A" ) << std::endl;
std::cout << "获取 'key3': " << value3. value_or ("N/A" ) << std::endl;
}
方法二:使用迭代器 当键值对是动态生成并存储在容器(如 std::vector)中时,使用迭代器的方式则更加灵活和强大。
#include <vector>
#include <string>
void test5_mset_mget_iterators (sw::redis::Redis& redis) {
std::cout << "\n>> 4.2 演示 MSET 和 MGET (使用迭代器)" << std::endl;
redis.flushall ();
std::vector<std::pair<std::string, std::string>> kvs = {
{"key1" , "111" },
{"key2" , "222" },
{"key3" , "333" }
};
std::cout << "使用 vector 和迭代器进行 MSET" << std::endl;
redis.mset (kvs.begin (), kvs.end ());
std::vector<std::string> keys_to_get = {"key1" , "key_nonexistent" , "key3" };
std::vector<sw::redis::OptionalString> result_values;
auto inserter = std::back_inserter (result_values);
std::cout << "\n使用 MGET 和输出迭代器获取 'key1', 'key_nonexistent', 'key3'" << std::endl;
redis.mget (keys_to_get.begin (), keys_to_get.end (), inserter);
std::cout << "MGET 返回结果:" << std::endl;
for (size_t i = 0 ; i < keys_to_get.size (); ++i) {
const auto & key = keys_to_get[i];
const auto & val = result_values[i];
if (val) {
std::cout << " - " << key << ": " << val.value () << std::endl;
} else {
std::cout << " - " << key << ": (不存在)" << std::endl;
}
}
}
代码深度解析
**redis.mset(kvs.begin(), kvs.end())**: mset 的这个重载版本接受一对迭代器,代表一个包含键值对的区间。这体现了 redis-plus-plus 与 C++ 标准模板库(STL)的无缝集成。
MGET 与输出迭代器 : MGET 的设计尤为出色。MGET 命令返回一个值的列表,其顺序与请求的键的顺序完全对应。如果某个键不存在,其对应位置返回的是 nil。
**std::vector<sw::redis::OptionalString> result_values**: 接收结果的容器,元素类型必须是 OptionalString,以正确处理可能不存在的键。
**auto inserter = std::back_inserter(result_values)**: 这是理解此模式的关键。std::back_inserter 是一个由标准库提供的'输出迭代器适配器'。它会创建一个特殊的迭代器,对这个迭代器进行赋值操作(如 *inserter = value),会被神奇地转换为对其关联容器的 push_back(value) 调用。
**redis.mget(..., inserter)**: mget 方法在内部获取到 Redis 返回的每一个值后,就通过这个 inserter 迭代器将其'写入'。这避免了在 mget 函数内部创建并返回一个临时 vector(可能涉及额外的内存分配和拷贝),而是直接将结果填充到调用者提供的容器中,既高效又灵活。
第五章:精雕细琢 —— GETRANGE 与 SETRANGE 子串操作 有时候我们不需要获取或修改整个字符串,而只关心其中的一部分。GETRANGE 和 SETRANGE 就像是为 Redis 字符串提供了数组切片和修改的能力。
void test6_range_operations (sw::redis::Redis& redis) {
std::cout << "\n>> 5. 演示 GETRANGE 和 SETRANGE" << std::endl;
redis.flushall ();
redis.set ("key" , "Hello, Redis World!" );
std::cout << "原始字符串:'Hello, Redis World!'" << std::endl;
std::string substring = redis.getrange ("key" , 7 , 11 );
std::cout << "getrange('key', 7, 11) -> '" << substring << "'" << std::endl;
substring = redis.getrange ("key" , -6 , -1 );
std::cout << "getrange('key', -6, -1) -> '" << substring << "'" << std::endl;
std::cout << "\n执行 setrange('key', 7, 'C++')" << std::endl;
long long new_len = redis.setrange ("key" , 7 , "C++" );
auto final_value = redis.get ("key" );
std::cout << "修改后字符串:'" << final_value.value_or ("N/A" ) << "'" << std::endl;
std::cout << "SETRANGE 返回的新长度:" << new_len << std::endl;
redis.setrange ("key" , 25 , "End" );
std::cout << "\n在远超末尾的位置执行 setrange('key', 25, 'End')" << std::endl;
std::cout << "最终字符串:'" << redis.get ("key" ).value_or ("N/A" ) << "'" << std::endl;
}
代码深度解析
**redis.getrange("key", 2, 5)**: 对应 GETRANGE key 2 5,它提取从索引 2 到索引 5(包括 2 和 5)的子串。Redis 的字符串是二进制安全的,这意味着它可以包含任何字节,包括 \0。
**redis.setrange("key", 2, "abc")**: 对应 SETRANGE key 2 abc,从索引 2 开始,用 'abc' 覆盖原有的字符。如果原字符串长度不足,SETRANGE 会自动扩展字符串,并在必要时用空字节填充。
应用场景
位图(Bitmap) : 这是 GETRANGE 和 SETRANGE 的一个高级应用。通过将字符串视为一个位的序列,并使用 GETBIT 和 SETBIT(redis-plus-plus 也支持)命令,可以实现高效的位图数据结构,用于用户签到、在线状态统计等场景。
固定大小的二进制数据块操作 : 当你将一个结构体或复杂二进制数据序列化后存入 Redis 字符串时,SETRANGE 可以让你在不读取整个数据的情况下,精确地修改其中某一部分字段。
第六章:原子之力 —— INCR 与 DECR 计数器 在 Web 应用中,计数器是一个极其常见的需求,如文章阅读量、用户点赞数、API 调用频率限制等。如果采用传统的'读取 - 修改 - 写回'模式,在高并发下会立刻遇到**竞争条件(Race Condition)**问题,导致计数不准。
Redis 的 INCR 和 DECR 命令提供了一种原子性的解决方案。由于 Redis 的命令执行是单线程的,所以 INCR 操作从读取到写回的整个过程不会被任何其他命令打断,从而保证了计数的绝对准确性。
void test7_atomic_counters (sw::redis::Redis& redis) {
std::cout << "\n>> 6. 演示 INCR 和 DECR 原子计数器" << std::endl;
redis.flushall ();
redis.set ("counter" , "10" );
std::cout << "初始值 counter: " << redis.get ("counter" ).value_or ("N/A" ) << std::endl;
long long result = redis.incr ("counter" );
std::cout << "\n执行 incr('counter') 后..." << std::endl;
std::cout << " - INCR 命令返回值:" << result << std::endl;
std::cout << " - Redis 中存储的值:" << redis.get ("counter" ).value_or ("N/A" ) << std::endl;
result = redis.decr ("counter" );
std::cout << "\n执行 decr('counter') 后..." << std::endl;
std::cout << " - DECR 命令返回值:" << result << std::endl;
std::cout << " - Redis 中存储的值:" << redis.get ("counter" ).value_or ("N/A" ) << std::endl;
result = redis.incrby ("counter" , 5 );
std::cout << "\n执行 incrby('counter', 5) 后..." << std::endl;
std::cout << " - INCRBY 命令返回值:" << result << std::endl;
result = redis.incr ("new_counter" );
std::cout << "\n对不存在的 'new_counter' 执行 incr 后..." << std::endl;
std::cout << " - INCR 命令返回值:" << result << std::endl;
std::cout << " - Redis 中存储的值:" << redis.get ("new_counter" ).value_or ("N/A" ) << std::endl;
redis.set ("float_counter" , "10.5" );
double float_result = redis.incrbyfloat ("float_counter" , 2.1 );
std::cout << "\n执行 incrbyfloat('float_counter', 2.1) 后..." << std::endl;
std::cout << " - INCRBYFLOAT 命令返回值:" << float_result << std::endl;
}
代码深度解析
**long long result = redis.incr("key")**: incr 方法对应 Redis 的 INCR 命令。它会尝试将 key 对应的字符串值解析为 64 位有符号整数,对其加 1,然后将新值存回。最关键的是,该方法返回的是执行加法之后的新值 。这一点非常有用。
数据类型 : INCR 和 DECR 只能对可以被解释为整数的字符串操作,否则 Redis 会报错。
原子性保证 : INCR 的原子性是 Redis 最强大的特性之一。它简化了并发编程,让你无需关心复杂的加锁机制就能实现可靠的计数器。
扩展命令 :
INCRBY/DECRBY : 按指定的整数步长进行增减。
INCRBYFLOAT : 按指定的浮点数步长进行增减,这使得 Redis 也可以用作浮点数计数器。
应用场景
网站计数器 : 统计页面浏览量(PV)、用户独立访客数(UV)。
限流器(Rate Limiting) : 可以为每个用户或 IP 创建一个计数器,例如 rate:limit:user_id:api_endpoint,并为其设置 1 分钟的过期时间。每次请求时执行 INCR,如果返回值超过了预设的阈值(如 100),则拒绝该请求。
总结 通过本文的深入探讨,我们全面地学习了 Redis String 类型的核心命令,并掌握了如何使用 redis-plus-plus 这一优秀的 C++ 库在实际项目中应用它们。
我们从最基础的 SET 和 GET 出发,理解了 redis-plus-plus 借助 std::optional 带来的类型安全;我们学会了使用 std::chrono 为键赋予生命周期,使其能够自动过期;我们探索了 NX/XX 选项在实现分布式锁等场景下的精妙用途;我们领略了 MSET/MGET 结合迭代器模式在批量操作中带来的巨大性能提升;我们还实践了 GETRANGE/SETRANGE 对字符串的局部精细操作,以及 INCR/DECR 家族在构建高并发原子计数器时的核心价值。
Redis String 远不止是一个简单的键值存储。它是一个功能强大、灵活多变的多面手。熟练掌握其各种命令,并结合像 redis-plus-plus 这样设计精良的客户端库,将使您在构建高性能、高并发的 C++ 应用时如虎添翼。希望本文能成为您 Redis C++ 开发道路上一份有价值的参考。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online