Jsoup 是一款 Java 的 HTML 解析工具,主要是对 HTML 和 XML 文件进行解析。所以,对 JS 动态生成内容的支持并不好。
如果想解析 HTML,因为不同网站的情况不同,一些简单的网站可以通过下面的方法尝试(复杂的我也还不会)。
具体解析要依据网站的结构,如果对前端有些了解大概能更好理解。
HTML 解析 首先添加依赖:
1 implementation 'org.jsoup:jsoup:1.14.1'
第一种方式 :
通过 Jsoup.connect 的方式来解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private fun parseHtml (url: String ) { Thread { val document = Jsoup.connect(url).get () Log.d(TAG, document.title()) 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" ) } } }.start() }
有时还可能需要提交参数(比如搜索),可以尝试以下方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 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" ) } } }.start() }
第二种方式 :
通过 WebView 的 addJavascriptInterface 结合 Jsoup.parse 也是可以解析 HTML 的。不过,速度来讲可能会比 Jsoup.connect 的方式慢一些。(看注释的地方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 private lateinit var mWebView: WebViewprivate fun parseWebView () { mWebView = WebView(this ) mWebView.settings.javaScriptEnabled = true mWebView.webChromeClient = WebChromeClient() mWebView.webViewClient = object : WebViewClient() { } mWebView.loadUrl(url) } private inner class InJavaScriptLocalObj () { @JavascriptInterface fun showSource (html: String , test: String ) { val document = Jsoup.parse(html) } }
示例 不同类型的网站,都有它的特点。
比如小说类:主要以文字内容为主。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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(" " , " " ) } novel_sl.fullScroll(ScrollView.FOCUS_UP); }.start() }
比如漫画类:
有的时候,显示全部章节列表需要点击一下按钮。我们可以内置 js 脚本的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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); if (newProgress == 100 ) { } } } mWebView.webViewClient = object : WebViewClient() { override fun onPageFinished (view: WebView , url: String ) { super .onPageFinished(view, url) Log.d(TAG, "onPageFinished" ) 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。
1 2 3 4 5 6 7 8 9 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 里创建了一个。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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) if (newProgress == 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) ... } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?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 等来执行播放的逻辑了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private lateinit var mWebView: WebViewprivate fun parseWebView () { mWebView = WebView(this ) mWebView.settings.javaScriptEnabled = true mWebView.webChromeClient = WebChromeClient() mWebView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest (view: WebView ?, url: String ) : WebResourceResponse? { 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 标签的方式。)
1、虽然得到 video src 的值为 blog 格式,但是在WebView中依然可以拦截得到.m3u8格式。
2、m3u8视频格式简介
m3u8视频格式原理:将完整的视频拆分成多个 .ts 视频碎片,.m3u8 文件详细记录每个视频片段的地址。
视频播放时,会先读取 .m3u8 文件,再逐个下载播放 .ts 视频片段。
常用于直播业务,也常用该方法规避视频窃取的风险。加大视频窃取难度。
其他 报错 :
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 的检查。
1 val document = Jsoup.connect(url).ignoreContentType(true ).get ()
备注 参考资料 :
易百教程
WIKI教程
欢迎关注微信公众号:非也缘也