bms_table/
fetch.rs

1//! 数据获取与 HTML 解析辅助模块
2//!
3//! 在启用 `scraper` 特性时提供 HTML 解析能力,用于从页面的
4//! `<meta name="bmstable" content="...">` 中提取头部 JSON 的地址。
5//! 同时提供一个统一的入口将响应字符串解析为头部 JSON 或其 URL。
6//!
7//! # 示例
8//!
9//! ```rust
10//! # use bms_table::fetch::{get_web_header_json_value, HeaderQueryContent};
11//! let html = r#"
12//! <!DOCTYPE html>
13//! <html>
14//!   <head>
15//!     <meta name="bmstable" content="header.json">
16//!   </head>
17//!   <body></body>
18//! </html>
19//! "#;
20//! match get_web_header_json_value(html).unwrap() {
21//!     HeaderQueryContent::Url(u) => assert_eq!(u, "header.json"),
22//!     _ => unreachable!(),
23//! }
24//! ```
25#![cfg(feature = "scraper")]
26
27pub mod reqwest;
28
29use anyhow::{anyhow, Result};
30use scraper::{Html, Selector};
31use serde_json::Value;
32
33/// [`get_web_header_json_value`] 的返回类型。
34///
35/// - 当输入是 HTML 时,返回从 `<meta name="bmstable">` 提取的 URL;
36/// - 当输入是 JSON 时,返回解析后的 JSON 值。
37pub enum HeaderQueryContent {
38    /// 提取到的头部 JSON 地址。
39    ///
40    /// 可能为相对或绝对 URL,建议使用 `url::Url::join` 进行拼接。
41    Url(String),
42    /// 原始头部 JSON 内容。
43    Json(Value),
44}
45
46/// 将响应字符串解析为头部 JSON 或其 URL。
47///
48/// 解析策略:优先尝试按 JSON 解析;若失败则按 HTML 解析并提取 bmstable URL。
49///
50/// # 返回
51///
52/// - `HeaderQueryContent::Json`:输入是 JSON;
53/// - `HeaderQueryContent::Url`:输入是 HTML。
54///
55/// # 错误
56///
57/// 当输入为 HTML 且未找到 bmstable 字段时返回错误。
58pub fn get_web_header_json_value(response_str: &str) -> anyhow::Result<HeaderQueryContent> {
59    // 先尝试按 JSON 解析;失败则当作 HTML 提取 bmstable URL
60    match serde_json::from_str::<Value>(response_str) {
61        Ok(header_json) => Ok(HeaderQueryContent::Json(header_json)),
62        Err(_) => {
63            let bmstable_url = extract_bmstable_url(response_str)?;
64            Ok(HeaderQueryContent::Url(bmstable_url))
65        }
66    }
67}
68
69/// 从 HTML 页面内容中提取 bmstable 字段指向的 JSON 文件 URL。
70///
71/// 该函数会扫描 `<meta>` 标签,寻找 `name="bmstable"` 的元素,并读取其 `content` 属性。
72///
73/// # 错误
74///
75/// 当未找到目标标签或 `content` 为空时返回错误。
76pub fn extract_bmstable_url(html_content: &str) -> Result<String> {
77    let document = Html::parse_document(html_content);
78
79    // 查找所有meta标签
80    let Ok(meta_selector) = Selector::parse("meta") else {
81        return Err(anyhow!("未找到meta标签"));
82    };
83
84    for element in document.select(&meta_selector) {
85        // 检查是否有name属性为"bmstable"的meta标签
86        if let Some(name_attr) = element.value().attr("name") {
87            if name_attr == "bmstable" {
88                // 获取content属性
89                if let Some(content_attr) = element.value().attr("content") {
90                    if !content_attr.is_empty() {
91                        return Ok(content_attr.to_string());
92                    }
93                }
94            }
95        }
96    }
97
98    Err(anyhow!("未找到bmstable字段"))
99}