跳转到内容

Lesson 46: sscanf 高级格式解析

练习任务

难度: 易-中

sscanf 实现 parse_addr() 函数,一行代码解析 "IP:Port" 格式字符串,不借助任何正则库:

c
int parse_addr(const char *input, char *host, int *port);
// 成功解析返回 1(host 和 port 通过参数传出),失败返回 0

核心格式符:%[^:] —— 匹配"除冒号外的所有字符",实现轻量级字段提取。

验证用例:
  输入 "192.168.1.100:8080\n"  -> host: 192.168.1.100  port: 8080
  输入 "localhost:3000\n"       -> host: localhost      port: 3000
  输入 "invalid\n"              -> parse error

提示: 格式串 "%[^:]:%d" 解析 "host:port"——%[^:] 读 host 至冒号,: 匹配并消费字面冒号,%d 读端口整数。sscanf 返回值 = 成功匹配的字段数——n == 2 说明两个字段都匹配成功,否则格式错误。


核心知识点

  • 扫描集 %[^X] + ^ 取反: 匹配到字符 X 前的所有字符,不消费 X(X 留给下一个格式符)
  • 返回值 = 成功赋值字段数: 这是判断格式有效性的金标准——不要用 != EOF
  • 格式中字面量必须精确匹配且被消费: "%d:%d" 要求输入中有字面冒号 :
  • %s vs %[^:]: %s 遇任意空白停止, %[^:] 仅遇 : 停止——处理 "hello world:80" 有显著差异
  • 宽度限制 %63[^:] 防溢出: 对扫描集的 field-width 和对 %s 一样重要
  • 赋值抑制 %*d / %*[^:]: 跳过字段但不存值——跳过不需要的部分
  • sscanf 线程安全: 无内部静态状态、不修改输入字符串——对比 strtok 的非线程安全
  • 字段提取 vs 手写 strchr 遍历取舍: 固定格式用 sscanf(一行搞定),灵活解析用手写(状态机)

代码框架

46_sscanf.c
c
#include <stdio.h>
#include <string.h>

/* 用 sscanf 解析 host:port 格式
 * 成功返回 1(host 和 port 通过参数传出),失败返回 0 */
int parse_addr(const char *input, char *host, int *port) {
    // 在这里实现 parse_addr:
    // 1. int n = sscanf(input, "%[^:]:%d", host, port);
    // 2. return (n == 2) ? 1 : 0;
}

int main(void) {
    char line[256];
    fgets(line, sizeof(line), stdin);
    int len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';

    char host[128];
    int port;

    // 在这里实现 main:
    // 1. if (parse_addr(line, host, &port))
    //        printf("host: %s\nport: %d\n", host, port);
    // 2. else
    //        printf("parse error\n");
    return 0;
}

阅读框架后思考:

  1. "%[^:]:%d" 三个部分各做什么?
  2. 为什么 port 参数传的是 &port(取地址)?
  3. 如果输入是 "hello world:80",host 会拿到什么?

TIP

先不要往下翻看参考解答。在纸上拆解 "%[^:]:%d" 三个格式符的工作流程:%[^:] 扫描 host 直到 :、字面 : 消费冒号、%d 读端口号。试试用 %s 替代 %[^:] 会发生什么(思考 "hello world:80")。


深度讲解

1. 扫描集 %[^X] —— 核心武器

扫描集 (scanset) 是 sscanf 最强大的功能——它允许你定义一个字符集合sscanf 会持续读入字符,直到遇到不在集合中的字符为止。

1.1 基本语法

c
%[charset]   // 只读取 charset 中出现的字符
%[^charset]  // 读取除 charset 外的所有字符(^ 取反)

关键规则^ 必须放在方括号内的首位才表示取反,否则就是普通字符 ^

1.2 常用扫描集速查

格式含义示例输入匹配结果
%[^:]除冒号外所有字符"abc:123""abc"
%[^\n]除换行外所有字符(读整行)"hello\n""hello"
%[^,]除逗号外所有字符"a,b,c""a"
%[^=]除等号外所有字符"key=val""key"
%[0-9]仅数字"123abc""123"
%[a-zA-Z]仅字母"Hello123""Hello"
%[^;]除分号外所有字符"x;y;z""x"
%[^ ]除空格外所有字符"hello world""hello"

1.3 停止但不消费

扫描集遇到停止字符时的行为非常优雅:停止,但不消费

输入: "192.168.1.100:8080"
格式: "%[^:]:%d"

步骤 1: %[^:] 扫描
  逐个读取 '1','9','2','.','1','6','8','.','1','.','1','0','0'
  遇到 ':' 停止! host = "192.168.1.100"
 冒号未被消费,它在输入流中的位置不变

步骤 2: 字面 ':' 匹配
  当前字符是 ':' 匹配成功! 消费 ':'
 此时才能前进到端口号

步骤 3: %d 扫描
  读取 '8','0','8','0' port = 8080

n = 2 解析成功

TIP

"停止但不消费"这个行为是设计亮点——你可以用 %[^:] 读字段,然后用字面 : 消费分隔符,下一个格式符自然地从字段内容开始。这就像"我只读到分界线,但不把分界线拿走"。

1.4 不用 ^ 的正向扫描集

%[...] 也可以不取反——只匹配集合中的字符:

c
char buf[32];
sscanf("123abc", "%[0-9]", buf);    // buf = "123"
sscanf("Hello123", "%[a-zA-Z]", buf); // buf = "Hello"
sscanf("abc123", "%[abc]", buf);      // buf = "abc" (只匹配 a,b,c)

WARNING

正向扫描集 %[charset] 要求输入至少有一个字符在集合内,否则匹配失败(返回 0)。这与 %[^charset] 不同——后者几乎总能匹配到一些字符,除非输入为空或第一个字符就是被排除的字符。


2. 返回值——格式有效性的金标准

sscanf 的返回值是成功赋值的字段数,不是读取的字符数,也不是 EOF

2.1 逐级返回调含义

c
int sscanf(const char *str, const char *format, ...);
// 返回值 = 成功匹配置赋值的字段数
// 如果匹配置败或输入不匹配: 返回 0
// 如果遇到输入结束: 返回 EOF(通常为 -1)
c
int a, b, c;
int n;

// 全部匹配
n = sscanf("10:20:30", "%d:%d:%d", &a, &b, &c);
// n = 3, a=10, b=20, c=30

// 部分匹配
n = sscanf("hello:80", "%d:%d", &a, &b);
// n = 0 ← %d 遇到 'h' 立即失败,a 和 b 未赋值!

// 只有 host 匹配
n = sscanf("invalid", "%[^:]:%d", buf, &a);
// n = 1 ← %[^:] 匹配了 "invalid",但 ':' 匹配失败,a 未赋值

// 输入为空字符串
n = sscanf("", "%d", &a);
// n = EOF (或 -1) ← 输入在第一个格式符前就结束了

2.2 正确的防御性检查

c
// ❌ 错误:用 != EOF 判断成功率
int n = sscanf(input, "%[^:]:%d", host, &port);
if (n != EOF) {  // 即使只匹配 1 个字段也不等于 EOF!
    printf("host: %s, port: %d\n", host, port);  // 可能用未初始化的 port
}

// ✅ 正确:检查返回值是否等于期望字段数
int n = sscanf(input, "%[^:]:%d", host, &port);
if (n == 2) {
    printf("host: %s, port: %d\n", host, port);  // 两个字段都拿到了
} else {
    printf("parse error\n");  // 安全失败
}

IMPORTANT

返回值检查是防御性编程的第一道防线。用 n == expected_count 而非 n != EOF——前者是"我拿到了所有字段"的确切保证,后者只能告诉你"没到输入末尾"。

2.3 与 scanf/fscanf 的对比

c
// scanf 返回匹配数,但不是所有场景都可信
int n = scanf("%d", &x);  // 如果输入不匹配,返回 0

// sscanf 同样返回匹配数
int n = sscanf(str, "%d", &x);  // 一致

// fscanf 从文件读,返回值含义相同
int n = fscanf(fp, "%d", &x);

三者返回值的语义完全相同——成功赋值的字段数。所以学会 sscanf 的返回值用法,scanf/fscanf 也自然掌握了。


3. 格式中字面量必须精确匹配

格式串中的普通字符(非 % 引导的格式符)是字面匹配要求——它们必须在输入中精确出现,且会被消费。

3.1 字面量匹配示例

c
int a, b;

// 格式 "%d:%d" 要求输入中有字面冒号
n = sscanf("10:20", "%d:%d", &a, &b);
// n = 2, a = 10, b = 20  ✓ 冒号被匹配并消费

n = sscanf("10 20", "%d:%d", &a, &b);
// n = 1, a = 10, b 未赋值  ✗ 空格无法匹配 ':'

n = sscanf("10: 20", "%d:%d", &a, &b);
// n = 2, a = 10, b = 20  ✓ %d 会自动跳过前导空白

NOTE

格式中的空白字符(空格、\t\n)含义特殊——它不是字面匹配一个空白,而是跳过零个或多个空白。所以 "%d %d""%d\n%d" 在匹配行为上完全等价。

3.2 多分隔符场景

c
// 日期格式 "YYYY-MM-DD"
int y, m, d;
sscanf("2024-01-15", "%d-%d-%d", &y, &m, &d);
// y=2024, m=1, d=15

// 只要分隔符一致即可
sscanf("2024/01/15", "%d/%d/%d", &y, &m, &d);
// y=2024, m=1, d=15

// 混合分隔符
sscanf("2024-01/15", "%d-%d/%d", &y, &m, &d);
// y=2024, m=1, d=15

3.3 字面量不匹配 = 停止消费

输入: "10 20"
格式: "%d:%d"

扫描过程:
 %d "10", ' ' 停止。a = 10
 字面 ':' 当前是 ' ' 不匹配! 整个 sscanf 停止
  b 未被赋值
  n = 1

CAUTION

字面量匹配失败后,sscanf 立即停止——后续的格式符(包括 %d)不再执行。你不能依赖"后面的字段不会被赋值所以应该有自己的默认值"——未赋值的字段是未定义的。


4. %s vs %[^:] —— 根本性差异

这是实际开发中最容易踩的坑之一。

4.1 %s:遇任意空白停止

c
char word[64];

sscanf("hello", "%s", word);        // word = "hello"
sscanf("hello world", "%s", word);  // word = "hello"  ← 遇到空格停了!
sscanf("hello:world", "%s", word);  // word = "hello"  ← 遇到 : 停了!

%s 的停止条件:空格、\t\n\r\v\f……几乎所有空白字符。

4.2 %[^:]:仅遇 : 停止

c
char part[64];

sscanf("hello", "%[^:]", part);        // part = "hello"
sscanf("hello world", "%[^:]", part);  // part = "hello world"  ← 空格不停止!
sscanf("hello:world", "%[^:]", part);  // part = "hello"        ← 遇到 : 停了

4.3 关键对比:"hello world:80"

总是最经典的对照实验:

c
char host_s[64];
int port_s;

// 用 %s
int n1 = sscanf("hello world:80", "%s:%d", host_s, &port_s);
// 扫描过程:
//   %s 读 "hello" → 遇到空格停止 → host_s = "hello"
//   字面 ':' → 当前是空格 ' ' → 不匹配! 失败
// n1 = 1  ← 只有 host 匹配成功

char host_bracket[64];
int port_bracket;

// 用 %[^:]
int n2 = sscanf("hello world:80", "%[^:]:%d", host_bracket, &port_bracket);
// 扫描过程:
//   %[^:] 读 "hello world" → 遇到 ':' 停止 → host_bracket = "hello world"
//   字面 ':' → 当前是 ':' → 匹配!
//   %d 读 "80" → port_bracket = 80
// n2 = 2  ← 全部匹配成功!
text
┌─────────────┬──────────────────┬─────────────────────┐
│ 格式        │ 输入             │ 结果                │
├─────────────┼──────────────────┼─────────────────────┤
│ "%s:%d"     │ "hello world:80" │ host="hello", n=1   │
│ "%[^:]:%d"  │ "hello world:80" │ host="hello world"  │
│             │                  │ port=80, n=2        │
└─────────────┴──────────────────┴─────────────────────┘

4.4 选型指南

场景推荐原因
字段不含空白%s最简单
IP 地址解析两者都可以(IP 不含空白和冒号)效果等效
域名解析%[^:]域名不含冒号,但可能出现在含空白的文本中
含空格的值%[^:]%s 会截断
CSV 解析%[^,]逗号是分隔符
任意单行文本%[^\n]读到行尾

5. 宽度限制 %63[^:] —— 防溢出的安全带

扫描集在读入时不知道目标缓冲区有多大——你必须在格式串中设置上限。

5.1 不带宽度的危险

c
char host[16];  // 只有 16 字节的缓冲区

// ❌ 危险:没有宽度限制
sscanf("very-long-hostname.example.com:8080", "%[^:]:%d", host, &port);
// host 缓冲区只有 16 字节,但 %[^:] 写了远超 16 字节的内容
// → 栈溢出 (stack buffer overflow)!

5.2 加宽度限制

c
char host[16];

// ✅ 安全:最多读 15 个字符,为 '\0' 留 1 字节
sscanf("very-long-hostname.example.com:8080", "%15[^:]:%d", host, &port);
// host = "very-long-hostn" (截断),port = 8080
// 缓冲区安全 ✓

5.3 宽度限制规则

c
#define HOST_MAX 64

char host[HOST_MAX];   // 64 字节
int port;

// 宽度 = 缓冲区大小 - 1(为 '\0' 预留)
sscanf(input, "%63[^:]:%d", host, &port);
//              ^^  — 63 = 64 - 1

WARNING

扫描集的宽度限制规则和 %s 完全一致:%Ns / %N[...] 最多读 N 个字符,自动追加 \0。N 必须是缓冲区大小减 1。忘加宽度限制是 C 中最常见的缓冲区溢出来源之一。

5.4 宽度限制对扫描集的影响

输入: "aaaaaaaaaab:123"  (12  'a' 后跟 ":123")
host 缓冲区: host[8]  (8 字节)

不加限制: %[^:] 读到 12 'a' 写入 13 字节 (12 + '\0')
 缓冲区溢出!✗

加限制: %7[^:] 最多读 7 'a' host = "aaaaaaa"
 剩余输入位置: "aaab:123" 之后的部分
 ':' 被字面匹配消费,%d 读取 123
 安全

6. 赋值抑制 %* —— 跳过不关心的字段

赋值抑制(assignment suppression)用星号 * 标记——匹配但不存储。这在"只关心部分字段"的场景中非常有用。

"user:123:admin"
user
 关心  跳过  关心

6.1 基本语法

c
%*d      // 匹配整数组不赋值(吃掉这个字段)
%*s      // 匹配字符串但不赋值
%*[^:]   // 匹配到冒号但不保存
%*[^,]   // 匹配到逗号但不保存

6.2 实战示例

c
char username[32];
char role[32];

// 输入 "user:123:admin" —— 跳过中间的数字
int n = sscanf("user:123:admin", "%[^:]:%*d:%[^:]", username, role);
// n = 2 ← %*d 不返回值
// username = "user"
// role = "admin"
// 中间的 123 被跳过

6.3 多个字段跳过

c
// 日志 "2024-01-15 14:30:00 INFO user login" → 只提取时间和消息
char date[16], time[16], message[64];

sscanf(log, "%s %s:%*d:%*d %*s %[^\n]", date, time, message);
// date = "2024-01-15"
// time = "14"
// 跳过了 :30 :00 和 "INFO" 级别
// message = "user login"

NOTE

赋值域制不改变 sscanf 的返回值——%*d 匹配了整数但不赋值,所以它不计入返回的字段数。上面例子中 n = 2(只有 usernamerole 被赋值)。

6.4 常见用例

场景格式效果
跳中间字段"%[^:]:%*d:%[^:]"提取首尾字段
跳前缀"%*[^=]=%s"只取等号后的值
跳后缀"%s%*[^\n]"只取第一个词
跳标记"%*[#]%s"跳过注释符

7. sscanf 线程安全 —— 与 strtok 的关键区别

7.1 sscanf 是完全线程安全的

c
// sscanf 的内部结构(简化)
int sscanf(const char *str, const char *format, ...) {
    // str 是 const — 只读不写
    // 无内部静态变量
    // 无全局状态
    // 每次调用完全独立
    return internal_scan(str, format, va_args);
}

sscanf 不修改输入字符串(参数是 const char *),没有全部静态状态,每次调用完全独立。多个线程可以同时调用 sscanf 而不会互相干扰。

7.2 strtok 是非线程安全的

c
// strtok 的内部(简化)
char *strtok(char *str, const char *delim) {
    static char *saved;  // ← 静态变量! 多线程共享!
    if (str) saved = str;
    return strtok_r(saved, delim, &saved);  // 实际这个版本才有 saveptr
}

strtok 使用静态变量保存分词位置——两个线程同时调用会导致状态互相覆盖。相关课程详见 Lesson 43: strtok_r

7.3 完整对比表

维度sscanfstrtok (不安全)strtok_r (安全)
修改输入不修改 (const)修改(写入 \0修改(写入 \0
线程安全否(静态变量)是(调用者提供状态)
内部状态有(static 指针)有(saveptr 参数)
重入性天然可重入不可重入可重入
适用场景固定格式一次提取逐 token 切分逐 token 切分

IMPORTANT

如果你在多线程程序中需要字符串切分,有两个安全选择:sscanf(固定格式,一次提取所有字段)或 strtok_r(逐 token 切分,调用者提供状态)。


8. 字段提取 vs 手写 strchr 遍历 —— 何时选哪个

这是工程中的常见抉择——同样的功能可以用 sscanf 一行搞定义也可以手写字符遍历。

8.1 用 sscanf 的场景

固定格式、分隔符明确:

c
// 解析 IP:Port
sscanf(input, "%[^:]:%d", host, &port);  // 1 行

// 解析日期 YYYY-MM-DD
sscanf(date, "%d-%d-%d", &y, &m, &d);   // 1 行

// 解析 key=value
sscanf(line, "%[^=]=%s", key, value);    // 1 行

// 解析 CSV
sscanf(row, "%[^,],%[^,],%d", name, email, &age);  // 1 行

8.2 用手写遍历的场景

格式灵活、需要状态机:

c
// 引号内有分隔符的 CSV: "Smith, John",42,"NY"
// sscanf 处理不了 —— 引号内的逗号不应该被当作分隔符

// 多层嵌套的配置: section: { key1 = val1; key2 = val2 }
// sscanf 无法处理嵌套结构

// 需要跳过注释/空白行
// sscanf 需要配合外部逻辑,不如手写直观

// 需要精确控制错误位置和错误消息
// sscanf 只告诉你 "匹配了几个字段"

8.3 手写 strchr 等价实现

c
// sscanf 版本: 1 行
int parse_sscanf(const char *input, char *host, int *port) {
    return sscanf(input, "%[^:]:%d", host, port) == 2;
}

// 手写 strchr 版本: ~8 行
int parse_manual(const char *input, char *host, int *port) {
    const char *colon = strchr(input, ':');
    if (!colon) return 0;                      // 没有冒号
    size_t len = colon - input;
    if (len >= 128) return 0;                  // host 太长
    memcpy(host, input, len);                   // 复制 host
    host[len] = '\0';                         // 补 '\0'
    char *end;
    long p = strtol(colon + 1, &end, 10);      // 解析端口
    if (end == colon + 1) return 0;             // 端口不同数字
    *port = (int)p;
    return 1;
}

8.4 决策对比

维度sscanf手写 strchr
代码行数1-3 行8-15 行
可读性格式串即文档需逐步阅读
错误定位只能知道匹配了几个字段精确知道哪里出错
性能库优化,通常更快相当
灵活性仅限固定格式任意复杂解析
溢出保护需要宽度限制需要手动检查长度
学习曲线需记住格式符通用 C 知识

8.5 黄金法则

能用 sscanf 一行搞定的,就用 sscanf。
重要状态机、嵌套结构、或精确错误信息的,用手写遍历。
两者结合也是好策略——sscanf 提取固定义分,手写处理灵活部分。

NOTE

在性能敏感的路径上,两者差异很小(sscanf 内部也是字符遍历)。真正的性能差异来自算法选择,而不是 sscanf vs 手写。


参考解答

parse_addr 实现
solution_46_sscanf.c
c
#include <stdio.h>
#include <string.h>

int parse_addr(const char *input, char *host, int *port) {
    /* 一行 sscanf 解析 host:port
     *
     * %[^:] — 扫描集取反:匹配除冒号外的所有字符
     *         "192.168.1.100:8080" → host = "192.168.1.100"
     *         读到 ':' 停止但不消费冒号
     * :      — 字面匹配:消费输入中的冒号
     * %d     — 整数:匹配端口号
     *
     * 返回值 = 成功赋值域字段数
     *   n == 2 → host 和 port 都拿到了 → 格式正确
     *   n < 2  → 格式不匹配 → 解析失败
     */
    int n = sscanf(input, "%127[^:]:%d", host, port);
    return (n == 2) ? 1 : 0;
}

IMPORTANT

注意 %127[^:] 中的 127 是宽度限———host 缓冲区大小为 128,最多读 127 个字符,为 \0 留 1 字节。这是必须的安全措施。

main 实现
solution_46_sscanf.c
c
int main(void) {
    char line[256];
    fgets(line, sizeof(line), stdin);
    int len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';

    char host[128];
    int port;

    if (parse_addr(line, host, &port)) {
        printf("host: %s\nport: %d\n", host, port);
    } else {
        printf("parse error\n");
    }
    return 0;
}
完整编译和测试
bash
# 编译
gcc -Wall -Wextra -o 46_sscanf solution_46_sscanf.c

# 测试
echo "192.168.1.100:8080" | ./46_sscanf
# 输出:
# host: 192.168.1.100
# port: 8080

echo "localhost:3000" | ./46_sscanf
# 输出:
# host: localhost
# port: 3000

echo "invalid" | ./46_sscanf
# 输出:
# parse error

课堂讨论

讨论问题

Q1: %s%[^:] 的本质区别是什么?

%s任意空白字符停止: 空格、\t\n\r 等),而 %[^:] 仅遇 : 停止

这决定了它们适用的场景完全不同:

  • %s 适合提取不含空白的单词
  • %[^:] 适合提取"直到冒号"的整段文字(可能包含空格)

经典差异示例:"hello world:80"

  • %s"hello"(空格截断)
  • %[^:]"hello world"(直到冒号)
Q2: sscanfstrtok 各适合什么场景?
场景选择原因
固定格式(IP:Port, YYYY-MM-DD)sscanf一行搞定,格式串即文档
按分隔符切分不定长 token 列表strtok/strtok_r逐个取 token 更自然
多线程环境sscanfstrtok_rstrtok 有静态变量不安全
需要保留开始字符串sscanfstrtok 会修改输入
字段间有复杂关系手写状态机两者都不适合
Q3: 为什么 %[^:] 中的 ^ 要放在方括号里面?

方括号 [...] 定义的是一种"字符集"(character set),这和正则表达式中的字符类 [...] 语法一致:

  • ^ 在方括号内首位 → 取反(negation):匹配不在集合中的字符
  • 不在首位 → 普通字符 ^:匹配字面上的 ^
c
%[^abc]   // ^ = 取反,匹配除 a,b,c 外的字符
%[abc^]   // ^ = 普通字符,匹配 a,b,c,^ 中的字符
%[a^bc]   // ^ = 普通字符,匹配 a,^,b,c 中的字符

NOTE

这和正则表达式中的 [^abc] 含义完全一致,只是 sscanf 的扫描集不是完整的正则引擎。

Q4: sscanf 能解析 CSV 吗?

能解析简单的 CSV,但不能处理复杂情况。

c
// 简单 CSV 行:字段不含引号、逗号、换行
sscanf(line, "%[^,],%[^,],%d", name, email, &age);  // 可以

不能处理的情况

  • 引号内的逗号:"Smith, John",42——sscanf 看到逗号就切
  • 引号内的换行:跨行字段
  • 转义引号:"He said \"hello\""

对于复杂 CSV,重要手写状态机(逐字符处理,跟踪是否在引号内)。

Q5: 如何防止 %[^:] 导致的缓冲区溢出?

在格式中加宽度限制

c
char host[32];    // 32 字节缓冲区

// ❌ 危险
sscanf(input, "%[^:]:%d", host, &port);

// ✅ 安全:最多读 31 个字符,为 '\0' 留 1 字节
sscanf(input, "%31[^:]:%d", host, &port);

规则:%N[^chars] — N = 缓冲区大小 - 1。这和 snprintf 的 size 参数、fgets 的 size 参数遵循同样的"减一"原则。


课后练习

知识点提示: 每次写 sscanf 后立即检查返回值是否等于期望的字段数。宽度限制是必须的安全措施。

练习 1: 解析日期格式

编写代码用 sscanf 解析日期字符串 "YYYY-MM-DD",提取年、月、日三个整数。如果格式不匹配,打印 "invalid date"

输入示例: "2024-01-15" Year: 2024, Month: 1, Day: 15
输入示例: "hello" invalid date

知识点提示: 格式串 "%d-%d-%d" 中的 - 是字面量,必须精确匹配输入中的 -

参考解答
c
int y, m, d;
if (sscanf(input, "%d-%d-%d", &y, &m, &d) == 3) {
    printf("Year: %d, Month: %d, Day: %d\n", y, m, d);
} else {
    printf("invalid date\n");
}
练习 2: 提取文件名和扩展名

编写代码解析文件名字符串 "document.txt",用扫描集分别提取文件名和扩展名。

输入示例: "document.txt" name: document   ext: txt
输入示例: "archive.tar.gz" name: archive.tar   ext: gz (注意只有最后一个点)
输入示例: "Makefile" invalid filename

知识点提示: %[^.] 匹配直到点号的字符,再用字面 . 消费点号,最后 %[^.] 提取扩展名。

参考解答
c
char name[128], ext[32];
if (sscanf(input, "%127[^.].%31[^.]", name, ext) == 2) {
    printf("name: %s   ext: %s\n", name, ext);
} else {
    printf("invalid filename\n");
}

NOTE

archive.tar.gz → name = archive.tar, ext = gz。如果需要先去除最后一个点号之前的所有字符,可以用 strrchr 配合 sscanf,或手写遍历。

练习 3: 跳过中间字段

编写代码解析 "user:123:admin",只提取用户名和角色,跳过中间的数字 ID。

输入: "user:123:admin" user: user, role: admin
输入: "root:0:superuser" user: root, role: superuser

知识点提示: 用 %*d 跳过中间的数字字段——匹配但不赋值。

参考解答
c
char user[64], role[64];
if (sscanf(input, "%63[^:]:%*d:%63[^:]", user, role) == 2) {
    printf("user: %s, role: %s\n", user, role);
}
// %*d 匹配并跳过中间数字,不计入返回值
练习 4: CSV 行解析

解析一行包含三个字段的简单 CSV:"John,john@mail.com,25"

输入: "John,john@mail.com,25" Name: John, Email: john@mail.com, Age: 25
输入: "Alice,alice@dev.io" invalid CSV

知识点提示: 三个 %[^,] 用字面逗号分隔。第三个字段是整数,用 %d 而非 %[^,]

参考解答
c
char name[64], email[64];
int age;
if (sscanf(input, "%63[^,],%63[^,],%d", name, email, &age) == 3) {
    printf("Name: %s, Email: %s, Age: %d\n", name, email, age);
} else {
    printf("invalid CSV\n");
}
练习 5: 带空格的配置行解析

解析可能的配置行 "key = value"(等号两边可能有空格),正确提取 keyvalue

输入: "timeout = 30" key: timeout, value: 30
输入: "host=192.168.1.1" key: host, value: 192.168.1.1
输入: "=onlyvalue" invalid config

知识点提示: 格式中加空格可以跳过输入中的任意空白。%[^= ] 排除等号和空格,确保 key 不含尾部空格。

参考解答
c
char key[64], value[64];
// 格式 " %[^= ] = %s" 中的空格会跳过任意空白
if (sscanf(input, " %63[^= ] = %63s", key, value) == 2) {
    printf("key: %s, value: %s\n", key, value);
} else {
    printf("invalid config\n");
}
// %[^= ] 排除 '=' 和空格 → key = "timeout" (不含尾部空格)
// 格式中的空格跳过 '=' 两边的空白
// %s 读取 value

参考资料

  • man 3 sscanf — 官方手册页,包含所有格式符和扫描集语法的完整说明

    "The sscanf() function reads input from the character string str and interprets it according to format."

  • C 标准 §7.21.6.7sscanf 函数的规范定义,包括返回值语义和所有格式化选项

  • Lesson 43: strtok_r — 线程安全切分 — 另一种字符串解析方式,与 sscanf 互补:strtok_r 适合逐 token 切分,sscanf 适合固定格式一次提取

  • Lesson 42: snprintf — 安全格式化输出 — 格式字符串的对称面:snprintf 是输出格式化,sscanf 是输入解析,两者共用相同的格式符体系(%d, %s, 宽度限制等)

  • man 3 scanf — 完整的格式符参考手册,包含 %,%[,%n 等全部格式符的详细说明

    "The scanf() family of functions scans formatted input like sscanf(3), but read from a FILE or stdin."

  • Linux Programmer's Manual: scanf(3) — 扫描集 %[...]%[^...] 的官方语法说明,包含宽度限制和赋值抑制的组合用法

    "Matches a nonempty sequence of characters from the specified set of accepted characters; the next pointer must be a pointer to char."

Released under the MIT License.