Swift 的正则表达式与 I/O 处理总结

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

正则表达式

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 文档 写了一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 []
}
}

用法如下:

1
2
3
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
进行一个扩展,就可以直接对字符串进行操作。这个函数写的就不如喵神的有更好的扩展性,但能满足一般使用了。

I/O 操作

类似 C 语言文件操作

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

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

逐行读取文件

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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()
}
}
}

用法:

1
2
3
4
5
6
7
8
if let aStreamReader = StreamReader(path: "/path/to/file") {
defer {
aStreamReader.close()
}
while let line = aStreamReader.nextLine() {
print(line)
}
}

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

后记

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

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