Swift: Swift CLI 日志分析工具实战教程:综合运用 Swift 核心知识

实战是最好的学习方式——本课将用 80 行 Swift 代码构建一个完整的 CLI 日志分析工具,综合运用前 27 课的全部核心知识。


1. 你将学到



2. 项目背景

Bob 是一名后端工程师,每天需要分析 10 万行以上的服务器访问日志。他厌倦了手动用 grep 和 awk 拼凑统计,决定用 Swift 写一个专用 CLI 工具——既能跨平台运行(macOS/Linux),又能完全掌控解析逻辑。 这个项目的目标是: `ash

1. 统计日志中各种 HTTP 状态码的出现次数

swift log-analyzer.swift access.log --status

2. 筛选出响应时间超过 500ms 的慢请求

swift log-analyzer.swift access.log --slow 500

3. 生成 Top 10 最频繁 IP 的报告

swift log-analyzer.swift access.log --top-ip 10


3. 项目架构

`mermaid graph TB A["main.swift"] --> B["parseArguments()"] A --> C["readLogFile()"] C --> D["LogEntry models"] D --> E["analyzeByStatus()"] D --> F["filterSlowRequests()"] D --> G["topIPAddresses()"] E --> H["输出统计报告"] F --> H G --> H

(1) 模块划分

模块 职责 涉及知识点
LogEntry 模型 日志行解析为结构体 Codable、正则、可选链
LogParser 批量解析日志行 泛型、高阶函数、错误处理
Analyzer 统计分析 闭包、泛型、集合操作
CLI 参数解析和输出 枚举、switch 模式匹配


4. 核心模块实现

(1) 日志模型

`mermaid classDiagram class LogEntry { +String ip +Date date +String method +String path +Int statusCode +Int responseTime +Int bytesSent } class LogParser { +parseLine(String) LogEntry? +parseLines([String]) [LogEntry] } class Analyzer { +statusCount([LogEntry]) [Int: Int] +slowRequests([LogEntry], Int) [LogEntry] +topIPs([LogEntry], Int) [(String, Int)] }

▶ 示例:日志解析函数

`swift // ============================================ // 核心模块:日志条目模型与解析 // ============================================ import Foundation struct LogEntry { let ip: String let date: Date let method: String let path: String let statusCode: Int let responseTime: Int let bytesSent: Int } func parseLogLine(_ line: String) -> LogEntry? { let parts = line.split(separator: " ").map(String.init) guard parts.count >= 7 else { return nil } let ip = parts[0] let method = parts[2] let path = parts[3] guard let statusCode = Int(parts[4]), let responseTime = Int(parts[5]), let bytesSent = Int(parts[6]) else { return nil } let date = Date() return LogEntry( ip: ip, date: date, method: method, path: path, statusCode: statusCode, responseTime: responseTime, bytesSent: bytesSent ) }

▶ 示例:核心分析函数

`swift // ============================================ // 使用高阶函数进行统计分析 // ============================================ import Foundation func countByStatus(_ entries: [LogEntry]) -> [Int: Int] { Dictionary(grouping: entries, by: { .statusCode }) .mapValues { .count } } func filterSlowRequests(_ entries: [LogEntry], threshold: Int) -> [LogEntry] { entries.filter { .responseTime > threshold } .sorted { .responseTime > .responseTime } } func topIPAddresses(_ entries: [LogEntry], limit: Int) -> [(String, Int)] { Dictionary(grouping: entries, by: { .ip }) .mapValues { .count } .sorted { .value > .value } .prefix(limit) .map { (.key, .value) } } print("分析函数已定义")

▶ 示例:错误日志聚合与时段统计

除了 HTTP 状态码分布,还可以按小时聚合错误日志,帮助定位问题高发时段:

SWIFT
// ============================================
// 错误日志按小时聚合统计
// ============================================
import Foundation
struct ErrorReport {
    let hour: Int
    let count: Int
    let endpoints: [String: Int]
}
func aggregateErrorsByHour(_ entries: [LogEntry]) -> [ErrorReport] {
    // 筛选错误请求(状态码 >= 400)
    let errors = entries.filter { $0.statusCode >= 400 }
    // 按小时分组
    let calendar = Calendar.current
    let grouped = Dictionary(grouping: errors) { entry in
        calendar.component(.hour, from: entry.date)
    }
    // 生成报告
    return grouped.map { hour, items in
        let endpoints = Dictionary(grouping: items, by: { $0.path })
            .mapValues { $0.count }
        return ErrorReport(hour: hour, count: items.count, endpoints: endpoints)
    }
    .sorted { $0.hour < $1.hour }
}
// 模拟数据
let sampleEntries = [
    LogEntry(ip: "192.168.1.1", date: Date(), method: "GET", path: "/api/login", statusCode: 500, responseTime: 1200, bytesSent: 200),
    LogEntry(ip: "10.0.0.1", date: Date(), method: "POST", path: "/api/order", statusCode: 404, responseTime: 300, bytesSent: 50),
    LogEntry(ip: "192.168.1.2", date: Date(), method: "GET", path: "/api/login", statusCode: 500, responseTime: 800, bytesSent: 180)
]
let reports = aggregateErrorsByHour(sampleEntries)
for report in reports {
    print("时段 \(report.hour):00 — \(report.count) 个错误")
    for (endpoint, count) in report.endpoints {
        print("   \(endpoint): \(count) 次")
    }
}

输出:

TEXT 📖 仅展示
时段 12:00 — 3 个错误
   /api/login: 2 次
   /api/order: 1 次


5. 完整代码:CLI 日志分析工具

`swift // ============================================ // log-analyzer.swift - CLI 日志分析工具 // 综合运用:泛型、枚举、Codable、闭包、文件IO // 使用:swift log-analyzer.swift access.log --status // ============================================ import Foundation // MARK: - 1. 日志模型 struct LogEntry { let ip: String let method: String let path: String let statusCode: Int let responseTime: Int let bytesSent: Int init?(from line: String) { let parts = line.split(separator: " ").map(String.init) guard parts.count >= 7, let code = Int(parts[4]), let time = Int(parts[5]), let bytes = Int(parts[6]) else { return nil } ip = parts[0] method = parts[2] path = parts[3] statusCode = code responseTime = time bytesSent = bytes } } // MARK: - 2. 命令行参数解析 enum Command { case status case slow(Int) case topIP(Int) } func parseCommand(from args: [String]) -> (String, Command)? { guard args.count >= 2 else { return nil } let filePath = args[1] guard args.count >= 3 else { return (filePath, .status) } switch args[2] { case "--status": return (filePath, .status) case "--slow": let threshold = args.count > 3 ? Int(args[3]) ?? 500 : 500 return (filePath, .slow(threshold)) case "--top-ip": let limit = args.count > 3 ? Int(args[3]) ?? 10 : 10 return (filePath, .topIP(limit)) default: return (filePath, .status) } } // MARK: - 3. 分析引擎 struct Analyzer { let entries: [LogEntry] func statusBreakdown() -> [(Int, Int)] { Dictionary(grouping: entries, by: { .statusCode }) .mapValues { .count } .sorted { .key < .key } } func slowRequests(above ms: Int) -> [LogEntry] { entries.filter { .responseTime > ms } .sorted { .responseTime > .responseTime } } func topIPs(limit: Int) -> [(String, Int)] { Dictionary(grouping: entries, by: { .ip }) .mapValues { .count } .sorted { .value > .value } .prefix(limit) .map { (.key, .value) } } } // MARK: - 4. 文件读取 func readLogFile(at path: String) -> Result<String, String> { let fm = FileManager.default guard fm.fileExists(atPath: path) else { return .failure("文件不存在: (path)") } guard let content = try? String(contentsOfFile: path, encoding: .utf8) else { return .failure("无法读取文件: (path)") } return .success(content) } // MARK: - 5. 主程序 func main() { let arguments = CommandLine.arguments guard let (filePath, command) = parseCommand(from: arguments) else { print("用法: swift log-analyzer.swift <file> [选项]") print("选项: --status, --slow <ms>, --top-ip <n>") return } let fileResult = readLogFile(at: filePath) switch fileResult { case .failure(let error): print("错误: (error)") return case .success(let content): let entries = content.split(separator: "\n") .compactMap { LogEntry(from: String()) } let analyzer = Analyzer(entries: entries) print("=== 日志分析报告 ===") print("总行数: (content.split(separator: "\n").count)") print("有效条目: (entries.count)") switch command { case .status: print("\n--- 状态码分布 ---") for (code, count) in analyzer.statusBreakdown() { print(" (code): (count) 次") } case .slow(let ms): let slow = analyzer.slowRequests(above: ms) print("\n--- 慢请求 (>(ms)ms) ---") for entry in slow.prefix(20) { print(" (entry.ip) (entry.method) (entry.path) (entry.responseTime)ms") } case .topIP(let n): print("\n--- Top (n) IP ---") for (ip, count) in analyzer.topIPs(limit: n) { print(" (ip): (count) 次") } } } } // 运行:在终端取消注释后执行 swift log-analyzer.swift access.log --status // main()

输出(示例运行结果): ` ext === 日志分析报告 === 总行数: 1500 有效条目: 1485

--- 状态码分布 --- 200: 1200 次 301: 50 次 404: 30 次 500: 15 次

--- 慢请求 (>500ms) --- 192.168.1.5 GET /api/report 2300ms 10.0.0.3 POST /upload 1800ms

--- Top 10 IP --- 192.168.1.1: 450 次 10.0.0.1: 320 次 `


❓ 常见问题

Q 这个 CLI 工具能在 Linux 上运行吗?
A 可以。Swift 开源版支持 Linux,在 Ubuntu 上安装 Swift 后,用 swift log-analyzer.swift access.log 直接运行脚本。
Q 为什么不使用 Foundation 的 JSONSerialization 而自己解析?
A 日志格式通常不是 JSON 而是固定分隔符的纯文本格式。自己解析更灵活,也可以直接用正则表达式。
Q CommandLine.arguments 的第一个元素是什么?
A 是脚本的文件路径。通过 swift 命令运行时,argc[0] 是脚本路径,argc[1] 开始是传入的参数。
Q 如何测试这个 CLI 工具?
A 在终端执行 swift log-analyzer.swift test.log --slow 500。本课示例用于展示逻辑,实际测试需先准备包含日志数据的 test.log 文件。
Q compactMap 和 flatMap 有什么不同?
A compactMap 过滤 nil 并解包(Swift 4.1+),flatMap 用于展平嵌套集合。前者用于数组中的可选值清理,后者用于展平多维数组。
Q Result 类型和 throw 哪个更适合 CLI?
A CLI 工具推荐 Result,因为调用者可以决定如何处理错误——是打印友好信息还是继续执行。throw 强制调用方使用 try 语法。

📖 小节


📝 作业

  1. 基础题: 修改第 5 节的 CLI 工具,增加 --errors 参数,只显示状态码 >= 400 的请求和关键信息。
  2. 进阶题: 给 Analyzer 添加 andwidthReport() 方法,按 IP 统计每个 IP 的总 bytesSent,返回 Top 5 带宽消耗者。
  3. 挑战题: 扩展日志解析器支持 Apache Common Log Format(标准格式),提取 User-Agent 字段,并统计每种浏览器的访问次数分布。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏