Swfit爬虫通过作者ID无接口获取简书文章列表,正则匹配HTML标签存储模型数据

上篇文章写过Python爬虫的方法,用的Scrapy框架。
Python--Scrapy爬虫获取简书作者ID的全部文章列表数据
最近闲来想用Swift写个瀑布流然后展示一些数据,奈何没有测试接口,后来想到可不可自己从HTML网页获取数据并展示出来呢?当然,理论上是可行的,只要拿到HTML源码,通过正则表达式是可以匹配到我们想要的数据的。随后经过断断续续几天的开发,完成了一个小demo,我会分三部分写大家分享,因为其中用到三个独立的技术模块:HTML解析、瀑布流布局、WKWbeView与JS交互。今天我想先讲第一部分。

1 - 效果图

如上篇文章所说,因为本人文章列表数据较少,为了获取多页数据,所以选取了简书一位作者(@CC老师_MissCC
)的ID来进行开发。(如有侵权,可联系删除。)

既然要分析源码,首先我们要获取源码。Swift4.0提供了一个简单的方法,一行代码可以搞定,但是此方法可能会抛出异常,所有我们有必要做下校验,防止崩溃:

    //直接强制解包是不安全的
    //let authorId = "1b4c832fb2ca"
    //var str = try! String(contentsOf:URL.init(string: "//www.greatytc.com/u/\(authorId)?page=1")!, encoding: .utf8)
    do {
            //作者ID
            let authorId = "1b4c832fb2ca"
            //获取HTML源码(先获取第一页),此方法获取的是PC版的源码并不是移动端的
            var str = try String(contentsOf:URL.init(string: "//www.greatytc.com/u/\(authorId)?page=1")!, encoding: .utf8)
            print(str)
    } catch {
            print(error)
    }

控制台打印因为是格式化输出,看不到"\n"换行符,但是打断点可以看出,为了后面正则匹配方便无误,我们去掉所有的换行符和空格:


2 - 格式化输出HTML源码
    //剔除换行符和空格
    str = str.replacingOccurrences(of: "\n", with: "")
    str = str.replacingOccurrences(of: " ", with: "")

1.获取头部信息

源码拿到了,先获取头部信息,Chrome浏览器浏览器打开URL,鼠标放在头像位置右击,呼出菜单点击“检查”:


3 - 检查元素

鼠标在源码适当移动,当选中到整个我们需要的头部区域时停止,不难看出标签"<div class="main-top">...</div>"包含的信息是我们需要分析的:


4 - 头部信息

通过正则表达式拿到这些标签内容:
      let headTop =  "<divclass=\"main-top\">(.*?)</div><ulclass=\"trigger-menu\""
      //获取头部div标签数据
      let topInfo:String = self.extractStr(str, headTop)

有人可能会问为什么用</div>结尾不就行了,后面又接"<ulclass="trigger-menu""是什么鬼?因为头部信息中还包含有div元素,不加后面临近的ul标签的话,只会匹配到最近一个div结尾的元素,造成少匹配数据。所以具体问题具体分析。
下面给出两个Swift的两正则匹配获取字符串的方法,一个是获取单条数据的,一个是获取多条数据的,大家可以根据实际情况灵活选取:

     //MARK: - --- 根据正则表达式提取字符串(获取单条)
    static func extractStr(_ str:String, _ pattern:String) -> String{
        
        do{
            let regex = try NSRegularExpression(pattern: pattern , options: .caseInsensitive)
            
            let firstMatch = regex.firstMatch(in: str, options: .reportProgress, range: NSMakeRange(0, str.count))
            if firstMatch != nil {
                let resultRange = firstMatch?.range(at: 0)
                let result = (str as NSString).substring(with: resultRange!)
                //print(result)
                return result
            }
        }catch{
            print(error)
            return ""
        }
        return ""
    }
    
    //MARK: - --- 根据正则表达式提取字符串(获取多条)
    static func regexGetSub(_ pattern:String, _ str:String) -> [String] {
        var subStr = [String]()
        
        do {
            let regex = try NSRegularExpression(pattern: pattern, options:[NSRegularExpression.Options.caseInsensitive])
            let results = regex.matches(in: str, options: NSRegularExpression.MatchingOptions.init(rawValue: 0), range: NSMakeRange(0, str.count))
            //解析出子串
            for  rst in results {
                let nsStr = str as  NSString  //可以方便通过range获取子串
                subStr.append(nsStr.substring(with: rst.range))
            }
        }catch{
            print(error)
            return [""]
        }
        return subStr.count == 0 ? [""]:subStr
    }

上面的定义的属性topInfo正则匹配得到的字符串就是我们要的头部HTML内容,从中我们可以拿到头像、用户名、性别、关注数、粉丝数、文章数等全部信息。正则表达式就不一一分析了,可以有多种写法。

值得注意的是:你可能会先在在线工具上先测试再用在项目中,但是往往可能在上面测试是好的,可是项目中却匹配不出来,那你就要考虑换一种写法。

因为正则表达式不熟,笔者下面用到的类似写法是试了多次才试出来的。大家可以参考,如果有更好的写法,你也可以写自己的,这不是固定的,达到匹配的目的就行。熟悉正则表达式的朋友应该很容易就能拿到自己想要的数据。
下面贴上获取头部各参数信息的代码:

                //获取头部div标签数据
                let headTop =  "<divclass=\"main-top\">(.*?)</div><ulclass=\"trigger-menu\""
                let topInfo:String = self.extractStr(str, headTop)

                //获取头像url
                let headImagRegex = "(?<=aclass=\"avatar\"href=\".{0,200}\"><imgsrc=\")(.*?)(?=\"alt=\".*?\"/></a>)"
                let headImge = self.extractStr(topInfo, headImagRegex)

                //用户名
                let nameRegex = "(?<=aclass=\"name\"href=\".{0,200}\">)(.*?)(?=</a>)"
                let name = self.extractStr(topInfo, nameRegex)
                
                //性别
                let sexRegex = "(?<=iclass=\"iconfontic-)(.*?)(?=\">.*?</i>)"
                let sex = self.extractStr(topInfo, sexRegex)

                //[关注,粉丝,文章,字数,收获喜欢] 。 li标签一般是多个,匹配出来自然是数组
                let infoListRegex = "(?<=li><divclass=\"meta-block\">.{0,200}<p>)(.*?)(?=</p>.*?</li>)"
                let infoList = self.regexGetSub(infoListRegex, topInfo)

                //总页数(PC默认每页9个数据,所以可以通过文章总数计算总页数)
                let articleCount = Int(Double((infoList[2]))!)
                let totalPage = articleCount % 9 > 0 ? (articleCount / 9 + 1) : articleCount / 9
                
                //个人介绍
                let introRegex = "(?<=divclass=\"js-intro\">)(.*?)(?=</div>)"
                var intro = self.regexGetSub(introRegex, str)[0]
                intro = intro.replacingOccurrences(of: "<br>", with: "\n")

                //计算头部高度(这个高度是下篇文章瀑布流要用到的collocationView的头部高度,包含每个元素的高度及其间隙,看header的xib布局就知道每个数字代表的意思了。这里大家可以跳过。)
                let headerH = 10 + 60 + 5 + 12 + 8 + GETSTRHEIGHT(fontSize: 11, width: CGFloat(SCREEN_WIDTH - (10 + 30 + 15 + 10)) , words: intro) + 10 + 1
                //返回头部信息(存入自定义元组:typealias Yuanzu = (headImge: String, name: String, sex:String, infoList: Array<String>, totalPage: Int, intro: String, headerH:CGFloat))
                let headCallBackInfo = (headImge:headImge, name:name, sex:sex, infoList:infoList, totalPage:totalPage, intro:intro, headerH:headerH)

代码中有些宏和方法可能没有展示出来,但是是有关联的,要查看他们联系或者为了不报错,可以下载我放在GitHub的源码

2.获取列表数据

接下来分析列表数据,这就是我们主要要展示的有规律的数据,分析HTML源码可以看出,列表数据所在的ul标签下有多个li标签。我们通过URL加页码page字段请求返回的只有9个数据,但是直接在浏览器看是动态加载的远不止9个一直往下滑会一直加载,这个我们不用理会,只要知道每个li标签下对应的数据结构是一样的有规律的就行。也好为我们后面用一个正则表达式获取多条数据做铺垫。首先,拿到包裹li标签的ul标签下的字符串:


5 - 拿到包括ul标签的所有li标签字符串
    //列表数据
    let articleListStrRegex = "<ulclass=\"note-list\"infinite-scroll-url=\".*?\">(.*?)</ul>"
    //获取文章列表ul标签数据
    let articleListStrArr = self.regexGetSub(articleListStrRegex, str)
    let articleListStr = articleListStrArr[0]
    //单条数据正则
    let liLableRegex = "<liid=(.*?)</li>"
    //匹配获取li标签,得到一个元素不大于9的数组
    let liLableArr = self.regexGetSub(liLableRegex, articleListStr)

    //拿到li标签后,遍历数组liLableArr,遍历时就可以分析每个li标签的数据结构,对应写出我们要拿的每个字段的正则表达式,得到数据,存入模型。
    //单页数据
    var dataArr = [JianshuModel]()  
    //遍历li标签 匹配需要的数据
    for item in liLableArr {
        //print(item)
        //正则 ↓
        let wrapRegex = "(?<=aclass=\"wrap-img\".{0,300}src=\")(.*?)(?=\"alt=\".*?\"/></a>)"
        let articleUrlRegex = "(?<=aclass=\"title\"target=\"_blank\"href=\")(.*?)(?=\">.*?</a><pclass)"
        let titleRegex = "(?<=aclass=\"title\".{0,200}>)(.*?)(?=</a><pclass)"
        let abstractRegex = "(?<=pclass=\"abstract\">)(.*?)(?=</p>)"
        //let readCommentsRegex = "(?<=atarget=\"_blank\".{0,200}></i>)(.*?)(?=</a>)"
        let readRegex = "(?<=atarget=\"_blank\".{0,200}><iclass=\"iconfontic-list-read\"></i>)(.*?)(?=</a>)"
        let commentsRegex = "(?<=atarget=\"_blank\".{0,200}><iclass=\"iconfontic-list-comments\"></i>)(.*?)(?=</a>)"
        let likeRegex = "(?<=span><iclass=\"iconfontic-list-like\"></i>)(.*?)(?=</span>)"
        let timeRegex = "(?<=spanclass=\"time\"data-shared-at=\")(.*?)(?=\"></span>)"
        
        //数据模型
        let model = JianshuModel()
        
        //封面(可能有文章没有封面) 获取的图片URL最后面类似"w/300/h/240"代表长宽,修改长宽如"w/600/h/480"可得到2倍尺寸的图片,清晰度相应提高,反之亦然。假如超过原图长或宽的尺寸就会显示原图
        model.wrap = self.regexGetSub(wrapRegex, item)[0]
        model.imgW = itemWith - 16
        //如果长度大于0个字符
        if model.wrap!.count > 0  {
            //此步是为了按比例缩放图片,但是发现所有的图片都是 宽 * 120 / 150 ,所以可不写这步直接通过宽计算高即可
            //后来(也就是下篇文章我们将瀑布流的时候)cell赋值发现SDWebImage拿不到图片,必须用原图,也就是model.wrap中"?"之前的部分
            let temp1 = self.matchingStr(str: model.wrap!)
            var temp2 = temp1.replacingOccurrences(of: "w/", with: "")
            temp2 = temp2.replacingOccurrences(of: "/h/", with: " ")
            let tempArr = temp2.components(separatedBy: " ")
            model.imgH = model.imgW! * Float(tempArr[1])! / Float(tempArr[0])!
            let temp3 = String(format: "w/%.f/h/%.f", model.imgW!, model.imgH!)
            model.wrap = model.wrap!.replacingOccurrences(of: temp1, with: temp3)
        }
        //文章url
        model.articleUrl = self.regexGetSub(articleUrlRegex, item)[0]
        //文章title
        model.title = self.regexGetSub(titleRegex, item)[0]
        //文摘
        model.abstract = self.regexGetSub(abstractRegex, item)[0]
        
        //此方法可以只写一个正则表达式,返回一个(两个元素的数组)
        //                    let redComments = self.regexGetSub(readCommentsRegex, item)
        //                    let red = redComments[0]    //查看人数
        //                    let comments = redComments[1]       //评论人数
        
        //查看人数
        model.read = self.regexGetSub(readRegex, item)[0]
        //评论人数
        model.comments = self.regexGetSub(commentsRegex, item)[0]
        //喜欢
        model.like = self.regexGetSub(likeRegex, item)[0]
        //发布时间
        var time = self.regexGetSub(timeRegex, item)[0]
        time = time.replacingOccurrences(of: "T", with: " ")
        time = time.replacingOccurrences(of: "+08:00", with: "")
        model.time = time
        
        //计算标题和摘要的高度
        model.titleH = GETSTRHEIGHT(fontSize: 20, width: CGFloat(model.imgW!) , words: model.title!) + 1
        model.abstractH = GETSTRHEIGHT(fontSize: 14, width: CGFloat(model.imgW!) , words: model.abstract!) + 1
        
        //item高度
        var computeH:CGFloat = 8 + 25 + 3 + 10 + 8 + (model.imgH != nil ? CGFloat(model.imgH!) : 0) + 8 + model.titleH! + 8 + model.abstractH! + 8 + 10 + 8
        //如果没有图片减去一个间隙8
        computeH = computeH - (model.wrap!.count > 0 ? 0 : 8)
        model.itemHeight = String(format: "%.f", computeH)
        dataArr.append(model)
    }
                
//  jianshuModel.swift
//  SwiftApp
//
//  Created by leeson on 2018/7/16.
//  Copyright © 2018年 李斯芃 ---> 512523045@qq.com. All rights reserved.
//

import UIKit

class JianshuModel: NSObject {
    ///封面
    var wrap:String?
    ///文章URL
    var articleUrl:String?
    ///标题
    var title:String?
    ///文摘
    var abstract:String?
    ///阅读人数
    var read:String?
    ///评论个数
    var comments:String?
    ///喜欢
    var like:String?
    ///发布时间
    var time:String?
    
    //======================== 分割线 ========================
    ///图片宽度
    var imgW:Float?
    ///图片高度
    var imgH:Float?
    ///item高度
    var itemHeight:String?
    
    ///title高度
    var titleH:CGFloat?
    ///摘要高度
    var abstractH:CGFloat?

}
//注释:如果单纯的存网页获取的属性分割线以下的字段是不需要的,因为下篇文章涉瀑布流要率先计算layout布局要计算高度,所以提前计算了一些信息。

以上就是本文要讲的全部内容,可能有写的不清楚或不好的地方,请海涵多指教,也可以下载GitHub源码,那里面关联效果会比较明显,可以调试。上面可能有些代码是本文无关的请自行过滤,源码中有些代码会比较啰嗦或者有更简便的方法或者一个功能写了多种写法,这些都只是笔者为了测试多种效果故意为之,可读性不是那么强,请大家包容哈。此文中的代码都做了比较详细的注释,如果还有不懂的码友可以在评论区留言。谢谢。
GitHub源码
下一篇文章:Swift瀑布流展示/切换简书列表数据

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,968评论 3 119
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,422评论 1 45
  • 【捭阖第一】 (1.7)捭阖者,道之大化,说之变也。必豫审其变化,吉凶大命系焉。口者,心之门户也;心者,神之主也。...
    路过的小强阅读 144评论 0 0
  • 电视剧看完了,结尾略无语 午睡又做了一场n多年前的梦,内容始终想不起来,但那种诡异感却清晰得很熟悉 两个火车票软件...
    薛先生阅读 370评论 0 1
  • 在一起的时候想的是一辈子在一起,现在分开了,才发现一辈子也不过如此。
    MITTY477阅读 90评论 0 0