内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

Swift正则表达式与爬虫详解

2017-10-11 13:08 出处:清屏网 人气: 评论(0

这段时间在自己的项目中需要写一个爬虫组件,于是最先使用了 WebView 注入 JS 代码解析 DOM 结构获取想要的数据,后来发现太不给力,因为有时候某个图片链接资源等久久不能加载,速度不行。

后来在朋友的帮助下知道了正则表达式解析,直接 GET 请求发出去,得到的网页数据之间转换成文本再用正则表达式解析,经过查资料发现 Swift 居然没有支持正则表达式的方法,幸亏 OC 的 NSString 有支持正则表达式,那就写一个桥接吧!

下面我将做一个 Demo 给大家展示我是如何使用正则表达式解析网页文本数据的,那些想要上手正则表达式的童鞋参考!

需求目标

我们要解析的是这个网页

抓取的是电影的信息,我这里就抓取演员的信息,不过你可以看到 演员的信息的 HTML 代码是这样子的

很明显是不能直接通过 DOM 获取到各个演员标签的值的, 那么经过我们分析要一下步骤解析:

  • 先截取所有主演信息

  • 再截取每个主演的名字

好的,那对应的正则表达式是

  • ◎主s+演[^◎]+

  • s*[^>]+

乍一看跟天书一样,不急,我们一点一点分析,首先看第一句 HTML 代码

◎主  演 山新 Shan Xin
      郝祥海 Xianghai Hao
      李姝洁 Jill Lee
      藤新 Xin Teng
      图特哈蒙 Sheng Feng
      李佳怡 Jiayi Li
      C小调
      九孔 Kong Jiu
      卢恒宇 Henry Lu
      宝木中阳 Bao Mu Zhong Yang
 
◎简  介

主演包含所有的信息在 ◎主 演 和 ◎简 介 之间,我们可以看成 ◎主 演 和 ◎,那如何表示 ◎主 演 呢?

◎主s+演

s 代表一个空白符,s+ 代表一个或多个空白符,整句话的意思是 ◎主 与 演 之间有多个空白符的字符串

接下来如何表示内容和结尾呢?我们可以看到 ◎ 就是主演和简介的分割字符,而中间没有任何 ◎ 字符,所以表示内容和结尾的正则表达式是

[^◎]+

[^◎] 是指非 ◎ 的任意一个字符,[^◎]+是指一个或多个非 ◎ 字符,这样它就只匹配到后面字符串第一个 ◎ 之前的内容,开头+内容+结尾 连起来就是

◎主s+演[^◎]+

当我们截取到主演内部的 HTML 代码时,也就是如下代码

山新 Shan Xin
     郝祥海 Xianghai Hao
     李姝洁 Jill Lee
     藤新 Xin Teng
     图特哈蒙 Sheng Feng
     李佳怡 Jiayi Li
     C小调
     九孔 Kong Jiu
     卢恒宇 Henry Lu
     宝木中阳 Bao Mu Zhong Yang

对于每个演员的名字是通过

来换行分割的,不过前面还有很多空白符,这样我们就得到了解析每个演员的正则表达式

s*[^>]+

名字前面是0个或多个空白符,所以用 s* 表示

名字是中文+英文+空格+特殊符号,其实我们不用一个一个适配对应的正则符号,只要不是<符号就是演员的名字,所以演员名字的正则式就是 [^>]+ 多个非 > 符号

这个演员名字的结尾就是

, w+ 代表一个或多个字符对应 br,得到结尾的正则式是 ,连起来就是

s*[^>]+

如何写程序?

我们知道正则式后如何写这个爬虫程序呢?首先创建一个 Mac App 工程,然后打开访问网络、HTTP访问权限

接着在 ViewController 编写如下代码
/// 顶层标签结构,包含标签名+属性名
struct ParserTagRule {
    var tag : String
    var isTagPaser : Bool
    var attrubutes : [ParserAttrubuteRule]
    var inTagRegexString : String
    var hasSuffix : String?
    var innerRegex : String?
    var prefix : String {
        return isTagPaser ? "":inTagRegexString
    }
    var suffix : String {
        return isTagPaser ? "":(hasSuffix ?? "")
    }
    var regex : String {
        return "(prefix)(innerRegex ?? "[\s\S]*?")(suffix)"
    }
}
/// 属性结构
struct ParserAttrubuteRule {
    var key : String
    var prefix : String {
        return "(key)=""
    }
    var suffix : String {
        return """
    }
    var regex : String {
        return "(prefix)[^"]*(suffix)"
    }
}
/// 解析结果,内层字符串+抓取的属性名
struct ParserResult {
    var innerHTML : String
    var attributes : [String:String]
}
struct RegexRule {
    /// 主演列表
    static let mainActor = ParserTagRule(tag: "", isTagPaser: false, attrubutes: [], inTagRegexString: "◎主[\s]+演", hasSuffix: "◎", innerRegex: "[^◎]+")
    /// 主演名称
    static let singleActor = ParserTagRule(tag: "", isTagPaser: false, attrubutes: [], inTagRegexString: "\s*", hasSuffix: "", innerRegex: "[^>]+")
}
extension ViewController {
    func fetchData(url: String) {
        guard let realURL = URL.init(string: url) else {
            return
        }
        let request = browserRequest(url: realURL)
        let task = URLSession.shared.dataTask(with: request) { (data, response, err) in
            if let e = err {
                DispatchQueue.main.async {
                    self.textView.string = "(e)"
                }
                return
            }
            /// GBK编码,非UTF8编码
            let enc = CFStringConvertEncodingToNSStringEncoding(UInt32(CFStringEncodings.GB_18030_2000.rawValue))
            guard let realData = data, let html = String(data: realData, encoding: String.Encoding(rawValue: enc)) else {
                DispatchQueue.main.async {
                    self.textView.string = "网页数据为空或数据错误"
                }
                return
            }
            let actorRule = RegexRule.mainActor
            if let actors = self.parse(string: html, rule: actorRule)?.first {
                guard let list = self.parse(string: actors.innerHTML, rule: RegexRule.singleActor) else {
                    DispatchQueue.main.async {
                        self.textView.string = "解析主演名称失败"
                    }
                    return
                }
                var text = ""
                for (index, item) in list.enumerated() {
                    text += "主演(index+1): (item.innerHTML)
"
                }
                DispatchQueue.main.async {
                    self.textView.string = text
                }
            }   else    {
                print("解析主演列表失败")
            }
        }
        task.resume()
    }
    /// 解析HTML标签
    ///
    /// - Parameters:
    ///   - string: HTML字符串
    ///   - rule: 解析规则,如正则表达式
    /// - Returns: 解析结果,包含标签内部HTML代码和属性值
    func parse(string: String, rule: ParserTagRule) -> [ParserResult]? {
        var results = [ParserResult]()
        do {
            let tagRegex = try NSRegularExpression(pattern: rule.regex, options: .caseInsensitive)
            let result = tagRegex.matches(in: string, options: NSRegularExpression.MatchingOptions.init(rawValue: 0), range: NSMakeRange(0, (string as NSString).length))
            let tagPrefixPrint = rule.isTagPaser ? "+++ tag +++":"--- no tag ---"
            if result.count > 0 {
                for checkingRes in result {
                    var range = checkingRes.range
                    range.length -= (rule.suffix as NSString).length
                    let str = (string as NSString).substring(with: range)
                    var result = ParserResult(innerHTML: "", attributes: [:])
                    let titleRegex = try NSRegularExpression(pattern: rule.prefix, options: .caseInsensitive)
                    if let first = titleRegex.firstMatch(in: str, options: NSRegularExpression.MatchingOptions.init(rawValue: 0), range: NSMakeRange(0, (str as NSString).length)) {
                        var subRange = range
                        subRange.location = first.range.length
                        subRange.length = (str as NSString).length - subRange.location
                        print("(tagPrefixPrint) innerHTML: ((str as NSString).substring(with: subRange))")
                        result.innerHTML = (str as NSString).substring(with: subRange)
                    }
                    if rule.isTagPaser {
                        var attrs = [String:String]()
                        for attr in rule.attrubutes {
                            let attrRegex = try NSRegularExpression(pattern: attr.regex, options: .caseInsensitive)
                            if let attrResult = attrRegex.firstMatch(in: str, options: NSRegularExpression.MatchingOptions.init(rawValue: 0), range: NSMakeRange(0, (str as NSString).length)) {
                                var subRange = attrResult.range
                                subRange.location += (attr.prefix as NSString).length
                                subRange.length -= (attr.prefix as NSString).length + 1
                                print("attribute: (attr.key)
value: ((str as NSString).substring(with: subRange))")
                                attrs[attr.key] = (str as NSString).substring(with: subRange)
                            }
                        }
                        result.attributes = attrs
                    }
                    results.append(result)
                }
            }   else    {
                print("未查找到内容模块: (rule.regex)")
            }
            return results
        } catch  {
            print(error)
            return nil
        }
    }
    /// 模仿浏览器URL请求
    ///
    /// - Parameter url: URL对象
    /// - Returns: URLRequest请求对象
    func browserRequest(url : URL) -> URLRequest {
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.addValue("zh-CN,zh;q=0.8,en;q=0.6", forHTTPHeaderField: "Accept-Language")
        request.addValue("Refer", forHTTPHeaderField: "http://www.dytt8.net")
        request.addValue("1", forHTTPHeaderField: "Upgrade-Insecure-Requests")
        request.addValue("max-age=0", forHTTPHeaderField: "Cache-Control")
        request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
        request.addValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36", forHTTPHeaderField: "User-Agent")
        request.addValue("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", forHTTPHeaderField: "Accept")
        return request
    }
}

没错,这代码很长! 核心方法是

func parse(string: String, rule: ParserTagRule) -> [ParserResult]?

string 参数是获取的 HTML 字符串,我们直接传整个网页进去;rule 包含了各项正则信息,前面我们之所以分析头部+中部+尾部的正则表达式是因为上面的两个完整正则表达式截取到的字符串是需要去头和去尾的,隐藏头尾的正则表达式是必须的!

上面的代码还支持 HTML 标签里面属性值得获取,接着我们使用 IB 在 Main.Stroyboard 里面的 ViewController 上添加一个 NSTextView,拉线 IBOutlet 到 ViewController.swift 里面, 这里我们命名为 textView

在 ViewController.swift 添加如下代码

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    fetchData(url: "http://www.dytt8.net/html/gndy/dyzz/20170930/55154.html")
}

接着运行程序,你就会看到如下结果

完全解析出各个演员的名字信息!当然 还分中英文,这里我们就略过了,根据上面的经验继续在解析也是可以的。

美中不足的是还是碰了 OC 的荤, 期待 Swift 的正则表达式原生支持!


分享给小伙伴们:
本文标签: Swift正则表达式爬虫

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号