Files

874 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
沪深交易所公告与数据爬虫模块
数据源: sse.com.cn(上交所)、szse.cn(深交所)
提供 IPO 日历、上市公司列表、公告搜索等功能。
可作为巨潮资讯 (cninfo.com.cn) 的补充数据源。
主要函数:
- get_ipo_calendar() — 获取近期 IPO 日历
- get_listed_companies(market="sh") — 获取上市公司列表
- search_announcements(keyword, market="both") — 搜索公告(跨交易所)
API:
深交所:
- 上市公司列表: http://www.szse.cn/api/report/ShowReport/data (POST)
SHOWTYPE=JSON, CATALOGID=1110, TABKEY=tab1
上交所:
- 股票列表: http://query.sse.com.cn/security/stock/getStockListData.do (GET)
jsonCallBack=jsonpCallback, isPagination=false, sqlId=COMMON_SSE_GP_GPLB_C
Referer: www.sse.com.cn
注意事项:
- 上交所 API 需要 Referer: https://www.sse.com.cn/
- 两个交易所均可能对请求频率有限制,使用 rate_limit 控制
- 数据更新频率: 交易日盘后更新
"""
import json
import re
import logging
from typing import Optional, Dict, Any, List
try:
from .http_utils import http_get, http_get_json, http_post, rate_limit, get_session
except ImportError:
from http_utils import http_get, http_get_json, http_post, rate_limit, get_session
log = logging.getLogger(__name__)
# ==================== 常量 ====================
# 深交所 API
SZSE_BASE = "http://www.szse.cn"
SZSE_LISTED_COMPANIES_URL = "http://www.szse.cn/api/report/ShowReport/data"
SZSE_IPO_CALENDAR_URL = "http://www.szse.cn/api/report/ShowReport/data"
SZSE_ANNOUNCEMENT_URL = "http://www.szse.cn/api/disc/announcement/annList"
# 上交所 API
SSE_BASE = "https://www.sse.com.cn"
SSE_STOCK_LIST_URL = "http://query.sse.com.cn/security/stock/getStockListData.do"
SSE_IPO_CALENDAR_URL = "http://query.sse.com.cn/security/stock/getStockListData.do"
# 深交所报表系统目录/标签参数(提取为常量,便于交易所改版时集中维护)
SZSE_CATALOG_IPO = "1110" # IPO 日历目录ID
SZSE_CATALOG_LISTED = "1110" # 上市公司列表目录ID
SZSE_CATALOG_ANNOUNCEMENT = "1800_cxda" # 公告查询目录ID
SZSE_TABKEY = "tab1"
# 请求头
SZSE_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "http://www.szse.cn/",
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/json",
}
SSE_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://www.sse.com.cn/",
"X-Requested-With": "XMLHttpRequest",
}
# ==================== 安全类型转换 ====================
def _safe_float(val, default=None):
"""安全转换为浮点数"""
if val is None or val == "" or val == "-":
return default
try:
return float(val)
except (ValueError, TypeError):
return default
def _safe_str(val, default=""):
"""安全转换为字符串"""
if val is None:
return default
return str(val)
# ==================== IPO 日历 ====================
def _get_szse_ipo_calendar() -> List[Dict[str, Any]]:
"""
获取深交所 IPO 日历(内部函数)。
"""
params = {
"SHOWTYPE": "JSON",
"CATALOGID": SZSE_CATALOG_IPO,
"TABKEY": SZSE_TABKEY,
"PAGENO": "1",
"random": "0.123456789",
}
rate_limit(url=SZSE_BASE)
log.info("正在获取深交所 IPO 日历...")
try:
resp = http_post(
SZSE_LISTED_COMPANIES_URL,
data=params,
headers=SZSE_HEADERS,
timeout=30,
)
if resp is None:
log.warning("深交所 IPO 日历请求失败")
return []
data = resp.json()
except Exception as e:
log.error(f"深交所 IPO 日历解析失败: {e}")
return []
# 解析返回数据
items = data if isinstance(data, list) else data.get("data", [])
if not items:
return []
result = []
for item in items:
result.append({
"market": "sz",
"stock_code": _safe_str(item.get("zqdm", "")),
"stock_name": _safe_str(item.get("zqjc", "")),
"ipo_date": _safe_str(item.get("ssrq", "")),
"ipo_price": _safe_float(item.get("fxjg")),
"ipo_pe": _safe_float(item.get("fxssrpe")),
"status": _safe_str(item.get("status", "")),
})
log.info(f"深交所 IPO 日历: {len(result)} 条")
return result
def _get_sse_ipo_calendar() -> List[Dict[str, Any]]:
"""
获取上交所 IPO 日历(内部函数)。
上交所股票查询接口也可查到待上市/新上市股票。
"""
params = {
"jsonCallBack": "jsonpCallback",
"isPagination": "false",
"sqlId": "COMMON_SSE_GP_GPLB_C", # 股票列表通用查询
"pageHelp.cacheSize": "1",
"pageHelp.beginPage": "1",
"pageHelp.pageSize": "200",
"pageHelp.pageNo": "1",
"pageHelp.endPage": "1",
}
rate_limit(url=SSE_BASE)
log.info("正在获取上交所 IPO 日历...")
resp = http_get(
SSE_IPO_CALENDAR_URL,
params=params,
headers=SSE_HEADERS,
timeout=30,
)
if resp is None:
log.warning("上交所 IPO 日历请求失败")
return []
# 上交所返回 JSONP 格式,需要提取 JSON 部分
text = resp.text
json_match = re.search(r'jsonpCallback\((.*)\)', text, re.DOTALL)
if not json_match:
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if not json_match:
log.error("上交所 IPO 数据 JSON 提取失败")
return []
try:
data = json.loads(json_match.group(1) if json_match.lastindex else json_match.group(0))
except json.JSONDecodeError as e:
log.error(f"上交所 IPO 数据解析失败: {e}")
return []
items = data.get("result", []) or data.get("pageHelp", {}).get("data", [])
if not items:
return []
result = []
for item in items:
result.append({
"market": "sh",
"stock_code": _safe_str(item.get("SECURITY_CODE_A", "")),
"stock_name": _safe_str(item.get("SECURITY_ABBR_A", "")),
"list_date": _safe_str(item.get("LISTING_DATE", "")),
"total_shares": _safe_float(item.get("TOTAL_SHARES")),
"info": _safe_str(item.get("REMARK", "")),
})
log.info(f"上交所 IPO 日历: {len(result)} 条")
return result
def get_ipo_calendar(market: str = "both") -> List[Dict[str, Any]]:
"""
获取近期 IPO 日历(新上市公司信息)。
分别从深交所和上交所获取最近上市/待上市的公司数据。
Args:
market: 交易所选择,可选值:
- "sh" : 仅上交所
- "sz" : 仅深交所
- "both" : 沪深两市(默认)
Returns:
IPO 日历列表,每项包含:
- market: 交易所 (sh/sz)
- stock_code: 股票代码
- stock_name: 股票名称
- ipo_date / list_date: 上市日期
失败返回空列表
"""
results = []
if market in ("sz", "both"):
try:
results.extend(_get_szse_ipo_calendar())
except Exception as e:
log.error(f"深交所 IPO 日历获取异常: {e}")
if market in ("sh", "both"):
try:
results.extend(_get_sse_ipo_calendar())
except Exception as e:
log.error(f"上交所 IPO 日历获取异常: {e}")
log.info(f"IPO 日历汇总: {len(results)} 条(market={market})")
return results
# ==================== 上市公司列表 ====================
def _get_szse_listed_companies() -> List[Dict[str, Any]]:
"""
获取深交所上市公司列表(内部函数)。
使用深交所报表接口以 POST 请求获取。
"""
params = {
"SHOWTYPE": "JSON",
"CATALOGID": SZSE_CATALOG_LISTED,
"TABKEY": SZSE_TABKEY,
"PAGENO": "1",
"random": "0.123456789",
}
rate_limit(url=SZSE_BASE)
log.info("正在获取深交所上市公司列表...")
try:
resp = http_post(
SZSE_LISTED_COMPANIES_URL,
data=params,
headers=SZSE_HEADERS,
timeout=30,
)
if resp is None:
log.warning("深交所上市公司列表请求失败")
return []
data = resp.json()
except Exception as e:
log.error(f"深交所上市公司列表解析失败: {e}")
return []
items = data if isinstance(data, list) else data.get("data", [])
if not items:
return []
companies = []
for item in items:
company = {
"market": "sz",
"stock_code": _safe_str(item.get("zqdm", "")),
"stock_name": _safe_str(item.get("zqjc", "")),
"company_name": _safe_str(item.get("gsjc", "")),
"industry": _safe_str(item.get("ssrq", "")), # 上市板块
"list_date": _safe_str(item.get("ssrq", "")),
"total_shares": _safe_float(item.get("zgb")),
"area": _safe_str(item.get("area", "")),
}
companies.append(company)
log.info(f"深交所上市公司: {len(companies)} 家")
return companies
def _get_sse_listed_companies() -> List[Dict[str, Any]]:
"""
获取上交所上市公司列表(内部函数)。
"""
params = {
"jsonCallBack": "jsonpCallback",
"isPagination": "false",
"sqlId": "COMMON_SSE_GP_GPLB_C",
"pageHelp.cacheSize": "1",
"pageHelp.beginPage": "1",
"pageHelp.pageSize": "2000", # 上交所约2000家公司
"pageHelp.pageNo": "1",
"pageHelp.endPage": "1",
}
rate_limit(url=SSE_BASE)
log.info("正在获取上交所上市公司列表...")
resp = http_get(
SSE_STOCK_LIST_URL,
params=params,
headers=SSE_HEADERS,
timeout=30,
)
if resp is None:
log.warning("上交所上市公司列表请求失败")
return []
text = resp.text
json_match = re.search(r'jsonpCallback\((.*)\)', text, re.DOTALL)
if not json_match:
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if not json_match:
log.error("上交所股票列表 JSON 提取失败")
return []
try:
data = json.loads(json_match.group(1) if json_match.lastindex else json_match.group(0))
except json.JSONDecodeError as e:
log.error(f"上交所股票列表解析失败: {e}")
return []
items = data.get("result", []) or data.get("pageHelp", {}).get("data", [])
if not items:
return []
companies = []
for item in items:
company = {
"market": "sh",
"stock_code": _safe_str(item.get("SECURITY_CODE_A", "")),
"stock_name": _safe_str(item.get("SECURITY_ABBR_A", "")),
"company_name": _safe_str(item.get("COMPANY_ABBR", "")),
"list_date": _safe_str(item.get("LISTING_DATE", "")),
"total_shares": _safe_float(item.get("TOTAL_SHARES")),
"remark": _safe_str(item.get("REMARK", "")),
}
companies.append(company)
log.info(f"上交所上市公司: {len(companies)} 家")
return companies
def get_listed_companies(market: str = "both") -> List[Dict[str, Any]]:
"""
获取沪深上市公司列表。
Args:
market: 交易所选择,可选值:
- "sh" : 仅上交所
- "sz" : 仅深交所
- "both" : 沪深两市(默认)
Returns:
上市公司列表,每项包含: market, stock_code, stock_name, list_date 等
失败返回空列表
"""
results = []
if market in ("sz", "both"):
try:
results.extend(_get_szse_listed_companies())
except Exception as e:
log.error(f"深交所上市公司列表获取异常: {e}")
if market in ("sh", "both"):
try:
results.extend(_get_sse_listed_companies())
except Exception as e:
log.error(f"上交所上市公司列表获取异常: {e}")
log.info(f"上市公司列表汇总: {len(results)} 家(market={market})")
return results
# ==================== 公告搜索 ====================
def _search_szse_announcements(keyword: str,
max_results: int = 50) -> List[Dict[str, Any]]:
"""
搜索深交所公告(内部函数)。
"""
params = {
"SHOWTYPE": "JSON",
"CATALOGID": SZSE_CATALOG_ANNOUNCEMENT,
"TABKEY": SZSE_TABKEY,
"txtK": keyword, # 关键词
"txtM": "", # 证券代码(可选)
"PAGENO": "1",
"pageSize": str(min(max_results, 50)),
}
rate_limit(url=SZSE_BASE)
log.info(f"正在搜索深交所公告: keyword={keyword}")
try:
resp = http_post(
SZSE_ANNOUNCEMENT_URL,
data=params,
headers=SZSE_HEADERS,
timeout=30,
)
if resp is None:
log.warning(f"深交所公告搜索请求失败: {keyword}")
return []
data = resp.json()
except Exception as e:
log.error(f"深交所公告搜索解析失败: {e}")
return []
items = data if isinstance(data, list) else data.get("data", [])
if not items:
return []
results = []
for item in items:
ann = {
"market": "sz",
"title": _safe_str(item.get("title", "")
or item.get("gsggbt", "")
or item.get("announcementTitle", "")),
"stock_code": _safe_str(item.get("stockCode", "")
or item.get("zqdm", "")),
"stock_name": _safe_str(item.get("stockName", "")
or item.get("zqjc", "")),
"publish_date": _safe_str(item.get("publishDate", "")
or item.get("ggrq", "")),
"url": _safe_str(item.get("url", "")
or item.get("attachPath", "")),
"category": _safe_str(item.get("category", "")
or item.get("announcementType", "")),
}
results.append(ann)
log.info(f"深交所公告搜索结果: {len(results)} 条")
return results
def _search_sse_announcements(keyword: str,
max_results: int = 50) -> List[Dict[str, Any]]:
"""
搜索上交所公告(内部函数)。
上交所公告搜索通过披露查询接口。
"""
url = "http://query.sse.com.cn/security/stock/queryCompanyBulletin.do"
params = {
"jsonCallBack": "jsonpCallback",
"isPagination": "true",
"keyWord": keyword,
"pageHelp.pageSize": str(min(max_results, 50)),
"pageHelp.pageNo": "1",
"pageHelp.beginPage": "1",
"pageHelp.endPage": "1",
"pageHelp.cacheSize": "1",
"productId": "",
"securityType": "0101", # A股
"reportType2": "DQGG", # 定期公告
"reportType": "ALL", # 所有类型
"beginDate": "",
"endDate": "",
}
rate_limit(url=SSE_BASE)
log.info(f"正在搜索上交所公告: keyword={keyword}")
resp = http_get(url, params=params, headers=SSE_HEADERS, timeout=30)
if resp is None:
log.warning(f"上交所公告搜索请求失败: {keyword}")
return []
text = resp.text
json_match = re.search(r'jsonpCallback\((.*)\)', text, re.DOTALL)
if not json_match:
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if not json_match:
log.error("上交所公告 JSON 提取失败")
return []
try:
data = json.loads(json_match.group(1) if json_match.lastindex else json_match.group(0))
except json.JSONDecodeError as e:
log.error(f"上交所公告解析失败: {e}")
return []
items = data.get("result", []) or data.get("pageHelp", {}).get("data", [])
if not items:
return []
results = []
for item in items:
ann = {
"market": "sh",
"title": _safe_str(item.get("title", "")
or item.get("bulletinTitle", "")),
"stock_code": _safe_str(item.get("securityCode", "")
or item.get("stockCode", "")),
"stock_name": _safe_str(item.get("securityAbbr", "")
or item.get("stockName", "")),
"publish_date": _safe_str(item.get("publishDate", "")
or item.get("bulletinDate", "")),
"url": _safe_str(item.get("URL", "")
or item.get("bulletinUrl", "")),
"category": _safe_str(item.get("bulletinType", "")
or item.get("reportType", "")),
}
results.append(ann)
log.info(f"上交所公告搜索结果: {len(results)} 条")
return results
def search_announcements(keyword: str,
market: str = "both",
max_results: int = 100) -> List[Dict[str, Any]]:
"""
搜索沪深交易所公告(跨交易所合并结果)。
作为巨潮资讯 (cninfo.com.cn) 的补充数据源,直接从交易所
官方接口获取公告数据。
Args:
keyword: 搜索关键词(如 "分红"、"增发"、"重组" 等)
market: 交易所选择,可选值:
- "sh" : 仅上交所
- "sz" : 仅深交所
- "both" : 沪深两市(默认)
max_results: 每个交易所最大返回条数(默认 100)
Returns:
公告列表,每项包含: market, title, stock_code, stock_name,
publish_date, url, category
失败返回空列表
"""
results = []
per_market_max = max_results if market != "both" else max_results // 2
if market in ("sz", "both"):
try:
results.extend(_search_szse_announcements(keyword, per_market_max))
except Exception as e:
log.error(f"深交所公告搜索异常: {e}")
if market in ("sh", "both"):
try:
results.extend(_search_sse_announcements(keyword, per_market_max))
except Exception as e:
log.error(f"上交所公告搜索异常: {e}")
# 按发布日期降序排列
results.sort(key=lambda x: x.get("publish_date", ""), reverse=True)
log.info(f"公告搜索汇总: '{keyword}' 共 {len(results)} 条(market={market})")
return results
# ==================== 便捷查询 ====================
def get_recent_ipo_list(days: int = 30) -> List[Dict[str, Any]]:
"""
获取最近 N 天内 IPO 的公司列表。
Args:
days: 最近天数(默认 30 天)
Returns:
近期 IPO 公司列表
"""
from datetime import datetime, timedelta
cutoff = datetime.now() - timedelta(days=days)
cutoff_str = cutoff.strftime("%Y-%m-%d")
ipo_list = get_ipo_calendar(market="both")
recent = []
for ipo in ipo_list:
date_str = ipo.get("ipo_date") or ipo.get("list_date", "")
if date_str and date_str >= cutoff_str:
recent.append(ipo)
recent.sort(key=lambda x: x.get("ipo_date") or x.get("list_date", ""),
reverse=True)
log.info(f"近 {days} 天 IPO 公司: {len(recent)} 家")
return recent
def find_company_announcements(stock_code: str,
keyword: str = "",
market: str = "both",
max_results: int = 30) -> List[Dict[str, Any]]:
"""
查找指定公司的公告。
将公司代码作为关键词的一部分进行搜索。
Args:
stock_code: 股票代码(如 "600519")
keyword: 额外搜索关键词(可选,如 "分红")
market: 交易所
Returns:
该公司相关公告列表
"""
search_keyword = f"{stock_code} {keyword}".strip()
return search_announcements(
keyword=search_keyword,
market=market,
max_results=max_results,
)
# ==================== 北交所(v4.7 新增) ====================
# 北交所官网接口
BSE_BASE = "https://www.bse.cn"
BSE_LISTED_COMPANIES_API = f"{BSE_BASE}/nqxxController/nqxxCnzq.do"
BSE_IPO_AUDIT_API = f"{BSE_BASE}/nqxxController/nqxxAuditList.do"
# 东方财富北交所板块(fs 参数),作为北交所数据的可靠降级源
EM_BSE_CLIST_API = "https://push2.eastmoney.com/api/qt/clist/get"
def _fetch_bse_native(params: Dict[str, Any], api_url: str) -> Optional[str]:
"""调用北交所官网 JSONP 接口,返回剥离回调后的原始文本。"""
try:
import urllib.parse
body = urllib.parse.urlencode(params)
rate_limit(url=BSE_BASE)
full = f"{api_url}?{body}&callback=cb"
resp = http_get(full, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0",
"Referer": f"{BSE_BASE}/nq/listedcompany.html",
}, timeout=20)
if resp is None:
return None
text = resp.text
# 剥离 JSONP 回调: callback([...]) 或 null([...])
m = re.search(r"\((.*)\)\s*;?\s*$", text, re.DOTALL)
return m.group(1) if m else text
except Exception as e:
log.error(f"北交所原生接口请求失败: {e}")
return None
def _fetch_em_bse_stocks(limit: int) -> List[Dict[str, Any]]:
"""通过东方财富 clist 接口获取北交所上市公司列表(降级源)。"""
try:
import urllib.parse
params = {
"pn": "1", "pz": str(min(limit, 200)), "po": "1", "np": "1",
"fltt": "2", "invt": "2", "fid": "f26",
"fs": "m:0+t:81+t:82+t:83", # 北交所
"fields": "f12,f14,f13,f26,f100,f2",
}
qs = urllib.parse.urlencode(params)
rate_limit(url="push2.eastmoney.com")
resp = http_get(f"{EM_BSE_CLIST_API}?{qs}", headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0",
"Referer": "https://quote.eastmoney.com/",
}, timeout=20)
if resp is None:
return []
data = json.loads(resp.text)
items = data.get("data", {}).get("diff", []) if isinstance(data.get("data"), dict) else []
results = []
for it in items:
results.append({
"code": _safe_str(it.get("f12", "")),
"name": _safe_str(it.get("f14", "")),
"short_code": _safe_str(it.get("f13", "")),
"listing_date": _safe_str(it.get("f26", "")),
"industry": _safe_str(it.get("f100", "")),
"market": "bj",
})
return results
except Exception as e:
log.error(f"东方财富北交所列表请求失败: {e}")
return []
def get_bse_stocks(limit: int = 200) -> List[Dict[str, Any]]:
"""获取北交所上市公司列表。
优先调用北交所官网接口,失败时降级到东方财富北交所板块数据。
Args:
limit: 返回条数,默认 200,最大 500
Returns:
[
{"code": "837592", "name": "华信永道", "short_code": "837592",
"listing_date": "2023-02-16", "industry": "信息技术", "market": "bj"}, ...
]
"""
# 1) 北交所原生接口
raw = _fetch_bse_native(
{"page": 0, "typejb": "T", "xxfcbj": 2, "xxzqdm": "",
"sortfield": "xxzqdm", "sorttype": "asc", "pagesize": min(limit, 100)},
BSE_LISTED_COMPANIES_API,
)
if raw:
try:
data = json.loads(raw)
if isinstance(data, list) and data and isinstance(data[0], dict) and "msg" not in data[0]:
results = []
for it in data[:limit]:
results.append({
"code": _safe_str(it.get("xxzqdm", it.get("zqdm", ""))),
"name": _safe_str(it.get("jc", it.get("zqjc", ""))),
"short_code": _safe_str(it.get("zqdm", "")),
"listing_date": _safe_str(it.get("ssrq", "")),
"industry": _safe_str(it.get("hy", it.get("hymc", ""))),
"market": "bj",
})
if results:
log.info(f"北交所原生接口获取 {len(results)} 条")
return results
except Exception as e:
log.warning(f"北交所原生接口解析失败,降级东方财富: {e}")
# 2) 降级:东方财富北交所板块
results = _fetch_em_bse_stocks(limit)
log.info(f"北交所列表获取 {len(results)} 条(东方财富降级源)")
return results
def get_bse_ipo(limit: int = 50) -> List[Dict[str, Any]]:
"""获取北交所 IPO 审核/发行信息。
Args:
limit: 返回条数,默认 50
Returns:
[
{"code": "...", "name": "...", "status": "已注册",
"audit_date": "2026-07-31", "market": "bj"}, ...
]
"""
raw = _fetch_bse_native(
{"page": 0, "xxfcbj": 2, "typejb": "T"},
BSE_IPO_AUDIT_API,
)
if raw:
try:
data = json.loads(raw)
if isinstance(data, list) and data and isinstance(data[0], dict) and "msg" not in data[0]:
results = []
for it in data[:limit]:
results.append({
"code": _safe_str(it.get("xxzqdm", it.get("zqdm", ""))),
"name": _safe_str(it.get("jc", it.get("zqjc", ""))),
"status": _safe_str(it.get("zt", it.get("status", ""))),
"audit_date": _safe_str(it.get("sxrq", it.get("ssrq", ""))),
"market": "bj",
})
if results:
log.info(f"北交所 IPO 审核获取 {len(results)} 条")
return results
except Exception as e:
log.warning(f"北交所 IPO 审核解析失败: {e}")
# 降级:东方财富 IPO 日历(北交所部分由 get_ipo_calendar 覆盖)
ipo = get_ipo_calendar(market="both")
results = [i for i in ipo if str(i.get("stock_code", "")).startswith(("8", "4"))]
log.info(f"北交所 IPO 获取 {len(results)} 条(降级源)")
return results[:limit]
# ==================== 测试入口 ====================
if __name__ == "__main__":
print("=" * 60)
print("沪深交易所数据爬虫 - 功能测试")
print("=" * 60)
# 测试1: 获取上市公司列表
print("\n[测试1] 获取上交所上市公司列表(前10家)...")
companies = get_listed_companies(market="sh")
if companies:
print(f"共获取 {len(companies)} 家上交所上市公司")
for c in companies[:10]:
print(f" {c.get('market')} {c.get('stock_code')} {c.get('stock_name')} "
f"上市日:{c.get('list_date', 'N/A')}")
else:
print("获取失败(上交所接口可能需要特定 Referer)")
# 测试2: 获取深交所上市公司列表
print("\n[测试2] 获取深交所上市公司列表(前10家)...")
sz_companies = get_listed_companies(market="sz")
if sz_companies:
print(f"共获取 {len(sz_companies)} 家深交所上市公司")
for c in sz_companies[:10]:
print(f" {c.get('market')} {c.get('stock_code')} {c.get('stock_name')}")
else:
print("获取失败")
# 测试3: 搜索公告
print("\n[测试3] 搜索 '分红' 相关公告(深交所)...")
anns = search_announcements(keyword="分红", market="sz", max_results=10)
if anns:
print(f"搜索到 {len(anns)} 条公告")
for ann in anns[:5]:
print(f" [{ann.get('market')}] {ann.get('stock_code')} "
f"{ann.get('title', '')[:60]} "
f"日期:{ann.get('publish_date', '')}")
else:
print("搜索无结果或接口不可用")
# 测试4: 搜索 '增发' 公告(上交所)
print("\n[测试4] 搜索 '增发' 相关公告(上交所)...")
anns_sse = search_announcements(keyword="增发", market="sh", max_results=10)
if anns_sse:
print(f"搜索到 {len(anns_sse)} 条公告")
for ann in anns_sse[:5]:
print(f" [{ann.get('market')}] {ann.get('stock_code')} "
f"{ann.get('title', '')[:60]}")
else:
print("搜索无结果或接口不可用")
# 测试5: 查找指定公司公告
print("\n[测试5] 查找贵州茅台(600519)近期公告...")
moutai_anns = find_company_announcements("600519", keyword="分红")
if moutai_anns:
for ann in moutai_anns[:5]:
print(f" [{ann.get('market')}] {ann.get('title', '')[:60]} "
f"日期:{ann.get('publish_date', '')}")
else:
print("未找到相关公告或接口不可用")
print("\n" + "=" * 60)
print("测试完成")
print("提示: 交易所接口可能有反爬限制,如测试失败属正常现象。")
print(" 实际使用时建议在工作日盘后时段调用。")