# -*- 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