aggroot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ """
2
+ 股票对比分析
3
+
4
+ 用法: python stock_compare.py <stock_codes> [--metrics <list>]
5
+ stock_codes: 逗号分隔的股票代码,如 600519,000858(A股) 或 03888,00700(港股)
6
+ --metrics: 对比指标,逗号分隔(默认全部)
7
+
8
+ AKShare接口:
9
+ A股主: ak.stock_zh_a_spot() — 新浪全市场行情(稳定可用)
10
+ A股备: ak.stock_zh_a_spot_em() — 东方财富全市场行情
11
+ 港股: ak.stock_hk_financial_indicator_em(symbol) — 逐只获取财务指标对比
12
+
13
+ 注意: 新浪接口代码列含市场前缀(sh/sz/bj),频繁调用会被封IP
14
+ 港股对比为逐只获取,速度较慢但数据丰富
15
+ """
16
+
17
+ import sys
18
+ import argparse
19
+
20
+ sys.path.insert(0, '.')
21
+ from _utils import normalize_code, get_market_prefix, output_json, output_error, check_akshare, safe_float, is_hk_code
22
+
23
+
24
+ # A股可对比的指标(东方财富列名)
25
+ METRIC_MAP_EM = {
26
+ "pe_ratio": "市盈率-动态",
27
+ "pb_ratio": "市净率",
28
+ "total_market_cap": "总市值",
29
+ "circulating_market_cap": "流通市值",
30
+ "turnover_rate": "换手率",
31
+ "change_pct": "涨跌幅",
32
+ "volume_ratio": "量比",
33
+ "amplitude": "振幅",
34
+ }
35
+
36
+ # A股新浪接口可用的指标
37
+ METRIC_MAP_SINA = {
38
+ "change_pct": "涨跌幅",
39
+ "amount": "成交额",
40
+ "high": "最高",
41
+ "low": "最低",
42
+ "open": "今开",
43
+ "prev_close": "昨收",
44
+ "volume": "成交量",
45
+ }
46
+
47
+ # 港股可对比的指标
48
+ METRIC_MAP_HK = {
49
+ "pe_ratio": "市盈率",
50
+ "pb_ratio": "市净率",
51
+ "market_cap": "总市值(港元)",
52
+ "roe": "股东权益回报率(%)",
53
+ "net_margin": "销售净利率(%)",
54
+ "net_profit_growth": "净利润滚动环比增长(%)",
55
+ "revenue_growth": "营业总收入滚动环比增长(%)",
56
+ "dividend_yield": "股息率TTM(%)",
57
+ }
58
+
59
+ ALL_A_METRICS = list(METRIC_MAP_EM.keys())
60
+ ALL_HK_METRICS = list(METRIC_MAP_HK.keys())
61
+
62
+
63
+ def _fetch_em_batch(codes, metrics):
64
+ """东方财富A股批量获取"""
65
+ import akshare as ak
66
+
67
+ df = ak.stock_zh_a_spot_em()
68
+ stocks = []
69
+ for code in codes:
70
+ row = df[df['代码'] == code]
71
+ if row.empty:
72
+ stocks.append({"code": code, "name": "未找到", "error": f"未找到股票代码 {code}"})
73
+ continue
74
+
75
+ r = row.iloc[0]
76
+ stock_data = {
77
+ "code": code,
78
+ "name": str(r.get('名称', '')),
79
+ "price": safe_float(r.get('最新价')),
80
+ "change_pct": safe_float(r.get('涨跌幅')),
81
+ }
82
+ for metric_key in metrics:
83
+ col_name = METRIC_MAP_EM.get(metric_key)
84
+ if col_name:
85
+ stock_data[metric_key] = safe_float(r.get(col_name))
86
+ stocks.append(stock_data)
87
+ return stocks, metrics, [METRIC_MAP_EM[m] for m in metrics if m in METRIC_MAP_EM]
88
+
89
+
90
+ def _fetch_sina_batch(codes, metrics):
91
+ """新浪A股批量获取(主接口)"""
92
+ import akshare as ak
93
+
94
+ df = ak.stock_zh_a_spot()
95
+ stocks = []
96
+ sina_metrics = [m for m in metrics if m in METRIC_MAP_SINA]
97
+ for code in codes:
98
+ prefix = get_market_prefix(code)
99
+ sina_code = f"{prefix}{code}"
100
+ row = df[df['代码'] == sina_code]
101
+ if row.empty:
102
+ stocks.append({"code": code, "name": "未找到", "error": f"未找到股票代码 {code}"})
103
+ continue
104
+
105
+ r = row.iloc[0]
106
+ stock_data = {
107
+ "code": code,
108
+ "name": str(r.get('名称', '')),
109
+ "price": safe_float(r.get('最新价')),
110
+ "change_pct": safe_float(r.get('涨跌幅')),
111
+ "data_source": "sina",
112
+ }
113
+ for metric_key in sina_metrics:
114
+ col_name = METRIC_MAP_SINA.get(metric_key)
115
+ if col_name:
116
+ stock_data[metric_key] = safe_float(r.get(col_name))
117
+ stocks.append(stock_data)
118
+ return stocks, sina_metrics, [METRIC_MAP_SINA[m] for m in sina_metrics if m in METRIC_MAP_SINA]
119
+
120
+
121
+ def _fetch_hk_batch(codes, metrics):
122
+ """港股逐只获取财务指标对比"""
123
+ import akshare as ak
124
+
125
+ stocks = []
126
+ hk_metrics = [m for m in metrics if m in METRIC_MAP_HK]
127
+ for code in codes:
128
+ try:
129
+ fi = ak.stock_hk_financial_indicator_em(symbol=code)
130
+ if fi.empty:
131
+ stocks.append({"code": code, "name": "未找到", "error": f"未找到港股 {code}"})
132
+ continue
133
+
134
+ r = fi.iloc[0]
135
+ stock_data = {
136
+ "code": code,
137
+ "pe_ratio": safe_float(r.get('市盈率')),
138
+ "pb_ratio": safe_float(r.get('市净率')),
139
+ "market_cap": safe_float(r.get('总市值(港元)')),
140
+ "data_source": "eastmoney_hk",
141
+ }
142
+ for metric_key in hk_metrics:
143
+ col_name = METRIC_MAP_HK.get(metric_key)
144
+ if col_name:
145
+ stock_data[metric_key] = safe_float(r.get(col_name))
146
+ stocks.append(stock_data)
147
+ except Exception as e:
148
+ stocks.append({"code": code, "error": str(e)})
149
+
150
+ return stocks, hk_metrics, [METRIC_MAP_HK[m] for m in hk_metrics if m in METRIC_MAP_HK]
151
+
152
+
153
+ def main():
154
+ parser = argparse.ArgumentParser(description='股票对比分析')
155
+ parser.add_argument('stock_codes', help='股票代码,逗号分隔,如 600519,000858 或 03888,00700')
156
+ parser.add_argument('--metrics', default=None,
157
+ help='对比指标,逗号分隔(默认全部)')
158
+ args = parser.parse_args()
159
+
160
+ err = check_akshare()
161
+ if err:
162
+ output_error(err)
163
+ return
164
+
165
+ codes = [normalize_code(c.strip()) for c in args.stock_codes.split(',') if c.strip()]
166
+
167
+ if not codes:
168
+ output_error("请提供至少一个股票代码")
169
+ return
170
+
171
+ if len(codes) > 10:
172
+ output_error("最多对比10只股票")
173
+ return
174
+
175
+ # 自动判断市场类型
176
+ hk_codes = [c for c in codes if is_hk_code(c)]
177
+ a_codes = [c for c in codes if not is_hk_code(c)]
178
+
179
+ try:
180
+ import akshare as ak
181
+
182
+ # 纯港股对比
183
+ if hk_codes and not a_codes:
184
+ metrics = ALL_HK_METRICS
185
+ if args.metrics:
186
+ metrics = [m.strip() for m in args.metrics.split(',') if m.strip() in METRIC_MAP_HK]
187
+
188
+ stocks, used_metrics, metrics_cn = _fetch_hk_batch(hk_codes, metrics)
189
+
190
+ data = {
191
+ "market": "HK",
192
+ "stocks": stocks,
193
+ "metrics_compared": used_metrics,
194
+ "metrics_compared_cn": metrics_cn,
195
+ }
196
+ output_json(data)
197
+ return
198
+
199
+ # A股对比(含混合情况,只比A股部分)
200
+ if a_codes:
201
+ metrics = ALL_A_METRICS
202
+ if args.metrics:
203
+ metrics = [m.strip() for m in args.metrics.split(',') if m.strip() in METRIC_MAP_EM]
204
+
205
+ stocks = None
206
+ used_metrics = metrics
207
+ metrics_cn = []
208
+
209
+ try:
210
+ stocks, used_metrics, metrics_cn = _fetch_sina_batch(a_codes, metrics)
211
+ except Exception:
212
+ pass
213
+
214
+ if stocks is None:
215
+ try:
216
+ stocks, used_metrics, metrics_cn = _fetch_em_batch(a_codes, metrics)
217
+ except Exception as e2:
218
+ output_error(f"股票对比失败: {str(e2)}")
219
+ return
220
+
221
+ # 如果有港股混合,追加港股数据
222
+ if hk_codes:
223
+ hk_metrics = ALL_HK_METRICS
224
+ hk_stocks, hk_used, hk_cn = _fetch_hk_batch(hk_codes, hk_metrics)
225
+ stocks.extend(hk_stocks)
226
+ used_metrics = list(set(used_metrics + hk_used))
227
+ metrics_cn = list(set(metrics_cn + hk_cn))
228
+
229
+ data = {
230
+ "stocks": stocks,
231
+ "metrics_compared": used_metrics,
232
+ "metrics_compared_cn": metrics_cn,
233
+ }
234
+ output_json(data)
235
+ return
236
+
237
+ output_error("未提供有效的股票代码")
238
+
239
+ except Exception as e:
240
+ output_error(f"股票对比失败: {str(e)}")
241
+
242
+
243
+ if __name__ == '__main__':
244
+ main()
@@ -0,0 +1,211 @@
1
+ """
2
+ 股票财务数据查询
3
+
4
+ 用法: python stock_financial.py <stock_code> [--report <type>] [--count <n>]
5
+ stock_code: 6位A股代码(如 600519) 或 5位港股代码(如 03888)
6
+ --report: 报表类型 income/balance/cashflow,默认income
7
+ --count: 报告期数,默认4
8
+
9
+ AKShare接口:
10
+ A股: ak.stock_financial_report_sina(stock, symbol) — 新浪完整多期报表
11
+ 港股补: ak.stock_hk_financial_indicator_em(symbol) — 东方财富关键财务指标
12
+ 港股补: ak.stock_hk_dividend_payout_em(symbol) — 东方财富分红派息历史
13
+ 港股补: ak.stock_hk_valuation_comparison_em(symbol) — 东方财富估值行业对比
14
+ 港股补: ak.stock_hk_growth_comparison_em(symbol) — 东方财富成长性行业对比
15
+ """
16
+
17
+ import sys
18
+ import argparse
19
+
20
+ sys.path.insert(0, '.')
21
+ from _utils import normalize_code, output_json, output_error, check_akshare, safe_float, is_hk_code
22
+
23
+
24
+ REPORT_MAP = {
25
+ "income": "利润表",
26
+ "balance": "资产负债表",
27
+ "cashflow": "现金流量表",
28
+ }
29
+
30
+
31
+ def _fetch_hk_financial(code):
32
+ """港股财务数据:指标+分红+估值对比+成长对比"""
33
+ import akshare as ak
34
+
35
+ result = {
36
+ "code": code,
37
+ "report_type": "hk_comprehensive",
38
+ "report_type_cn": "港股综合财务",
39
+ "data_source": "eastmoney_hk",
40
+ }
41
+
42
+ # 1. 关键财务指标
43
+ try:
44
+ fi = ak.stock_hk_financial_indicator_em(symbol=code)
45
+ if not fi.empty:
46
+ r = fi.iloc[0]
47
+ result["indicators"] = {
48
+ "eps": safe_float(r.get('基本每股收益(元)')),
49
+ "bps": safe_float(r.get('每股净资产(元)')),
50
+ "dividend_per_share_ttm": safe_float(r.get('每股股息TTM(港元)')),
51
+ "payout_ratio": safe_float(r.get('派息比率(%)')),
52
+ "dividend_yield_ttm": safe_float(r.get('股息率TTM(%)')),
53
+ "revenue": safe_float(r.get('营业总收入')),
54
+ "revenue_growth": safe_float(r.get('营业总收入滚动环比增长(%)')),
55
+ "net_profit": safe_float(r.get('净利润')),
56
+ "net_profit_growth": safe_float(r.get('净利润滚动环比增长(%)')),
57
+ "net_margin": safe_float(r.get('销售净利率(%)')),
58
+ "roe": safe_float(r.get('股东权益回报率(%)')),
59
+ "roa": safe_float(r.get('总资产回报率(%)')),
60
+ "pe": safe_float(r.get('市盈率')),
61
+ "pb": safe_float(r.get('市净率')),
62
+ "market_cap": safe_float(r.get('总市值(港元)')),
63
+ "shares_outstanding": safe_float(r.get('已发行股本(股)')),
64
+ }
65
+ else:
66
+ result["indicators"] = None
67
+ result["indicators_error"] = "东方财富港股指标接口返回空数据"
68
+ except Exception as e:
69
+ result["indicators"] = None
70
+ result["indicators_error"] = f"获取关键指标失败: {str(e)}"
71
+
72
+ # 2. 分红派息历史
73
+ try:
74
+ div_df = ak.stock_hk_dividend_payout_em(symbol=code)
75
+ if not div_df.empty:
76
+ dividends = []
77
+ for _, row in div_df.head(5).iterrows():
78
+ dividends.append({
79
+ "announce_date": str(row.get('最新公告日期', '')),
80
+ "fiscal_year": str(row.get('财政年度', '')),
81
+ "plan": str(row.get('分红方案', '')),
82
+ "type": str(row.get('分配类型', '')),
83
+ "ex_date": str(row.get('除净日', '')),
84
+ "pay_date": str(row.get('发放日', '')),
85
+ })
86
+ result["dividends"] = dividends
87
+ result["dividend_count"] = len(dividends)
88
+ else:
89
+ result["dividends"] = []
90
+ result["dividends_error"] = "分红派息接口返回空数据"
91
+ except Exception as e:
92
+ result["dividends"] = []
93
+ result["dividends_error"] = f"获取分红数据失败: {str(e)}"
94
+
95
+ # 3. 估值行业对比
96
+ try:
97
+ val_df = ak.stock_hk_valuation_comparison_em(symbol=code)
98
+ if not val_df.empty:
99
+ r = val_df.iloc[0]
100
+ result["valuation_comparison"] = {
101
+ "pe_ttm": safe_float(r.get('市盈率-TTM')),
102
+ "pe_ttm_rank": safe_float(r.get('市盈率-TTM排名')),
103
+ "pe_lyr": safe_float(r.get('市盈率-LYR')),
104
+ "pb_mrq": safe_float(r.get('市净率-MRQ')),
105
+ "pb_mrq_rank": safe_float(r.get('市净率-MRQ排名')),
106
+ "ps_ttm": safe_float(r.get('市销率-TTM')),
107
+ "ps_ttm_rank": safe_float(r.get('市销率-TTM排名')),
108
+ }
109
+ else:
110
+ result["valuation_comparison"] = None
111
+ result["valuation_comparison_error"] = "估值对比接口返回空数据"
112
+ except Exception as e:
113
+ result["valuation_comparison"] = None
114
+ result["valuation_comparison_error"] = f"获取估值对比失败: {str(e)}"
115
+
116
+ # 4. 成长性行业对比
117
+ try:
118
+ grow_df = ak.stock_hk_growth_comparison_em(symbol=code)
119
+ if not grow_df.empty:
120
+ r = grow_df.iloc[0]
121
+ result["growth_comparison"] = {
122
+ "eps_growth": safe_float(r.get('基本每股收益同比增长率')),
123
+ "eps_growth_rank": safe_float(r.get('基本每股收益同比增长率排名')),
124
+ "revenue_growth": safe_float(r.get('营业收入同比增长率')),
125
+ "revenue_growth_rank": safe_float(r.get('营业收入同比增长率排名')),
126
+ "op_profit_growth": safe_float(r.get('营业利润率同比增长率')),
127
+ "op_profit_growth_rank": safe_float(r.get('营业利润率同比增长率排名')),
128
+ }
129
+ else:
130
+ result["growth_comparison"] = None
131
+ result["growth_comparison_error"] = "成长性对比接口返回空数据"
132
+ except Exception as e:
133
+ result["growth_comparison"] = None
134
+ result["growth_comparison_error"] = f"获取成长性对比失败: {str(e)}"
135
+
136
+ return result
137
+
138
+
139
+ def main():
140
+ parser = argparse.ArgumentParser(description='股票财务数据查询')
141
+ parser.add_argument('stock_code', help='股票代码')
142
+ parser.add_argument('--report', default='income', choices=REPORT_MAP.keys(),
143
+ help='报表类型: income/balance/cashflow')
144
+ parser.add_argument('--count', type=int, default=4, help='报告期数(默认4)')
145
+ args = parser.parse_args()
146
+
147
+ err = check_akshare()
148
+ if err:
149
+ output_error(err)
150
+ return
151
+
152
+ code = normalize_code(args.stock_code)
153
+ report_cn = REPORT_MAP[args.report]
154
+
155
+ try:
156
+ import akshare as ak
157
+
158
+ # 港股:综合财务数据
159
+ if is_hk_code(code):
160
+ data = _fetch_hk_financial(code)
161
+ if data.get("indicators") is None and not data.get("dividends"):
162
+ output_error(f"未找到港股 {code} 的财务数据")
163
+ return
164
+ output_json(data)
165
+ return
166
+
167
+ # A股:新浪财务报表
168
+ df = None
169
+ try:
170
+ df = ak.stock_financial_report_sina(stock=code, symbol=report_cn)
171
+ except (TypeError, KeyError, AttributeError):
172
+ pass
173
+
174
+ if df is None or df.empty:
175
+ output_error(f"未找到 {code} 的{report_cn}数据,该接口仅支持A股")
176
+ return
177
+
178
+ # 取前N期
179
+ df = df.head(args.count)
180
+
181
+ # 转为字典列表,处理NaN
182
+ periods = []
183
+ for _, row in df.iterrows():
184
+ period_data = {}
185
+ for col in df.columns:
186
+ val = row[col]
187
+ if str(val) == 'nan':
188
+ period_data[col] = None
189
+ else:
190
+ period_data[col] = safe_float(val, str(val))
191
+ periods.append(period_data)
192
+
193
+ # 获取股票名称
194
+ name = periods[0].get('股票代码', code) if periods else code
195
+
196
+ data = {
197
+ "code": code,
198
+ "name": name,
199
+ "report_type": args.report,
200
+ "report_type_cn": report_cn,
201
+ "periods": periods,
202
+ }
203
+
204
+ output_json(data)
205
+
206
+ except Exception as e:
207
+ output_error(f"获取财务数据失败: {str(e)}")
208
+
209
+
210
+ if __name__ == '__main__':
211
+ main()
@@ -0,0 +1,181 @@
1
+ """
2
+ 股票新闻/公司资讯查询
3
+
4
+ 用法: python stock_news.py <stock_code> [--count <n>]
5
+ stock_code: 6位A股代码(如 600519) 或 5位港股代码(如 03888)
6
+ --count: 新闻条数,默认10
7
+
8
+ AKShare接口:
9
+ A股: ak.stock_news_em(symbol) — 东方财富个股新闻
10
+ 港股: ak.stock_hk_company_profile_em(symbol) — 东方财富公司资料(港股无新闻接口)
11
+ 港股: ak.stock_hk_security_profile_em(symbol) — 东方财富证券资料
12
+ """
13
+
14
+ import sys
15
+ import argparse
16
+
17
+ sys.path.insert(0, '.')
18
+ from _utils import normalize_code, output_json, output_error, check_akshare, safe_float, is_hk_code
19
+
20
+
21
+ def _fetch_hk_profile(code):
22
+ """港股公司资料(替代新闻)"""
23
+ import akshare as ak
24
+
25
+ data = {"code": code, "type": "hk_company_profile"}
26
+
27
+ # 公司资料
28
+ try:
29
+ profile = ak.stock_hk_company_profile_em(symbol=code)
30
+ if not profile.empty:
31
+ p = profile.iloc[0]
32
+ data["company"] = {
33
+ "name": str(p.get('公司名称', '')),
34
+ "name_en": str(p.get('英文名称', '')),
35
+ "industry": str(p.get('所属行业', '')),
36
+ "chairman": str(p.get('董事长', '')),
37
+ "employees": safe_float(p.get('员工人数')),
38
+ "website": str(p.get('公司网址', '')),
39
+ "founded": str(p.get('公司成立日期', '')),
40
+ "description": str(p.get('公司介绍', ''))[:500],
41
+ }
42
+ except Exception:
43
+ data["company"] = None
44
+
45
+ # 证券资料
46
+ try:
47
+ sec = ak.stock_hk_security_profile_em(symbol=code)
48
+ if not sec.empty:
49
+ s = sec.iloc[0]
50
+ data["security"] = {
51
+ "name": str(s.get('证券简称', '')),
52
+ "list_date": str(s.get('上市日期', '')),
53
+ "type": str(s.get('证券类型', '')),
54
+ "board": str(s.get('板块', '')),
55
+ "ipo_price": safe_float(s.get('发行价')),
56
+ "lot_size": safe_float(s.get('每手股数')),
57
+ "exchange": str(s.get('交易所', '')),
58
+ "is_sh_hkconnect": str(s.get('是否沪港通标的', '')),
59
+ "is_sz_hkconnect": str(s.get('是否深港通标的', '')),
60
+ }
61
+ except Exception:
62
+ data["security"] = None
63
+
64
+ return data
65
+
66
+
67
+ def main():
68
+ parser = argparse.ArgumentParser(description='股票新闻/公司资讯查询')
69
+ parser.add_argument('stock_code', help='股票代码')
70
+ parser.add_argument('--count', type=int, default=10, help='新闻条数(默认10)')
71
+ args = parser.parse_args()
72
+
73
+ err = check_akshare()
74
+ if err:
75
+ output_error(err)
76
+ return
77
+
78
+ code = normalize_code(args.stock_code)
79
+
80
+ try:
81
+ import akshare as ak
82
+
83
+ # 港股:先尝试东方财富新闻接口,失败则返回公司资料
84
+ if is_hk_code(code):
85
+ # 1. 尝试东方财富新闻(已支持港股代码)
86
+ try:
87
+ df = ak.stock_news_em(symbol=code)
88
+ if df is not None and not df.empty:
89
+ df = df.head(args.count * 3)
90
+ seen = set()
91
+ news_items = []
92
+ for _, row in df.iterrows():
93
+ title = str(row.get('新闻标题', ''))
94
+ url = str(row.get('新闻链接', ''))
95
+ dedup_key = (title.strip(), url.strip())
96
+ if dedup_key in seen:
97
+ continue
98
+ seen.add(dedup_key)
99
+ item = {
100
+ "title": title,
101
+ "content": str(row.get('新闻内容', ''))[:200],
102
+ "source": str(row.get('文章来源', '')),
103
+ "publish_time": str(row.get('发布时间', '')),
104
+ "url": url,
105
+ }
106
+ news_items.append(item)
107
+ if len(news_items) >= args.count:
108
+ break
109
+
110
+ data = {
111
+ "code": code,
112
+ "type": "hk_news",
113
+ "news_count": len(news_items),
114
+ "news": news_items,
115
+ }
116
+ # 同时获取公司资料作为补充
117
+ profile_data = _fetch_hk_profile(code)
118
+ if profile_data.get("company"):
119
+ data["company"] = profile_data["company"]
120
+ if profile_data.get("security"):
121
+ data["security"] = profile_data["security"]
122
+ output_json(data)
123
+ return
124
+ except Exception:
125
+ pass
126
+
127
+ # 2. 新闻接口失败,回退到公司资料
128
+ data = _fetch_hk_profile(code)
129
+ data["type"] = "hk_company_profile"
130
+ data["news_note"] = "东方财富新闻接口暂不可用,仅返回公司资料"
131
+ if data.get("company") is None and data.get("security") is None:
132
+ output_error(f"未找到港股 {code} 的公司资料")
133
+ return
134
+ output_json(data)
135
+ return
136
+
137
+ # A股:东方财富新闻
138
+ df = ak.stock_news_em(symbol=code)
139
+
140
+ if df.empty:
141
+ output_error(f"未找到 {code} 的新闻数据")
142
+ return
143
+
144
+ df = df.head(args.count * 3) # 取更多以应对去重后不足
145
+
146
+ seen = set()
147
+ news_items = []
148
+ for _, row in df.iterrows():
149
+ title = str(row.get('新闻标题', ''))
150
+ url = str(row.get('新闻链接', ''))
151
+ # 去重:标题+链接组合唯一
152
+ dedup_key = (title.strip(), url.strip())
153
+ if dedup_key in seen:
154
+ continue
155
+ seen.add(dedup_key)
156
+
157
+ item = {
158
+ "title": title,
159
+ "content": str(row.get('新闻内容', ''))[:200],
160
+ "source": str(row.get('文章来源', '')),
161
+ "publish_time": str(row.get('发布时间', '')),
162
+ "url": url,
163
+ }
164
+ news_items.append(item)
165
+ if len(news_items) >= args.count:
166
+ break
167
+
168
+ data = {
169
+ "code": code,
170
+ "news_count": len(news_items),
171
+ "news": news_items,
172
+ }
173
+
174
+ output_json(data)
175
+
176
+ except Exception as e:
177
+ output_error(f"获取新闻失败: {str(e)}")
178
+
179
+
180
+ if __name__ == '__main__':
181
+ main()