Swfit爬蟲通過作者ID無接口獲取簡書文章列表,正則匹配HTML標簽存儲模型數(shù)據(jù)

上篇文章寫過Python爬蟲的方法,用的Scrapy框架。
Python--Scrapy爬蟲獲取簡書作者ID的全部文章列表數(shù)據(jù)
最近閑來想用Swift寫個瀑布流然后展示一些數(shù)據(jù),奈何沒有測試接口,后來想到可不可自己從HTML網(wǎng)頁獲取數(shù)據(jù)并展示出來呢?當然,理論上是可行的,只要拿到HTML源碼,通過正則表達式是可以匹配到我們想要的數(shù)據(jù)的。隨后經(jīng)過斷斷續(xù)續(xù)幾天的開發(fā),完成了一個小demo,我會分三部分寫大家分享,因為其中用到三個獨立的技術模塊:HTML解析、瀑布流布局、WKWbeView與JS交互。今天我想先講第一部分。

1 - 效果圖

如上篇文章所說,因為本人文章列表數(shù)據(jù)較少,為了獲取多頁數(shù)據(jù),所以選取了簡書一位作者(@CC老師_MissCC
)的ID來進行開發(fā)。(如有侵權,可聯(lián)系刪除。)

既然要分析源碼,首先我們要獲取源碼。Swift4.0提供了一個簡單的方法,一行代碼可以搞定,但是此方法可能會拋出異常,所有我們有必要做下校驗,防止崩潰:

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

控制臺打印因為是格式化輸出,看不到"\n"換行符,但是打斷點可以看出,為了后面正則匹配方便無誤,我們?nèi)サ羲械膿Q行符和空格:


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

1.獲取頭部信息

源碼拿到了,先獲取頭部信息,Chrome瀏覽器瀏覽器打開URL,鼠標放在頭像位置右擊,呼出菜單點擊“檢查”:


3 - 檢查元素

鼠標在源碼適當移動,當選中到整個我們需要的頭部區(qū)域時停止,不難看出標簽"<div class="main-top">...</div>"包含的信息是我們需要分析的:


4 - 頭部信息

通過正則表達式拿到這些標簽內(nèi)容:
      let headTop =  "<divclass=\"main-top\">(.*?)</div><ulclass=\"trigger-menu\""
      //獲取頭部div標簽數(shù)據(jù)
      let topInfo:String = self.extractStr(str, headTop)

有人可能會問為什么用</div>結(jié)尾不就行了,后面又接"<ulclass="trigger-menu""是什么鬼?因為頭部信息中還包含有div元素,不加后面臨近的ul標簽的話,只會匹配到最近一個div結(jié)尾的元素,造成少匹配數(shù)據(jù)。所以具體問題具體分析。
下面給出兩個Swift的兩正則匹配獲取字符串的方法,一個是獲取單條數(shù)據(jù)的,一個是獲取多條數(shù)據(jù)的,大家可以根據(jù)實際情況靈活選取:

     //MARK: - --- 根據(jù)正則表達式提取字符串(獲取單條)
    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: - --- 根據(jù)正則表達式提取字符串(獲取多條)
    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內(nèi)容,從中我們可以拿到頭像、用戶名、性別、關注數(shù)、粉絲數(shù)、文章數(shù)等全部信息。正則表達式就不一一分析了,可以有多種寫法。

值得注意的是:你可能會先在在線工具上先測試再用在項目中,但是往往可能在上面測試是好的,可是項目中卻匹配不出來,那你就要考慮換一種寫法。

因為正則表達式不熟,筆者下面用到的類似寫法是試了多次才試出來的。大家可以參考,如果有更好的寫法,你也可以寫自己的,這不是固定的,達到匹配的目的就行。熟悉正則表達式的朋友應該很容易就能拿到自己想要的數(shù)據(jù)。
下面貼上獲取頭部各參數(shù)信息的代碼:

                //獲取頭部div標簽數(shù)據(jù)
                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)

                //[關注,粉絲,文章,字數(shù),收獲喜歡] 。 li標簽一般是多個,匹配出來自然是數(shù)組
                let infoListRegex = "(?<=li><divclass=\"meta-block\">.{0,200}<p>)(.*?)(?=</p>.*?</li>)"
                let infoList = self.regexGetSub(infoListRegex, topInfo)

                //總頁數(shù)(PC默認每頁9個數(shù)據(jù),所以可以通過文章總數(shù)計算總頁數(shù))
                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布局就知道每個數(shù)字代表的意思了。這里大家可以跳過。)
                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)

代碼中有些宏和方法可能沒有展示出來,但是是有關聯(lián)的,要查看他們聯(lián)系或者為了不報錯,可以下載我放在GitHub的源碼。

2.獲取列表數(shù)據(jù)

接下來分析列表數(shù)據(jù),這就是我們主要要展示的有規(guī)律的數(shù)據(jù),分析HTML源碼可以看出,列表數(shù)據(jù)所在的ul標簽下有多個li標簽。我們通過URL加頁碼page字段請求返回的只有9個數(shù)據(jù),但是直接在瀏覽器看是動態(tài)加載的遠不止9個一直往下滑會一直加載,這個我們不用理會,只要知道每個li標簽下對應的數(shù)據(jù)結(jié)構是一樣的有規(guī)律的就行。也好為我們后面用一個正則表達式獲取多條數(shù)據(jù)做鋪墊。首先,拿到包裹li標簽的ul標簽下的字符串:


5 - 拿到包括ul標簽的所有l(wèi)i標簽字符串
    //列表數(shù)據(jù)
    let articleListStrRegex = "<ulclass=\"note-list\"infinite-scroll-url=\".*?\">(.*?)</ul>"
    //獲取文章列表ul標簽數(shù)據(jù)
    let articleListStrArr = self.regexGetSub(articleListStrRegex, str)
    let articleListStr = articleListStrArr[0]
    //單條數(shù)據(jù)正則
    let liLableRegex = "<liid=(.*?)</li>"
    //匹配獲取li標簽,得到一個元素不大于9的數(shù)組
    let liLableArr = self.regexGetSub(liLableRegex, articleListStr)

    //拿到li標簽后,遍歷數(shù)組liLableArr,遍歷時就可以分析每個li標簽的數(shù)據(jù)結(jié)構,對應寫出我們要拿的每個字段的正則表達式,得到數(shù)據(jù),存入模型。
    //單頁數(shù)據(jù)
    var dataArr = [JianshuModel]()  
    //遍歷li標簽 匹配需要的數(shù)據(jù)
    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>)"
        
        //數(shù)據(jù)模型
        let model = JianshuModel()
        
        //封面(可能有文章沒有封面) 獲取的圖片URL最后面類似"w/300/h/240"代表長寬,修改長寬如"w/600/h/480"可得到2倍尺寸的圖片,清晰度相應提高,反之亦然。假如超過原圖長或?qū)挼某叽缇蜁@示原圖
        model.wrap = self.regexGetSub(wrapRegex, item)[0]
        model.imgW = itemWith - 16
        //如果長度大于0個字符
        if model.wrap!.count > 0  {
            //此步是為了按比例縮放圖片,但是發(fā)現(xiàn)所有的圖片都是 寬 * 120 / 150 ,所以可不寫這步直接通過寬計算高即可
            //后來(也就是下篇文章我們將瀑布流的時候)cell賦值發(fā)現(xiàn)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]
        
        //此方法可以只寫一個正則表達式,返回一個(兩個元素的數(shù)組)
        //                    let redComments = self.regexGetSub(readCommentsRegex, item)
        //                    let red = redComments[0]    //查看人數(shù)
        //                    let comments = redComments[1]       //評論人數(shù)
        
        //查看人數(shù)
        model.read = self.regexGetSub(readRegex, item)[0]
        //評論人數(shù)
        model.comments = self.regexGetSub(commentsRegex, item)[0]
        //喜歡
        model.like = self.regexGetSub(likeRegex, item)[0]
        //發(fā)布時間
        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?
    ///閱讀人數(shù)
    var read:String?
    ///評論個數(shù)
    var comments:String?
    ///喜歡
    var like:String?
    ///發(fā)布時間
    var time:String?
    
    //======================== 分割線 ========================
    ///圖片寬度
    var imgW:Float?
    ///圖片高度
    var imgH:Float?
    ///item高度
    var itemHeight:String?
    
    ///title高度
    var titleH:CGFloat?
    ///摘要高度
    var abstractH:CGFloat?

}
//注釋:如果單純的存網(wǎng)頁獲取的屬性分割線以下的字段是不需要的,因為下篇文章涉瀑布流要率先計算layout布局要計算高度,所以提前計算了一些信息。

以上就是本文要講的全部內(nèi)容,可能有寫的不清楚或不好的地方,請海涵多指教,也可以下載GitHub源碼,那里面關聯(lián)效果會比較明顯,可以調(diào)試。上面可能有些代碼是本文無關的請自行過濾,源碼中有些代碼會比較啰嗦或者有更簡便的方法或者一個功能寫了多種寫法,這些都只是筆者為了測試多種效果故意為之,可讀性不是那么強,請大家包容哈。此文中的代碼都做了比較詳細的注釋,如果還有不懂的碼友可以在評論區(qū)留言。謝謝。
GitHub源碼
下一篇文章:Swift瀑布流展示/切換簡書列表數(shù)據(jù)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,836評論 6 540
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AGI閱讀 16,002評論 3 119
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標準。 注意:講述HT...
    kismetajun閱讀 27,577評論 1 45
  • 【捭闔第一】 (1.7)捭闔者,道之大化,說之變也。必豫審其變化,吉兇大命系焉??谡撸闹T戶也;心者,神之主也。...
    路過的小強閱讀 163評論 0 0
  • 電視劇看完了,結(jié)尾略無語 午睡又做了一場n多年前的夢,內(nèi)容始終想不起來,但那種詭異感卻清晰得很熟悉 兩個火車票軟件...
    薛先生閱讀 375評論 0 1
  • 在一起的時候想的是一輩子在一起,現(xiàn)在分開了,才發(fā)現(xiàn)一輩子也不過如此。
    MITTY477閱讀 99評論 0 0