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
相关产品推荐
相关产品推荐

