前文说了上海期货交易所的数据是json格式,非常容易用程序解析。这里再看看大连商品交易所的实现方式。
还是按F12先从浏览器查看网络请求,大商所的页面采用的传统ajax方式,返回的是一个html子页面,然后客户端做局部刷新。请求地址是:
POST http://www.dce.com.cn/publicweb/quotesdata/dayQuotesCh.html
html的解析相对而言是实现最繁琐且执行效率最低的方式,应该尽量避免。
进一步观察发现系统还提供了一个导出文本的接口,回应中包含的数据跟网页是一致的,且格式是结构化的,基本可以看做是tsv格式的文件。那么我们就用这个接口来实现。
POST http://www.dce.com.cn/publicweb/quotesdata/exportDayQuotesChData.html
除去前文提到过的实现模式,这里的要点是对文本文件的解析。先看文本文件的格式:
每种合约一行,字段之前用tab分割(准确的说有时候一个tab,有时候两个tab,所以说不是完全合规的tsv格式 )。其中每一类商品合约结束的时候有一个小计行,整个文件结束的时候还有一个合计行(截图上没显示,可以自行查看官方数据)。
这里需要做的就是从中正确提取各个字段的值,对应的包括按换行符分割数据,清洗不需要的统计行,处理分隔符得到正确的数值,代码片段如下:
r = requests.get(dce_url, headers=headers)
tsv_origin = r.content.decode().split('\r\n')
tsv = []
for item in tsv_origin:
item = item.strip('\t')
if '商品名称' in item or '小计' in item or '总计' in item or item == '':
continue
else:
tsv.append(re.compile("\t+").sub('\t', item).split('\t'))
在保存的时候,还要自己把当天的日期加上,因为服务端返回的数据是根据请求的日期查询出来的,所以在返回的数据中省略的日期。
columns = ['name', 'month', 'open', 'high', 'low', 'close', 'presettlement', 'settlement', 'zd1_chg', 'zd2_chg', 'volume', 'openinterest', 'openinterestchg', 'turnover']
df = pd.DataFrame(data=tsv, columns=columns)
df['trade_date'] = trade_date
self._save(df)
完整的代码如下:
import multiprocessing
import re
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from common.utils import *
from tushare_client.base import AbstractDataRetriever
from tushare_client.stock_calendar import StockCalendar
class DceDaily(AbstractDataRetriever):
def __init__(self):
super().__init__('futures_dce_daily')
def _full(self, **kwargs):
self._get_data_list('20110101', today())
def _delta(self, **kwargs):
df_origin = self.query(fields='max(trade_date)')
if df_origin.empty or df_origin.iat[0, 0] is None:
self._get_data_list('20110101', today())
else:
self._get_data_list(df_origin.iat[0, 0], today())
def _get_data_list(self, start_date, end_date, max_worker=multiprocessing.cpu_count() * 2):
df_cal_date = StockCalendar().query(
fields='cal_date',
where=f'`exchange`=\'dce\' and is_open=\'1\' and cal_date >\'{start_date}\' and cal_date <= \'{end_date}\'',
order_by='cal_date')
with ThreadPoolExecutor(max_worker) as executor:
future_to_date = \
{executor.submit(self._get_daily_data, trade_date=row['cal_date']): row
for index, row in df_cal_date.iterrows()}
for future in as_completed(future_to_date):
row = future_to_date[future]
try:
data = future.result()
except Exception as ex:
self.logger.error(f"failed to retrieve {row['cal_date']}")
self.logger.exception(ex)
def _get_daily_data(self, trade_date):
dce_url = f'http://www.dce.com.cn/publicweb/quotesdata/exportDayQuotesChData.html?dayQuotes.variety=all&dayQuotes.trade_type=0&year={trade_date[0:4]}&month={int(trade_date[4:6])-1}&day={trade_date[6:8]}&exportFlag=txt'
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0',
'Accept': '*/*',
'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'
}
r = requests.get(dce_url, headers=headers)
tsv_origin = r.content.decode().split('\r\n')
tsv = []
for item in tsv_origin:
item = item.strip('\t')
if '商品名称' in item or '小计' in item or '总计' in item or item == '':
continue
else:
tsv.append(re.compile("\t+").sub('\t', item).split('\t'))
columns = ['name', 'month', 'open', 'high', 'low', 'close', 'presettlement', 'settlement', 'zd1_chg', 'zd2_chg', 'volume', 'openinterest', 'openinterestchg', 'turnover']
df = pd.DataFrame(data=tsv, columns=columns)
df['trade_date'] = trade_date
self._save(df)
if __name__ == '__main__':
DceDaily().retrieve()