上篇文章写过Python爬虫的方法,用的Scrapy框架。
Python--Scrapy爬虫获取简书作者ID的全部文章列表数据
最近闲来想用Swift写个瀑布流然后展示一些数据,奈何没有测试接口,后来想到可不可自己从HTML网页获取数据并展示出来呢?当然,理论上是可行的,只要拿到HTML源码,通过正则表达式是可以匹配到我们想要的数据的。随后经过断断续续几天的开发,完成了一个小demo,我会分三部分写大家分享,因为其中用到三个独立的技术模块:HTML解析、瀑布流布局、WKWbeView与JS交互。今天我想先讲第一部分。
如上篇文章所说,因为本人文章列表数据较少,为了获取多页数据,所以选取了简书一位作者(@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"换行符,但是打断点可以看出,为了后面正则匹配方便无误,我们去掉所有的换行符和空格:
//剔除换行符和空格
str = str.replacingOccurrences(of: "\n", with: "")
str = str.replacingOccurrences(of: " ", with: "")
1.获取头部信息
源码拿到了,先获取头部信息,Chrome浏览器浏览器打开URL,鼠标放在头像位置右击,呼出菜单点击“检查”:
鼠标在源码适当移动,当选中到整个我们需要的头部区域时停止,不难看出标签"<div class="main-top">...</div>"包含的信息是我们需要分析的:
通过正则表达式拿到这些标签内容:
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标签下的字符串:
//列表数据
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瀑布流展示/切换简书列表数据