美文网首页Swift编程Swift Advance
Swift 最简单的方式来解析HTML

Swift 最简单的方式来解析HTML

作者: Hellolad | 来源:发表于2018-05-13 02:54 被阅读43次

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标签:


屏幕快照 2018-05-13 上午1.41.43.png

在代码里就这么简单就可以了,看下图已经得到了div中间的内容:

var parser = SwiftParser(str)
parser.parse(.div, "dropdown")
print(parser.sources)

具体逻辑

  1. 这基本上是一个标签的逻辑代码
/// 通过正则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))
  1. 我们看看怎么用状态实现一个假的协程
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

相关文章

网友评论

    本文标题:Swift 最简单的方式来解析HTML

    本文链接:https://www.haomeiwen.com/subject/xrskdftx.html