上篇文章寫過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交互。今天我想先講第一部分。
如上篇文章所說,因為本人文章列表數(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行符和空格:
//剔除換行符和空格
str = str.replacingOccurrences(of: "\n", with: "")
str = str.replacingOccurrences(of: " ", with: "")
1.獲取頭部信息
源碼拿到了,先獲取頭部信息,Chrome瀏覽器瀏覽器打開URL,鼠標放在頭像位置右擊,呼出菜單點擊“檢查”:
鼠標在源碼適當移動,當選中到整個我們需要的頭部區(qū)域時停止,不難看出標簽"<div class="main-top">...</div>"包含的信息是我們需要分析的:
通過正則表達式拿到這些標簽內(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標簽下的字符串:
//列表數(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ù)