网页内容抓取工具(Swift-使用CoreData进行数据持久化存储(2))

优采云 发布时间: 2021-10-25 17:12

  网页内容抓取工具(Swift-使用CoreData进行数据持久化存储(2))

  (本文代码已升级为Swift3)

  市场上有很多新闻客户端提供离线阅读功能。用户可以在本地下载新闻页面,然后在没有网络的情况下离线浏览。或者在使用wifi的时候缓存一些页面,这样在使用手机网络的时候就不再做网络请求,直接读取读取缓存,既提高了响应速度,又节省了流量。

  本文介绍如何在本地缓存网页,然后从缓存中读取。

  一、实现原理

  整个应用使用的核心技术是:URLProtocol 拦截。通过 URLProtocol 拦截,我们可以捕获并处理所有网络请求。

  例如,当我们通过WebView访问一个网页时,我们首先将抓取到的url地址与本地缓存中的数据进行匹配。如果有这个url对应的缓存数据,则直接使用缓存数据。

  如果未找到,则发出网络请求并将接收到的数据缓存以备下次使用。

  前面说过,所有的请求都可以通过URLProtocol拦截,也就是说无论是html页面还是其中用到的css、js、image等文件都可以被拦截缓存。

  2. 使用 Core Data 进行数据持久化存储。本示例使用 Core Data 来缓存网页数据。如果对Core Data不熟悉,可以参考我的原创文章文章:Swift-Use Core Data for Data Persistent storage

  (1) 创建项目时,勾选“Use Core Data”

  

  (2)打开项目中的xcdatamodeld文件,添加实体“CachedURLResponse”,在Attribute栏中添加如下属性:

  

  数据:内容数据

  encoding: 响应编码

  mimeType:响应数据类型

  timestamp:时间戳(记录缓存时间)

  url: 请求地址

  3.创建拦截类(1)通过继承URLProtocol,我们创建拦截类:MyURLProtocol.swift

  import UIKit

import CoreData

//记录请求数量

var requestCount = 0

class MyURLProtocol: URLProtocol , URLSessionDataDelegate, URLSessionTaskDelegate{

//URLSession数据请求任务

var dataTask:URLSessionDataTask?

//url请求响应

var urlResponse: URLResponse?

//url请求获取到的数据

var receivedData: NSMutableData?

//判断这个 protocol 是否可以处理传入的 request

override class func canInit(with request: URLRequest) -> Bool {

//对于已处理过的请求则跳过,避免无限循环标签问题

if URLProtocol.property(forKey: "MyURLProtocolHandledKey", in: request) != nil {

return false

}

return true

}

//回规范化的请求(通常只要返回原来的请求就可以)

override class func canonicalRequest(for request: URLRequest) -> URLRequest {

return request

}

//判断两个请求是否为同一个请求,如果为同一个请求那么就会使用缓存数据。

//通常都是调用父类的该方法。我们也不许要特殊处理。

override class func requestIsCacheEquivalent(_ aRequest: URLRequest,

to bRequest: URLRequest) -> Bool {

return super.requestIsCacheEquivalent(aRequest, to:bRequest)

}

//开始处理这个请求

override func startLoading() {

requestCount+=1

print("Request请求\(requestCount): \(request.url!.absoluteString)")

//判断是否有本地缓存

let possibleCachedResponse = self.cachedResponseForCurrentRequest()

if let cachedResponse = possibleCachedResponse {

print("----- 从缓存中获取响应内容 -----")

//从本地缓中读取数据

let data = cachedResponse.value(forKey: "data") as! Data!

let mimeType = cachedResponse.value(forKey: "mimeType") as! String!

let encoding = cachedResponse.value(forKey: "encoding") as! String!

//创建一个NSURLResponse 对象用来存储数据。

let response = URLResponse(url: self.request.url!, mimeType: mimeType,

expectedContentLength: data!.count,

textEncodingName: encoding)

//将数据返回到客户端。然后调用URLProtocolDidFinishLoading方法来结束加载。

//(设置客户端的缓存存储策略.NotAllowed ,即让客户端做任何缓存的相关工作)

self.client!.urlProtocol(self, didReceive: response,

cacheStoragePolicy: .notAllowed)

self.client!.urlProtocol(self, didLoad: data!)

self.client!.urlProtocolDidFinishLoading(self)

} else {

//请求网络数据

print("===== 从网络获取响应内容 =====")

let newRequest = (self.request as NSURLRequest).mutableCopy() as! NSMutableURLRequest

//NSURLProtocol接口的setProperty()方法可以给URL请求添加自定义属性。

//(这样把处理过的请求做个标记,下一次就不再处理了,避免无限循环请求)

URLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey",

in: newRequest)

//使用URLSession从网络获取数据

let defaultConfigObj = URLSessionConfiguration.default

let defaultSession = Foundation.URLSession(configuration: defaultConfigObj,

delegate: self, delegateQueue: nil)

self.dataTask = defaultSession.dataTask(with: self.request)

self.dataTask!.resume()

}

}

//结束处理这个请求

override func stopLoading() {

self.dataTask?.cancel()

self.dataTask = nil

self.receivedData = nil

self.urlResponse = nil

}

//URLSessionDataDelegate相关的代理方法

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

didReceive response: URLResponse,

completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {

self.client?.urlProtocol(self, didReceive: response,

cacheStoragePolicy: .notAllowed)

self.urlResponse = response

self.receivedData = NSMutableData()

completionHandler(.allow)

}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

didReceive data: Data) {

self.client?.urlProtocol(self, didLoad: data)

self.receivedData?.append(data)

}

//URLSessionTaskDelegate相关的代理方法

func urlSession(_ session: URLSession, task: URLSessionTask

, didCompleteWithError error: Error?) {

if error != nil {

self.client?.urlProtocol(self, didFailWithError: error!)

} else {

//保存获取到的请求响应数据

saveCachedResponse()

self.client?.urlProtocolDidFinishLoading(self)

}

}

//保存获取到的请求响应数据

func saveCachedResponse () {

print("+++++ 将获取到的数据缓存起来 +++++")

//获取管理的数据上下文 对象

let app = UIApplication.shared.delegate as! AppDelegate

let context = app.persistentContainer.viewContext

//创建NSManagedObject的实例,来匹配在.xcdatamodeld 文件中所对应的数据模型。

let cachedResponse = NSEntityDescription

.insertNewObject(forEntityName: "CachedURLResponse",

into: context) as NSManagedObject

cachedResponse.setValue(self.receivedData, forKey: "data")

cachedResponse.setValue(self.request.url!.absoluteString, forKey: "url")

cachedResponse.setValue(Date(), forKey: "timestamp")

cachedResponse.setValue(self.urlResponse?.mimeType, forKey: "mimeType")

cachedResponse.setValue(self.urlResponse?.textEncodingName, forKey: "encoding")

//保存(Core Data数据要放在主线程中保存,要不并发是容易崩溃)

DispatchQueue.main.async(execute: {

do {

try context.save()

} catch {

print("不能保存:\(error)")

}

})

}

//检索缓存请求

func cachedResponseForCurrentRequest() -> NSManagedObject? {

//获取管理的数据上下文 对象

let app = UIApplication.shared.delegate as! AppDelegate

let context = app.persistentContainer.viewContext

//创建一个NSFetchRequest,通过它得到对象模型实体:CachedURLResponse

let fetchRequest = NSFetchRequest()

let entity = NSEntityDescription.entity(forEntityName: "CachedURLResponse",

in: context)

fetchRequest.entity = entity

//设置查询条件

let predicate = NSPredicate(format:"url == %@", self.request.url!.absoluteString)

fetchRequest.predicate = predicate

//执行获取到的请求

do {

let possibleResult = try context.fetch(fetchRequest)

as? Array

if let result = possibleResult {

if !result.isEmpty {

return result[0]

}

}

}

catch {

print("获取缓存数据失败:\(error)")

}

return nil

}

}

  (2)在 AppDelegate.swift 的 didFinishLaunchingWithOptions 方法中注册。

  这样,程序运行后,MyURLProtocol 会处理每一个请求,并传递给 URL Loading System。包括直接调用加载系统的代码,很多系统组件依赖加载框架的URL,比如UIWebView。

  import UIKit

import CoreData

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(application: UIApplication,

didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

// Override point for customization after application launch.

//注册URL Loading System协议,让每一个请求都会经过MyURLProtocol处理

URLProtocol.registerClass(MyURLProtocol.self)

return true

}

//................

  4.测试页面(1)我们在textField中输入url,点击“确定”,然后使用下面的webView加载对应的页面。

  

  (2)页面对应的代码如下,这个和之前用webView没什么区别。(其实程序已经在后台默默的拦截并缓存了请求)

  import UIKit

class ViewController: UIViewController , UITextFieldDelegate {

//网址输入框

@IBOutlet var textField: UITextField!

@IBOutlet var webView: UIWebView!

override func viewDidLoad() {

super.viewDidLoad()

}

//确定按钮点击

@IBAction func buttonGoClicked(_ sender: UIButton) {

if self.textField.isFirstResponder {

self.textField.resignFirstResponder()

}

self.sendRequest()

}

//键盘确定按钮点击

func textFieldShouldReturn(_ textField: UITextField) -> Bool {

textField.resignFirstResponder()

self.sendRequest()

return true

}

//请求页面

func sendRequest() {

if let text = self.textField.text {

let url = URL(string:text)

let request = URLRequest(url:url!)

self.webView.loadRequest(request)

}

}

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

}

}

  5.开始测试(1)访问

  

  (2)可以看到控制台会打印出所有的请求请求,因为是第一次访问,本地没有发现缓存数据,程序通过网络请求数据,缓存起来。

  

  (3) 然后再访问几个页面(无论是点击页面跳转还是重新输入url请求)

  

  

  (4) 然后关闭网络,访问之前访问过的页面。可以看到页面在没有网络的情况下仍然可以加载。

  

  (5)查看控制台,发现这些请求请求都是从本地缓存中获取数据的(如果网络不变,也会从缓存中获取数据)

  

  源码下载:

  

  hangge_1118.zip

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线