直接上代码

import requests, random, re, json
from datetime import datetime, date
from chinese_calendar import is_workday
from pydantic import BaseModel

class StockInfo(BaseModel):
    stock_code: str
    stock_name: str
    date: date
    open: float
    high: float
    low: float
    close: float
    yid: float
    volumn: float
    price: float
    PE: float

class Error(BaseModel):
    Error: str

class SSEQuery(object):
    def __init__(self):
        self.url = 'http://query.sse.com.cn/commonQuery.do'
        self.headers = {
            'Host': 'query.sse.com.cn',
            'Referer': 'http://www.sse.com.cn/',
        }

    def get_jsonp(self):
        return f'jsonpCallback{int(random.random()*(10000000+1))}'

    def get_timestamp(self):
        return str(int(datetime.timestamp(datetime.now()) * 1000))

    def start_request(self, stock_code, date):
        jsonp = self.get_jsonp()
        timestamp = self.get_timestamp()
        url = self.url
        headers = self.headers
        data = {
            'jsonCallBack': jsonp,
            'sqlId': 'COMMON_SSE_CP_GPJCTPZ_GPLB_CJGK_MRGK_C',
            'SEC_CODE': stock_code,
            'TX_DATE': date,
            'TX_DATE_MON': '',
            'TX_DATE_YEAR': '',
            '_': timestamp,
        }
        return requests.post(url, data=data, headers=headers)

    def parse_jsonp(self, r):
        return json.loads(re.search(r'jsonpCallback\d+\((.*?)\)', r.text).group(1))['result'][0]

    def check_date(self, date):
        if not is_workday(datetime.date(datetime.strptime(date, '%Y-%m-%d'))):
            return False
        if datetime.date(datetime.strptime(date, '%Y-%m-%d')).weekday()>4:
            return False
        return date

    def format_dict(self, data):
        return {
            'stock_code': data['SEC_CODE'],
            'stock_name': data['SEC_NAME'],
            'open': data['OPEN_PRICE'],
            'high': data['HIGH_PRICE'],
            'low': data['LOW_PRICE'],
            'close': data['CLOSE_PRICE'],
            'yid': data['CHANGE_RATE'],
            'volumn': data['TRADE_VOL'],
            'price': data['TRADE_AMT'],
            'PE': data['PE_RATE'],
        }

    def query(self, stock_code, date):
        date = self.check_date(date)
        if not date:
            return Error(**{'Error':'非交易日!'})
        r = self.start_request(stock_code, date)
        data = self.parse_jsonp(r)
        data = self.format_dict(data)
        data.update({'date': date})
        return StockInfo(**data)

class SZSEQuery(object):
    def __init__(self):
        self.url = 'http://www.szse.cn/api/report/ShowReport/data'

    def check_date(self, date):
        if not is_workday(datetime.date(datetime.strptime(date, '%Y-%m-%d'))):
            return False
        if datetime.date(datetime.strptime(date, '%Y-%m-%d')).weekday()>4:
            return False
        return date

    def start_request(self, stock_code, date):
        data = {
            'SHOWTYPE': 'JSON',
            'CATALOGID': '1815_stock',
            'TABKEY': 'tab1',
            'txtDMorJC': str(stock_code),
            'txtBeginDate': date,
            'txtEndDate': date,
            'radioClass': '00%2C20%2C30%2CC6%2CC7%2CGE%2C14',
            'txtSite': 'all',
            'random': random.random(),
        }
        return requests.get(self.url, data)

    def format_dict(self, data):
        return {
            'stock_code': data['zqdm'],
            'stock_name': data['zqjc'].replace(' ',''),
            'open': data['ks'],
            'high': data['zg'],
            'low': data['zd'],
            'close': data['ss'],
            'yid': data['sdf'],
            'volumn': data['cjgs'].replace(',' ,''),
            'price': data['cjje'].replace(',' ,''),
            'PE': data['syl1'],
        }

    def query(self, stock_code, date):
        date = self.check_date(date)
        if not date:
            return Error(**{'Error':'非交易日!'})
        r = self.start_request(stock_code, date)
        data = self.format_dict(r.json()[0]['data'][0])
        data.update({'date': date})
        return StockInfo(**data)

def query(stock_code, date):
    try:
        if stock_code[:1] in ['0', ['3']]:
            szsequery = SZSEQuery()
            return szsequery.query(stock_code, date)
        else:
            ssequery = SSEQuery()
            return ssequery.query(stock_code, date)
    except:
        try:
            szsequery = SZSEQuery()
            return szsequery.query(stock_code, date)
        except:
            return Error(**{"Error": "查询失败!"})

最先想搞一个股票数据库来着,所以准备开始爬上交所的数据,被一个jsonpcallback搞昏了头,弄了一个多小时,结果发现是真的坑,后来就不高兴全爬下来当数据库了,直接写一个从上交所/深交所查单日行情的小工具算了。

数据来源

上交所:http://www.sse.com.cn/assortment/stock/list/info/company/index.shtml?COMPANY_CODE=600008

深交所:https://www.szse.cn/market/trend/index.html?code=000002

返回格式

其实是一个pydantic的BaseModel,你可以用.dict()/.json()方法把它变成列表/JSON。

stock_code: str	# 股票代码
stock_name: str	# 股票名称
date: date	# 日期
open: float	# 开盘价
high: float	# 最高价
low: float	# 最低价
close: float	# 收盘价
yid: float	# 当日盈亏率
volumn: float	# 成交股数(万)
price: float	# 成交金额(万)
PE: float	# 市盈率

使用方法

直接query(stock_code, date)

或者另起一个文件,比如main.py,然后

from tickerlookup import query

# 上交所 600008 首创环保
stock_code = '600008'
date = '2023-01-04'
print(query(stock_code, date).dict())

# 深交所 000002 万科A
stock_code = '000002'
date = '2023-01-04'
print(query(stock_code, date).dict())

运行结果:

{'stock_code': '600008', 'stock_name': '首创环保', 'date': datetime.date(2023, 1, 4),
 'open': 2.89, 'high': 2.91, 'low': 2.87, 'close': 2.89, 'yid': 0.34722, 
'volumn': 4234.94, 'price': 12255.84, 'PE': 9.27414}

{'stock_code': '000002', 'stock_name': '万科A', 'date': datetime.date(2023, 1, 4), 
'open': 18.25, 'high': 19.28, 'low': 18.07, 'close': 19.07, 'yid': 4.61, 
'volumn': 10871.46, 'price': 206031.74, 'PE': 9.84}