Jsoup 在 Android 中的尝试

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("&nbsp;", " ")
            }

            //设置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()

备注

参考资料

易百教程

WIKI教程

欢迎关注微信公众号:非也缘也

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值