Ziglings 笔记 103: 文本分词 (Tokenization)

切碎它!

在 Ziglings 的第 87 个练习中,我们开始接触标准库中的文本处理工具。 很多时候,我们需要处理像 “Hello, World!” 这样的字符串。如果我们想提取单词,就必须处理空格、逗号、感叹号等各种干扰。

手动写循环去判断每个字符是很繁琐且容易出错的。Zig 提供了 Tokenizer 来帮我们做这件脏活。

挑战:数单词

我们有一首雪莱的诗《奥斯曼狄斯》(Ozymandias)。 任务是统计这首诗里有多少个单词。 单词之间可能由空格、换行、分号、逗号或感叹号分隔。

解决方案

使用 std.mem.tokenizeAny,它可以指定多个分隔符:

const std = @import("std");
const print = std.debug.print;

pub fn main() !void {
    const poem =
        \\My name is Ozymandias, King of Kings;
        \\Look on my Works, ye Mighty, and despair!
    ;

    // 核心函数:tokenizeAny
    // 参数 1: 元素类型 (u8)
    // 参数 2: 输入字符串
    // 参数 3: 分隔符集合 (空格, 分号, 逗号, 感叹号, 换行)
    // 注意:tokenize 会自动跳过连续的分隔符(例如 ", " 不会产生空字符串)
    var it = std.mem.tokenizeAny(u8, poem, " ;,!\n");

    var cnt: usize = 0;
    
    // 迭代器模式
    // it.next() 返回 ?[]const u8
    // 使用 while capture 语法,只要 next() 不返回 null,循环就继续
    while (it.next()) |word| {
        cnt += 1;
        print("{s}\n", .{word});
    }

    print("This little poem has {d} words!\n", .{cnt});
}

核心知识点总结

1. tokenize vs split

这是 Zig 标准库中容易混淆的两个概念:

  • Tokenize: 忽略空结果。适用于 "a, b" -> ["a", "b"]
  • Split: 保留空结果。适用于 "a,,b" -> ["a", "", "b"](比如 CSV 的空列)。

2. 迭代器 (Iterator)

tokenizeAny 不会一次性分配一个数组来存所有单词(那样需要内存分配)。相反,它返回一个轻量级的结构体(迭代器),每次调用 next() 时才去计算下一个单词的位置。 这是 零分配 (Zero Allocation) 的,非常高效。

3. 多行字符串

代码中使用了 \\ 语法。这是 Zig 的多行字符串字面量。它不需要在每行末尾加 \n,也不需要引号闭合,非常适合嵌入大段文本。


后续预告:我们已经学会了把字符串切开。接下来,我们要学习如何把字符串转换成数字(Parsing),以及如何处理更复杂的文件读写。下一篇我们将深入 std.fmtstd.fs