20252211 2025-2026-2 《Python程序设计》实验四报告

课程:《Python程序设计》
班级: 2522
姓名: 陈鸿超
学号:20252211
实验教师:王志强
实验日期:2026年5月30日
必修/选修: 公选课

1.实验内容

利用LLM生成爬虫,并合法使用官方API获取世界银行官网上的相关数据,并对数据进行处理,具有选择特定国家搜索世界银行经济指标,获取国家最新数据,对某个国家时间序列分析,对多国家的某个数据对比,批量导出数据等功能

2. 实验过程及结果

1.安装依赖包requests、pandas、openpyxl
1
2.初始化爬虫,发送API请求,用一个具备请求延迟控制、超时保护与自动重试机制的API客户端基础模块:__init__方法通过参数化配置(默认延迟1秒、超时60秒)和requests.Session复用TCP连接与请求头,优化了资源效率并模拟浏览器行为;_make_request方法则封装了指数退避的简化重试逻辑——它在发起请求前强制睡眠指定延迟以遵守礼貌爬虫规范,捕获Timeout异常后会在非最后一次重试时额外等待2秒再继续,而对其他异常则直接短路返回None,成功时解析JSON返回。

点击查看代码
    def __init__(self, request_delay: float = 1.0, timeout: int = 60):
        """初始化爬虫"""
        self.base_url = "http://api.worldbank.org/v2"
        self.request_delay = request_delay
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "application/json"
        })

    def _make_request(self, url: str, params: Dict = None, max_retries: int = 3) -> Optional[Dict]:
        """发送API请求(带重试机制)"""
        for attempt in range(max_retries):
            try:
                time.sleep(self.request_delay)
                response = self.session.get(url, params=params, timeout=self.timeout)
                response.raise_for_status()
                data = response.json()
                return data
            except requests.exceptions.Timeout:
                print(f"请求超时 (尝试 {attempt + 1}/{max_retries})")
                if attempt == max_retries - 1:
                    return None
                time.sleep(2)
            except Exception as e:
                print(f"请求失败: {e}")
                return None
        return None

3.功能一,根据关键词在预定义的指标库中搜索匹配的经济/社会指标,用硬编码的静态字典(覆盖GDP、人口、通胀、环境等约20个常用指标)替代实时的API网络搜索,从而规避世界银行官方API返回海量数据(超16000个指标)导致的响应慢和解析复杂问题;搜索逻辑采用大小写不敏感的子串模糊匹配,让用户输入"gdp"、"per capita"等部分关键词就能命中对应的指标代码和名称,并通过max_results参数支持提前截断以优化性能。
点击查看代码
def search_indicator(self, keyword: str, max_results: int = 20) -> List[Dict]:
        """
        【功能1】根据关键词搜索指标
        用于查找需要的经济指标代码
        """
        # 常用指标本地库
        local_indicators = {
            # 经济类
            "NY.GDP.MKTP.CD": "GDP (current US$) - 国内生产总值(现价美元)",
            "NY.GDP.PCAP.CD": "GDP per capita (current US$) - 人均GDP(现价美元)",
            "NY.GDP.MKTP.KD.ZG": "GDP growth (annual %) - GDP年增长率",
            "NY.GDP.MKTP.PP.CD": "GDP, PPP (current international $) - 购买力平价GDP",
            "NY.GDP.PCAP.PP.CD": "GDP per capita, PPP - 人均购买力平价GDP",
            "NE.EXP.GNFS.ZS": "Exports of goods and services (% of GDP) - 出口占比",
            "NE.IMP.GNFS.ZS": "Imports of goods and services (% of GDP) - 进口占比",
            "BX.KLT.DINV.CD.WD": "Foreign direct investment, net inflows - 外国直接投资",
            "FP.CPI.TOTL.ZG": "Inflation, consumer prices (annual %) - 通货膨胀率",

            # 人口与社会
            "SP.POP.TOTL": "Population, total - 人口总数",
            "SP.POP.GROW": "Population growth (annual %) - 人口增长率",
            "SP.DYN.LE00.IN": "Life expectancy at birth, total (years) - 人均预期寿命",
            "SL.UEM.TOTL.ZS": "Unemployment, total (% of total labor force) - 失业率",

            # 环境与能源
            "EN.ATM.CO2E.PC": "CO2 emissions (metric tons per capita) - 人均CO2排放",
            "EG.USE.ELEC.KH.PC": "Electric power consumption (kWh per capita) - 人均用电量",
            "AG.LND.FRST.ZS": "Forest area (% of land area) - 森林面积占比",

            # 教育与健康
            "SE.PRM.ENRR": "Primary school enrollment rate - 小学入学率",
            "SE.SEC.ENRR": "Secondary school enrollment rate - 中学入学率",
            "SH.XPD.CHEX.GD.ZS": "Current health expenditure (% of GDP) - 医疗支出占比",

            # 科技
            "IT.NET.USER.ZS": "Internet users (% of population) - 互联网使用率",
        }

        results = []
        keyword_lower = keyword.lower()

        for code, name in local_indicators.items():
            if keyword_lower in name.lower() or keyword_lower in code.lower():
                results.append({
                    "code": code,
                    "name": name,
                    "source": "World Bank"
                })
                if len(results) >= max_results:
                    break

        return results
4.功能二,先构造符合RESTful规范的API URL(/country/{国家代码}/indicator/{指标代码}),并设置参数要求返回JSON格式且每页最多5条记录,然后调用底层的_make_request方法完成带重试机制的HTTP请求;拿到原始数据后,解析世界银行API特有的双层数组结构(第一层是元数据,第二层是实际数据数组),通过遍历第二层找到第一个数值非空的条目作为最新有效数据点,并对数值进行安全的浮点数转换处理(捕获可能的转换异常),最终返回一个结构化的字典,其中包含国家名称、指标名称、数值、年份等可直接使用的字段,如果数据不存在则返回no_data状态,解析失败则返回error状态。
点击查看代码
def get_country_data(self, country_code: str, indicator_code: str) -> Dict:
        """
        【功能2】获取指定国家、指定指标的最新数据
        """
        url = f"{self.base_url}/country/{country_code}/indicator/{indicator_code}"
        params = {"format": "json", "per_page": 5}

        data = self._make_request(url, params)

        if not data or len(data) < 2 or not data[1]:
            return {"status": "no_data", "message": "未找到数据"}

        try:
            # 查找第一个有效数值
            latest_valid = None
            for item in data[1]:
                value = item.get("value")
                if value is not None and value != "":
                    latest_valid = item
                    break

            if not latest_valid:
                latest_valid = data[1][0]

            value = latest_valid.get("value")
            if value is not None and value != "":
                try:
                    value = float(value)
                except (ValueError, TypeError):
                    pass
            else:
                value = None

            return {
                "status": "success",
                "country": latest_valid.get("country", {}).get("value", "Unknown"),
                "country_code": country_code,
                "indicator": latest_valid.get("indicator", {}).get("value", "Unknown"),
                "indicator_code": indicator_code,
                "value": value,
                "year": latest_valid.get("date", "N/A")
            }
        except Exception as e:
            return {"status": "error", "message": f"数据解析失败: {e}"}
5.功能三,获取指定国家、指定指标在连续年份区间内完整时间序列数据。构造API请求URL并设置date参数为{start_year}:{end_year}格式以限定年份范围,同时将per_page设为100以尽量减少分页请求次数;调用_make_request获取数据后,遍历世界银行API返回的第二层数组(即实际数据列表),对每个数据点进行严格的空值过滤——只有value和date字段都存在且非空时才保留,然后尝试将年份转为整数、数值转为浮点数(转换失败则丢弃该条记录),成功转换后以中文字段名("年份"、"数值"、"国家"、"指标")组装成字典追加到列表中;最终将所有记录构建成Pandas DataFrame,并按年份升序排序后返回。
点击查看代码
    def get_time_series(self, country_code: str, indicator_code: str,
                        start_year: int, end_year: int) -> pd.DataFrame:
        """获取时间序列数据"""
        all_records = []

        url = f"{self.base_url}/country/{country_code}/indicator/{indicator_code}"
        params = {
            "format": "json",
            "per_page": 100,
            "date": f"{start_year}:{end_year}"
        }

        data = self._make_request(url, params)

        if data and len(data) >= 2 and data[1]:
            for item in data[1]:
                value = item.get("value")
                date = item.get("date")

                if value is None or value == "" or date is None:
                    continue

                try:
                    all_records.append({
                        "年份": int(date),
                        "数值": float(value),
                        "国家": item.get("country", {}).get("value"),
                        "指标": item.get("indicator", {}).get("value")
                    })
                except (ValueError, TypeError):
                    continue

        df = pd.DataFrame(all_records)
        if not df.empty:
            df = df.sort_values("年份")
        return df
6.功能四,横向对比多个国家在指定经济指标上表现。接收国家代码列表、指标代码和可选的目标年份,对每个国家依次调用API获取数据(每页10条),然后通过智能数据匹配逻辑找到最合适的数值——如果指定了年份则精确匹配该年份的数据并立即跳出循环,如果未指定年份则遍历所有返回数据找到年份最新的那条记录(通过字符串比较实现),确保用户总能获得最新的有效数据;找到最佳匹配后,提取数值并转换为浮点数,将国家名称、代码、数值和年份暂存到列表中;最后将所有国家的数据构建为DataFrame,按数值降序排序后自动生成从1开始的排名,并重新调整列顺序为"排名、国家、国家代码、数值、年份"后返回。
点击查看代码
def compare_countries(self, country_codes: List[str], indicator_code: str,
                          year: int = None) -> pd.DataFrame:
        """多国家对比"""
        results = []

        for code in country_codes:
            url = f"{self.base_url}/country/{code}/indicator/{indicator_code}"
            params = {"format": "json", "per_page": 10}

            data = self._make_request(url, params)

            if data and len(data) >= 2 and data[1]:
                best_match = None
                for item in data[1]:
                    item_year = item.get("date")
                    item_value = item.get("value")

                    if item_value is None or item_value == "":
                        continue

                    if year is not None:
                        try:
                            if int(item_year) == year:
                                best_match = item
                                break
                        except:
                            continue
                    else:
                        if best_match is None or (item_year and str(item_year) > str(best_match.get("date", ""))):
                            best_match = item

                if best_match:
                    try:
                        value = float(best_match.get("value"))
                        results.append({
                            "排名": 0,
                            "国家": best_match.get("country", {}).get("value", code),
                            "国家代码": code,
                            "数值": value,
                            "年份": best_match.get("date", "N/A")
                        })
                    except:
                        pass

        df = pd.DataFrame(results)
        if not df.empty:
            df = df.sort_values("数值", ascending=False)
            df["排名"] = range(1, len(df) + 1)
            df = df.reset_index(drop=True)
            df = df[["排名", "国家", "国家代码", "数值", "年份"]]

        return df
7.功能五。先通过os.makedirs("exports", exist_ok=True)确保导出目录存在(避免文件写入异常),然后定义了一个格式策略字典(formats),将"csv"、"json"、"excel"三种格式分别映射到对应的文件扩展名和Pandas导出方法——CSV使用UTF-8-BOM编码以兼容Excel中文显示,JSON以records格式美化输出并保留中文,Excel则直接导出为xlsx文件;接着进行格式有效性校验和数据非空校验,通过后生成包含时间戳的文件名(格式如指标_20250126_143022.csv)以避免重名覆盖,从策略字典中动态调用对应的导出方法完成写入。
点击查看代码
    def export_data(self, df: pd.DataFrame, filename: str, format: str = "csv") -> Dict:
        """导出数据"""
        os.makedirs("exports", exist_ok=True)

        formats = {
            "csv": {"ext": "csv", "method": lambda d, f: d.to_csv(f, index=False, encoding="utf-8-sig")},
            "json": {"ext": "json", "method": lambda d, f: d.to_json(f, orient="records", force_ascii=False, indent=2)},
            "excel": {"ext": "xlsx", "method": lambda d, f: d.to_excel(f, index=False)}
        }

        if format not in formats:
            return {"status": "error", "message": f"不支持的格式"}

        if df.empty:
            return {"status": "error", "message": "数据为空"}

        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filepath = f"exports/{filename}_{timestamp}.{formats[format]['ext']}"
            formats[format]["method"](df, filepath)
            return {"status": "success", "message": f"已导出: {filepath}", "filepath": filepath}
        except Exception as e:
            return {"status": "error", "message": f"导出失败: {e}"}
8.构建交互式菜单,实现了一个命令行交互界面,封装了世界银行数据爬虫,让用户通过菜单编号选择国家(12个预定义)和指标(12个预定义),无需记忆代码。select_countries支持多选并实时反馈,select_indicator提供单选菜单,select_years引导年份输入并校验范围,format_value自动为大数值选择合适的单位(万亿/十亿/百万)。
点击查看代码
class InteractiveWorldBankApp:
    """交互式应用程序"""

    def __init__(self):
        self.scraper = WorldBankScraper()
        self.selected_countries = []

        # 常用国家代码
        self.country_dict = {
            "1": {"code": "CN", "name": "中国"},
            "2": {"code": "US", "name": "美国"},
            "3": {"code": "JP", "name": "日本"},
            "4": {"code": "DE", "name": "德国"},
            "5": {"code": "GB", "name": "英国"},
            "6": {"code": "FR", "name": "法国"},
            "7": {"code": "IN", "name": "印度"},
            "8": {"code": "BR", "name": "巴西"},
            "9": {"code": "KR", "name": "韩国"},
            "10": {"code": "RU", "name": "俄罗斯"},
            "11": {"code": "CA", "name": "加拿大"},
            "12": {"code": "AU", "name": "澳大利亚"},
        }

        # 常用指标
        self.indicator_dict = {
            "1": {"code": "NY.GDP.MKTP.CD", "name": "GDP总量(现价美元)"},
            "2": {"code": "NY.GDP.PCAP.CD", "name": "人均GDP(现价美元)"},
            "3": {"code": "NY.GDP.MKTP.KD.ZG", "name": "GDP增长率(%)"},
            "4": {"code": "SP.POP.TOTL", "name": "人口总数"},
            "5": {"code": "SP.POP.GROW", "name": "人口增长率(%)"},
            "6": {"code": "SP.DYN.LE00.IN", "name": "人均预期寿命(岁)"},
            "7": {"code": "SL.UEM.TOTL.ZS", "name": "失业率(%)"},
            "8": {"code": "EN.ATM.CO2E.PC", "name": "人均CO2排放量(吨)"},
            "9": {"code": "IT.NET.USER.ZS", "name": "互联网使用率(%)"},
            "10": {"code": "FP.CPI.TOTL.ZG", "name": "通货膨胀率(%)"},
            "11": {"code": "BX.KLT.DINV.CD.WD", "name": "外国直接投资(美元)"},
            "12": {"code": "SH.XPD.CHEX.GD.ZS", "name": "医疗支出占GDP比重(%)"},
        }

    def select_countries(self):
        """选择要分析的国家"""
        print("\n" + "="*60)
        print("🌍 请选择要分析的国家")
        print("="*60)

        # 分两列显示
        items = list(self.country_dict.items())
        half = len(items) // 2
        for i in range(half):
            left = f"  {items[i][0]:2s}. {items[i][1]['name']:8s} ({items[i][1]['code']})"
            right = f"  {items[i+half][0]:2s}. {items[i+half][1]['name']:8s} ({items[i+half][1]['code']})"
            print(f"{left}  {right}")

        if len(items) % 2 == 1:
            print(f"  {items[-1][0]:2s}. {items[-1][1]['name']:8s} ({items[-1][1]['code']})")

        print("  0. 完成选择")

        while True:
            try:
                choice = input("\n请输入国家编号(可多选,用空格分隔): ").strip()
                if choice == "0":
                    if not self.selected_countries:
                        print("⚠️  请至少选择一个国家!")
                        continue
                    break

                choices = choice.split()
                for ch in choices:
                    if ch in self.country_dict and ch != "0":
                        country = self.country_dict[ch]
                        if country not in self.selected_countries:
                            self.selected_countries.append(country)
                            print(f"  ✓ 已添加: {country['name']}")

                print(f"\n当前已选 {len(self.selected_countries)} 个国家:")
                for c in self.selected_countries:
                    print(f"    - {c['name']}")

            except Exception as e:
                print(f"输入错误: {e}")

        print(f"\n✅ 最终选择: {', '.join([c['name'] for c in self.selected_countries])}")

    def select_indicator(self, prompt: str = "请选择指标") -> Dict:
        """选择指标"""
        print("\n" + "="*60)
        print(f"📊 {prompt}")
        print("="*60)

        for key, indicator in self.indicator_dict.items():
            print(f"  {key:2s}. {indicator['name']}")

        while True:
            try:
                choice = input("\n请输入指标编号: ").strip()
                if choice in self.indicator_dict:
                    return self.indicator_dict[choice]
                else:
                    print("无效选择,请重新输入")
            except Exception as e:
                print(f"输入错误: {e}")

    def select_years(self) -> tuple:
        """选择年份范围"""
        print("\n" + "="*60)
        print("📅 请选择年份范围")
        print("="*60)

        while True:
            try:
                start = int(input("开始年份 (建议 2010-2020): ").strip())
                end = int(input("结束年份 (建议 2020-2024): ").strip())
                if start < 1960 or end > 2024 or start > end:
                    print("年份范围无效,请重新输入")
                else:
                    return start, end
            except:
                print("请输入有效的数字年份")

    def format_value(self, value):
        """格式化数值显示"""
        if value is None:
            return "无数据"

        if abs(value) >= 1e12:
            return f"{value/1e12:.2f} 万亿美元"
        elif abs(value) >= 1e9:
            return f"{value/1e9:.2f} 十亿美元"
        elif abs(value) >= 1e6:
            return f"{value/1e6:.2f} 百万美元"
        else:
            return f"{value:,.2f}"

    # ==================== 功能1:搜索经济指标(简化版 - 去掉保存功能) ====================
    def function1_search_indicator(self):
        """【功能1】搜索经济指标 - 简化版"""
        print("\n" + "="*60)
        print("【功能1】搜索经济指标")
        print("="*60)

        print("\n💡 搜索示例:")
        print("   - 输入 'GDP' 查找所有GDP相关指标")
        print("   - 输入 'population' 查找人口相关指标")
        print("   - 输入 'inflation' 查找通胀相关指标\n")

        keyword = input("🔎 请输入搜索关键词: ").strip()

        if not keyword:
            print("❌ 请至少输入一个关键词")
            return

        print(f"\n⏳ 正在搜索与 '{keyword}' 相关的指标...")

        results = self.scraper.search_indicator(keyword, max_results=15)

        if results:
            print(f"\n✅ 找到 {len(results)} 个相关指标:\n")
            print("-" * 60)
            for i, ind in enumerate(results, 1):
                print(f"\n{i}. 指标代码: {ind['code']}")
                print(f"   指标名称: {ind['name']}")
                print(f"   数据来源: {ind['source']}")
            print("\n" + "-" * 60)

            print("\n💡 提示:记住指标代码,然后在【功能2】中使用")
        else:
            print(f"\n❌ 未找到与 '{keyword}' 相关的指标")
            print("\n💡 建议:")
            print("   - 尝试使用英文关键词")
            print("   - 尝试更通用的词语,如 'economy', 'population'")
            print("   - 直接使用【功能2】的预定义指标")

    # ==================== 功能2:获取最新数据(简化版) ====================
    def function2_get_latest_data(self):
        """【功能2】获取最新经济数据 - 简化版"""
        print("\n" + "="*60)
        print("【功能2】获取最新经济数据")
        print("="*60)

        # 直接显示指标选择
        print("\n请选择要查询的指标:")
        for key, indicator in self.indicator_dict.items():
            print(f"  {key:2s}. {indicator['name']}")

        # 也可以手动输入代码
        print("\n  0. 手动输入指标代码")

        choice = input("\n请输入指标编号或代码: ").strip()

        if choice == "0":
            indicator_code = input("请输入指标代码 (如: NY.GDP.MKTP.CD): ").strip()
            indicator_name = indicator_code
        elif choice in self.indicator_dict:
            indicator_code = self.indicator_dict[choice]["code"]
            indicator_name = self.indicator_dict[choice]["name"]
        else:
            indicator_code = choice
            indicator_name = choice

        print(f"\n✅ 使用指标: {indicator_name} ({indicator_code})")

        print("\n正在获取数据...")
        print("="*60)

        for country in self.selected_countries:
            data = self.scraper.get_country_data(country['code'], indicator_code)

            if data.get("status") == "success":
                value = data.get('value')
                if value is not None:
                    value_str = self.format_value(value)
                else:
                    value_str = "无数据"

                print(f"\n📌 {country['name']} ({country['code']})")
                print(f"   数值: {value_str}")
                print(f"   年份: {data.get('year', 'N/A')}")
            else:
                print(f"\n❌ {country['name']}: 数据获取失败")

        print("\n" + "="*60)

    # ==================== 功能3:时间序列分析 ====================
    def function3_time_series(self):
        """【功能3】时间序列分析"""
        print("\n" + "="*60)
        print("【功能3】时间序列趋势分析")
        print("="*60)

        # 选择国家
        print("\n请选择要分析的国家:")
        for i, country in enumerate(self.selected_countries, 1):
            print(f"  {i}. {country['name']}")

        while True:
            try:
                choice = int(input("\n请输入编号: ")) - 1
                if 0 <= choice < len(self.selected_countries):
                    country = self.selected_countries[choice]
                    break
                else:
                    print("无效选择")
            except:
                print("请输入有效数字")

        indicator = self.select_indicator("请选择要分析的指标")
        start_year, end_year = self.select_years()

        print(f"\n⏳ 正在获取 {country['name']} 的 {indicator['name']} 历史数据...")
        df = self.scraper.get_time_series(country['code'], indicator['code'], start_year, end_year)

        if not df.empty:
            print(f"\n✅ 成功获取 {len(df)} 年数据:\n")
            print(df.to_string(index=False))

            # 统计信息
            print(f"\n📊 统计:")
            print(f"   平均值: {self.format_value(df['数值'].mean())}")
            print(f"   最大值: {self.format_value(df['数值'].max())} ({df.loc[df['数值'].idxmax(), '年份']}年)")
            print(f"   最小值: {self.format_value(df['数值'].min())} ({df.loc[df['数值'].idxmin(), '年份']}年)")

            # 导出选项
            export = input("\n是否导出数据?(y/n): ").strip().lower()
            if export == 'y':
                result = self.scraper.export_data(df, f"timeseries_{country['code']}", "csv")
                print(f"\n{result.get('message', '导出完成')}")
        else:
            print("\n❌ 未获取到数据")

    # ==================== 功能4:多国家对比 ====================
    def function4_compare_countries(self):
        """【功能4】多国家数据对比"""
        print("\n" + "="*60)
        print("【功能4】多国家数据对比")
        print("="*60)

        indicator = self.select_indicator("请选择对比指标")

        print("\n对比方式:")
        print("  1. 最新数据")
        print("  2. 指定年份")

        choice = input("\n请选择 (1/2): ").strip()

        year = None
        if choice == "2":
            year = int(input("请输入年份 (如: 2020): ").strip())

        print("\n⏳ 正在获取对比数据...")
        country_codes = [c['code'] for c in self.selected_countries]
        df = self.scraper.compare_countries(country_codes, indicator['code'], year)

        if not df.empty:
            print(f"\n✅ 对比结果:\n")
            # 显示时格式化数值
            df_display = df.copy()
            df_display['数值'] = df_display['数值'].apply(self.format_value)
            print(df_display.to_string(index=False))

            # 导出选项
            export = input("\n是否导出数据?(y/n): ").strip().lower()
            if export == 'y':
                result = self.scraper.export_data(df, f"comparison", "excel")
                print(f"\n{result.get('message', '导出完成')}")
        else:
            print("\n❌ 未获取到对比数据")

    # ==================== 功能5:批量导出所有数据 ====================
    def function5_export_all_data(self):
        """【功能5】批量导出所有数据"""
        print("\n" + "="*60)
        print("【功能5】批量导出所有数据")
        print("="*60)

        indicator = self.select_indicator("请选择要导出的指标")
        start_year, end_year = self.select_years()

        print("\n⏳ 正在批量获取数据...")
        exported_files = []

        for country in self.selected_countries:
            print(f"   正在处理 {country['name']}...")
            df = self.scraper.get_time_series(country['code'], indicator['code'], start_year, end_year)

            if not df.empty:
                filename = f"{country['code']}_{indicator['code']}"
                result = self.scraper.export_data(df, filename, "csv")
                if result.get("status") == "success":
                    exported_files.append(result.get("filepath"))

        if exported_files:
            print(f"\n✅ 成功导出 {len(exported_files)} 个文件到 'exports' 文件夹")
        else:
            print("\n❌ 没有数据被导出")
9.以下是完整代码
点击查看代码
"""
世界银行数据爬虫程序 - 简化版
基于官方API: http://api.worldbank.org/v2/
功能:搜索指标、获取数据、时间序列、多国对比、批量导出
"""

import requests
import pandas as pd
import time
import json
from typing import List, Dict, Optional
from datetime import datetime
import os


class WorldBankScraper:
    """
    世界银行数据爬虫核心类
    """

    def __init__(self, request_delay: float = 1.0, timeout: int = 60):
        """初始化爬虫"""
        self.base_url = "http://api.worldbank.org/v2"
        self.request_delay = request_delay
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "application/json"
        })

    def _make_request(self, url: str, params: Dict = None, max_retries: int = 3) -> Optional[Dict]:
        """发送API请求(带重试机制)"""
        for attempt in range(max_retries):
            try:
                time.sleep(self.request_delay)
                response = self.session.get(url, params=params, timeout=self.timeout)
                response.raise_for_status()
                data = response.json()
                return data
            except requests.exceptions.Timeout:
                print(f"请求超时 (尝试 {attempt + 1}/{max_retries})")
                if attempt == max_retries - 1:
                    return None
                time.sleep(2)
            except Exception as e:
                print(f"请求失败: {e}")
                return None
        return None

    # ==================== 功能1:指标查询 ====================
    def search_indicator(self, keyword: str, max_results: int = 20) -> List[Dict]:
        """
        【功能1】根据关键词搜索指标
        用于查找需要的经济指标代码
        """
        # 常用指标本地库
        local_indicators = {
            # 经济类
            "NY.GDP.MKTP.CD": "GDP (current US$) - 国内生产总值(现价美元)",
            "NY.GDP.PCAP.CD": "GDP per capita (current US$) - 人均GDP(现价美元)",
            "NY.GDP.MKTP.KD.ZG": "GDP growth (annual %) - GDP年增长率",
            "NY.GDP.MKTP.PP.CD": "GDP, PPP (current international $) - 购买力平价GDP",
            "NY.GDP.PCAP.PP.CD": "GDP per capita, PPP - 人均购买力平价GDP",
            "NE.EXP.GNFS.ZS": "Exports of goods and services (% of GDP) - 出口占比",
            "NE.IMP.GNFS.ZS": "Imports of goods and services (% of GDP) - 进口占比",
            "BX.KLT.DINV.CD.WD": "Foreign direct investment, net inflows - 外国直接投资",
            "FP.CPI.TOTL.ZG": "Inflation, consumer prices (annual %) - 通货膨胀率",

            # 人口与社会
            "SP.POP.TOTL": "Population, total - 人口总数",
            "SP.POP.GROW": "Population growth (annual %) - 人口增长率",
            "SP.DYN.LE00.IN": "Life expectancy at birth, total (years) - 人均预期寿命",
            "SL.UEM.TOTL.ZS": "Unemployment, total (% of total labor force) - 失业率",

            # 环境与能源
            "EN.ATM.CO2E.PC": "CO2 emissions (metric tons per capita) - 人均CO2排放",
            "EG.USE.ELEC.KH.PC": "Electric power consumption (kWh per capita) - 人均用电量",
            "AG.LND.FRST.ZS": "Forest area (% of land area) - 森林面积占比",

            # 教育与健康
            "SE.PRM.ENRR": "Primary school enrollment rate - 小学入学率",
            "SE.SEC.ENRR": "Secondary school enrollment rate - 中学入学率",
            "SH.XPD.CHEX.GD.ZS": "Current health expenditure (% of GDP) - 医疗支出占比",

            # 科技
            "IT.NET.USER.ZS": "Internet users (% of population) - 互联网使用率",
        }

        results = []
        keyword_lower = keyword.lower()

        for code, name in local_indicators.items():
            if keyword_lower in name.lower() or keyword_lower in code.lower():
                results.append({
                    "code": code,
                    "name": name,
                    "source": "World Bank"
                })
                if len(results) >= max_results:
                    break

        return results

    # ==================== 功能2:国家数据获取 ====================
    def get_country_data(self, country_code: str, indicator_code: str) -> Dict:
        """
        【功能2】获取指定国家、指定指标的最新数据
        """
        url = f"{self.base_url}/country/{country_code}/indicator/{indicator_code}"
        params = {"format": "json", "per_page": 5}

        data = self._make_request(url, params)

        if not data or len(data) < 2 or not data[1]:
            return {"status": "no_data", "message": "未找到数据"}

        try:
            # 查找第一个有效数值
            latest_valid = None
            for item in data[1]:
                value = item.get("value")
                if value is not None and value != "":
                    latest_valid = item
                    break

            if not latest_valid:
                latest_valid = data[1][0]

            value = latest_valid.get("value")
            if value is not None and value != "":
                try:
                    value = float(value)
                except (ValueError, TypeError):
                    pass
            else:
                value = None

            return {
                "status": "success",
                "country": latest_valid.get("country", {}).get("value", "Unknown"),
                "country_code": country_code,
                "indicator": latest_valid.get("indicator", {}).get("value", "Unknown"),
                "indicator_code": indicator_code,
                "value": value,
                "year": latest_valid.get("date", "N/A")
            }
        except Exception as e:
            return {"status": "error", "message": f"数据解析失败: {e}"}

    # ==================== 功能3:时间序列获取 ====================
    def get_time_series(self, country_code: str, indicator_code: str,
                        start_year: int, end_year: int) -> pd.DataFrame:
        """获取时间序列数据"""
        all_records = []

        url = f"{self.base_url}/country/{country_code}/indicator/{indicator_code}"
        params = {
            "format": "json",
            "per_page": 100,
            "date": f"{start_year}:{end_year}"
        }

        data = self._make_request(url, params)

        if data and len(data) >= 2 and data[1]:
            for item in data[1]:
                value = item.get("value")
                date = item.get("date")

                if value is None or value == "" or date is None:
                    continue

                try:
                    all_records.append({
                        "年份": int(date),
                        "数值": float(value),
                        "国家": item.get("country", {}).get("value"),
                        "指标": item.get("indicator", {}).get("value")
                    })
                except (ValueError, TypeError):
                    continue

        df = pd.DataFrame(all_records)
        if not df.empty:
            df = df.sort_values("年份")
        return df

    # ==================== 功能4:多国家对比 ====================
    def compare_countries(self, country_codes: List[str], indicator_code: str,
                          year: int = None) -> pd.DataFrame:
        """多国家对比"""
        results = []

        for code in country_codes:
            url = f"{self.base_url}/country/{code}/indicator/{indicator_code}"
            params = {"format": "json", "per_page": 10}

            data = self._make_request(url, params)

            if data and len(data) >= 2 and data[1]:
                best_match = None
                for item in data[1]:
                    item_year = item.get("date")
                    item_value = item.get("value")

                    if item_value is None or item_value == "":
                        continue

                    if year is not None:
                        try:
                            if int(item_year) == year:
                                best_match = item
                                break
                        except:
                            continue
                    else:
                        if best_match is None or (item_year and str(item_year) > str(best_match.get("date", ""))):
                            best_match = item

                if best_match:
                    try:
                        value = float(best_match.get("value"))
                        results.append({
                            "排名": 0,
                            "国家": best_match.get("country", {}).get("value", code),
                            "国家代码": code,
                            "数值": value,
                            "年份": best_match.get("date", "N/A")
                        })
                    except:
                        pass

        df = pd.DataFrame(results)
        if not df.empty:
            df = df.sort_values("数值", ascending=False)
            df["排名"] = range(1, len(df) + 1)
            df = df.reset_index(drop=True)
            df = df[["排名", "国家", "国家代码", "数值", "年份"]]

        return df

    # ==================== 功能5:数据导出 ====================
    def export_data(self, df: pd.DataFrame, filename: str, format: str = "csv") -> Dict:
        """导出数据"""
        os.makedirs("exports", exist_ok=True)

        formats = {
            "csv": {"ext": "csv", "method": lambda d, f: d.to_csv(f, index=False, encoding="utf-8-sig")},
            "json": {"ext": "json", "method": lambda d, f: d.to_json(f, orient="records", force_ascii=False, indent=2)},
            "excel": {"ext": "xlsx", "method": lambda d, f: d.to_excel(f, index=False)}
        }

        if format not in formats:
            return {"status": "error", "message": f"不支持的格式"}

        if df.empty:
            return {"status": "error", "message": "数据为空"}

        try:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filepath = f"exports/{filename}_{timestamp}.{formats[format]['ext']}"
            formats[format]["method"](df, filepath)
            return {"status": "success", "message": f"已导出: {filepath}", "filepath": filepath}
        except Exception as e:
            return {"status": "error", "message": f"导出失败: {e}"}


# ==================== 交互式应用程序 ====================

class InteractiveWorldBankApp:
    """交互式应用程序"""

    def __init__(self):
        self.scraper = WorldBankScraper()
        self.selected_countries = []

        # 常用国家代码
        self.country_dict = {
            "1": {"code": "CN", "name": "中国"},
            "2": {"code": "US", "name": "美国"},
            "3": {"code": "JP", "name": "日本"},
            "4": {"code": "DE", "name": "德国"},
            "5": {"code": "GB", "name": "英国"},
            "6": {"code": "FR", "name": "法国"},
            "7": {"code": "IN", "name": "印度"},
            "8": {"code": "BR", "name": "巴西"},
            "9": {"code": "KR", "name": "韩国"},
            "10": {"code": "RU", "name": "俄罗斯"},
            "11": {"code": "CA", "name": "加拿大"},
            "12": {"code": "AU", "name": "澳大利亚"},
        }

        # 常用指标
        self.indicator_dict = {
            "1": {"code": "NY.GDP.MKTP.CD", "name": "GDP总量(现价美元)"},
            "2": {"code": "NY.GDP.PCAP.CD", "name": "人均GDP(现价美元)"},
            "3": {"code": "NY.GDP.MKTP.KD.ZG", "name": "GDP增长率(%)"},
            "4": {"code": "SP.POP.TOTL", "name": "人口总数"},
            "5": {"code": "SP.POP.GROW", "name": "人口增长率(%)"},
            "6": {"code": "SP.DYN.LE00.IN", "name": "人均预期寿命(岁)"},
            "7": {"code": "SL.UEM.TOTL.ZS", "name": "失业率(%)"},
            "8": {"code": "EN.ATM.CO2E.PC", "name": "人均CO2排放量(吨)"},
            "9": {"code": "IT.NET.USER.ZS", "name": "互联网使用率(%)"},
            "10": {"code": "FP.CPI.TOTL.ZG", "name": "通货膨胀率(%)"},
            "11": {"code": "BX.KLT.DINV.CD.WD", "name": "外国直接投资(美元)"},
            "12": {"code": "SH.XPD.CHEX.GD.ZS", "name": "医疗支出占GDP比重(%)"},
        }

    def select_countries(self):
        """选择要分析的国家"""
        print("\n" + "="*60)
        print("🌍 请选择要分析的国家")
        print("="*60)

        # 分两列显示
        items = list(self.country_dict.items())
        half = len(items) // 2
        for i in range(half):
            left = f"  {items[i][0]:2s}. {items[i][1]['name']:8s} ({items[i][1]['code']})"
            right = f"  {items[i+half][0]:2s}. {items[i+half][1]['name']:8s} ({items[i+half][1]['code']})"
            print(f"{left}  {right}")

        if len(items) % 2 == 1:
            print(f"  {items[-1][0]:2s}. {items[-1][1]['name']:8s} ({items[-1][1]['code']})")

        print("  0. 完成选择")

        while True:
            try:
                choice = input("\n请输入国家编号(可多选,用空格分隔): ").strip()
                if choice == "0":
                    if not self.selected_countries:
                        print("⚠️  请至少选择一个国家!")
                        continue
                    break

                choices = choice.split()
                for ch in choices:
                    if ch in self.country_dict and ch != "0":
                        country = self.country_dict[ch]
                        if country not in self.selected_countries:
                            self.selected_countries.append(country)
                            print(f"  ✓ 已添加: {country['name']}")

                print(f"\n当前已选 {len(self.selected_countries)} 个国家:")
                for c in self.selected_countries:
                    print(f"    - {c['name']}")

            except Exception as e:
                print(f"输入错误: {e}")

        print(f"\n✅ 最终选择: {', '.join([c['name'] for c in self.selected_countries])}")

    def select_indicator(self, prompt: str = "请选择指标") -> Dict:
        """选择指标"""
        print("\n" + "="*60)
        print(f"📊 {prompt}")
        print("="*60)

        for key, indicator in self.indicator_dict.items():
            print(f"  {key:2s}. {indicator['name']}")

        while True:
            try:
                choice = input("\n请输入指标编号: ").strip()
                if choice in self.indicator_dict:
                    return self.indicator_dict[choice]
                else:
                    print("无效选择,请重新输入")
            except Exception as e:
                print(f"输入错误: {e}")

    def select_years(self) -> tuple:
        """选择年份范围"""
        print("\n" + "="*60)
        print("📅 请选择年份范围")
        print("="*60)

        while True:
            try:
                start = int(input("开始年份 (建议 2010-2020): ").strip())
                end = int(input("结束年份 (建议 2020-2024): ").strip())
                if start < 1960 or end > 2024 or start > end:
                    print("年份范围无效,请重新输入")
                else:
                    return start, end
            except:
                print("请输入有效的数字年份")

    def format_value(self, value):
        """格式化数值显示"""
        if value is None:
            return "无数据"

        if abs(value) >= 1e12:
            return f"{value/1e12:.2f} 万亿美元"
        elif abs(value) >= 1e9:
            return f"{value/1e9:.2f} 十亿美元"
        elif abs(value) >= 1e6:
            return f"{value/1e6:.2f} 百万美元"
        else:
            return f"{value:,.2f}"

    # ==================== 功能1:搜索经济指标(简化版 - 去掉保存功能) ====================
    def function1_search_indicator(self):
        """【功能1】搜索经济指标 - 简化版"""
        print("\n" + "="*60)
        print("【功能1】搜索经济指标")
        print("="*60)

        print("\n💡 搜索示例:")
        print("   - 输入 'GDP' 查找所有GDP相关指标")
        print("   - 输入 'population' 查找人口相关指标")
        print("   - 输入 'inflation' 查找通胀相关指标\n")

        keyword = input("🔎 请输入搜索关键词: ").strip()

        if not keyword:
            print("❌ 请至少输入一个关键词")
            return

        print(f"\n⏳ 正在搜索与 '{keyword}' 相关的指标...")

        results = self.scraper.search_indicator(keyword, max_results=15)

        if results:
            print(f"\n✅ 找到 {len(results)} 个相关指标:\n")
            print("-" * 60)
            for i, ind in enumerate(results, 1):
                print(f"\n{i}. 指标代码: {ind['code']}")
                print(f"   指标名称: {ind['name']}")
                print(f"   数据来源: {ind['source']}")
            print("\n" + "-" * 60)

            print("\n💡 提示:记住指标代码,然后在【功能2】中使用")
        else:
            print(f"\n❌ 未找到与 '{keyword}' 相关的指标")
            print("\n💡 建议:")
            print("   - 尝试使用英文关键词")
            print("   - 尝试更通用的词语,如 'economy', 'population'")
            print("   - 直接使用【功能2】的预定义指标")

    # ==================== 功能2:获取最新数据(简化版) ====================
    def function2_get_latest_data(self):
        """【功能2】获取最新经济数据 - 简化版"""
        print("\n" + "="*60)
        print("【功能2】获取最新经济数据")
        print("="*60)

        # 直接显示指标选择
        print("\n请选择要查询的指标:")
        for key, indicator in self.indicator_dict.items():
            print(f"  {key:2s}. {indicator['name']}")

        # 也可以手动输入代码
        print("\n  0. 手动输入指标代码")

        choice = input("\n请输入指标编号或代码: ").strip()

        if choice == "0":
            indicator_code = input("请输入指标代码 (如: NY.GDP.MKTP.CD): ").strip()
            indicator_name = indicator_code
        elif choice in self.indicator_dict:
            indicator_code = self.indicator_dict[choice]["code"]
            indicator_name = self.indicator_dict[choice]["name"]
        else:
            indicator_code = choice
            indicator_name = choice

        print(f"\n✅ 使用指标: {indicator_name} ({indicator_code})")

        print("\n正在获取数据...")
        print("="*60)

        for country in self.selected_countries:
            data = self.scraper.get_country_data(country['code'], indicator_code)

            if data.get("status") == "success":
                value = data.get('value')
                if value is not None:
                    value_str = self.format_value(value)
                else:
                    value_str = "无数据"

                print(f"\n📌 {country['name']} ({country['code']})")
                print(f"   数值: {value_str}")
                print(f"   年份: {data.get('year', 'N/A')}")
            else:
                print(f"\n❌ {country['name']}: 数据获取失败")

        print("\n" + "="*60)

    # ==================== 功能3:时间序列分析 ====================
    def function3_time_series(self):
        """【功能3】时间序列分析"""
        print("\n" + "="*60)
        print("【功能3】时间序列趋势分析")
        print("="*60)

        # 选择国家
        print("\n请选择要分析的国家:")
        for i, country in enumerate(self.selected_countries, 1):
            print(f"  {i}. {country['name']}")

        while True:
            try:
                choice = int(input("\n请输入编号: ")) - 1
                if 0 <= choice < len(self.selected_countries):
                    country = self.selected_countries[choice]
                    break
                else:
                    print("无效选择")
            except:
                print("请输入有效数字")

        indicator = self.select_indicator("请选择要分析的指标")
        start_year, end_year = self.select_years()

        print(f"\n⏳ 正在获取 {country['name']} 的 {indicator['name']} 历史数据...")
        df = self.scraper.get_time_series(country['code'], indicator['code'], start_year, end_year)

        if not df.empty:
            print(f"\n✅ 成功获取 {len(df)} 年数据:\n")
            print(df.to_string(index=False))

            # 统计信息
            print(f"\n📊 统计:")
            print(f"   平均值: {self.format_value(df['数值'].mean())}")
            print(f"   最大值: {self.format_value(df['数值'].max())} ({df.loc[df['数值'].idxmax(), '年份']}年)")
            print(f"   最小值: {self.format_value(df['数值'].min())} ({df.loc[df['数值'].idxmin(), '年份']}年)")

            # 导出选项
            export = input("\n是否导出数据?(y/n): ").strip().lower()
            if export == 'y':
                result = self.scraper.export_data(df, f"timeseries_{country['code']}", "csv")
                print(f"\n{result.get('message', '导出完成')}")
        else:
            print("\n❌ 未获取到数据")

    # ==================== 功能4:多国家对比 ====================
    def function4_compare_countries(self):
        """【功能4】多国家数据对比"""
        print("\n" + "="*60)
        print("【功能4】多国家数据对比")
        print("="*60)

        indicator = self.select_indicator("请选择对比指标")

        print("\n对比方式:")
        print("  1. 最新数据")
        print("  2. 指定年份")

        choice = input("\n请选择 (1/2): ").strip()

        year = None
        if choice == "2":
            year = int(input("请输入年份 (如: 2020): ").strip())

        print("\n⏳ 正在获取对比数据...")
        country_codes = [c['code'] for c in self.selected_countries]
        df = self.scraper.compare_countries(country_codes, indicator['code'], year)

        if not df.empty:
            print(f"\n✅ 对比结果:\n")
            # 显示时格式化数值
            df_display = df.copy()
            df_display['数值'] = df_display['数值'].apply(self.format_value)
            print(df_display.to_string(index=False))

            # 导出选项
            export = input("\n是否导出数据?(y/n): ").strip().lower()
            if export == 'y':
                result = self.scraper.export_data(df, f"comparison", "excel")
                print(f"\n{result.get('message', '导出完成')}")
        else:
            print("\n❌ 未获取到对比数据")

    # ==================== 功能5:批量导出所有数据 ====================
    def function5_export_all_data(self):
        """【功能5】批量导出所有数据"""
        print("\n" + "="*60)
        print("【功能5】批量导出所有数据")
        print("="*60)

        indicator = self.select_indicator("请选择要导出的指标")
        start_year, end_year = self.select_years()

        print("\n⏳ 正在批量获取数据...")
        exported_files = []

        for country in self.selected_countries:
            print(f"   正在处理 {country['name']}...")
            df = self.scraper.get_time_series(country['code'], indicator['code'], start_year, end_year)

            if not df.empty:
                filename = f"{country['code']}_{indicator['code']}"
                result = self.scraper.export_data(df, filename, "csv")
                if result.get("status") == "success":
                    exported_files.append(result.get("filepath"))

        if exported_files:
            print(f"\n✅ 成功导出 {len(exported_files)} 个文件到 'exports' 文件夹")
        else:
            print("\n❌ 没有数据被导出")

    # ==================== 主菜单 ====================
    def run(self):
        """运行主程序"""
        print("\n" + "="*60)
        print("🌍 世界银行数据智能分析系统")
        print("="*60)

        # 选择国家
        self.select_countries()

        # 主菜单
        while True:
            print("\n" + "="*60)
            print("📋 功能菜单")
            print("="*60)
            print("  1. 🔍 搜索经济指标")
            print("  2. 📊 获取最新数据")
            print("  3. 📈 时间序列分析")
            print("  4. 🌍 多国家对比")
            print("  5. 💾 批量导出数据")
            print("  0. 🚪 退出程序")
            print("="*60)

            choice = input("\n请选择功能 (0-5): ").strip()

            if choice == "1":
                self.function1_search_indicator()
            elif choice == "2":
                self.function2_get_latest_data()
            elif choice == "3":
                self.function3_time_series()
            elif choice == "4":
                self.function4_compare_countries()
            elif choice == "5":
                self.function5_export_all_data()
            elif choice == "0":
                print("\n👋 感谢使用!")
                break
            else:
                print("❌ 无效选择")

            input("\n按回车键继续...")


# ==================== 主程序入口 ====================
if __name__ == "__main__":
    app = InteractiveWorldBankApp()
    app.run()

10.仓库地址:
仓库

2

3. 程序实现过程

查询世界银行的 robots.txt ,了解允许爬取的内容
获取世界银行API
最初版程序只能获取固定几个国家数据
第二版程序可以自己选定想要的国家
但是出现获取数据时出现错误的情况,发现是最近两年的数据没有更新,因此对程序中选择年份范围最新年份调整至2024年

程序运行视频

课程总结

1.Python 概念:是解释型、面向对象的语言。变量无需声明类型,支持数字、字符串、列表、元组、字典、集合等数据类型。
2.序列:有序数据集合,支持索引、切片、成员操作(in/not in)。包括可变序列(列表)和不可变序列(字符串、元组)。通用操作有 len()、min()、max()、index()、count()。
3.正则表达式):用于复杂字符串匹配。主要函数:match()(开头匹配)、search()(搜索首个)、findall()(返回所有匹配列表)、sub()(替换)、split()(分割)。常用元字符:\d(数字)、\w(单词字符)、\s(空白)、.(任意字符)、(0次或多次)、+(1次或多次)、?(0次或1次)、^(开头)、$(结尾)、[](字符集)、|(或)、()(分组)。
4.函数:def 定义,return 返回值。参数类型:位置参数、默认参数、关键字参数、
args(可变位置参数,接收元组)、**kwargs(可变关键字参数,接收字典)。作用域分局部和全局,global 修改全局变量。lambda 匿名函数适用于简单操作。装饰器可在不修改原函数代码的情况下增加功能。
5.爬虫:四步流程——发送请求、获取响应、解析数据、存储数据。常用库:requests(HTTP 请求)、BeautifulSoup(HTML 解析,配合 lxml 解析器)、re(正则辅助)。需设置 User-Agent 模拟浏览器,动态页面用 Selenium。反爬对策包括 IP 代理、请求间隔、Cookie/Session 维持。遵守 robots.txt,控制频率。

课程感想体会、意见和建议

初中高中上的python课在开课之前已经忘光了,但是老师让我发现以前学的不过是python大世界里很小一部分,然后就跟着老师开了一大片图【大笑】
每节课上跟着老师敲代码敲着敲着,还觉着挺简单的,但是敲完一看是自己能头疼的那种【无奈的笑】
每节课没有压力,很轻松,还学会好多,选这节课真的是对得不能再对了【认真】
讲真的,超级喜欢这样的课程模式,对这节课唯一的建议是求求老师多给几分【超级认真的求求】

参考资料

世界银行的robot.txt

https://www.worldbank.org/robots.txt
使用大模型:DEEPSEEK

posted @ 2026-05-30 21:00  chen__c  阅读(46)  评论(0)    收藏  举报