HTMLParser
HTMLParser说白了就是对HTML网页的数据的解析,HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。libxml2在iOS中是解析XML和HTML的利器,当然它也是一个跨平台的库,但是对libxml2我本人研究了一点点,讲真,API真的一头雾水,当前据我了解,最有名的应该是脚本语言的BeautifulSoup,可能技术有限,能了解到的只有这么多。
在iOS开发中,我们其实很少使用对HTML解析,最多的方式是对HTML网页直接使用Web或WK进行展示,这样的好处是开发速度快,但是当我们想要自由的控制该网页中的一些元素,做到动态处理的话,就非常的不好办,不但要和JS交互,甚至可能自己还要去了解JS,HTML,CSS等知识,无疑增加了时间成本。
Github上,我通过搜索HTMLParser之后JAVA有132个repository,而OC+Swift也就仅仅有25个repository,所以做iOS项目时,需要对网页解析的时候,如果不去学习处理的话,直接到Github上找,是一件多么可怕的事情。
开始干活
当然,本篇文章主要是想以最简单的方式去解析HTML,在写这篇文章的时候,我也仅仅对核心的逻辑代码,和其中一部分HTML标签进行了解析和处理,本篇主要是以原理为主,当你掌握了,其他的标签解析就简单了很多。
首先,我的这种解析方式,并不罕见,可以说是个程序员都能想到,但真的花时间去把它写出来真的就少了,其实很简单,就是用字符串的截取做到的。就截取两个字看似简单,其实里面的知识还是很多的。我来列举一下:
- String.Index 获取位置
- Scanner 扫描字符串
- 通过状态判断实现一个假的
协程
来实现函数的自由跳转执行 - 通过正则表达式来匹配标签
- 了解HTML标签对应关系
- 相同标签的嵌套处理
OK,所有核心就在这里了,现在来看看具体逻辑怎么实现:
实现
首先为了简单,我使用了平常我最喜欢去的网站SwiftDoc下手,去做了这件事情,因为它的结构相对于其他的来说会简单一些,但是看了源码发现也是不太简单/(ㄒoㄒ)/~~。源码我就不贴了。
比如说我要,获取到76行到110行的这些东西,并且是div里面包含的所有的button、ul、li标签:

在代码里就这么简单就可以了,看下图已经得到了div中间的内容:
var parser = SwiftParser(str)
parser.parse(.div, "dropdown")
print(parser.sources)

具体逻辑
- 这基本上是一个标签的逻辑代码
/// 通过正则class获取div
/// 通过正则class获取div
let regx = "<\(type.val).*class=\"\(tag)\".*>"
// 获取对应的Range<String.Index>
let rIndex = sources.range(of: regx)
print(sources[rIndex.upperBound..<sources.endIndex])
let start = CFAbsoluteTimeGetCurrent()
// 获取Range里最右边的值
let front = rIndex.upperBound
// 从loop中获取最后我们截取到的</div>的位置
let behindEndedOffset = _findLoop(rIndex.upperBound.encodedOffset, type)
let end = CFAbsoluteTimeGetCurrent()
print(rIndex.lowerBound.encodedOffset, behindEndedOffset, "time: \(end-start)")
let behind = String.Index(encodedOffset: behindEndedOffset)
let rangeIndex = Range(front..<behind)
let container = sources[rangeIndex]
/// 最后获取到我们想要的内容
print(Substring(container))
- 我们看看怎么用状态实现一个假的
协程
private var findFrontCount = -1 // 找到了几个tag -front
private var findLastLocation = -1 // 最后一个对应的前缀在哪个位置
private var isFindFront = true // 查找front是否已经被找到
private var isFindBehind = true // 查找behind是否已经被找到
private mutating func _findLoop(_ location: Int,
_ type: SwiftParserEnum) -> Int {
scanner = Scanner(string: sources)
scanner.scanLocation = location
_findFront()
if findLastLocation != -1 {
return findLastLocation
}
return -1
}
private mutating func _findFront() {
let value = "<\(type.val)"
var isRecursive = false // 是否自己location+1之后又递归一次 是的话就不在递归
func loop() {
while !scanner.isAtEnd {
print("scanner.scanLocation isAtEnd", scanner.scanLocation)
print("scources.count", sources.count)
let bool = scanner.scanString(value, into: nil)
if bool {
// 如果找到+1
self.findFrontCount += 1
// 设置front为true 表示找到了
self.isFindFront = true
} else {
// 设置front为false 表示没有找到
self.isFindFront = false
// 如果都没有找到 让扫描器的游标+1开始进行下一个位置的扫描
if !self.isFindFront && !self.isFindBehind {
scanner.scanLocation += 1
print("scanner.scanLocation", scanner.scanLocation)
if !isRecursive {
isRecursive = true
loop()
}
}
_findBehind()
}
}
}
loop()
}
private mutating func _findBehind() {
let value = "</\(type.val)>"
var isRecursive = false // 是否自己location+1之后又递归一次 是的话就不在递归
func loop() {
while !scanner.isAtEnd {
let bool = scanner.scanString(value, into: nil)
if bool {
// 如果找到设置behind为true
self.isFindBehind = true
// 如果findfront大于1说明是层级嵌套所以我们要减去1直到找到最外层的的那个和它对应的标签
if findFrontCount > -1 {
findFrontCount -= 1
}
// 如果找到了behind并发现findfront为-1说明已经是最外层了并且可以返回当前的游标
if findFrontCount == -1 {
self.findLastLocation = scanner.scanLocation - value.count
// 最后把游标设置到文档的末尾停止扫描
scanner.scanLocation = sources.count
}
} else {
// 如果没有找到设置为false
self.isFindBehind = false
// 如果都没有找到 让扫描器的游标+1开始进行下一个位置的扫描
if !self.isFindFront && !self.isFindBehind {
scanner.scanLocation += 1
print("scanner.scanLocation", scanner.scanLocation)
if !isRecursive {
isRecursive = true
loop()
}
}
_findFront()
}
}
}
loop()
}
上面的基本就是核心代码了,至于其他的几个标签只要按照正则去做就可以了,下面我们队<li></li>
标签进行过滤,核心代码逻辑不变只要加个枚举就可以了:
var parser = SwiftParser(str)
let result = parser.parse(.div, "dropdown").parse(.ul, "dropdown-menu")
print(result.sources)

可以看出,这种解析速度也是相当快的,速度几乎可以说是毫秒级的,可以忽略不计的。
结语
对于其他的标签<p> <a> <img>...
等还需要再进行验证,所以还需要努力,也许这不是最优Parser,但是是我想到的最简单的并且真的实践出来的Parser。
SwiftParser github地址:https://github.com/hellolad/SwiftParser
网友评论