绝大多数编程语言可以进行正则匹配和文件的 I/O 操作,但也有方不方便的区别。比如你用 C/C++ 和 Java 那就很方便,当然 Python 写起来也很好。那么苹果最近一直强推的 Swift 语言呢?经过我一番试探,感觉其在文件,尤其是对文本文件操作有些不友好。毕竟现在很多数据都是存储在数据库或者简化为 JSON 格式了,没有必要按照初学 C 语言那样进行文本文件的按块/行读写操作了。但由于个人课程实验需要,于是用 Swift 写了一下,并总结出来一些点。

正则表达式

如果不懂正则表达式的可以去看看正 则表达式 30 分钟入门教程 或者参考 8 个常用正则表达式 吧。

Swift 虽然是一门先进的编程语言,但至今为止并没有在语言层面上支持正则表达式。

可能是 app 并不需要处理很多文本匹配的功能,因此开发者不是很依赖正则表达式。但进行正则匹配的话,可以用 Cocoa 中的 NSRegularExpression 写一个简单的包装。喵神在其 《 Swift 必备 Tips 》 中写了一章正则的详解,如下:

struct RegexHelper {
    let regex: NSRegularExpression

    init(_ pattern: String) throws {
        try regex = NSRegularExpression(pattern: pattern,
            options: .caseInsensitive)
    }

    func match(_ input: String) -> Bool {
        let matches = regex.matches(in: input,
                    options: [],
                    range: NSMakeRange(0, input.utf16.count))
        return matches.count > 0
    }
}

let mailPattern =
"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"

let matcher: RegexHelper
do {
    matcher = try RegexHelper(mailPattern)
}

let maybeMailAddress = "onev@onevcat.com"

if matcher.match(maybeMailAddress) {
    print("有效的邮箱地址")
}

这是一个简单的匹配判断,而我需要的是将匹配的结果返回,于是我参照喵神的代码和看 NSRegularExpression 文档 写了一个函数:

func matches(for regex: String, in text: String) -> [String] {
    do {
        let regex = try NSRegularExpression(pattern: regex, options: .caseInsensitive)
        let nsString = text as NSString
        let results = regex.matches(in: text, options: [], range: NSMakeRange(0, nsString.length))
        return results.map {
            nsString.substring(with: $0.range)
        }
    } catch let error {
        print("invalid regex : \(error.localizedDescription)")
        return []
    }
}

用法如下:

let matched = matches(for: "([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})", in: "xxxxx@gmail.com,sss@gmail.com")
print(matched) 
// ["xxxxx@gmail.com", "sss@gmail.com"]

这只是一个函数,如果有更高或者更灵活的需求,那么可以对 String
进行一个扩展,就可以直接对字符串进行操作。这个函数写的就不如喵神的有更好的扩展性,但能满足一般使用了。

类似 C 语言文件操作

如果想体验用 Swift 写类似 C 语言那种文件操作,那就有很多种方法。这里我不过多进行总结,可以自行谷歌,这里我放一个有关使用FileManager,FileHandle等类来实现的方法博客。

Swift - 文件,文件夹操作大全 这个博客可以说是总结很全了,也很详细。虽然代码是 Swift3.0 的,也使用于 Swift4.0 版本(不得不说 Swift 版本更新太坑了……)。

逐行读取文件

基本上没人会真的需要逐行读取文件,iOS 开发中更没有从文件里逐行读取。但找这个相关功能文档时,发现真的有,方法很多种,都是用 Swift 写 macOS 应用的大神根据 Cocoa Streams 文档写的。

下面的代码出自 osx - 在Swift中逐行阅读文件/ URL 这个论坛。如果有误,希望大家告诉我原地址,再重新标明出处。

下面的方法是原作者根据 NSFileHandle 的相关文档及各种解决方案的启发。并使用了 guard 和新的 Data 值类型。

class StreamReader  {

    let encoding : String.Encoding
    let chunkSize : Int
    var fileHandle : FileHandle!
    let delimData : Data
    var buffer : Data
    var atEof : Bool

    init?(path: String, delimiter: String = "\n", encoding: String.Encoding = .utf8,
          chunkSize: Int = 4096) {

        guard let fileHandle = FileHandle(forReadingAtPath: path),
            let delimData = delimiter.data(using: encoding) else {
                return nil
        }
        self.encoding = encoding
        self.chunkSize = chunkSize
        self.fileHandle = fileHandle
        self.delimData = delimData
        self.buffer = Data(capacity: chunkSize)
        self.atEof = false
    }

    deinit {
        self.close()
    }

    /// Return next line, or nil on EOF.
    func nextLine() -> String? {
        precondition(fileHandle != nil, "Attempt to read from closed file")

        // Read data chunks from file until a line delimiter is found:
        while !atEof {
            if let range = buffer.range(of: delimData) {
                // Convert complete line (excluding the delimiter) to a string:
                let line = String(data: buffer.subdata(in: 0..<range.lowerBound), encoding: encoding)
                // Remove line (and the delimiter) from the buffer:
                buffer.removeSubrange(0..<range.upperBound)
                return line
            }
            let tmpData = fileHandle.readData(ofLength: chunkSize)
            if tmpData.count > 0 {
                buffer.append(tmpData)
            } else {
                // EOF or read error.
                atEof = true
                if buffer.count > 0 {
                    // Buffer contains last line in file (not terminated by delimiter).
                    let line = String(data: buffer as Data, encoding: encoding)
                    buffer.count = 0
                    return line
                }
            }
        }
        return nil
    }

    /// Start reading from the beginning of file.
    func rewind() -> Void {
        fileHandle.seek(toFileOffset: 0)
        buffer.count = 0
        atEof = false
    }

    /// Close the underlying file. No reading must be done after calling this method.
    func close() -> Void {
        fileHandle?.closeFile()
        fileHandle = nil
    }
}

extension StreamReader : Sequence {
    func makeIterator() -> AnyIterator<String> {
        return AnyIterator {
            return self.nextLine()
        }
    }
}

用法:

if let aStreamReader = StreamReader(path: "/path/to/file") {
    defer {
        aStreamReader.close()
    }
    while let line = aStreamReader.nextLine() {
        print(line)
    }
}

真心感谢原作者提供的方法,让我免去了重复造轮子的时间。

后记

可以看出使用 Swift 进行文件操作是很不方便的。如果写的够多,并熟悉 Cocoa 框架的人来说,并没有多难。

正则和文件操作对 iOS/macOS 开发来说用的地方并不多,真正需要的地方也是很简单。如果再遇到这种实验课程,还是用 Java 吧!毕竟类多,方法多。