Jsoup 是一款 Java 的 HTML 解析工具,主要是对 HTML 和 XML 文件进行解析。所以,对 JS 动态生成内容的支持并不好。
如果想解析 HTML,因为不同网站的情况不同,一些简单的网站可以通过下面的方法尝试(复杂的我也还不会)。
具体解析要依据网站的结构,如果对前端有些了解大概能更好理解。
HTML 解析
首先添加依赖:
implementation 'org.jsoup:jsoup:1.14.1'
第一种方式:
通过 Jsoup.connect 的方式来解析
private fun parseHtml(url: String) {
Thread {
val document = Jsoup.connect(url).get()
Log.d(TAG, document.title())
// 先看网页结构。比如:先获取 id 为 player1 的元素,然后获取其中所有的 li 元素,并打印出每个 li 元素内的 a 标签属性信息。
val elements = document.getElementById("player1")
val li = elements?.select("li")
if (li != null) {
for (e in li) {
val aElement = e.select("a")
val movieName: String = aElement.text()
val url: String = aElement.attr("href")
// Log.d("TAG_电影名称","movieName: $movieName")
// Log.d("TAG_电影详情页","url: $url")
}
}
}.start()
}
有时还可能需要提交参数(比如搜索),可以尝试以下方式。
private fun parseHtml(url: String, searchData: String) {
Log.d(TAG, "parseHtml: " + url + searchData)
Thread {
val document = Jsoup.connect(url)
.postDataCharset("GBK") // 提交的参数有汉字时,避免乱码问题。
.data("searchkey", searchData)
.post()
// 获取的内容有乱码时,也要注意编码格式。
// val document = Jsoup.parse(URL(url).openStream(), "GBK", url);
// 具体情况具体分析,看网站的结构来解析。写法不固定。
val link = document.select("link").first()
if (link!=null) {
href = link.attr("href")
Log.d(TAG, "href: " + href)
}
val elements = document.getElementById("list")
val li = elements?.select("dd")
if (li != null) {
for (e in li) {
val aElement = e.select("a")
val movieName: String = aElement.text()
val url: String = aElement.attr("href")
// Log.d("TAG_电影名称", "movieName: $movieName")
// Log.d("TAG_电影详情页url", "url: $url")
}
}
}.start()
}
第二种方式:
通过 WebView 的 addJavascriptInterface 结合 Jsoup.parse 也是可以解析 HTML 的。不过,速度来讲可能会比 Jsoup.connect 的方式慢一些。(看注释的地方)
private lateinit var mWebView: WebView
private fun parseWebView() {
mWebView = WebView(this)
mWebView.settings.javaScriptEnabled = true
// mWebView.addJavascriptInterface(InJavaScriptLocalObj(), "local_obj")
mWebView.webChromeClient = WebChromeClient()
mWebView.webViewClient = object : WebViewClient() {
// override fun onPageFinished(view: WebView, url: String) {
// super.onPageFinished(view, url)
// view.loadUrl("javascript:window.local_obj.showSource('<head>'+document.getElementsByTagName('html')[0].innerHTML+'</head>','test');")
// }
}
mWebView.loadUrl(url)
}
private inner class InJavaScriptLocalObj() {
@JavascriptInterface
fun showSource(html: String, test: String) {
// 将给定的html代码解析成文档
val document = Jsoup.parse(html)
// 具体解析要看网页的格式
// val elements = document.getElementById("player1")
// val li = elements?.select("li")
//
// if (li != null) {
// for (e in li) {
// val aElement = e.select("a")
// //电影名称
// val movieName: String = aElement.text()
// //电影详情页url
// val url: String = aElement.attr("href")
Log.d("TAG","movieName: $movieName")
Log.d("TAG","url: $url")
// }
// }
// Log.d("TAG_a[href]","===============================================")
// val links = document.select("a[href]")
// for (link in links) {
// Log.d("TAG_parse", "link : " + link.attr("href"))
// Log.d("TAG_parse", "text : " + link.text())
// }
// Log.d("TAG_a[href]","===============================================")
}
}
示例
不同类型的网站,都有它的特点。
比如小说类:主要以文字内容为主。
private fun parseHtml(url: String) {
Log.d(TAG,"从章节点击:"+url)
Thread {
val document = Jsoup.connect(url).get()
val elements = document.getElementById("content")
Log.d(TAG, elements.toString())
runOnUiThread {
tv_novel_content.text = elements?.html()
.toString()
.replace("<br>", "")
.replace(" ", " ")
}
//设置ScrollView滚动到顶部
novel_sl.fullScroll(ScrollView.FOCUS_UP);
// //设置ScrollView滚动到顶部
// novel_sl.fullScroll(ScrollView.FOCUS_DOWN);
}.start()
}
比如漫画类:
有的时候,显示全部章节列表需要点击一下按钮。我们可以内置 js 脚本的方式。
private fun parseWebView() {
mWebView = WebView(this)
mWebView.settings.javaScriptEnabled = true
mWebView.addJavascriptInterface(InJavaScriptLocalObj(), "local_obj")
mWebView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
Log.d(TAG, "on page progress changed and progress is " + newProgress);
// 进度是100就代表dom树加载完成了
if (newProgress == 100) {
}
}
}
mWebView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
Log.d(TAG, "onPageFinished")
// 这里分开写为了便于理解,休眠两秒的逻辑是为了等待章节列表加载完毕再解析页面。下面会写在一起。
// 第一个 view.loadUrl:获取到 id 为 all_mores1 的 <a> 标签,然后执行点击。
// 第二个 view.loadUrl:在 InJavaScriptLocalObj 中解析页面。
view.loadUrl("javascript:function aa({document.getElementById('all_mores1').click();};aa();")
Thread.sleep(2000)
view.loadUrl("javascript:window.local_obj.showSource('<head>'+document.getElementsByTagName('html')[0].innerHTML+'</head>','test');")
}
}
mWebView.loadUrl(url)
}
private inner class InJavaScriptLocalObj() {
@JavascriptInterface
fun showSource(html: String, test: String) {
val document = Jsoup.parse(html)
...
}
}
有时拿到的属性值不单单是个 url 地址,还会包含其他的内容(比如这样:background-image: url(https://...)
)。可以通过如下方法过滤出 url。
fun findUrlByStr(data: String): String {
val pattern =
Pattern.compile("https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]");
val matcher = pattern.matcher(data);
if (matcher.find()) {
return matcher.group();
}
return "";
}
当进入到详情页时,漫画类网站有些是需要滚动到页面底部才会加载图片。这时可以通过 WebView 来模拟浏览器行为。(这里,我动态创建的 WebView 执行 js 代码来滚动页面无效,所以在 xml 里创建了一个。)
override fun onAdapterListener(position: Int) {
Log.d(TAG, "显示详细内容:" + resultUrl + seriesList[position].url)
wv.settings.javaScriptEnabled = true
wv.addJavascriptInterface(MyJavaScriptLocalObj(), "local_obj")
wv.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
Log.d(TAG, "on page progress changed and progress is " + newProgress)
// 进度是100就代表dom树加载完成了
if (newProgress == 100) {
// 这里写到一起了。每 100 毫秒执行一次页面滚动到底部,然后根据页面能够提供的信息,比如可以拿到页数,判断页数到底后,跳出循环,执行解析。
view.loadUrl("javascript:function aa(){var interal = setInterval(function () {var ll = document.getElementById('js_staticPage').innerText.split('/');window.scrollTo(0,document.body.scrollHeight);if(ll[0] == ll[1]){window.local_obj.showSource('<head>'+document.getElementsByTagName('html')[0].innerHTML+'</head>');clearInterval(interal)}}, 100)};aa();")
}
}
}
wv.webViewClient = WebViewClient()
wv.loadUrl(resultUrl + seriesList[position].url)
}
private inner class MyJavaScriptLocalObj() {
@JavascriptInterface
fun showSource(html: String) {
val document = Jsoup.parse(html)
...
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/wv"
android:visibility="invisible"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_comic"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>
比如影视类:
影视类一般需要获取到视频的 url 地址
通过 WebViewClient 的 shouldInterceptRequest() 可以尝试过滤得到视频 url,有了视频地址便可以通过 ExoPlayer 或者 GSYPlayer 等来执行播放的逻辑了。
private lateinit var mWebView: WebView
private fun parseWebView() {
mWebView = WebView(this)
mWebView.settings.javaScriptEnabled = true
mWebView.webChromeClient = WebChromeClient()
mWebView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
// 过滤出视频格式url
if (url.contains(".mp4") || url.contains(".m3u8") || url.contains(".avi") ||
url.contains(".mov") || url.contains(".mkv") || url.contains(".flv") ||
url.contains(".f4v") || url.contains(".rmvb")
) {
Log.d(TAG, "$url")
runOnUiThread {
videoPlay(url)
}
}
return super.shouldInterceptRequest(view, url)
}
}
mWebView.loadUrl(url)
}
如果视频地址写在 script 标签中,具体解析看网页结构。大体上就是先拿到 script 标签内容,然后通过 split 分割等,过滤出 url。
private fun aaa(url : String){
Thread {
val document = Jsoup.connect(url).get()
Log.d(TAG,"document: $document")
val script = document.select("script")
Log.d(TAG,"scripe: $script")
for(e in script){
val data = e.data().toString().split("var")
for(w in data){
if (w.contains("=")){
if (w.contains("video")){
val vv = w.split("\"")
for(q in vv){
if (q.contains(".mp4") || q.contains(".m3u8") || q.contains(".avi") ||
q.contains(".mov") || q.contains(".mkv") || q.contains(".flv") ||
q.contains(".f4v") || q.contains(".rmvb")
) {
Log.d(TAG, "过滤的地址 q:"+"$q")
runOnUiThread {
videoPlay(q)
}
break
}
}
}
}
}
}
}.start()
}
如果得到的视频地址是 m3u8 格式或者是 mp4 格式,可以通过 ExoPlayer 或者 GSYPlayer 直接播放。但许多网站其实是 blog url 格式的(blob:https://),这个还没有找到解析的办法。(有一种方式感觉成功几率不高,并且也不是由代码解决的。记录一下,就是在 F12 检查时,在 video 标签内插入 a 标签的方式。)
其他
报错:
org.jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/, application/xml, or application/+xml. Mimetype=video/mp4, URL=""
原因:可能是请求头里面的请求类型(ContextType)不符合要求。
解决:只需要在 Connection con = Jsoup.connect(url); 中添加 ignoreContentType(true) 即可,意思就是忽略ContextType 的检查。
val document = Jsoup.connect(url).ignoreContentType(true).get()
备注
参考资料:
欢迎关注微信公众号:非也缘也