feat: 初始提交 - 科创企业特有风险的识别与管理 (数智风控系统)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据采集模块"""
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
年报 PDF 文本解析模块
|
||||
从年报中提取关键风险信息:核心技术人员、技术路线、诉讼、风险提示等
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_annual_report(pdf_path: str) -> dict:
|
||||
"""
|
||||
解析年报 PDF,提取关键风险相关信息
|
||||
返回结构化的风险要素字典
|
||||
"""
|
||||
text = _extract_text_from_pdf(pdf_path)
|
||||
if not text:
|
||||
return {"error": "PDF 解析失败", "raw_text": ""}
|
||||
|
||||
return {
|
||||
"core_personnel_info": _extract_core_personnel(text),
|
||||
"tech_route_info": _extract_tech_route(text),
|
||||
"litigation_info": _extract_litigation(text),
|
||||
"risk_factors": _extract_risk_factors(text),
|
||||
"rd_capitalization_info": _extract_rd_capitalization(text),
|
||||
"customer_concentration": _extract_customer_concentration(text),
|
||||
"raw_text_length": len(text),
|
||||
}
|
||||
|
||||
|
||||
def _extract_text_from_pdf(pdf_path: str) -> Optional[str]:
|
||||
"""使用 pdfplumber 提取 PDF 全文"""
|
||||
try:
|
||||
import pdfplumber
|
||||
text_parts = []
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text_parts.append(page_text)
|
||||
return "\n".join(text_parts)
|
||||
except ImportError:
|
||||
logger.warning("pdfplumber 未安装,尝试 PyPDF2")
|
||||
try:
|
||||
from PyPDF2 import PdfReader
|
||||
reader = PdfReader(pdf_path)
|
||||
return "\n".join(
|
||||
page.extract_text() or "" for page in reader.pages
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PyPDF2 解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 解析失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_core_personnel(text: str) -> dict:
|
||||
"""提取核心技术人员相关信息"""
|
||||
result = {
|
||||
"has_departure": False,
|
||||
"departure_details": [],
|
||||
"personnel_count": 0,
|
||||
"key_mentions": [],
|
||||
}
|
||||
|
||||
# 匹配离职/辞职相关表述
|
||||
departure_patterns = [
|
||||
r"(核心技术人员|核心人员|关键技术人员).{0,30}(离职|辞职|离任|不再担任)",
|
||||
r"(CTO|首席技术官|技术总监|研发总监).{0,30}(离职|辞职|离任)",
|
||||
r"(离职|辞职).{0,30}(核心技术人员|核心人员)",
|
||||
]
|
||||
for pattern in departure_patterns:
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
result["has_departure"] = True
|
||||
result["departure_details"].extend([str(m) for m in matches])
|
||||
|
||||
# 统计核心技术人员数量
|
||||
count_match = re.search(r"核心技术人员\s*(\d+)\s*[名人]", text)
|
||||
if count_match:
|
||||
result["personnel_count"] = int(count_match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_tech_route(text: str) -> dict:
|
||||
"""提取技术路线相关信息"""
|
||||
result = {
|
||||
"competing_tech_mentioned": False,
|
||||
"tech_keywords": [],
|
||||
"risk_mentions": [],
|
||||
}
|
||||
|
||||
# 技术竞争关键词
|
||||
tech_keywords = [
|
||||
"技术路线", "技术迭代", "技术替代", "技术颠覆",
|
||||
"竞争技术", "替代方案", "新一代技术",
|
||||
]
|
||||
for kw in tech_keywords:
|
||||
if kw in text:
|
||||
result["tech_keywords"].append(kw)
|
||||
result["competing_tech_mentioned"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_litigation(text: str) -> dict:
|
||||
"""提取诉讼/仲裁相关信息"""
|
||||
result = {
|
||||
"has_litigation": False,
|
||||
"litigation_count": 0,
|
||||
"ip_related": False,
|
||||
}
|
||||
|
||||
# 诉讼关键词
|
||||
litigation_patterns = [
|
||||
r"(诉讼|仲裁|起诉|被告).{0,50}(知识产权|专利|商标|著作权)",
|
||||
r"(专利侵权|商标侵权|著作权纠纷)",
|
||||
]
|
||||
for pattern in litigation_patterns:
|
||||
if re.search(pattern, text):
|
||||
result["has_litigation"] = True
|
||||
result["ip_related"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_risk_factors(text: str) -> list:
|
||||
"""提取风险因素章节的关键信息"""
|
||||
risk_keywords = [
|
||||
"地缘政治", "实体清单", "出口管制", "贸易摩擦",
|
||||
"数据安全", "数据合规", "算法备案", "数据出境",
|
||||
"客户集中", "供应商集中", "单一客户", "单一供应商",
|
||||
"研发资本化", "开发支出", "无形资产",
|
||||
"人才流失", "核心人员", "竞业限制",
|
||||
]
|
||||
found_risks = []
|
||||
for kw in risk_keywords:
|
||||
if kw in text:
|
||||
found_risks.append(kw)
|
||||
return found_risks
|
||||
|
||||
|
||||
def _extract_rd_capitalization(text: str) -> dict:
|
||||
"""提取研发资本化相关信息"""
|
||||
result = {
|
||||
"has_capitalization": False,
|
||||
"capitalization_mentioned": False,
|
||||
"amount_keywords": [],
|
||||
}
|
||||
|
||||
cap_keywords = ["开发支出", "研发资本化", "资本化研发", "开发阶段支出"]
|
||||
for kw in cap_keywords:
|
||||
if kw in text:
|
||||
result["capitalization_mentioned"] = True
|
||||
result["amount_keywords"].append(kw)
|
||||
|
||||
# 检查是否有具体的资本化金额
|
||||
cap_amount = re.search(r"开发支出.{0,30}([\d,\.]+)\s*(万元|百万|亿)", text)
|
||||
if cap_amount:
|
||||
result["has_capitalization"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_customer_concentration(text: str) -> dict:
|
||||
"""提取客户/供应商集中度信息"""
|
||||
result = {
|
||||
"top5_customer_ratio": None,
|
||||
"top5_supplier_ratio": None,
|
||||
"single_customer_dependency": False,
|
||||
}
|
||||
|
||||
# 前五大客户占比
|
||||
customer_match = re.search(
|
||||
r"前五[名大]客户.{0,30}([\d\.]+)\s*%", text
|
||||
)
|
||||
if customer_match:
|
||||
result["top5_customer_ratio"] = float(customer_match.group(1)) / 100
|
||||
|
||||
# 单一客户依赖
|
||||
if re.search(r"(第一大客户|最大客户).{0,30}([\d\.]+)\s*%", text):
|
||||
result["single_customer_dependency"] = True
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
算法备案合规数据采集与查询模块
|
||||
匹配企业是否已完成网信办算法备案
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_algo_filings() -> list:
|
||||
"""加载算法备案数据"""
|
||||
filepath = DATA_DIR / "algo_filings.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_algo_filing(company_name: str) -> dict:
|
||||
"""
|
||||
查询企业的算法备案状态
|
||||
"""
|
||||
filings = _load_algo_filings()
|
||||
result = {
|
||||
"has_filing": False,
|
||||
"filings": [],
|
||||
"needs_filing": False, # 是否需要备案但未备案
|
||||
"risk_level": "低",
|
||||
}
|
||||
|
||||
for filing in filings:
|
||||
if company_name in filing["company"] or filing["company"] in company_name:
|
||||
result["has_filing"] = True
|
||||
result["filings"].append(filing)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def assess_algo_compliance_risk(company_data: dict) -> dict:
|
||||
"""
|
||||
综合评估企业的算法合规风险
|
||||
考虑因素:是否涉及 AI 业务、是否已备案、数据出境风险
|
||||
"""
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", ""))
|
||||
sector = company_data.get("sector", "")
|
||||
compliance = company_data.get("compliance", {})
|
||||
|
||||
# 查询备案状态
|
||||
filing_status = check_algo_filing(company_name)
|
||||
|
||||
# 判断是否需要备案
|
||||
ai_related_sectors = ["AI", "软件", "互联网", "消费电子"]
|
||||
needs_filing = sector in ai_related_sectors or "AI" in str(company_data.get("tech_route", {}))
|
||||
|
||||
# 综合评估
|
||||
risk_level = "低"
|
||||
risk_details = []
|
||||
|
||||
if needs_filing and not filing_status["has_filing"]:
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
if algo_status == "不适用":
|
||||
risk_level = "低"
|
||||
else:
|
||||
risk_level = "高"
|
||||
risk_details.append("涉及AI业务但未查到算法备案记录")
|
||||
|
||||
data_export_risk = compliance.get("data_export_risk", "低")
|
||||
if data_export_risk == "高":
|
||||
risk_level = "高"
|
||||
risk_details.append("存在大量跨境数据传输,数据出境评估风险高")
|
||||
elif data_export_risk == "中":
|
||||
if risk_level != "高":
|
||||
risk_level = "中"
|
||||
risk_details.append("存在部分跨境数据传输,需关注数据出境合规")
|
||||
|
||||
return {
|
||||
"company_name": company_name,
|
||||
"needs_filing": needs_filing,
|
||||
"filing_status": filing_status,
|
||||
"data_export_risk": data_export_risk,
|
||||
"overall_risk_level": risk_level,
|
||||
"risk_details": risk_details,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
BIS 实体清单采集与匹配模块
|
||||
支持企业名模糊匹配 + 别名映射 + 供应链上游穿透
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_entity_list() -> list:
|
||||
"""加载实体清单数据"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_entity_list(company_name: str) -> dict:
|
||||
"""
|
||||
检查企业是否在 BIS 实体清单中
|
||||
支持模糊匹配和别名匹配
|
||||
"""
|
||||
entities = _load_entity_list()
|
||||
result = {
|
||||
"is_sanctioned": False,
|
||||
"match_type": None,
|
||||
"entity_detail": None,
|
||||
"supply_chain_risk": [], # 供应链上游被制裁的情况
|
||||
}
|
||||
|
||||
for entity in entities:
|
||||
# 精确匹配
|
||||
if company_name in entity["entity_name"]:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "直接命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
# 别名匹配
|
||||
for alias in entity.get("aliases", []):
|
||||
if company_name in alias or alias in company_name:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "别名命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_supply_chain_sanctions(company_name: str, suppliers: list) -> list:
|
||||
"""
|
||||
检查企业供应链上游是否有被制裁的实体
|
||||
返回受制裁的供应商列表
|
||||
"""
|
||||
sanctioned_suppliers = []
|
||||
entities = _load_entity_list()
|
||||
|
||||
for supplier in suppliers:
|
||||
# 清洗供应商名称(去掉括号中的说明文字)
|
||||
clean_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
|
||||
for entity in entities:
|
||||
all_names = [entity["entity_name"]] + entity.get("aliases", [])
|
||||
for name in all_names:
|
||||
if clean_name in name or name in clean_name:
|
||||
sanctioned_suppliers.append({
|
||||
"supplier": supplier,
|
||||
"matched_entity": entity["entity_name"],
|
||||
"restrictions": entity["restrictions"],
|
||||
"date_added": entity["date_added"],
|
||||
})
|
||||
break
|
||||
|
||||
return sanctioned_suppliers
|
||||
|
||||
|
||||
def get_all_sanctioned_entities() -> list:
|
||||
"""获取所有被制裁实体列表"""
|
||||
return _load_entity_list()
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务数据采集模块
|
||||
双轨策略:优先尝试 AKShare 在线采集,失败则回退到预置数据
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_preset_data() -> list:
|
||||
"""加载预置的科创板企业数据"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def collect_financial_data(stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
采集指定股票代码的财务数据
|
||||
双轨策略:在线采集 → 离线预置
|
||||
"""
|
||||
# 尝试在线采集
|
||||
try:
|
||||
return _collect_online(stock_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"在线采集 {stock_code} 失败: {e},回退到预置数据")
|
||||
|
||||
# 回退到预置数据
|
||||
return _collect_from_preset(stock_code)
|
||||
|
||||
|
||||
def _collect_online(stock_code: str) -> Optional[dict]:
|
||||
"""通过 AKShare 在线采集财务数据"""
|
||||
try:
|
||||
import akshare as ak
|
||||
|
||||
# 科创板企业利润表
|
||||
profit_df = ak.stock_profit_sheet_by_report_em(symbol=stock_code)
|
||||
# 科创板企业资产负债表
|
||||
balance_df = ak.stock_balance_sheet_by_report_em(symbol=stock_code)
|
||||
|
||||
if profit_df is not None and not profit_df.empty:
|
||||
latest = profit_df.iloc[0]
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"revenue": float(latest.get("营业收入", 0)),
|
||||
"net_profit": float(latest.get("净利润", 0)),
|
||||
"rd_expense": float(latest.get("研发费用", 0)),
|
||||
"source": "akshare_online",
|
||||
}
|
||||
except ImportError:
|
||||
logger.warning("AKShare 未安装,跳过在线采集")
|
||||
except Exception as e:
|
||||
logger.warning(f"AKShare 采集异常: {e}")
|
||||
|
||||
raise RuntimeError("在线采集失败")
|
||||
|
||||
|
||||
def _collect_from_preset(stock_code: str) -> Optional[dict]:
|
||||
"""从预置数据中查找企业"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"company_name": company["company_name"],
|
||||
"industry": company["industry"],
|
||||
"sector": company["sector"],
|
||||
"financials": company["financials"],
|
||||
"core_tech_personnel": company["core_tech_personnel"],
|
||||
"tech_route": company["tech_route"],
|
||||
"compliance": company["compliance"],
|
||||
"supply_chain": company["supply_chain"],
|
||||
"source": "preset_data",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_all_companies() -> list:
|
||||
"""获取所有预置企业列表"""
|
||||
return _load_preset_data()
|
||||
|
||||
|
||||
def get_company_by_code(stock_code: str) -> Optional[dict]:
|
||||
"""通过股票代码查找企业完整数据"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return company
|
||||
return None
|
||||
|
||||
|
||||
def get_company_by_name(name: str) -> Optional[dict]:
|
||||
"""通过企业名称查找(支持简称)"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if name in company["company_name"] or name in company["short_name"]:
|
||||
return company
|
||||
return None
|
||||
Reference in New Issue
Block a user