1use eyre::{anyhow, Result};
2use std::collections::HashSet;
3
4const 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 pub fn count(&self) -> usize {
20 let SHSZ { sh1, sh8, sz } = &self;
21 sh1 + sh8 + sz
22 }
23}
24
25pub 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
53pub 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 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
114fn 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 ®_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
128pub 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 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}