189 lines
5.8 KiB
Python
189 lines
5.8 KiB
Python
# -*- 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
|