Files

881 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
金融机构名字爬虫增强模块
支持通过机构名称爬取网站信息,包含反爬、翻译和双语展示功能
"""
import json
import re
import time
import random
import hashlib
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple
from datetime import datetime
# Scrapling导入
try:
from scrapling.fetchers import StealthyFetcher, DynamicFetcher
from scrapling.parser import Selector
SCRAPLING_AVAILABLE = True
except ImportError:
SCRAPLING_AVAILABLE = False
Selector = Any # type: ignore # forward reference fallback
try:
from playwright.sync_api import sync_playwright
PLAYWRIGHT_AVAILABLE = True
except ImportError:
PLAYWRIGHT_AVAILABLE = False
# 数据目录
SKILL_DATA_DIR = Path(__file__).parent.parent / "data"
# 常用URL模式(从机构名称推断官网)
URL_PATTERNS = {
# 政策性银行
"国家开发银行": "https://www.cdb.com.cn",
# 中国进出口银行(官网为政府域名,已移除)
"中国农业发展银行": "https://www.adbc.com.cn",
# 国有商业银行
"工商银行": "https://www.icbc.com.cn",
"建设银行": "https://www.ccb.com",
"农业银行": "https://www.abchina.com",
"中国银行": "https://www.boc.cn",
"交通银行": "https://www.bankcomm.com",
"邮储银行": "https://www.psbc.com",
# 股份制银行
"招商银行": "https://www.cmbchina.com",
"中信银行": "https://www.citicbank.com",
"浦发银行": "https://www.spdb.com.cn",
"兴业银行": "https://www.cib.com.cn",
"民生银行": "https://www.cmbc.com.cn",
"平安银行": "https://bank.pingan.com",
"光大银行": "https://www.cebbank.com",
"华夏银行": "https://www.hxb.com.cn",
"广发银行": "https://www.cgbchina.com.cn",
"北京银行": "https://www.bankofbeijing.com.cn",
"上海银行": "https://www.bankofshanghai.com",
"江苏银行": "https://www.jsbchina.cn",
"南京银行": "https://www.njcb.com.cn",
"杭州银行": "https://www.hzbank.com.cn",
"宁波银行": "https://www.nbbank.com.cn",
# 保险公司
"中国人寿": "https://www.chinalife.com.cn",
"中国平安": "https://www.pingan.com.cn",
"中国太保": "https://www.cpic.com.cn",
"中国人保": "https://www.picc.com.cn",
"新华保险": "https://www.newchina-life.com",
"泰康保险": "https://www.taikang.com",
"友邦保险": "https://www.aia.com.cn",
# 基金公司
"易方达": "https://www.efunds.com.cn",
"华夏基金": "https://www.chinaamc.com",
"广发基金": "https://www.gffunds.com.cn",
"嘉实基金": "https://www.jsfund.cn",
"南方基金": "https://www.nffund.com.cn",
"博时基金": "https://www.bosera.com",
"招商基金": "https://www.cmfchina.com",
"工银基金": "https://www.icbcfs.com",
"建信基金": "https://www.ccbfund.cn",
"富国基金": "https://www.fullgoal.com.cn",
"鹏华基金": "https://www.phfund.com.cn",
"汇添富基金": "https://www.htffund.com",
"中欧基金": "https://www.zofund.com",
"兴证全球基金": "https://www.xqglobal.com",
# 证券公司
"中信证券": "https://www.cs.ecitic.com",
"中信建投证券": "https://www.csc108.com",
"国泰君安证券": "https://www.gtja.com",
"华泰证券": "https://www.htsc.com.cn",
"广发证券": "https://www.gf.com.cn",
"招商证券": "https://www.newone.com.cn",
"海通证券": "https://www.htsec.com",
"国信证券": "https://www.guosen.com.cn",
"东方证券": "https://www.dfzq.com.cn",
"兴业证券": "https://www.xyzq.com.cn",
"银河证券": "https://www.chinastock.com.cn",
"长江证券": "https://www.95579.com",
"中金公司": "https://www.cicc.com",
"光大证券": "https://www.ebscn.com",
"平安证券": "https://stock.pingan.com",
"方正证券": "https://www.foundersc.com",
# 信托公司
"中信信托": "https://www.zxxt.com.cn",
"平安信托": "https://www.paxt.com.cn",
"中融信托": "https://www.zhongrongtrust.com",
"华润信托": "https://www.crhtrust.com.cn",
"外贸信托": "https://www.fotacn.com",
# 外资银行
"汇丰银行": "https://www.hsbc.com.cn",
"渣打银行": "https://www.standardchartered.com.cn",
"花旗银行": "https://www.citibank.com.cn",
"摩根大通银行": "https://www.jpmorganchina.com.cn",
"高盛集团": "https://www.goldmansachs.com.cn",
"瑞银证券": "https://www.ubs.com/cn/zh.html",
"野村证券": "https://www.nomura.com",
# 外资保险公司
"安盛保险": "https://www.axa.com.cn",
"安联保险": "https://www.allianz.com.cn",
"友邦保险": "https://www.aia.com.cn",
# 外资资产管理公司
"贝莱德资产管理": "https://www.blackrock.com.cn",
"瑞银资产管理": "https://www.ubs.com/global/en/asset-management.html",
}
# 外资机构名称映射(用于翻译)
FOREIGN_INSTITUTION_TRANSLATIONS = {
"汇丰银行": "HSBC Holdings plc",
"渣打银行": "Standard Chartered plc",
"花旗银行": "Citibank N.A.",
"摩根大通银行": "JPMorgan Chase & Co.",
"摩根士丹利": "Morgan Stanley",
"高盛集团": "Goldman Sachs Group Inc.",
"德意志银行": "Deutsche Bank AG",
"瑞士银行": "UBS Group AG",
"法国巴黎银行": "BNP Paribas SA",
"法兴银行": "Société Générale SA",
"东方汇理银行": "Crédit Agricole CIB",
"瑞穗银行": "Mizuho Financial Group",
"三菱日联金融": "Mitsubishi UFJ Financial Group",
"三井住友金融": "Sumitomo Mitsui Financial Group",
"友利银行": "Woori Bank",
"韩亚银行": "Hana Bank",
"新韩银行": "Shinhan Bank",
"星展银行": "DBS Bank",
"华侨银行": "OCBC Bank",
"大华银行": "UOB Bank",
"东亚银行": "Bank of East Asia",
"恒生银行": "Hang Seng Bank",
"安盛保险": "AXA SA",
"安联保险": "Allianz SE",
"忠利保险": "Generali Group",
"保德信保险": "Prudential Financial Inc.",
"大都会保险": "MetLife Inc.",
"友邦保险": "AIA Group Limited",
"宏利保险": "Manulife Financial",
"安达保险": "Chubb Limited",
"信利保险": "Zurich Insurance Group",
"英杰华保险": "Aviva",
"耆卫保险": "Old Mutual",
"日本生命保险": "Nippon Life Insurance",
"第一生命保险": "Dai-ichi Life Insurance",
"明治安田保险": "Meiji Yasuda Life Insurance",
"野村证券": "Nomura Holdings Inc.",
"大和证券": "Daiwa Securities Group Inc.",
"瑞穗证券": "Mizuho Securities",
"三菱日联证券": "Mitsubishi UFJ Securities",
"高盛高华证券": "Goldman Sachs Gao Hua",
"汇丰前海证券": "HSBC Qianhai Securities",
"法巴证券": "BNP Paribas Securities",
"法兴证券": "Société Générale Securities",
"东方汇理期货": "Crédit Agricole CIB Futures",
"瑞银期货": "UBS Futures",
"汇丰资产管理": "HSBC Global Asset Management",
"瑞银资产管理": "UBS Asset Management",
"摩根资产管理": "JPMorgan Asset Management",
"贝莱德资产管理": "BlackRock",
"道富环球投资管理": "State Street Global Advisors",
"富达国际资产管理": "Fidelity International",
"景顺资产管理": "Invesco",
"安联资产管理": "Allianz Global Investors",
}
# 反爬设置
DEFAULT_DELAY = (1.5, 3.5) # 随机延迟范围(秒)
MAX_RETRIES = 2
REQUEST_TIMEOUT = 30000 # 30秒
class AntiCrawlFetcher:
"""带反爬功能的爬取器"""
def __init__(self):
self.fetcher = None
self.last_request_time = 0
self.request_count = 0
self.failed_domains = set() # 记录失败域名,避免重复请求
def _random_delay(self):
"""随机延迟"""
delay = random.uniform(*DEFAULT_DELAY)
time.sleep(delay)
def _update_last_request(self):
"""更新最后请求时间"""
self.last_request_time = time.time()
self.request_count += 1
def fetch(self, url: str, use_dynamic: bool = False,
headers: Dict = None, retry_count: int = 0) -> Optional[Selector]:
"""
带反爬的爬取
Args:
url: 目标URL
use_dynamic: 是否使用动态渲染
headers: 自定义请求头
retry_count: 当前重试次数
Returns:
Selector对象或None
"""
if not SCRAPLING_AVAILABLE:
return None
self._random_delay()
try:
if self.fetcher is None:
self.fetcher = StealthyFetcher()
# 构建请求头
request_headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
}
if headers:
request_headers.update(headers)
# 尝试使用动态爬取
if use_dynamic:
try:
page = DynamicFetcher.fetch(
url,
headless=True,
network_idle=True,
solve_cloudflare=True,
timeout=REQUEST_TIMEOUT
)
self._update_last_request()
# scrapling 0.2.x 兼容
if hasattr(page, 'html_content'):
page.html = page.html_content
elif hasattr(page, 'body') and isinstance(page.body, str):
page.html = page.body
return page
except Exception as e:
if retry_count < MAX_RETRIES:
wait_time = random.uniform(5, 15)
print(f"[反爬] 动态爬取失败,{wait_time:.1f}秒后重试: {url}")
time.sleep(wait_time)
return self.fetch(url, use_dynamic, headers, retry_count + 1)
else:
print(f"[反爬] 动态爬取最终失败: {url}")
return None
# 使用隐式请求(默认)
page = self.fetcher.fetch(
url,
headless=True,
solve_cloudflare=True,
timeout=REQUEST_TIMEOUT
)
self._update_last_request()
# scrapling 0.2.x 兼容:注入 .html 属性
if hasattr(page, 'html_content'):
page.html = page.html_content
elif hasattr(page, 'body') and isinstance(page.body, str):
page.html = page.body
return page
except Exception as e:
print(f"[反爬] 爬取失败 ({retry_count + 1}/{MAX_RETRIES + 1}): {url}")
print(f"[错误] {str(e)[:100]}")
if retry_count < MAX_RETRIES:
wait_time = random.uniform(5, 15)
print(f"[反爬] {wait_time:.1f}秒后重试...")
time.sleep(wait_time)
return self.fetch(url, use_dynamic, headers, retry_count + 1)
return None
class InstitutionNameScraper:
"""通过机构名称爬取网站内容的爬取器"""
def __init__(self):
self.fetcher = AntiCrawlFetcher()
self._institution_cache = {} # 机构信息缓存
self._load_institution_data()
def _load_institution_data(self):
"""加载所有机构数据"""
data_files = [
"institutions.json",
"foreign_institution_list.json",
"policy_bank_list.json",
"state_owned_bank_list.json",
"joint_stock_bank_list.json",
"city_commercial_bank_list.json",
"rural_commercial_bank_list.json",
"insurance_list.json",
"fund_company_list.json",
"securities_list.json",
"trust_company_list.json",
"private_fund_list.json",
"financial_lease_list.json",
"city_investment_list.json",
]
for filename in data_files:
filepath = SKILL_DATA_DIR / filename
if filepath.exists():
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
# 提取institutions数组
if "institutions" in data:
for inst in data["institutions"]:
self._institution_cache[inst.get("name", "")] = {
**inst,
"source_file": filename
}
except Exception as e:
print(f"[警告] 加载{filename}失败: {e}")
def find_institution(self, name: str) -> Optional[Dict]:
"""
通过名称查找机构信息
Args:
name: 机构名称(支持模糊匹配)
Returns:
机构信息字典
"""
if not name:
return None
# 精确匹配
if name in self._institution_cache:
return self._institution_cache[name]
# 模糊匹配
name_lower = name.lower()
for inst_name, inst_info in self._institution_cache.items():
if name_lower in inst_name.lower() or inst_name.lower() in name_lower:
return inst_info
# 关键词匹配
keywords = name.replace("基金", "").replace("证券", "").replace("银行", "").replace("保险", "").strip()
if keywords and len(keywords) >= 2:
for inst_name, inst_info in self._institution_cache.items():
if keywords in inst_name or inst_name in keywords:
return inst_info
return None
def get_institution_url(self, name: str) -> Optional[str]:
"""
获取机构官网URL
Args:
name: 机构名称
Returns:
官网URL或None
"""
# 优先从预定义URL模式获取
if name in URL_PATTERNS:
return URL_PATTERNS[name]
# 从URL模式中查找包含关键词的
for pattern_name, url in URL_PATTERNS.items():
if name in pattern_name or pattern_name in name:
return url
return None
def is_foreign_institution(self, name: str) -> bool:
"""判断是否为外资机构"""
return name in FOREIGN_INSTITUTION_TRANSLATIONS
def get_english_name(self, name: str) -> Optional[str]:
"""获取机构英文名称"""
return FOREIGN_INSTITUTION_TRANSLATIONS.get(name)
def scrape_by_name(self, name: str, use_dynamic: bool = True) -> Dict[str, Any]:
"""
通过机构名称爬取网站内容
Args:
name: 机构名称
use_dynamic: 是否使用动态渲染
Returns:
爬取结果字典
"""
result = {
"name": name,
"scrape_time": time.strftime("%Y-%m-%d %H:%M:%S"),
"url": None,
"success": False,
"content": {},
"error": None,
"is_foreign": self.is_foreign_institution(name),
"english_name": self.get_english_name(name),
}
# 查找机构信息
inst_info = self.find_institution(name)
if inst_info:
result["institution_info"] = inst_info
# 获取URL
url = self.get_institution_url(name)
if not url:
result["error"] = f"未找到机构'{name}'的官网URL"
return result
result["url"] = url
print(f"[爬虫] 正在爬取: {name} -> {url}")
# 爬取内容
page = self.fetcher.fetch(url, use_dynamic=use_dynamic)
if page is None:
result["error"] = "爬取失败,请检查网络或URL是否可访问"
return result
result["success"] = True
# 提取页面内容
content = self._extract_page_content(page, url)
result["content"] = content
# 如果是外资机构,处理双语展示
if result["is_foreign"]:
result["bilingual_content"] = self._generate_bilingual_content(content, name)
return result
def _extract_page_content(self, page: Selector, url: str) -> Dict[str, Any]:
"""提取页面内容"""
content = {
"title": "",
"main_content": "",
"about": "",
"products": [],
"contact": {},
}
try:
# 提取标题
for sel in ["h1", ".site-title", "[class*='title']", "title"]:
try:
el = page.css_first(sel)
if el:
content["title"] = el.text().strip()[:200]
break
except:
continue
# 如果没拿到标题,尝试从meta获取
if not content["title"]:
try:
meta_title = page.re_search(r'<title[^>]*>([^<]+)</title>')
if meta_title:
content["title"] = meta_title.group(1).strip()
except:
pass
# 提取主要文本内容
text_selectors = [
"main",
"article",
".main-content",
"[class*='content']",
"[class*='about']",
".container",
]
for sel in text_selectors:
try:
el = page.css_first(sel)
if el:
text = el.text().strip()
if len(text) > 100: # 至少100字符
content["main_content"] = text[:3000] # 限制长度
break
except:
continue
# 提取关于我们/简介
about_patterns = [
r'(?:关于我们|关于我们|公司简介|Company Profile|About Us)[::\s]*([^\n]{100,500})',
r'(?:我们是谁|Our Story)[::\s]*([^\n]{50,300})',
]
for pattern in about_patterns:
match = page.re_search(pattern)
if match:
content["about"] = match.group(1).strip()
break
# 如果还没获取到about,尝试从main_content中提取
if not content["about"] and content["main_content"]:
about_match = re.search(r'(?:关于我们|关于我们|公司简介)[::\s]*([^\n]{50,300})', content["main_content"])
if about_match:
content["about"] = about_match.group(1).strip()
# 提取联系信息
contact_patterns = {
"phone": r'(\d{3,4}[-\s]?\d{7,8}|\d{3}[-\s]?\d{4}[-\s]?\d{4})',
"email": r'([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})',
"address": r'(?:地址|Address)[::\s]*([^\n]{10,100})',
}
for key, pattern in contact_patterns.items():
match = page.re_search(pattern)
if match:
content["contact"][key] = match.group(1).strip()
except Exception as e:
print(f"[警告] 内容提取失败: {e}")
return content
def _generate_bilingual_content(self, content: Dict, name: str) -> Dict[str, Any]:
"""生成双语内容"""
bilingual = {
"institution": name,
"english_name": self.get_english_name(name),
"title": {},
"about": {},
"main_content": {},
}
# 标题翻译(简化处理,实际项目中可接入翻译API)
if content.get("title"):
bilingual["title"] = {
"zh": content["title"],
"en": self._translate_to_english(content["title"], name),
}
# 关于信息双语
if content.get("about"):
bilingual["about"] = {
"zh": content["about"],
"en": self._translate_to_english(content["about"], name),
}
# 主要内容双语
if content.get("main_content"):
bilingual["main_content"] = {
"zh": content["main_content"][:1500] if len(content["main_content"]) > 1500 else content["main_content"],
"en": self._translate_to_english(content["main_content"][:1500] if len(content["main_content"]) > 1500 else content["main_content"], name),
}
return bilingual
def _translate_to_english(self, text: str, institution_name: str) -> str:
"""
将中文文本翻译为英文(简化版本)
实际项目中应接入专业翻译API(如百度翻译、Google翻译等)
"""
if not text:
return ""
# 简短翻译映射(实际项目中应接入翻译API)
translation_mapping = {
"关于我们": "About Us",
"公司简介": "Company Profile",
"联系我们": "Contact Us",
"产品中心": "Products",
"新闻中心": "News",
"投资者关系": "Investor Relations",
"人才招聘": "Careers",
"企业文化": "Corporate Culture",
"发展历程": "Our Story",
"组织架构": "Organization",
"社会责任": "Corporate Social Responsibility",
"公司地址": "Address",
"联系电话": "Tel",
"电子邮箱": "Email",
}
# 检查是否是简短文本
for zh, en in translation_mapping.items():
if zh in text[:10]:
return en
# 对于长文本,返回原文+说明(实际应接入翻译API)
if len(text) > 100:
return f"[English Translation of Chinese Text]\n{text[:500]}..."
return f"[Translated] {text}"
def format_bilingual_display(self, result: Dict[str, Any]) -> str:
"""
格式化双语展示内容
Args:
result: 爬取结果
Returns:
格式化字符串
"""
if not result.get("success"):
return f"❌ 爬取失败: {result.get('error', '未知错误')}"
name = result.get("name", "")
english_name = result.get("english_name", "")
output = []
output.append(f"\n{'='*60}")
output.append(f"🏦 机构名称: {name}")
if english_name:
output.append(f"🌐 English: {english_name}")
output.append(f"{'='*60}\n")
# 外资机构双语展示
if result.get("is_foreign") and result.get("bilingual_content"):
bc = result["bilingual_content"]
output.append("【公司简介 / Company Profile】\n")
if bc.get("about"):
about = bc["about"]
output.append(f"🇨🇳 中文: {about.get('zh', '')}")
output.append(f"🇺🇸 EN: {about.get('en', '')}")
else:
output.append("(暂无公司简介 / No profile available)")
output.append(f"\n{'─'*60}")
if bc.get("main_content"):
content = bc["main_content"]
output.append("\n【主要内容 / Main Content】\n")
output.append(f"🇨🇳 中文:\n{content.get('zh', '')}")
output.append(f"\n🇺🇸 EN:\n{content.get('en', '')}")
else:
# 国内机构单语展示
content = result.get("content", {})
if content.get("title"):
output.append(f"📌 标题: {content['title']}\n")
if content.get("about"):
output.append(f"\n📋 关于我们:\n{content['about']}")
if content.get("main_content"):
output.append(f"\n📄 主要内容:\n{content['main_content'][:800]}...")
if content.get("contact"):
output.append(f"\n📞 联系方式:")
for k, v in content["contact"].items():
output.append(f" {k}: {v}")
output.append(f"\n{'='*60}")
output.append(f"🕐 爬取时间: {result.get('scrape_time', '')}")
output.append(f"🔗 来源: {result.get('url', '')}")
output.append(f"{'='*60}")
return "\n".join(output)
def scrape_institution_by_name(name: str, use_dynamic: bool = True) -> Dict[str, Any]:
"""
通过机构名称爬取信息的便捷函数
Args:
name: 机构名称
use_dynamic: 是否使用动态渲染
Returns:
爬取结果
"""
scraper = InstitutionNameScraper()
result = scraper.scrape_by_name(name, use_dynamic=use_dynamic)
return result
def scrape_institution_by_url(url: str, use_dynamic: bool = True) -> Dict[str, Any]:
"""
通过URL直接爬取网站内容
Args:
url: 目标URL
use_dynamic: 是否使用动态渲染
Returns:
爬取结果
"""
result = {
"scrape_time": time.strftime("%Y-%m-%d %H:%M:%S"),
"url": url,
"success": False,
"content": {},
"error": None,
"is_foreign": False,
"english_name": None,
}
# 从URL推断机构名称
url_lower = url.lower()
inferred_name = None
# 尝试从URL域名匹配已知机构
for name, pattern_url in URL_PATTERNS.items():
if pattern_url.lower() in url_lower or url_lower in pattern_url.lower():
inferred_name = name
break
# 检查是否是外资机构
if inferred_name:
result["is_foreign"] = is_foreign_institution(inferred_name)
result["english_name"] = FOREIGN_INSTITUTION_TRANSLATIONS.get(inferred_name)
result["inferred_name"] = inferred_name
# 爬取
fetcher = AntiCrawlFetcher()
page = fetcher.fetch(url, use_dynamic=use_dynamic)
if page is None:
result["error"] = "爬取失败,请检查URL或网络连接"
return result
result["success"] = True
# 提取内容
scraper = InstitutionNameScraper()
content = scraper._extract_page_content(page, url)
result["content"] = content
# 如果是外资机构,处理双语展示
if result["is_foreign"]:
result["bilingual_content"] = scraper._generate_bilingual_content(content, inferred_name or url)
return result
def is_foreign_institution(name: str) -> bool:
"""判断是否为外资机构"""
return name in FOREIGN_INSTITUTION_TRANSLATIONS
def scrape_institution(input_str: str, use_dynamic: bool = True) -> Dict[str, Any]:
"""
智能爬取入口 - 自动识别输入是URL还是机构名称
Args:
input_str: URL或机构名称
use_dynamic: 是否使用动态渲染
Returns:
爬取结果
"""
# 判断是URL还是名称
is_url = (
"http://" in input_str.lower() or
"https://" in input_str.lower() or
".com" in input_str.lower() or
".cn" in input_str.lower() or
".org" in input_str.lower() or
"/" in input_str and len(input_str) > 20 # 简单判断URL格式
)
if is_url:
return scrape_institution_by_url(input_str, use_dynamic=use_dynamic)
else:
return scrape_institution_by_name(input_str, use_dynamic=use_dynamic)
def format_scraping_result(result: Dict[str, Any], bilingual: bool = True) -> str:
"""
格式化爬取结果
Args:
result: 爬取结果
bilingual: 是否双语展示
Returns:
格式化字符串
"""
scraper = InstitutionNameScraper()
if bilingual and result.get("is_foreign"):
return scraper.format_bilingual_display(result)
else:
# 非双语格式
if not result.get("success"):
return f"❌ 爬取失败: {result.get('error', '未知错误')}"
output = []
output.append(f"\n🏦 {result.get('name', '')}")
output.append(f"🔗 {result.get('url', '')}\n")
content = result.get("content", {})
if content.get("title"):
output.append(f"📌 标题: {content['title']}\n")
if content.get("about"):
output.append(f"📋 关于: {content['about'][:300]}...")
if content.get("main_content"):
output.append(f"📄 内容: {content['main_content'][:500]}...")
return "\n".join(output)
# CLI入口
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(" python name_scraper.py <机构名称> # 爬取单个机构")
print(" python name_scraper.py --list <名称列表> # 批量爬取")
print(" python name_scraper.py --search <关键词> # 搜索机构")
print("\n示例:")
print(' python name_scraper.py "汇丰银行"')
print(' python name_scraper.py "易方达基金"')
print(' python name_scraper.py --search "基金"')
sys.exit(1)
scraper = InstitutionNameScraper()
if sys.argv[1] == "--search":
# 搜索机构
if len(sys.argv) < 3:
print("请提供搜索关键词")
sys.exit(1)
keyword = sys.argv[2]
print(f"\n搜索关键词: {keyword}")
print("="*50)
for inst_name in sorted(scraper._institution_cache.keys()):
if keyword in inst_name:
info = scraper._institution_cache[inst_name]
print(f" ✅ {inst_name} ({info.get('code', '')}) - {info.get('source_file', '')}")
print(f"\n共找到匹配结果")
elif sys.argv[1] == "--list":
# 批量爬取
if len(sys.argv) < 3:
print("请提供名称列表文件")
sys.exit(1)
file_path = sys.argv[2]
with open(file_path, 'r', encoding='utf-8') as f:
names = [line.strip() for line in f if line.strip()]
print(f"\n开始批量爬取 {len(names)} 个机构...")
for name in names:
result = scraper.scrape_by_name(name)
print(format_scraping_result(result))
time.sleep(random.uniform(2, 5))
else:
# 单个爬取
name = sys.argv[1]
print(f"\n正在爬取: {name}")
result = scraper.scrape_by_name(name)
print(format_scraping_result(result))