1+ // kzz.mjs
2+ import axios from 'axios' ;
3+ import dayjs from 'dayjs' ;
4+ import customParseFormat from 'dayjs/plugin/customParseFormat.js' ;
5+
6+ // 扩展dayjs以支持自定义格式解析
7+ dayjs . extend ( customParseFormat ) ;
8+
9+ // 默认用户代理
10+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' ;
11+
12+ /**
13+ * 获取可转债数据
14+ * @param {string|number } taskId - 任务ID
15+ * @param {string } [url='http://data.eastmoney.com/kzz/'] - 东方财富网查询地址
16+ * @param {number } [dayeExtra=0] - 日期偏移量。正数:未来天数,负数:过去天数
17+ * @param {Object } [kwargs] - 其他可选参数
18+ * @returns {Promise<Array> } - 可转债列表
19+ */
20+ export async function getNowKzz ( taskId , url = 'http://data.eastmoney.com/kzz/' , dayeExtra = 0 , kwargs = { } ) {
21+ console . log ( `[任务 ${ taskId } ] 开始获取可转债数据,日期偏移量: ${ dayeExtra } , 参数: ${ JSON . stringify ( kwargs ) } ` ) ;
22+
23+ const headers = {
24+ 'User-Agent' : DEFAULT_USER_AGENT ,
25+ 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' ,
26+ 'Accept-Language' : 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2' ,
27+ 'Connection' : 'keep-alive' ,
28+ 'Upgrade-Insecure-Requests' : '1' ,
29+ 'Pragma' : 'no-cache' ,
30+ 'Cache-Control' : 'no-cache'
31+ } ;
32+
33+ // 创建axios会话实例
34+ const session = axios . create ( {
35+ headers,
36+ timeout : 15000 ,
37+ withCredentials : true
38+ } ) ;
39+
40+ try {
41+ // 先访问首页获取必要的cookies
42+ console . log ( `[任务 ${ taskId } ] 访问首页获取cookies...` ) ;
43+ await session . get ( url ) ;
44+
45+ // 构建数据API URL - 使用随机回调函数名
46+ const callbackName = `jQuery${ Math . floor ( Math . random ( ) * 1000000000 ) } _${ Date . now ( ) } ` ;
47+ const dataUrl = `https://datacenter-web.eastmoney.com/api/data/v1/get?callback=${ callbackName } &sortColumns=PUBLIC_START_DATE&sortTypes=-1&pageSize=50&pageNumber=1&reportName=RPT_BOND_CB_LIST&columns=ALL"eColumns=f2~01~CONVERT_STOCK_CODE~CONVERT_STOCK_PRICE%2Cf235~10~SECURITY_CODE~TRANSFER_PRICE%2Cf236~10~SECURITY_CODE~TRANSFER_VALUE%2Cf2~10~SECURITY_CODE~CURRENT_BOND_PRICE%2Cf237~10~SECURITY_CODE~TRANSFER_PREMIUM_RATIO%2Cf239~10~SECURITY_CODE~RESALE_TRIG_PRICE%2Cf240~10~SECURITY_CODE~REDEEM_TRIG_PRICE%2Cf23~01~CONVERT_STOCK_CODE~PBV_RATIO"eType=0&source=WEB&client=WEB` ;
48+
49+ console . log ( `[任务 ${ taskId } ] 请求API数据...` ) ;
50+ const response = await session . get ( dataUrl ) ;
51+
52+ // 解析JSONP响应
53+ console . log ( `[任务 ${ taskId } ] 解析响应数据...` ) ;
54+ const jsonStr = extractJsonFromJsonp ( response . data ) ;
55+ const data = safeParseJson ( jsonStr ) ;
56+
57+ console . log ( `[任务 ${ taskId } ] 处理可转债数据...` ) ;
58+ return processKzzData ( data , dayeExtra ) ;
59+ } catch ( error ) {
60+ const errorMsg = `[任务 ${ taskId } ] 获取可转债数据失败: ${ error . message } ` ;
61+ console . error ( errorMsg ) ;
62+ throw new Error ( errorMsg ) ;
63+ }
64+ }
65+
66+ /**
67+ * 从JSONP响应中提取JSON字符串
68+ * @param {string } jsonp - JSONP响应字符串
69+ * @returns {string } - JSON字符串
70+ */
71+ function extractJsonFromJsonp ( jsonp ) {
72+ // 尝试多种解析方式
73+ const patterns = [
74+ / ^ [ ^ { ] * ?\( ( { .* } ) \) [ ^ } ] * ?$ / ,
75+ / ^ .* ?\( ( { .* } ) \) $ / ,
76+ / ^ .* ?\( ( { .* } ) \) ; ? $ / ,
77+ / ( { .* } ) /
78+ ] ;
79+
80+ for ( const pattern of patterns ) {
81+ const match = jsonp . match ( pattern ) ;
82+ if ( match && match [ 1 ] ) {
83+ return match [ 1 ] ;
84+ }
85+ }
86+
87+ // 尝试直接解析为JSON
88+ try {
89+ JSON . parse ( jsonp ) ;
90+ return jsonp ;
91+ } catch ( e ) {
92+ throw new Error ( '无法解析JSONP响应: ' + jsonp . substring ( 0 , 100 ) + '...' ) ;
93+ }
94+ }
95+
96+ /**
97+ * 安全解析JSON字符串
98+ * @param {string } jsonStr - JSON字符串
99+ * @returns {Object } - 解析后的对象
100+ */
101+ function safeParseJson ( jsonStr ) {
102+ try {
103+ return JSON . parse ( jsonStr ) ;
104+ } catch ( e ) {
105+ // 尝试修复常见的JSON格式问题
106+ try {
107+ const fixedJson = jsonStr
108+ . replace ( / ( [ { , ] \s * ) ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) ( \s * : ) / g, '$1"$2"$3' )
109+ . replace ( / : \s * ' ( [ ^ ' ] * ) ' / g, ': "$1"' )
110+ . replace ( / ( \w + ) \s * : / g, '"$1":' ) // 修复键名缺少引号
111+ . replace ( / : \s * ( [ a - z A - Z _ ] [ \w ] * ) ( [ , \} ] ) / g, ':"$1"$2' ) ; // 修复字符串值缺少引号
112+
113+ return JSON . parse ( fixedJson ) ;
114+ } catch ( e2 ) {
115+ console . error ( 'JSON解析错误:' , e2 . message ) ;
116+ console . error ( '原始JSON字符串:' , jsonStr . substring ( 0 , 200 ) ) ;
117+ throw new Error ( 'JSON解析失败: ' + e2 . message ) ;
118+ }
119+ }
120+ }
121+
122+ function compareDates ( dateStr1 , dateStr2 ) {
123+ const date1 = new Date ( dateStr1 ) ;
124+ const date2 = new Date ( dateStr2 ) ;
125+ return date1 . getTime ( ) - date2 . getTime ( ) ;
126+ }
127+
128+ // 返回值>0表示date1较晚,<0表示date1较早,=0表示相同
129+
130+
131+ /**
132+ * 处理可转债数据
133+ * @param {Object } data - 原始数据
134+ * @param {number } dayeExtra - 日期偏移量
135+ * @returns {Array } - 处理后的可转债列表
136+ */
137+ function processKzzData ( data , dayeExtra ) {
138+ if ( ! data || ! data . result || ! Array . isArray ( data . result . data ) ) {
139+ throw new Error ( 'API返回的数据结构无效' ) ;
140+ }
141+
142+ const resultData = data . result . data ;
143+
144+ // 计算目标日期范围
145+ const today = dayjs ( ) ;
146+ const targetDate = today . add ( dayeExtra , 'day' ) ;
147+ const targetDateStr = targetDate . format ( 'YYYY-MM-DD' ) ;
148+
149+ console . log ( `筛选日期: ${ targetDateStr } (偏移量: ${ dayeExtra } 天)` ) ;
150+
151+ // 处理数据
152+ const kzzList = [ ] ;
153+ const dateFormats = [
154+ 'YYYY-MM-DD HH:mm:ss' ,
155+ 'YYYY/MM/DD HH:mm:ss' ,
156+ 'YYYY-MM-DD' ,
157+ 'YYYY/MM/DD' ,
158+ 'YYYY-M-DD' ,
159+ 'YYYY-M-D' ,
160+ 'YYYY/MM/DD'
161+ ] ;
162+
163+ for ( const item of resultData ) {
164+ if ( ! item . PUBLIC_START_DATE ) {
165+ console . warn ( `跳过无发行日期的项目: ${ item . SECURITY_NAME_ABBR } ` ) ;
166+ continue ;
167+ }
168+
169+ // 解析日期
170+ let publicDate ;
171+ for ( const format of dateFormats ) {
172+ publicDate = dayjs ( item . PUBLIC_START_DATE , format , true ) ;
173+ if ( publicDate . isValid ( ) ) break ;
174+ }
175+
176+ if ( ! publicDate . isValid ( ) ) {
177+ console . warn ( `无效的日期格式: ${ item . PUBLIC_START_DATE } (项目: ${ item . SECURITY_NAME_ABBR } )` ) ;
178+ continue ;
179+ }
180+
181+ const dateStr = publicDate . format ( 'YYYY-MM-DD' ) ;
182+
183+ // 检查是否匹配目标日期
184+ // if (dateStr === targetDateStr) {
185+ if ( compareDates ( dateStr , targetDateStr ) >= 0 ) {
186+ kzzList . push ( {
187+ name : item . SECURITY_NAME_ABBR ,
188+ code : item . SECURITY_CODE ,
189+ date : dateStr
190+ } ) ;
191+ }
192+ }
193+
194+ console . log ( `找到 ${ kzzList . length } 个匹配的可转债` ) ;
195+ return kzzList ;
196+ }
197+
198+ /**
199+ * 主函数 - 测试用
200+ */
201+ async function main ( ) {
202+ try {
203+ // 支持正负偏移量测试
204+ const testCases = [
205+ { offset : 0 , desc : "今天" } ,
206+ { offset : 1 , desc : "明天" } ,
207+ { offset : - 1 , desc : "昨天" } ,
208+ { offset : 7 , desc : "7天后" } ,
209+ { offset : - 7 , desc : "7天前" }
210+ ] ;
211+
212+ for ( const testCase of testCases ) {
213+ console . log ( `\n===== 测试 ${ testCase . desc } (偏移量: ${ testCase . offset } ) =====` ) ;
214+ const kzzData = await getNowKzz (
215+ `test_${ testCase . offset } ` ,
216+ 'http://data.eastmoney.com/kzz/' ,
217+ testCase . offset
218+ ) ;
219+
220+ if ( kzzData . length > 0 ) {
221+ console . log ( `找到 ${ kzzData . length } 个可转债:` ) ;
222+ kzzData . forEach ( ( item , index ) => {
223+ console . log ( `${ index + 1 } . ${ item . name } (${ item . code } ) - ${ item . date } ` ) ;
224+ } ) ;
225+ } else {
226+ console . log ( `未找到 ${ testCase . desc } 的可转债数据` ) ;
227+ }
228+ }
229+
230+ console . log ( '\n所有测试完成' ) ;
231+ } catch ( error ) {
232+ console . error ( '测试失败:' , error . message ) ;
233+ }
234+ }
235+
236+ // 使用立即执行的异步函数调用main
237+ ( async ( ) => {
238+ try {
239+ // await main();
240+ const kzzData = await getNowKzz (
241+ `test_7` ,
242+ 'http://data.eastmoney.com/kzz/' ,
243+ 7
244+ ) ;
245+ console . log ( kzzData ) ;
246+ console . log ( '程序执行完成' ) ;
247+ } catch ( error ) {
248+ console . error ( '程序执行失败:' , error . message ) ;
249+ process . exit ( 1 ) ;
250+ }
251+ } ) ( ) ;
0 commit comments