OMGPOP
OMGPOP

Reputation: 984

iOS Swift how to parse HTML file with <script> tag and get value inside

I have a HTML string like this:

<html>
<head>
<script>
window.user = {"username":"admin", "meta":{"city": "Los Angeles", "country": "US"}}
window.flash = {"success": "", "warning": "you cannot do this"}
</script>
</head>
</body>
</html>

I am implementing a function

func dictionaryWithHtml(html: String, variable: String) -> [String: AnyObject]? {

}

usage:

calling dictionaryWithHtml(html, variable: "window.user")

should return the dictionary

{"username":"admin", "meta":{"city": "Los Angeles", "country": "US"}}

I am trying to search window.user = substring as index1, then find window.flash = substring as index2, then I can easily find the range of JSON for user dictionary.

For window.flash, I could find </script> substring, and get the range similarly, but what if the server updates and add more variables in the tag? or if server changes the order of assignment.

So is there any better and more robust approach?

Upvotes: 1

Views: 4489

Answers (1)

Cristik
Cristik

Reputation: 32861

You could use an html parser to retrieve the values of the script tag, or you could load the html string into a WKIWebView, and ask it to execute the javascript code that would return to you the value of the variables you search within the window object. This has the advantage of being more reliable as it will not break if the script structure changes, and also works for dynamically javascript computed values.

let webView = WKWebView(frame: CGRectZero)
self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method 
while (webView.loading) {
    CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
// get the two objects in one round-trip to the javascript engine
webView.evaluateJavaScript("[window.user, window.flash]") { (result, error) -> Void in
    print(result, error)
}

In both cases you need to add the web view to another view, so this code likely will need to be run into a view controller. Another possible disadvantage is the fact that the code will need to run on the main thread.

Note 1. WKWebView was added in iOS 8, for iOS 7 and earlier you can use an UIWebView instead:

let webView = UIWebView(frame: CGRectZero)
self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method 
while (webView.loading) {
    CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
let userJSON = webView.stringByEvaluatingJavaScriptFromString("window.user")
let flashJSON = webView.stringByEvaluatingJavaScriptFromString("window.flash")

Note 2. The javascript engine will return the parsed json, if you need to obtain the values as strings you can query after JSON.stringify(window.user) and JSON.stringify(window.flash).


Playground execution

If you want to run this code in a playground, then you need to configure the playground to not terminate when the last line of code executes, and we need to wait until the javascript execution callback is called. Thanks to this SO question I found out how it can be done.

This code works in a playground:

import WebKit
import XCPlayground

// configuring the playground to not stop after evaluating the last line of code
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

let htmlString = "<html>\n" +
    "<head>\n" +
    "<title>aaa</title>\n" +
    "<script>\n" +
    "window.user = {\"username\":\"admin\", \"meta\":{\"city\": \"Los Angeles\", \"country\": \"US\"}};\n" +
    "window.flash = {\"success\": \"\", \"warning\": \"you cannot do this\"};\n" +
    "</script>\n" +
    "</head>\n" +
    "</body>\n" +
"</html>"

let webView = WKWebView(frame: CGRectZero)
// commented out the addSubview call as we don't have a view in a playground
//self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method
while (webView.loading) {
    CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
// get the two objects in one round-trip to the javascript engine
webView.evaluateJavaScript("[window.user, window.flash]") { (result, error) -> Void in
    print(result, error)
}

and prints the following to the console:

Optional((
        {
        meta =         {
            city = "Los Angeles";
            country = US;
        };
        username = admin;
    },
        {
        success = "";
        warning = "you cannot do this";
    }
)) nil

Upvotes: 3

Related Questions