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

Rust中quick-xml反序列化YouTube XML时media_group字段为空排查

YouTube XML数据反序列化后media_group字段为None的问题排查与解决

问题描述

使用Rust的quick-xml库反序列化YouTube频道XML数据接口返回的内容时,尽管原始XML中包含media:group相关数据,但Entry结构体中的media_group字段始终返回None。相关代码及依赖如下:

核心代码(youtube_data.rs)

// youtube_data.rs
use hyper::{Body, Client, Uri};
use hyper_tls::HttpsConnector;
use serde::Deserialize;
use quick_xml::de::{from_str, DeError};
use crate::internals::singularity::WidgetError;

impl From<DeError> for WidgetError {
    fn from(err: DeError) -> Self {
        WidgetError::XmlParse(err.to_string())
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename = "feed")]
pub struct Feed {
    id: String,
    #[serde(rename = "channelId", default)]
    yt_channel_id: Option<String>,
    title: String,
    published: String,
    #[serde(default)]
    #[serde(rename = "entry")]
    entries: Vec<Entry>,
}

#[derive(Debug, Deserialize)]
struct Entry {
    id: String,
    title: String,
    published: String,
    #[serde(rename = "mediaGroup")]
    media_group: Option<MediaGroup>,
}

#[derive(Debug, Deserialize)]
struct MediaGroup {
    #[serde(rename = "mediaTitle")]
    title: Option<String>,
    #[serde(rename = "mediaContent")]
    content: Option<MediaContent>,
    #[serde(rename = "mediaThumbnail")]
    thumbnail: Option<MediaThumbnail>,
    #[serde(rename = "mediaCommunity")]
    community: Option<MediaCommunity>,
}

#[derive(Debug, Deserialize)]
struct MediaContent {
    #[serde(rename = "@url")]
    url: String,
    #[serde(rename = "@width")]
    width: u32,
    #[serde(rename = "@height")]
    height: u32,
}

#[derive(Debug, Deserialize)]
struct MediaThumbnail {
    #[serde(rename = "@url")]
    url: String,
    #[serde(rename = "@width")]
    width: u32,
    #[serde(rename = "@height")]
    height: u32,
}

#[derive(Debug, Deserialize)]
struct MediaCommunity {
    #[serde(rename = "mediaStarRating")]
    star_rating: Option<StarRating>,
    #[serde(rename = "mediaStatistics")]
    statistics: Option<Statistics>,
}

#[derive(Debug, Deserialize)]
struct StarRating {
    #[serde(rename = "@count")]
    count: u64,
}

#[derive(Debug, Deserialize)]
struct Statistics {
    #[serde(rename = "@views")]
    views: u64,
}

pub async fn get_youtube_vids_for_a_channel(channel_id: String) -> Result<Feed, WidgetError> {
    let url = format!(
        "https://www.youtube.com/feeds/videos.xml?channel_id={}",
        channel_id
    );

    let uri: Uri = url.parse().unwrap();

    let https = HttpsConnector::new();
    let client = Client::builder().build::<_, Body>(https);
    let res = client.get(uri).await?;
    let body = hyper::body::to_bytes(res.into_body()).await?;
    let response_text = String::from_utf8(body.to_vec())
        .map_err(|e| WidgetError::Utf8Error(e.to_string()))?; 

    let feed: Feed = from_str(&response_text)?;

    Ok(feed)
}

错误定义(singularity.rs)

// singularity.rs

#[derive(Debug, Error)]
pub enum WidgetError {
    #[error("Hyper error: {0}")]
    Hyper(#[from] hyper::Error),
    #[error("No geocoding data found")]
    NoGeocodingData,
    #[error("Error in reading HTML file")]
    NoHtmlToString,
    #[error("Serde JSON error: {0}")]
    SerdeJson(#[from] serde_json::Error),
    #[error("No timezone found")]
    NoTimeZone,
    #[error("XML parsing error: {0}")]
    XmlParse(String),
    #[error("UTF-8 conversion error: {0}")]
    Utf8Error(String),
}

依赖配置(Cargo.toml)

[dependencies]
hyper = { version = "0.14", features = ["full"] }
hyper-tls = "0.5"
regex = "1.10.5"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0.63"
quick-xml = { version = "0.37", features = ["serialize"] }

问题原因

YouTube返回的XML中,media相关元素带有命名空间前缀,例如实际元素是<media:group>而非代码中期望的mediaGroup。quick-xml在反序列化时默认会严格匹配元素的完整名称(包括命名空间URI),而代码中没有处理命名空间,导致无法匹配到对应元素,最终media_group字段被反序列化为None。

查看YouTube XML的根元素可以看到命名空间声明:

<feed xmlns:media="http://search.yahoo.com/mrss/" ...>

其中media前缀对应的URI是http://search.yahoo.com/mrss/,所有media相关元素都属于这个命名空间。

解决方法

在serde的rename属性中,使用带命名空间URI的完整元素名称来匹配XML中的元素。格式为"{命名空间URI}元素名"。

修改后的核心结构体代码

#[derive(Debug, Deserialize)]
struct Entry {
    id: String,
    title: String,
    published: String,
    // 匹配<media:group>元素,使用命名空间URI
    #[serde(rename = "{http://search.yahoo.com/mrss/}group")]
    media_group: Option<MediaGroup>,
}

#[derive(Debug, Deserialize)]
struct MediaGroup {
    // 匹配<media:title>
    #[serde(rename = "{http://search.yahoo.com/mrss/}title")]
    title: Option<String>,
    // 匹配<media:content>
    #[serde(rename = "{http://search.yahoo.com/mrss/}content")]
    content: Option<MediaContent>,
    // 匹配<media:thumbnail>
    #[serde(rename = "{http://search.yahoo.com/mrss/}thumbnail")]
    thumbnail: Option<MediaThumbnail>,
    // 匹配<media:community>
    #[serde(rename = "{http://search.yahoo.com/mrss/}community")]
    community: Option<MediaCommunity>,
}

#[derive(Debug, Deserialize)]
struct MediaCommunity {
    // 匹配<media:starRating>
    #[serde(rename = "{http://search.yahoo.com/mrss/}starRating")]
    star_rating: Option<StarRating>,
    // 匹配<media:statistics>
    #[serde(rename = "{http://search.yahoo.com/mrss/}statistics")]
    statistics: Option<Statistics>,
}

补充说明

  • 确保所有media相关元素的rename属性都使用正确的命名空间URI,否则对应字段仍会返回None。
  • 如果XML中还有其他命名空间的元素(比如YouTube自己的yt:前缀),也需要用同样的方式处理,例如yt:channelId对应"{http://www.youtube.com/xml/schemas/2015}channelId"。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 06:34:56