use serde::Deserialize;
use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::MacroDataPoint;
#[derive(Debug, Deserialize)]
struct HurunResponse {
rows: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
struct XincaifuResponse {
data: Option<XincaifuData>,
}
#[derive(Debug, Deserialize)]
struct XincaifuData {
rows: Option<Vec<serde_json::Value>>,
}
impl AkShareClient {
pub async fn index_bloomberg_billionaires(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://www.bloomberg.com/billionaires";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0 (compatible; akshare-rust/0.1)")
.send()
.await?
.text()
.await?;
let items = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.contains("table-row") {
}
}
if items.is_empty() {
return Err(Error::upstream(
"bloomberg billionaires: page may require JS or be blocked",
));
}
Ok(items)
}
pub async fn index_bloomberg_billionaires_hist(
&self,
year: &str,
) -> Result<Vec<MacroDataPoint>> {
let short_year = if year.len() >= 2 {
&year[year.len() - 2..]
} else {
year
};
let url = format!("https://stats.areppim.com/listes/list_billionaires{short_year}xwor.htm");
let body = self.get(&url).send().await?.text().await?;
let mut items = Vec::new();
let mut in_table = false;
let headers: Vec<String> = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.contains("<table") {
in_table = true;
continue;
}
if trimmed.contains("</table>") {
in_table = false;
continue;
}
if !in_table {
continue;
}
if trimmed.contains("<tr") || trimmed.contains("<td") {
let cells = extract_html_cells(trimmed);
if !cells.is_empty()
&& !headers.is_empty()
&& let Some(rank_str) = cells.first()
&& rank_str.chars().all(|c| c.is_ascii_digit())
{
let net_worth = cells
.get(3)
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
items.push(MacroDataPoint {
date: year.to_string(),
value: net_worth,
name: cells
.get(1)
.cloned()
.unwrap_or_else(|| "Unknown".to_string()),
});
}
}
}
Ok(items)
}
pub async fn forbes_rank(&self, symbol: &str) -> Result<Vec<MacroDataPoint>> {
let url = "https://www.forbeschina.com/lists";
let body = self.get(url).send().await?.text().await?;
let mut list_urls: Vec<(String, String)> = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.contains("href=")
&& trimmed.contains("/lists/")
&& let (Some(href_start), Some(href_end)) =
(trimmed.find("href=\""), trimmed.find('"'))
{
let href = &trimmed[href_start + 6..href_end];
let text = extract_text_between_tags(trimmed);
if !text.is_empty() && href.contains("/lists/") {
list_urls.push((text, format!("https://www.forbeschina.com{href}")));
}
}
}
let target_url = list_urls
.iter()
.find(|(name, _)| name.contains(symbol))
.map(|(_, url)| url.as_str())
.ok_or_else(|| Error::not_found(format!("forbes: list '{symbol}' not found")))?;
let list_body = self.get(target_url).send().await?.text().await?;
let mut items = Vec::new();
for line in list_body.lines() {
let trimmed = line.trim();
if trimmed.contains("<tr") || trimmed.contains("<td") {
let cells = extract_html_cells(trimmed);
if cells.len() >= 3 {
items.push(MacroDataPoint {
date: symbol.to_string(),
value: cells
.get(2)
.and_then(|s| s.replace(',', "").parse::<f64>().ok())
.unwrap_or(0.0),
name: cells
.get(1)
.cloned()
.unwrap_or_else(|| "Unknown".to_string()),
});
}
}
}
Ok(items)
}
pub async fn hurun_rank(&self, indicator: &str, year: &str) -> Result<Vec<MacroDataPoint>> {
let url = "https://www.hurun.net/zh-CN/Rank/HsRankDetails?pagetype=rich";
let _body = self.get(url).send().await?.text().await?;
let list_url = "https://www.hurun.net/zh-CN/Rank/HsRankDetailsList";
let params = build_hurun_params(indicator, year);
let resp: HurunResponse = self
.get(list_url)
.query(¶ms)
.send()
.await?
.json()
.await?;
let rows = resp.rows.unwrap_or_default();
let mut items = Vec::new();
for row in &rows {
let wealth_key = get_hurun_wealth_key(indicator);
let name_key = get_hurun_name_key(indicator);
let wealth = row
.get(&wealth_key)
.and_then(|v| {
v.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| v.as_f64())
})
.unwrap_or(0.0);
let name = row
.get(&name_key)
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !name.is_empty() {
items.push(MacroDataPoint {
date: year.to_string(),
value: wealth,
name,
});
}
}
Ok(items)
}
pub async fn xincaifu_rank(&self, year: &str) -> Result<Vec<MacroDataPoint>> {
let url = "http://service.ikuyu.cn/XinCaiFu2/pcremoting/bdListAction.do";
let resp: XincaifuResponse = self
.get(url)
.query(&[
("method", "getPage"),
("callback", "jsonpCallback"),
("sortBy", ""),
("order", ""),
("type", "4"),
("keyword", ""),
("pageSize", "1000"),
("year", year),
("pageNo", "1"),
("from", "jsonp"),
])
.send()
.await?
.json()
.await?;
let rows = resp.data.and_then(|d| d.rows).unwrap_or_default();
let mut items = Vec::new();
for row in &rows {
let name = row
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let assets = row
.get("assets")
.and_then(|v| {
v.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| v.as_f64())
})
.unwrap_or(0.0);
if !name.is_empty() {
items.push(MacroDataPoint {
date: year.to_string(),
value: assets,
name,
});
}
}
Ok(items)
}
}
fn extract_html_cells(html: &str) -> Vec<String> {
let mut cells = Vec::new();
let mut remaining = html;
while let Some(start) = remaining.find('>') {
let after_start = &remaining[start + 1..];
if let Some(end) = after_start.find('<') {
let text = after_start[..end].trim();
if !text.is_empty() {
cells.push(text.to_string());
}
remaining = &after_start[end..];
} else {
break;
}
}
cells
}
fn extract_text_between_tags(html: &str) -> String {
let mut result = String::new();
let mut remaining = html;
while let Some(start) = remaining.find('>') {
let after_start = &remaining[start + 1..];
if let Some(end) = after_start.find('<') {
let text = after_start[..end].trim();
if !text.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(text);
}
remaining = &after_start[end..];
} else {
break;
}
}
result
}
fn build_hurun_params(indicator: &str, year: &str) -> Vec<(&'static str, String)> {
let num = match indicator {
"胡润全球富豪榜" => format!("global_{year}"),
"胡润印度榜" => format!("india_{year}"),
"胡润全球独角兽榜" => format!("unicorn_{year}"),
"中国瞪羚企业榜" => format!("cgazelles_{year}"),
"全球瞪羚企业榜" => format!("ggazelles_{year}"),
"胡润Under30s创业领袖榜" => format!("u30_{year}"),
"胡润中国500强民营企业" => format!("ctop500_{year}"),
"胡润世界500强" => format!("gtop500_{year}"),
"胡润艺术榜" => format!("art_{year}"),
_ => format!("rich_{year}"),
};
vec![
("num", num),
("search", String::new()),
("offset", "0".to_string()),
("limit", "20000".to_string()),
]
}
fn get_hurun_wealth_key(indicator: &str) -> String {
match indicator {
"胡润全球富豪榜" => "hs_Rank_Global_Wealth".to_string(),
"胡润印度榜" => "hs_Rank_India_Wealth".to_string(),
"胡润全球独角兽榜" => "hs_Rank_Unicorn_Wealth".to_string(),
"胡润中国500强民营企业" => "hs_Rank_CTop500_Wealth".to_string(),
"胡润世界500强" => "hs_Rank_GTop500_Wealth".to_string(),
"胡润艺术榜" => "hs_Rank_Art_Turnover".to_string(),
_ => "hs_Rank_Rich_Wealth".to_string(),
}
}
fn get_hurun_name_key(indicator: &str) -> String {
match indicator {
"胡润全球富豪榜" => "hs_Rank_Global_ChaName_Cn".to_string(),
"胡润印度榜" => "hs_Rank_India_ChaName_Cn".to_string(),
"胡润全球独角兽榜" => "hs_Rank_Unicorn_ChaName_Cn".to_string(),
"胡润中国500强民营企业" => "hs_Rank_CTop500_ChaName_Cn".to_string(),
"胡润世界500强" => "hs_Rank_GTop500_ChaName_Cn".to_string(),
"胡润艺术榜" => "hs_Rank_Art_Name_Cn".to_string(),
_ => "hs_Rank_Rich_ChaName_Cn".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_html_cells() {
let html = "<td>1</td><td>Name</td><td>100.5</td>";
let cells = extract_html_cells(html);
assert_eq!(cells.len(), 3);
assert_eq!(cells[0], "1");
assert_eq!(cells[1], "Name");
assert_eq!(cells[2], "100.5");
}
#[test]
fn test_extract_text_between_tags() {
let html = "<a href=\"/lists/123\">Forbes List</a>";
let text = extract_text_between_tags(html);
assert_eq!(text, "Forbes List");
}
#[test]
fn test_build_hurun_params() {
let params = build_hurun_params("胡润百富榜", "2023");
assert_eq!(params[0].1, "rich_2023");
}
#[test]
fn test_get_hurun_wealth_key() {
assert_eq!(get_hurun_wealth_key("胡润百富榜"), "hs_Rank_Rich_Wealth");
}
}