项目简介
一个字符串正则校验工具类,使用Swift中的enum实现并使用。
本文的思路亮点在于从Python使用的过程中,跳跃到Swift中的脑洞折腾方式,难点在于如何写出更Swifty的编码。
Python和Swift,API上的远方亲戚嘛。
项目背景
做前端和App端一般都会涉及表单输入,校验手机号、邮箱等等一些输入字符串的校验,大多数时候,我们需要配合正则表达式加上系统层的API去做这些。
而我要做的,就是封装一个使用简单,一目了然的正则校验工具类。
实践过程
从一个Python爬虫说起
一切源于我在学习Python写爬虫,想要获取一些有用的信息。
嗯,你问我为啥明明写Swift的,怎么就变成从Python开始说起,因为我就是这么脑袋一抽风,编程脑洞这么顺手牵羊过来的。
嗯,你问我写爬虫爬什么东东?学Python就没干过什么正经事,你瞧隔壁这位:偷偷告诉你中国小姐姐的真实Size!!。
而使用Python写最简单的爬虫,是通过正则表达式来获取关键信息的。我们举个例子:
# 系统层级的判断正则表达式的类
import re
# 大名鼎鼎的网络请求库
import requests
def getPages() -> int:
# ¸
url = "有效网址"
# 请求回来的响应
response = requests.get(url)
# 响应的html字符串
html = response.text
# 获取网页字符串中匹配的信息
result = re.findall(r'>第 1 页,共 ([1-9]\d+) 页</span>', html)
# 返回结果的第一次出现的值,也就是总页数
return int(result[0])
这段代码里面除去网络请求,最有价值的代码下面这段:
result = re.findall(r'>第 1 页,共 ([1-9]\d+) 页</span>', html)
通过re的findall函数,输入一个正则表达式与需要校验的字符串,去匹配结果集并以list的形式返回。
分析Python中的re中的源码
来,我们先看看findall这个函数的源码,另外我觉得findall这个函数名一点也不Pythonic,你给我们整个find_all或者findAll都比这个好呀。
def findall(pattern, string, flags=0):
"""Return a list of all non-overlapping matches in the string.
If one or more capturing groups are present in the pattern, return
a list of groups; this will be a list of tuples if the pattern
has more than one group.
Empty matches are included in the result."""
return _compile(pattern, flags).findall(string)
由于Python编写函数的时候,是可以不带参数类型的,所以可能看起来有点懵圈,我简单分析一下:
findall有三个参数:
pattern是str类型,为正则表达式,你会好奇为啥我写的入参字符串是这样:r'>第 1 页,共 ([1-9]\d+) 页</span>'
,这个r‘’
表示的字符串原意表达式,我们都知道,html中有很多标签,会有\这样的字符串,如果不使用r''
包裹起来,对于\这类字符串其实是需要转译的,\\
才表示\
不了解的可以度娘一把:转义字符
string是str类型,为需要验证的字符串
flags本质上一个枚举入参,默认给的0。
class RegexFlag(enum.IntFlag):
ASCII = sre_compile.SRE_FLAG_ASCII # assume ascii "locale"
IGNORECASE = sre_compile.SRE_FLAG_IGNORECASE # ignore case
LOCALE = sre_compile.SRE_FLAG_LOCALE # assume current 8-bit locale
UNICODE = sre_compile.SRE_FLAG_UNICODE # assume unicode "locale"
MULTILINE = sre_compile.SRE_FLAG_MULTILINE # make anchors look for newline
DOTALL = sre_compile.SRE_FLAG_DOTALL # make dot match newline
VERBOSE = sre_compile.SRE_FLAG_VERBOSE # ignore whitespace and comments
A = ASCII
I = IGNORECASE
L = LOCALE
U = UNICODE
M = MULTILINE
S = DOTALL
X = VERBOSE
# sre extensions (experimental, don't rely on these)
TEMPLATE = sre_compile.SRE_FLAG_TEMPLATE # disable backtracking
T = TEMPLATE
DEBUG = sre_compile.SRE_FLAG_DEBUG # dump pattern after compilation
我们可以用re.I作为最后一个入参,表示在正则查找中不在意大小写。
_compile(pattern, flags).findall(string),最后的这个方法已经涉及到其内部方法了,大致意思就说通过pattern与flag生成一个专门的正则匹配类,然后再去处理需要验证的字符串,通过注释,我们知道返回结果是一个list。
分析Swift中的NSRegularExpression类
你看我一上来,不务正业的巴拉巴拉说了一通Python,甚至连Python的源码都分析了一遍,为何?
在我学习编程语言的时候,我经常会发出这样的感慨,这个函数真好用,哎,为啥Swift中就没有这个轮子呢?如果没有,可不可以自己尝试写呢?
我最初的一个想法是Python中有re模块处理正则,那么Swift也会不会有类型的类呢?仔细一查,是有的————NSRegularExpression。
为了类比Python中的re,我贴一下NSRegularExpression的一些重要函数和参数枚举。
@available(iOS 4.0, *)
open class NSRegularExpression : NSObject, NSCopying, NSSecureCoding {
public init(pattern: String, options: NSRegularExpression.Options = []) throws
}
extension NSRegularExpression {
open func matches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange) -> [NSTextCheckingResult]
}
extension NSRegularExpression {
public struct Options : OptionSet {
public init(rawValue: UInt)
public static var caseInsensitive: NSRegularExpression.Options { get }
public static var allowCommentsAndWhitespace: NSRegularExpression.Options { get }
public static var ignoreMetacharacters: NSRegularExpression.Options { get }
public static var dotMatchesLineSeparators: NSRegularExpression.Options { get }
public static var anchorsMatchLines: NSRegularExpression.Options { get }
public static var useUnixLineSeparators: NSRegularExpression.Options { get }
public static var useUnicodeWordBoundaries: NSRegularExpression.Options { get }
}
public struct MatchingOptions : OptionSet {
public init(rawValue: UInt)
public static var reportProgress: NSRegularExpression.MatchingOptions { get }
public static var reportCompletion: NSRegularExpression.MatchingOptions { get }
public static var anchored: NSRegularExpression.MatchingOptions { get }
public static var withTransparentBounds: NSRegularExpression.MatchingOptions { get }
public static var withoutAnchoringBounds: NSRegularExpression.MatchingOptions { get }
}
}
使用init(pattern: String, options: NSRegularExpression.Options = [])
函数构造一个NSRegularExpression对象,传入的参数有两个pattern: String
和options: NSRegularExpression.Options = []
,分别是正则表达式和选项。
然后使用matches(in string: String, options: NSRegularExpression.MatchingOptions = [], range: NSRange)
方法去获取匹配的结果。
从Python到Swift,实现同一个函数有多远?
不远,也就换些API,从Pythonic到Swifty一点。
在iOS中,如果我们想要像Python一样爬一个网页,我们应该如何处理呢?
func getPages() -> Int? {
let urlString = "有效网址"
/// 网址字符串转URL
guard let url = URL(string: urlString) else {
return nil
}
/// 获取网址的html
guard let html = try? String(contentsOf: url, encoding: .utf8) else {
return nil
}
/// Swift5特性,在##之间的字符串,可以不用\对特殊符号进行转义了
let pattern = #">第 1 页,共 ([1-9]\d+) 页</span>"#
/// 构造NSRegularExpression对象
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
/// 获取结果集
let result = regex.matches(in: html, options: [], range: NSRange(location: 0, length: html.count))
/// 获取第一个结果
guard let firstResutl = result.first else {
return nil
}
/// 通过一个结果的range去截取html上的有效字符串
let num = (html as NSString).substring(with: firstResutl.range)
/// 字符串转Int
return Int(num)
}
对比Python的爬虫写法,也许你会觉得Swift感觉很复杂,那是因为代码的严谨性,在创建URL的构造函数、NSRegularExpression的构造函数返回的都是可选类型,而且获取html字符串也是返回的可选类型,结果集result的第一个元素也有可能是为nil,所以使用了大量的guard去做守护操作。
基于上面的代码,你需要了解到:
-
Swift内置的方法也是可以写爬虫的。
String(contentsOf: url, encoding: .utf8)
-
Swift中对于原意字符串也有支持的,字符串两端用
##
括起来即可。 -
Swift虽然写起来麻烦,不过考虑对于安全性和健壮性,是可以接受的。
如果你对Swift的解包有疑惑,可以转移到我此前写的这篇文章:Swift:解包的正确姿势
在Swift中,我们正则匹配字符串的核心代码在这里:
/// 构造NSRegularExpression对象
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
/// 获取结果集
let result = regex.matches(in: html, options: [], range: NSRange(location: 0, length: html.count))
是不是和Python的re.findall函数内部实现函数_compile(pattern, flags).findall(string)
非常类似呢?
到此,涉及Swift中通过正则表达式获取有效信息的类和方法就基本确定。下面就让我们进入正题,写一个Swifty的Regular工具类。
编写更Swifty的Regular工具类
想到工具类,我们总是想着以Utils,虚基类,没有构造方法,静态方法等等,以下省略500字:
class RegularUtils {
/// 是否匹配电话号码
///
/// - Parameters:
/// - pattern: 正则表达式字符串
/// - string: 待验证的字符串
/// - Returns: Bool
static func isContainPhoneNum(pattern:String, string: String) -> Bool {}
.
.
.
static func isContainEmail(pattern:String, string: String) -> Bool {}
.
.
.
private init() {}
}
上面的代码当然没有问题,我们甚至可以继续扩展类方法,字符串中是否包含邮箱,包含车牌号等等。
既然使用Swift编码,难道就不能写的更Swifty一点吗?
就如文章一开始所说,我们可能需要校验的类型很多,手机号、邮箱、车牌号等等,每一个类型对于的正则也不同,有没有想过用Swift的枚举来构造这个工具类?
使用枚举优势如下:
-
区分不同的校验类型,
-
通过Swift枚举带参的优势,直接将需要校验的字符串传入,
-
Swift的枚举本质上就是一个struct,所以可以添加方法、属性。
那么,就上一点enum写这个工具的思路吧:
首先突破对枚举只能表示状态的思维,带参数,带多个参数都是可以的。
枚举中有一个custom情况,相当于自定义正则类型,并与其待校验的字符串做匹配。
/// 正则表达式的判断
/// 这些枚举中的String都是待验证的字符串
/// - email: 邮箱
/// - phoneNum: 手机号
/// - carNum: 车牌号
/// - username: 用户名
/// - password: 密码
/// - nickname: 昵称
/// - checkCode: 验证码
/// - URL: url
/// - IP: ip地址
/// - idCard: 身份证
/// - custom: 自定义,注意custom枚举 其中有两个字符串, 第一个是待验证的字符串,第二个是正则表达式
public enum Regular {
case email(String)
case phoneNum(String)
case carNum(String)
case username(String)
case password(String)
case nickname(String)
case checkCode(String)
case URL(String)
case IP(String)
case idCard(String)
case custom(_ string: String, _ predicateString: String)
}
对于一些固定正则式,我们可以在这个枚举中先声明:
extension Regular {
/// 样式
private struct Pattern {
static let email = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
static let phoneNum = "^1[0-9]{10}$"
static let carNum = "^[A-Za-z]{1}[A-Za-z_0-9]{5}$"
static let username = "^[A-Za-z0-9_-]{6,20}+$"
static let password = "^[a-zA-Z0-9]{8,16}+$"
static let nickname = "^[\\u4e00-\\u9fa5]{4,8}$"
static let url = "^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"
static let ip = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
static let checkCode = "^[0-9]{4}$"
static let idCard = "^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9Xx])$"
}
}
再来,我们将涉及NSRegularExpression与返回NSTextCheckingResult结果的方法做简单的封装:
extension Regular {
/// 通过正则表达式获取一个NSRegularExpression
///
/// - Parameter pattern: 正则表达式
/// - Returns: NSRegularExpression
private func regex(pattern: String, options: NSRegularExpression.Options = []) -> NSRegularExpression? {
return try? NSRegularExpression(pattern: pattern, options: options)
}
/// 是否有匹配结果
///
/// - Parameters:
/// - regex: NSRegularExpression
/// - string: 字符串
/// - Returns: Bool
private func isMatch(regex: NSRegularExpression?, string: String?) -> Bool {
guard let regex = regex, let string = string else {
return false
}
return regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)).count > 0
}
}
最后,我们使用一个isMatch只读计算属性来完成这个正则表达类中的一个功能,用了返回字符串中是否有匹配内容:
extension Regular {
public var isMatch: Bool {
let re: NSRegularExpression?
let string: String
switch self {
case let .email(str):
re = regex(pattern: Pattern.email)
string = str
case let .phoneNum(str):
re = regex(pattern: Pattern.phoneNum)
string = str
case let .carNum(str):
re = regex(pattern: Pattern.carNum)
string = str
case let .username(str):
re = regex(pattern: Pattern.username)
string = str
case let .password(str):
re = regex(pattern: Pattern.password)
string = str
case let .nickname(str):
re = regex(pattern: Pattern.nickname)
string = str
case let .URL(str):
re = regex(pattern: Pattern.url)
string = str
case let .IP(str):
re = regex(pattern: Pattern.ip)
string = str
case let .checkCode(str):
re = regex(pattern: Pattern.checkCode)
string = str
case let .idCard(str):
re = regex(pattern: Pattern.idCard)
string = str
case .custom(let str, let pattern):
re = regex(pattern: pattern)
string = str
}
return isMatch(regex: re, string: string)
}
使用的时候,我们这样写代码就可以啦:
let isEmail = Regular.email("season@qq.com").isMatch // true
let isContain = Regular.custom("html字符串", #">第 1 页,共 ([1-9]\d+) 页</span>"#).isMatch // false
思考总结
-
从Python到Swift,编程的思维即使是跨语言,很多时候也不会跨思路,在掌握一门编程语言的同时,有空去看看其他编程世界的精彩,在启发和拓展上往往会有意想不到的的收获。
-
代码想要写的更Swifty,并不是一朝一夕就从脑袋里崩出来的,从Utils工具类到enum工具类,是大量codeReview和学习的结果,多思考,多总结,才能将代码写的更Swifty。
-
合理的使用系统提供的轮子和方法,加以封装使用,会让编码愉快。有的时候,折腾很有必要。
-
Swift中枚举,比其他语言的枚举强大很多,善于使用枚举特性,带参特性,有的时候会有出人意料的效果。
-
Swift可以写爬虫的,虽然没有Python里的bs4,没有lxml,但是只有正则表达式使用的溜,也就是语言差异而已。
-
编程语言没有特定的墙,你所谓的墙都是自己给自己界定的。
参考文章
本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情