Skip to content

Commit 2ebfffe

Browse files
committed
chore: 添加递归合并JSON的功能脚本
1 parent 2fa291b commit 2ebfffe

3 files changed

Lines changed: 155 additions & 12 deletions

File tree

merge.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import os
2+
import json
3+
import shutil
4+
5+
# 配置路径(Python 字符串不需要反斜杠转义空格)
6+
SRC_ROOT_BASE = "/Users/tsc/研究工作/金融ASIO/代码/finance-mcp_3"
7+
DST_ROOT_BASE = "/Users/tsc/研究工作/金融ASIO/代码/finance-mcp"
8+
9+
# 需要合并的根文件夹
10+
TARGET_FOLDERS = ["tool_cache", "cache"]
11+
12+
def load_json_data(file_path):
13+
"""加载 JSON 数据,支持列表或单个对象"""
14+
try:
15+
with open(file_path, 'r', encoding='utf-8') as f:
16+
data = json.load(f)
17+
return data if isinstance(data, list) else [data]
18+
except Exception as e:
19+
print(f" [跳过] 无法解析 JSON: {file_path}. 错误: {e}")
20+
return None
21+
22+
def save_json_data(file_path, data):
23+
"""保存 JSON 数据"""
24+
with open(file_path, 'w', encoding='utf-8') as f:
25+
json.dump(data, f, ensure_ascii=False, indent=2)
26+
27+
def get_code(sample):
28+
"""根据你提供的结构提取 code"""
29+
try:
30+
# 路径: sample -> tool_args -> code
31+
return sample.get("tool_args", {}).get("code")
32+
except:
33+
return None
34+
35+
def merge_json_files(src_file, dst_file):
36+
"""核心逻辑:合并两个文件中的 samples,按 code 去重"""
37+
src_data = load_json_data(src_file)
38+
dst_data = load_json_data(dst_file)
39+
40+
if src_data is None or dst_data is None:
41+
return False # 读取出错,不执行合并
42+
43+
# 获取目标文件中已有的所有 code
44+
existing_codes = {get_code(s) for s in dst_data if get_code(s) is not None}
45+
46+
initial_count = len(dst_data)
47+
for sample in src_data:
48+
code = get_code(sample)
49+
if code and code not in existing_codes:
50+
dst_data.append(sample)
51+
existing_codes.add(code)
52+
53+
new_added = len(dst_data) - initial_count
54+
if new_added > 0:
55+
save_json_data(dst_file, dst_data)
56+
print(f" [合并完成] {os.path.basename(dst_file)}: 新增了 {new_added} 条 code 样本")
57+
else:
58+
print(f" [无需合并] {os.path.basename(dst_file)}: 未发现新 code")
59+
return True
60+
61+
def start_recursive_merge():
62+
for target in TARGET_FOLDERS:
63+
src_folder = os.path.join(SRC_ROOT_BASE, target)
64+
dst_folder = os.path.join(DST_ROOT_BASE, target)
65+
66+
if not os.path.exists(src_folder):
67+
print(f"源文件夹不存在,跳过: {src_folder}")
68+
continue
69+
70+
print(f"\n>>> 正在扫描目录: {target}")
71+
72+
# 使用 os.walk 递归遍历所有子文件夹
73+
for root, dirs, files in os.walk(src_folder):
74+
# 计算当前子目录相对于源根目录的路径
75+
rel_path = os.path.relpath(root, src_folder)
76+
# 对应的目标子目录路径
77+
target_dst_dir = os.path.join(dst_folder, rel_path)
78+
79+
# 1. 如果目标子目录不存在,直接创建
80+
if not os.path.exists(target_dst_dir):
81+
os.makedirs(target_dst_dir)
82+
print(f"创建新目录: {target_dst_dir}")
83+
84+
# 2. 处理当前目录下的所有文件
85+
for filename in files:
86+
# 隐藏文件跳过 (如 .DS_Store)
87+
if filename.startswith('.'): continue
88+
89+
src_file_path = os.path.join(root, filename)
90+
dst_file_path = os.path.join(target_dst_dir, filename)
91+
92+
if not os.path.exists(dst_file_path):
93+
# 如果目标位置没有这个文件,直接整体复制
94+
shutil.copy2(src_file_path, dst_file_path)
95+
print(f" [新文件] 已复制: {rel_path}/{filename}")
96+
else:
97+
# 如果目标位置有重名文件,执行深度合并
98+
merge_json_files(src_file_path, dst_file_path)
99+
100+
if __name__ == "__main__":
101+
print("开始深度合并任务...")
102+
start_recursive_merge()
103+
print("\n所有任务已结束。")

test_op/test_ths_sse.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
# ("crawl_ths_equity", "提取股本结构信息:1.历次股本变动原因、日期及变动后的总股本;2.限售股份解禁的时间表、解禁数量及占总股本比例。"),
3535
# ("crawl_ths_capital", "提取资本运作详情:1.资产重组、收购、合并的详细历史记录;2.对外投资明细及进展情况。"),
3636
# ("crawl_ths_worth", "提取盈利预测信息:1.各机构最新评级汇总(买入/增持次数);2.未来三年的营收预测、净利润预测及EPS预测均值。"),
37-
("crawl_ths_news", "提取最新新闻公告:1.公司最新重要公告标题及日期;2.媒体报道的新闻摘要及舆情评价。"),
38-
("crawl_ths_concept", "提取所有概念题材:列出公司所属的所有概念板块,并详细提取每个概念对应的具体入选理由和业务关联性。"),
39-
("crawl_ths_position", "提取主力持仓情况:1.各类机构(基金、保险、QFII等)持仓总数及占比;2.前十大具体机构持仓名单及变动。"),
40-
("crawl_ths_finance", "提取财务分析详情:1.主要财务指标(盈利、成长、偿债等);2.资产负债表、利润表、现金流量表的核心科目及审计意见。"),
41-
("crawl_ths_bonus", "提取分红融资记录:1.历年现金分红、送转股份方案及实施日期;2.历次增发、配股等融资详情。"),
42-
("crawl_ths_event", "提取公司大事记录:1.股东及高管持股变动明细;2.对外担保记录、违规处理、机构调研及投资者互动记录。"),
37+
# ("crawl_ths_news", "提取最新新闻公告:1.公司最新重要公告标题及日期;2.媒体报道的新闻摘要及舆情评价。"),
38+
# ("crawl_ths_concept", "提取所有概念题材:列出公司所属的所有概念板块,并详细提取每个概念对应的具体入选理由和业务关联性。"),
39+
# ("crawl_ths_position", "提取主力持仓情况:1.各类机构(基金、保险、QFII等)持仓总数及占比;2.前十大具体机构持仓名单及变动。"),
40+
# ("crawl_ths_finance", "提取财务分析详情:1.主要财务指标(盈利、成长、偿债等);2.资产负债表、利润表、现金流量表的核心科目及审计意见。"),
41+
# ("crawl_ths_bonus", "提取分红融资记录:1.历年现金分红、送转股份方案及实施日期;2.历次增发、配股等融资详情。"),
42+
# ("crawl_ths_event", "提取公司大事记录:1.股东及高管持股变动明细;2.对外担保记录、违规处理、机构调研及投资者互动记录。"),
4343
("crawl_ths_field", "提取行业对比数据:1.公司在所属行业内的规模、成长、盈利各项排名;2.与行业均值及同类竞品的关键财务指标对比。")
4444
]
4545

test_op/ths_sse.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,17 @@
5555
# 针对每个页面结构设计的全量提取 Query
5656
TOOLS_CONFIG = [
5757
# ("crawl_ths_company", "提取公司的完整资料:1.基本信息(行业、产品、主营、办公地址);2.高管介绍(所有高管的姓名、职务、薪资、详细个人简历);3.发行相关(上市日期、首日表现、募资额);4.所有参控股公司的名称、持股比例、业务、盈亏情况。"),
58-
# ("crawl_ths_holder", "提取股东研究全量数据:1.历年股东人数及户均持股数;2.前十大股东及流通股东名单(含持股数、性质、变动情况);3.实际控制人详情及控股层级关系描述;4.股权质押、冻结的详细明细表。"),
59-
# ("crawl_ths_operate", "提取经营分析数据:1.主营构成分析表(按行业、产品、区域划分的营业收入、利润、毛利率及同比变化);2.经营评述(公司对业务、核心竞争力的详细自我评估)。"),
58+
("crawl_ths_holder", "提取股东研究全量数据:1.历年股东人数及户均持股数;2.前十大股东及流通股东名单(含持股数、性质、变动情况);3.实际控制人详情及控股层级关系描述;4.股权质押、冻结的详细明细表。"),
59+
("crawl_ths_operate", "提取经营分析数据:1.主营构成分析表(按行业、产品、区域划分的营业收入、利润、毛利率及同比变化);2.经营评述(公司对业务、核心竞争力的详细自我评估)。"),
6060
("crawl_ths_equity", "提取股本结构信息:1.历次股本变动原因、日期及变动后的总股本;2.限售股份解禁的时间表、解禁数量及占总股本比例。"),
6161
("crawl_ths_capital", "提取资本运作详情:1.资产重组、收购、合并的详细历史记录;2.对外投资明细及进展情况。"),
6262
("crawl_ths_worth", "提取盈利预测信息:1.各机构最新评级汇总(买入/增持次数);2.未来三年的营收预测、净利润预测及EPS预测均值。"),
6363
("crawl_ths_news", "提取最新新闻公告:1.公司最新重要公告标题及日期;2.媒体报道的新闻摘要及舆情评价。"),
64-
# ("crawl_ths_concept", "提取所有概念题材:列出公司所属的所有概念板块,并详细提取每个概念对应的具体入选理由和业务关联性。"),
64+
("crawl_ths_concept", "提取所有概念题材:列出公司所属的所有概念板块,并详细提取每个概念对应的具体入选理由和业务关联性。"),
6565
("crawl_ths_position", "提取主力持仓情况:1.各类机构(基金、保险、QFII等)持仓总数及占比;2.前十大具体机构持仓名单及变动。"),
6666
("crawl_ths_finance", "提取财务分析详情:1.主要财务指标(盈利、成长、偿债等);2.资产负债表、利润表、现金流量表的核心科目及审计意见。"),
6767
("crawl_ths_bonus", "提取分红融资记录:1.历年现金分红、送转股份方案及实施日期;2.历次增发、配股等融资详情。"),
68-
# ("crawl_ths_event", "提取公司大事记录:1.股东及高管持股变动明细;2.对外担保记录、违规处理、机构调研及投资者互动记录。"),
68+
("crawl_ths_event", "提取公司大事记录:1.股东及高管持股变动明细;2.对外担保记录、违规处理、机构调研及投资者互动记录。"),
6969
# ("crawl_ths_field", "提取行业对比数据:1.公司在所属行业内的规模、成长、盈利各项排名;2.与行业均值及同类竞品的关键财务指标对比。")
7070
]
7171

@@ -265,6 +265,13 @@ async def process_single_stock(
265265
except Exception as e:
266266
import traceback
267267
elapsed_time = (datetime.now() - start_time).total_seconds()
268+
error_str = str(e)
269+
270+
# 检查是否是“不当内容”错误,这种情况不需要重试
271+
if "inappropriate content" in error_str:
272+
logger.warning(f"⚠ {code}: 返回不当内容错误,跳过不重试")
273+
return
274+
268275
if attempt < MAX_RETRIES:
269276
wait_seconds = random.randint(MIN_WAIT_ON_EMPTY, MAX_WAIT_ON_EMPTY)
270277
logger.warning(f"✗ 失败 {code}: {e}, 等待 {wait_seconds} 秒后重试 ({attempt}/{MAX_RETRIES})...")
@@ -290,13 +297,46 @@ async def run_crawl_task():
290297
# 信号量控制并发数
291298
semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
292299

300+
# 【第二步】汇总统计各工具待爬取数量
301+
logger.info(f"\n{'='*60}")
302+
logger.info("【第二步】统计各工具待爬取数量...")
303+
logger.info(f"{'='*60}")
304+
305+
total_tasks = 0
306+
tool_stats = []
307+
for tool_name, deep_query in TOOLS_CONFIG:
308+
progress_tracker = ProgressTracker(tool_name)
309+
remaining_codes = progress_tracker.get_remaining_codes(stock_codes)
310+
total_remaining = len(remaining_codes)
311+
completed_count = len(stock_codes) - total_remaining
312+
total_tasks += total_remaining
313+
tool_stats.append((tool_name, completed_count, total_remaining))
314+
# 只列出还需要爬取的工具
315+
if total_remaining > 0:
316+
logger.info(
317+
f" {tool_name}: 已完成 {completed_count}, 待爬取 {total_remaining}"
318+
)
319+
320+
logger.info(f"{'='*60}")
321+
logger.info(f"汇总: 共 {len(TOOLS_CONFIG)} 个工具, 总计待爬取 {total_tasks} 条记录")
322+
logger.info(f"{'='*60}\n")
323+
324+
if total_tasks == 0:
325+
logger.info("所有工具已完成爬取,无需继续")
326+
return
327+
328+
# 【第三步】开始爬取任务
329+
logger.info(f"\n{'='*60}")
330+
logger.info("【第三步】开始爬取任务...")
331+
logger.info(f"{'='*60}\n")
332+
293333
async with FastMcpClient(name="full-info-crawler", config=mcp_config) as client:
294334
# 外层循环:遍历所有工具
295335
for tool_name, deep_query in TOOLS_CONFIG:
296-
logger.info(f"\n{'='*80}")
336+
logger.info(f"\n{'-'*60}")
297337
logger.info(f"开始爬取工具: {tool_name}")
298338
logger.info(f"查询内容: {deep_query}")
299-
logger.info(f"{'='*80}\n")
339+
logger.info(f"{'-'*60}")
300340

301341
saver = BatchResultSaver(tool_name)
302342
progress_tracker = ProgressTracker(tool_name)

0 commit comments

Comments
 (0)