Cannot Scrape Web Page Using Swift - html

I am building an application in Swift to find a USTA Ranking. I am using SwiftSoup to scrape a web page.
This is my code:
import UIKit
import SwiftSoup
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let myURL = URL(string:"https://www.usta.com/en/home/play/player-search/profile.html#?uaid=2010002111/")!
let html = try! String(contentsOf: myURL, encoding: .utf8)
do {
let doc: Document = try SwiftSoup.parseBodyFragment(html)
// my body
let body = doc.body()
var rating = try body?.getElementsByClass("cell__text")[8]
print(rating!)
print(body)
} catch Exception.Error(_, let message) {
print("Message: \(message)")
} catch {
print("error")
}
}
}
When this runs, it gives me an error, saying that it is out of range. After looking through the output, I figured out that anything that has "ng-cloak" in it, makes the remaining stuff inside of it disappear. How can I prevent this from happening, so that I can get the ranking I want?
Thanks.

Related

Is there an easy solution for parsing html in swift to get individual elements into their own variable?

I have code which I found online that pulls the HTML off a website and then prints it out. I need to save these into a variable to I can display/ use these in my app.
I am fairly new to this kind of thing and really just need pointers, I don't mind researching! I just need to know what steps I need to be looking into!
import UIKit
// run asynchronously in a playground
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// create a url
let url = URL(string: "https://www.stackoverflow.com")
// create a data task
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("there's a problem")
}
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
}
//running the task w/ resume
task.resume()
This (in Xcode playground) takes the HTML and prints it out using:
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
Can anyone please help me out getting maybe the <title>...</title> element into its own variable?
Parsing HTML without a third party is not achievable without a WebView, BUT YOU CAN easily use a webView and run a getElementsByTagName with JS on it to get anything from the HTML code like this:
1- Define the js code:
let js = "document.getElementsByTagName("title")[0].innerHTML"
2- Import WebKit and load the html into a webView
class MyViewController : UIViewController {
let html = """
<#the HTML code, can be loaded from anywhere#>
"""
override func loadView() {
let webView = WKWebView()
webView.navigationDelegate = self // Here is the Delegate
webView.loadHTMLString(html, baseURL: nil)
self.view = webView
}
}
3- Take the delegation and implement this method:
extension MyViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(js) {(result, error) in
guard error == nil else {
print(error!)
return
}
print(String(describing: result))
}
}
}
Note 1: remember getElementsByTagName returns an array and you must pass the index you want the get like [0]
Note 2: since it use JavaScriptCore, it can't be done without webView, and it must be run on mainThread. Only safari can do this off main thread, because it has V8 engine.
Note 3: You must wait for delegate to be completed even if you pass the HTML statically
Note 4: you can use a third party framework like SwiftSoap to do this.

Swift HTML content of URL of mobile site

I've got a URL and I'm trying to get the HTML content of the site the following way:
func getHtml(_ urlString: String) -> String? {
guard let url = URL(string: urlString) else {
return nil
}
do {
let html = try String(contentsOf: url, encoding: .ascii)
return html
} catch let error {
print("Error: \(error)")
return nil
}
}
if let html = getHtml("https://m.youtube.com/") {
print(html)
}
My issue is, that this gets me the html of the desktop version of the site, however I need the html of the mobile version.
I'm not looking for a workaround for this specific site, but for a general solution, so that, given any URL of a mobile site, it doesn't default to getting me the html of the desktop site.
If you use Viewcontroller in iOS to get the HTML, you can use hidden WKWebView as an alternative and implement the WKNavigationDelegate which has the didFinish method where you can use webView.evaluateJavaScript. As wkwebview is loading from mobile, you will get the mobile version html. Here is the sample of the code.
import UIKit
import WebKit
class YourViewController: UIViewController, WKNavigationDelegate {
let webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func BtnClicked(_ sender: Any) {
loadWeb(url: "https://m.youtube.com/")
}
func loadWeb(url: String) {
if let myURL = URL(string: url) {
let request = URLRequest(url: myURL)
webView.navigationDelegate = self
webView.load(request)
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML") { (data, error) in
//use html data
print("data", data, error.debugDescription)
}
}
}

Share JSON Data in TabBarController to view controllers

I am using a tabbarcontroller to show 3 xib's. I would like to decode JSON data in the UITabBarController subclass, and then share the data with the view controllers (as I understand that is the preferred way to do this). I had already successfully accomplished this individually in each view controller, where the same JSON data was getting decoded separately 3 times, but I am now trying to make the process more efficient by only dealing with JSON once.
I am currently getting the following error
"Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee7ab7d98)".
Below is the code I am currently using. I'm mostly only including the code for the first view controller, but it is the same for the others
Here is one of the view controllers. Any help would be appreciated, thank you!
class FirstCollectionViewController: UIViewController {
var tbvc = CustomTabBar()
var statisticsData = [Model]()
let firstCellIdentifier = "FirstCellIdentifier"
#IBOutlet weak var FirstCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
FirstCollectionView.delegate = self
FirstCollectionView.dataSource = self
FirstCollectionView.register(UINib(nibName: "FirstCollectionViewCell", bundle: nil),forCellWithReuseIdentifier: firstCellIdentifier)
}
}
Here is the subclasses UITabBarController
import UIKit
class CustomTabBar: UITabBarController {
let website = "https:......."
var statisticsData = [Model]()
override func viewDidLoad() {
super.viewDidLoad()
let firstTab = FirstCollectionViewController(nibName: "FirstCollectionViewController", bundle: nil)
let secondTab = SecondCollectionViewController(nibName: "SecondCollectionViewController", bundle: nil)
let thirdTab = ThirdCollectionViewController(nibName: "ThirdCollectionViewController", bundle: nil)
viewControllers = [firstTab, secondTab, thirdTab]
downloadJSON(website: website) {
firstTab.statisticsData = self.statisticsData
secondTab.statisticsData = self.statisticsData
thirdTab.statisticsData = self.statisticsData
firstTab.FirstCollectionView.reloadData()
secondTab.SecondCollectionView.reloadData()
thirdTab.ThirdCollectionView.reloadData()
}
}
func downloadJSON(website:String, completed:#escaping ()->()){
guard let qurl = URL(string: website) else { return }
URLSession.shared.dataTask(with: qurl) { (data, response, error) in
if error == nil {
do{
self.statisticsData = try JSONDecoder().decode([Model].self, from: data!)
DispatchQueue.main.async{
completed()
}
} catch {
print("JSON Error")
}}
}.resume()
}
}
Once the data is loaded, you should assign the data to the viewControllers that are added in the tabBarController's Child list as below,
downloadJSON(website: website) {
firstTab.statisticsData = self.statisticsData
secondTab.statisticsData = self.statisticsData
thirdTab.statisticsData = self.statisticsData
firstTab.FirstCollectionView.reloadData()
secondTab.SecondCollectionView.reloadData()
thirdTab.ThirdCollectionView.reloadData()
}
You can also remove the below lines from viewDidLoad of FirstCollectionViewController, SecondCollectionViewController and ThirdCollectionViewController
tbvc = tabBarController as! CustomTabBar
statisticsData = tbvc.statisticsData

Swift how to reuse my JSON HTTP Request header

I am making an application which makes a lot of requests from an API. So I don't want to copy and past the code over and over. I was wondering how I can reuse my code in a some more efficient way? Maybe with extensions?
This is my code know:
func apiRequest() {
let config = URLSessionConfiguration.default
let username = "****"
let password = "****"
let loginString = String(format: "%#:%#", username, password)
let userPasswordData = loginString.data(using: String.Encoding.utf8)
let base64EncodedCredential = userPasswordData?.base64EncodedString()
let authString = "Basic " + (base64EncodedCredential)!
print(authString)
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
var running = false
let urlProjects = NSURL(string: "https://start.jamespro.nl/v4/api/json/projects/?limit=10")
let task = session.dataTask(with: urlProjects! as URL) {
( data, response, error) in
if let taskHeader = response as? HTTPURLResponse {
print(taskHeader.statusCode)
}
if error != nil {
print("There is an error!!!")
print(error)
} else {
if let content = data {
do {
let dictionary = try JSONSerialization.jsonObject(with: content) as! [String:Any]
print(dictionary)
if let items = dictionary["items"] as? [[String:Any]] {
for item in items {
if let description = item["Description"] as? String {
self.projectNaam.append(description)
}
if let id = item["Id"] as? String {
self.projectId.append(id)
}
if let companyId = item["CompanyId"] as? String {
self.companyId.append(companyId)
}
}
}
self.apiRequestCompani()
}
catch {
print("Error: Could not get any data")
}
}
}
running = false
}
running = true
task.resume()
while running {
print("waiting...")
sleep(1)
}
}
Yes, you can use Extensions to create a BaseViewController and extend that where you want to use your code over and over again. Then you should abstract all dynamic data over input parameters to that method.
import UIKit
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func getApiRequest (Parameters) {
//API Request
}
And then in your view controller you just extend BaseViewController
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
//Call method in baseviewcontroller
getApiRequest(parameters)
//Call method in self
self.getApiRequest(parameters)
}
override func getApiRequest(Parameters) {
//IF you need to override default configuration
}
So I don't want to copy and past the code over and over.
Absolutely right, no one aiming to get duplicated code; That's the issue of massive view controller. This issue appears since the view controller layer in your application handles most of the responsibilities, such as: getting data from the network, how data should be represented, deliver the formatted data to the view layer, etc...
There are many approaches for solving such an issue (using an appropriate architectural pattern for your application), for simplicity, I would recommend to apply the MVC-N (or MVCNetworking) approach into your app, it is almost the same usual MVC, with a separated files (managers), represent a new layer for handling -for instance- the integration with the external APIs.
Applying the MVN-N should not be that complex, nevertheless it needs to be described well (which might be too abroad to be descried in the answer), I would suggest to check the above mentioned apple example, also watching this video should be useful.

Text not displaying from JSON in Swift

import UIKit
let cellid = "cell"
class Post {
var videoName: String?
var videoDescription: String?
}
class VideoFeedController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var posts = [Post]()
var json: [Any]?
var names: [String] = []
var contacts: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let CatagoryMain = Post()
CatagoryMain.videoDescription = "example text"
CatagoryMain.videoName = "wewewewew"
posts.append(CatagoryMain)
collectionView?.backgroundColor = UIColor.white
navigationItem.title = "Main Video Feed"
collectionView?.alwaysBounceVertical = true
collectionView?.register(VideoFeedCell.self, forCellWithReuseIdentifier: cellid)
let urlString = "example url"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print("failed")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: []) as! [String:Any]
let currentVideo = parsedData["video"] as! [String:Any]
if let currentVideoTitle = currentVideo["title"] as? String {
print(currentVideoTitle)
// have text display instead of wewewew and example text
}
} catch let error as NSError {
print(error)
}
}
}.resume()
}
So for now it says example text for the video description and wewewewew for the video name and those display fine.I am trying to get the info from the json though so instead of example text it would have the description and instead of wewewewew it would have the video name I thought it would go like this
if let currentVideoTitle = currentVideo["title"] as? String {
print(currentVideoTitle)
CatagoryMain.videoDescription = "\(currentVideoTitle)"
CatagoryMain.videoName = "\(currentVideoTitle)"
posts.append(CatagoryMain)
}
however that was wrong and if I do that nothing shows on the screen besides the navigation bar and the title for the navigation bar. Thank you in advance if you can help! The actual title does show in the console by the way.
Like the comment I left above, we need more code to figure out the situation and help you out :) And your question is a little bit unclear.
however that was wrong and if I do that nothing shows on the screen
besides the navigation bar and the title for the navigation bar.
You mean the UICollectionView is not loading the data? or the data are not loaded into posts array? is collectionView even init-ed?
Also, would like to have a look of the parsedData object. As you are using collectionView which means you want to display multiple cells. But your parsing codes do not look like you are parse an array of objects.(but only one object??)
Quick advice,
1. Print out the posts.counts to see if there are data?
2. If there is any data, add collectionView.reloadData() after you added data to posts.