rustdx_cmd/
fetch_code.rs

1use eyre::{anyhow, Result};
2use std::collections::HashSet;
3
4// 股票上限
5const SH8: &str = "800";
6const SH1: &str = "2000";
7
8pub type StockList = HashSet<String>;
9
10#[derive(Debug)]
11pub struct SHSZ {
12    sh1: usize,
13    sh8: usize,
14    sz: usize,
15}
16
17impl SHSZ {
18    /// 计算总和
19    pub fn count(&self) -> usize {
20        let SHSZ { sh1, sh8, sz } = &self;
21        sh1 + sh8 + sz
22    }
23}
24
25/// 获取上证和深证的股票代码
26///
27/// 数量如下:SHSZ { sh1: 1646, sh8: 407, sz: 2745, } -> 4798
28///
29/// 见 `tests-integration::fetch_code::offical_stocks` 测试
30pub fn offical_stocks(set: &mut StockList) -> Result<SHSZ> {
31    let count = SHSZ {
32        sh1: get_sh_stocks(set, "1", SH1)?,
33        sh8: get_sh_stocks(set, "8", SH8)?,
34        sz: get_sz_stocks(set)?,
35    };
36    info!("股票数量 {count:?}");
37    Ok(count)
38}
39
40pub fn get_offical_stocks(cond: &str) -> Result<StockList> {
41    let mut set = StockList::with_capacity(6000);
42    let len = match cond {
43        "official" => offical_stocks(&mut set)?.count(),
44        "szse" => get_sz_stocks(&mut set)?,
45        "sse" => get_sh_stocks(&mut set, "8", SH8)? + get_sh_stocks(&mut set, "1", SH1)?,
46        _ => unreachable!("请输入 official | szse | sse 中的一个"),
47    };
48
49    info!("获得上证和深证股票数量:{len}");
50    Ok(set)
51}
52
53/// 深交所官网的 A 股和创业板股票信息。
54pub fn get_sz_stocks(set: &mut StockList) -> Result<usize> {
55    use calamine::{Data, Reader, Xlsx};
56    use std::io::Read;
57    let (url, ex) = (
58        "http://www.szse.cn/api/report/ShowReport?\
59        SHOWTYPE=xlsx&CATALOGID=1110&TABKEY=tab1&random=0.8587844061443386",
60        "sz",
61    );
62    let bytes = &mut Vec::with_capacity(1 << 20);
63    ureq::get(url).call()?.into_reader().read_to_end(bytes)?;
64    let mut workbook = Xlsx::new(std::io::Cursor::new(bytes))?;
65    // 每个单元格被解析的类型可能会不一样,所以把股票代码统一转化成字符型
66    if let Some(Ok(range)) = workbook.worksheet_range_at(0) {
67        set.extend(range.rows().skip(1).map(|r| match &r[4] {
68            Data::Int(x) => format!("{ex}{x}"),
69            Data::Float(x) => format!("{ex}{}", *x as i64),
70            Data::String(x) => format!("{ex}{x}"),
71            _ => unreachable!(),
72        }));
73        Ok(range.height() - 1)
74    } else {
75        Err(anyhow!("xlsx parse error"))
76    }
77}
78
79const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36";
80const ACCEPT_LANGUAGE: &str =
81    "zh-CN,zh;q=0.9,de;q=0.8,ko;q=0.7,ru;q=0.6,it;q=0.5,ga;q=0.4,en;q=0.3";
82
83pub fn ureq_sz_with_headers(url: &str) -> ureq::Request {
84    ureq::get(url)
85        .set("ACCEPT", "*/*")
86        .set("ACCEPT_LANGUAGE", ACCEPT_LANGUAGE)
87        .set("CACHE_CONTROL", "no-cache")
88        .set("CONNECTION", "keep-alive")
89        .set("CONTENT_TYPE", "application/json")
90        .set("DNT", "1")
91        .set("PRAGMA", "no-cache")
92        .set("REFERER", "http://www.sse.com.cn/")
93        .set("USER_AGENT", USER_AGENT)
94}
95pub fn ureq_sh_with_headers(url: &str) -> ureq::Request {
96    let cookie = "ba17301551dcbaf9_gdp_user_key=; \
97                  ba17301551dcbaf9_gdp_session_id=0876b773-38a3-44d0-bb4a-2b5569025b82; \
98                  gdp_user_id=gioenc-2g8894g6%2C764a%2C50d8%2C8d6g%2C3bg6194752ce; \
99                  ba17301551dcbaf9_gdp_session_id_0876b773-38a3-44d0-bb4a-2b5569025b82=true; \
100                  JSESSIONID=0311A5533F5FD798EE9DAFDE6A1D70A7; \
101                  ba17301551dcbaf9_gdp_sequence_ids=\
102                  {%22globalKey%22:14%2C%22VISIT%22:2%2C%22PAGE%22:5%2C%22VIEW_CHANGE%22:2%2C%22CUSTOM%22:3%2C%22VIEW_CLICK%22:6}";
103    ureq::get(url)
104        .set("ACCEPT", "*/*")
105        .set("ACCEPT_LANGUAGE", ACCEPT_LANGUAGE)
106        .set("CACHE_CONTROL", "no-cache")
107        .set("CONNECTION", "keep-alive")
108        .set("COOKIE", cookie)
109        .set("PRAGMA", "no-cache")
110        .set("REFERER", "http://www.sse.com.cn/")
111        .set("USER_AGENT", USER_AGENT)
112}
113
114// 上交所 科创板 68 开头(目前 350 只,只需一次请求) => stockType=8, pagesize=400
115//        A 股 60 开头(目前 1650 只,只需一次请求) => stockType=1, pagesize=1700
116fn request_sh(stocktype: &str, pagesize: &str) -> ureq::Request {
117    let url = format!(
118        "http://query.sse.com.cn/sseQuery/commonQuery.do?\
119        jsonCallBack=jsonpCallback37525685&STOCK_TYPE={stocktype}\
120        &REG_PROVINCE=&CSRC_CODE=&STOCK_CODE=&sqlId=COMMON_SSE_CP_GPJCTPZ_GPLB_GP_L\
121        &COMPANY_STATUS=2%2C4%2C5%2C7%2C8&type=inParams&isPagination=true\
122        &pageHelp.cacheSize=1&pageHelp.beginPage=1&pageHelp.pageSize={pagesize}\
123        &pageHelp.pageNo=1&pageHelp.endPage=1&_=1680491539414"
124    );
125    ureq_sh_with_headers(&url)
126}
127
128// 上交所 科创板 68 开头(目前 350 只,只需一次请求) => stockType=8, pagesize=400
129//        A 股 60 开头(目前 1650 只,只需一次请求) => stockType=1, pagesize=1700
130pub fn get_sh_stocks(set: &mut StockList, stocktype: &str, pagesize: &str) -> Result<usize> {
131    let text = request_sh(stocktype, pagesize).call()?.into_string()?;
132    let pos1 = text
133        .find("total\":")
134        .ok_or(anyhow!("`Total` field not found"))?
135        + 7;
136    let pos2 = text[pos1..pos1 + 10]
137        .find('}')
138        .ok_or(anyhow!("`Total` field not found"))?
139        + pos1;
140    let n: usize = text[pos1..pos2].parse()?;
141    // 注意:如果不 take 的话,split 有一半是重复的
142
143    set.extend(
144        text.split("COMPANY_CODE")
145            .skip(1)
146            .take(n)
147            .map(|s| format!("sh{}", &s[3..9])),
148    );
149    Ok(n)
150}