You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android应用中如何从博客URL提取标题、描述及图片URL实现类Google卡片

实现Android中URL链接的元数据提取(类Google风格卡片视图)

我之前做过类似的需求,其实核心就是抓取目标网页的元数据标签(比如Open Graph标签,也就是OG标签),这也是Facebook、WhatsApp等APP自动生成链接预览的原理。下面给你一步步拆解实现方案:

一、核心原理

大部分网站都会在HTML的<head>里添加OG标签(专门用于社交分享预览),如果没有OG标签,我们就 fallback 到常规的<title>和<meta>标签。常见的关键标签:

  • og:title:页面标题(优先取这个)
  • og:description:页面描述
  • og:image:预览图片URL
  • 如果没有OG标签,就取<title>标签内容,以及<meta name="description" content="xxx">的内容

二、具体实现步骤

1. 依赖准备

首先在build.gradle(Module级别)添加需要的库:

// 网络请求用OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
// HTML解析用Jsoup
implementation 'org.jsoup:jsoup:1.17.2'
// 图片加载用Glide(可选,也可以用Picasso)
implementation 'com.github.bumptech.glide:glide:4.16.0'
// Kotlin协程(处理异步任务,推荐)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

2. 网络请求获取HTML内容

因为网络请求不能在主线程,这里用Kotlin协程来处理:

private suspend fun fetchHtml(url: String): String? {
    val client = OkHttpClient.Builder()
        // 设置User-Agent,避免被服务器拦截(模拟浏览器请求)
        .addInterceptor { chain ->
            val request = chain.request().newBuilder()
                .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36")
                .build()
            chain.proceed(request)
        }
        .build()

    return try {
        val request = Request.Builder().url(url).build()
        val response = client.newCall(request).await() // 协程异步请求
        if (response.isSuccessful) response.body?.string() else null
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

3. 解析HTML提取元数据

用Jsoup解析HTML,提取我们需要的信息:

data class LinkPreview(
    val title: String?,
    val description: String?,
    val imageUrl: String?,
    val url: String
)

private fun parseHtml(html: String, originalUrl: String): LinkPreview {
    val doc = Jsoup.parse(html)
    // 优先取OG标签
    val title = doc.select("meta[property=og:title]").attr("content").takeIf { it.isNotEmpty() } 
        ?: doc.title() // 没有OG标签就取页面title
    val description = doc.select("meta[property=og:description]").attr("content").takeIf { it.isNotEmpty() }
        ?: doc.select("meta[name=description]").attr("content")
    val imageUrl = doc.select("meta[property=og:image]").attr("content").takeIf { it.isNotEmpty() }

    return LinkPreview(title, description, imageUrl, originalUrl)
}

4. 整合异步流程

在ViewModel或者Activity里调用协程处理:

fun loadLinkPreview(url: String) {
    viewModelScope.launch {
        val html = fetchHtml(url)
        html?.let {
            val preview = parseHtml(it, url)
            // 拿到预览数据后,更新UI
            updateCardUi(preview)
        } ?: run {
            // 处理请求失败的情况,比如显示错误提示
        }
    }
}

三、类Google风格卡片UI实现

用CardView来实现卡片布局,示例XML:

<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="12dp">

        <!-- 预览图片 -->
        <ImageView
            android:id="@+id/iv_preview"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:scaleType="centerCrop"
            android:src="@drawable/placeholder_image"/>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginStart="12dp"
            android:orientation="vertical"
            android:gravity="center_vertical">

            <!-- 标题 -->
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:textStyle="bold"
                android:maxLines="2"
                android:ellipsize="end"/>

            <!-- 描述 -->
            <TextView
                android:id="@+id/tv_description"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:textColor="@color/gray"
                android:maxLines="3"
                android:ellipsize="end"
                android:layout_marginTop="4dp"/>

            <!-- 原始URL -->
            <TextView
                android:id="@+id/tv_url"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:textColor="@color/dark_gray"
                android:layout_marginTop="8dp"/>
        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

然后在updateCardUi方法里用Glide加载图片,填充文本:

private fun updateCardUi(preview: LinkPreview) {
    tv_title.text = preview.title
    tv_description.text = preview.description ?: "暂无描述"
    tv_url.text = preview.url

    preview.imageUrl?.let {
        Glide.with(this)
            .load(it)
            .placeholder(R.drawable.placeholder_image)
            .into(iv_preview)
    } ?: run {
        iv_preview.setImageResource(R.drawable.placeholder_image)
    }
}

四、注意事项

  • 权限配置:在AndroidManifest.xml中添加网络权限:
    <uses-permission android:name="android.permission.INTERNET" />
    
  • 缓存优化:可以把解析后的LinkPreview数据缓存到Room或者SharedPreferences,避免重复请求相同URL,提升性能。
  • 异常处理:要处理网络错误、URL无效、HTML解析失败等情况,给用户友好的提示。
  • 线程安全:所有网络请求和解析操作必须在子线程执行,用协程、RxJava或者其他异步框架,不要阻塞主线程。

内容的提问来源于stack exchange,提问作者Shivam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:40:47