Python爬虫实战:开放数据多格式入仓 - 构建统一数据管道(附CSV导出 + SQLite持久化存储)!

㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!

㊗️爬虫难度指数:⭐⭐⭐

🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:

    • [🌟 开篇语](#🌟 开篇语)
    • [📌 章节摘要](#📌 章节摘要)
    • [🌐 开放数据源概览](#🌐 开放数据源概览)
    • [📥 自动下载系统](#📥 自动下载系统)
    • [🔄 多格式解析器](#🔄 多格式解析器)
    • [🗂️ 统一数据目录索引系统](#🗂️ 统一数据目录索引系统)
    • [🏗️ 完整ETL流程示例](#🏗️ 完整ETL流程示例)
    • [💡 最佳实践总结](#💡 最佳实践总结)
      • [1. 下载策略](#1. 下载策略)
      • [2. 解析优化](#2. 解析优化)
      • [3. Schema设计原则](#3. Schema设计原则)
    • [📚 本章总结](#📚 本章总结)
    • [🌟 文末](#🌟 文末)
      • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
      • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈

💕订阅后更新会优先推送,按目录学习更高效💯~

📌 章节摘要

在现代数据工程中,数据来源越来越多样化。政府开放数据平台、学术研究机构、国际组织等都在发布各种格式的数据集:CSV表格、JSON API响应、Excel报表、PDF报告等。如何高效地自动下载、解析、标准化、入库这些异构数据,是数据工程师必须掌握的核心技能。

本章将深入讲解如何构建生产级的多格式数据采集系统,让你能够优雅地处理各种数据源,建立统一的数据仓库。

读完你将掌握:

  • 📥 自动下载系统的设计(断点续传、重试机制、并发下载)
  • 🔄 多格式解析器的实现(CSV/JSON/XLSX/PDF/XML)
  • 📊 Schema统一策略(字段映射、类型转换、数据验证)
  • 🗂️ 目录索引系统(元数据管理、版本控制、快速检索)
  • 🏗️ ETL流程设计(Extract-Transform-Load完整链路)

🌐 开放数据源概览

常见开放数据平台

python 复制代码
class OpenDataSources:
    """
    开放数据源目录
    
    收录主流的开放数据平台及其特点
    """
    
    def __init__(self):
        self.sources = {
            # 政府数据
            'us_data_gov': {
                'name': 'Data.gov',
                'url': 'https://catalog.data.gov',
                'api': 'https://catalog.data.gov/api/3',
                'formats': ['CSV', 'JSON', 'XML', 'PDF', 'XLSX'],
                'auth_required': False,
                'rate_limit': None,
                'description': '美国政府开放数据平台,30万+数据集'
            },
            'china_data_gov': {
                'name': '中国政府数据开放平台',
                'url': 'http://www.data.gov.cn',
                'formats': ['CSV', 'XLSX', 'PDF', 'JSON'],
                'auth_required': False,
                'description': '中国政府各部门开放数据'
            },
            'world_bank': {
                'name': 'World Bank Open Data',
                'url': 'https://data.worldbank.org',
                'api': 'https://api.worldbank.org/v2',
                'formats': ['CSV', 'XML', 'XLSX', 'JSON'],
                'auth_required': False,
                'rate_limit': None,
                'description': '世界银行发展指标数据'
            },
            'kaggle': {
                'name': 'Kaggle Datasets',
                'url': 'https://www.kaggle.com/datasets',
                'api': 'https://www.kaggle.com/api/v1',
                'formats': ['CSV', 'JSON', 'XLSX', 'SQLite', 'Parquet'],
                'auth_required': True,
                'auth_type': 'API Key',
                'rate_limit': '100 requests/hour',
                'description': '机器学习数据集平台,10万+数据集'
            },
            'eurostat': {
                'name': 'Eurostat',
                'url': 'https://ec.europa.eu/eurostat',
                'api': 'https://ec.europa.eu/eurostat/api',
                'formats': ['TSV', 'XML', 'JSON', 'XLSX'],
                'auth_required': False,
                'description': '欧盟统计局数据'
            },
            'un_data': {
                'name': 'UN Data',
                'url': 'http://data.un.org',
                'formats': ['CSV', 'XML', 'XLSX'],
                'auth_required': False,
                'description': '联合国统计数据'
            }
        }
    
    def get_source_info(self, source_id):
        """获取数据源信息"""
        return self.sources.get(source_id, {})
    
    def list_sources_by_format(self, format_type):
        """根据格式筛选数据源"""
        matching_sources = []
        
        for source_id, info in_type.upper() in info.get('formats', []):
                matching_sources.append({
                    'id': source_id,
                    'name': info['name'],
                    'url': info['url']
                })
        
        return matching_sources
    
    def print_catalog(self):
        """打印数据源目录"""
        print("\n" + "="*80)
        print("📚 开放数据源目录")
        print("="*80)
        
        for source_id, info in self.sources.items():
            print(f"\n【{info['name']}】")
            print(f"  ID: {source_id}")
            print(f"  URL: {info['url']}")
            print
            print(f"  需要认证: {'是' if info['auth_required'] else '否'}")
            if info.get('rate_limit'):
                print(f"  速率限制: {info['rate_limit']}")
            print(f"  说明: {info['description']}")
        
        print("\n" + "="*80)

# 使用示例
catalog = OpenDataSources()
catalog.print_catalog()

# 查找支持CSV格式的数据源
csv_sources = catalog.list_sources_by_format('CSV')
print(f"\n支持CSV格式的数据源: {len(csv_sources)} 个")
for source in csv_sources:
    print(f"  - {source['name']}: {source['url']}")

📥 自动下载系统

智能下载器(支持断点续传、重试、并发)

python 复制代码
import requests
import os
import hashlib
import time
from pathlib import Path
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor, as_completed
import json

class SmartDownloader:
    """
    智能下载器
    
    功能:
    1. 断点续传(支持Range请求)
    2. 自动重试(指数退避)
    3. 并发下载(线程池)
    4. 进度跟踪
    5. 完整性验证(MD5/SHA256)
    6. 下载历史记录
    """
    
    def __init__(self, download_dir='data/downloads', max_workers=5):
        """
        初始化下载器
        
        参数:
            download_dir: 下载目录
            max_workers: 最大并发数
        """
        self.download_dir = Path(download_dir)
        self.download_dir.mkdir(parents=True, exist_ok=True)
        
        self.max_workers = max_workers
        
        # 下载历史文件
        self.history_file = self.download_dir / 'download_history.json'
        self.download_history = self._load_history()
        
        # 会话(复用TCP连接)
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })
    
    def _load_history(self):
        """加载下载历史"""
        if self.history_file.exists():
            with open(self.history_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        return {}
    
    def _save_history(self):
        """保存下载历史"""
        with open(self.history_file, 'w', encoding='utf-8') as f:
            json.dump(self.download_history, f, ensure_ascii=False, indent=2)
    
    def download(self, url, filename=None, max_retries=3, chunk_size=8192, 
                 verify_hash=None, hash_type='md5', resume=True):
        """
        下载单个文件
        
        参数:
            url: 下载链接
            filename: 保存文件名(如果None,从URL提取)
            max_retries: 最大重试次数
            chunk_size: 分块大小(字节)
            verify_hash: 预期的文件哈希值(用于完整性验证)
            hash_type: 哈希类型('md5'或'sha256')
            resume: 是否支持断点续传
        
        返回:
            {
                'success': bool,
                'filepath': str,
                'size': int,
                'duration': float,
                'hash': str
            }
        """
        # 确定文件名
        if not filename:
            filename = self._extract_filename(url)
        
        filepath = self.download_dir / filename
        temp_filepath = filepath.with_suffix(filepath.suffix + '.tmp')
        
        # 检查是否已下载且有效
        if self._is_already_downloaded(url, filepath, verify_hash, hash_type):
            print(f"✅ 文件已存在且有效: {filename}")
            return {
                'success': True,
                'filepath': str(filepath),
                'size': filepath.stat().st_size,
                'duration': 0,
                'hash': self._calculate_hash(filepath, hash_type),
                'from_cache': True
            }
        
        # 开始下载
        start_time = time.time()
        
        for attempt in range(max_retries):
            try:
                # 检查是否支持断点续传
                existing_size = temp_filepath.stat().st_size if temp_filepath.exists() else 0
                
                headers = {}
                if resume and existing_size > 0:
                    headers['Range'] = f'bytes={existing_size}-'
                    print(f"🔄 断点续传: 已下载 {existing_size / 1024 / 1024:.2f} MB")
                
                # 发送请求
                response = self.session.get(url, headers=headers, stream=True, timeout=30)
                
                # 检查是否支持Range
                if resume and existing_size > 0:
                    if response.status_code != 206:
                        print(f"⚠️ 服务器不支持断点续传,重新下载")
                        existing_size = 0
                        response = self.session.get(url, stream=True, timeout=30)
                
                response.raise_for_status()
                
                # 获取总大小
                total_size = int(response.headers.get('Content-Length', 0))
                if resume and response.status_code == 206:
                    total_size += existing_size
                
                # 下载文件
                mode = 'ab' if existing_size > 0 else 'wb'
                downloaded = existing_size
                
                with open(temp_filepath, mode) as f:
                    for chunk in response.iter_content(chunk_size=chunk_size):
                        if chunk:
                            f.write(chunk)
                            downloaded += len(chunk)
                            
                            # 显示进度
                            if total_size > 0:
                                progress = (downloaded / total_size) * 100
                                print(f"\r⬇️ 下载进度: {progress:.1f}% "
                                      f"({downloaded / 1024 / 1024:.2f} / "
                                      f"{total_size / 1024 / 1024:.2f} MB)", end='')
                
                print()  # 换行
                
                # 验证完整性
                if verify_hash:
                    actual_hash = self._calculate_hash(temp_filepath, hash_type)
                    if actual_hash != verify_hash:
                        raise ValueError(f"文件哈希不匹配! "
                                       f"期望: {verify_hash}, 实际: {actual_hash}")
                    print(f"✅ 哈希验证通过: {actual_hash}")
                
                # 移动到正式文件名
                temp_filepath.rename(filepath)
                
                duration = time.time() - start_time
                file_hash = self._calculate_hash(filepath, hash_type)
                
                # 记录下载历史
                self._record_download(url, filepath, file_hash, hash_type)
                
                print(f"✅ 下载完成: {filename}")
                print(f"   大小: {downloaded / 1024 / 1024:.2f} MB")
                print(f"   耗时: {duration:.2f} 秒")
                print(f"   速度: {(downloaded / 1024 / 1024) / duration:.2f} MB/s")
                
                return {
                    'success': True,
                    'filepath': str(filepath),
                    'size': downloaded,
                    'duration': duration,
                    'hash': file_hash,
                    'from_cache': False
                }
            
            except Exception as e:
                print(f"❌ 下载失败 (尝试 {attempt + 1}/{max_retries}): {e}")
                
                if attempt < max_retries - 1:
                    # 指数退避
                    wait_time = 2 ** attempt
                    print(f"⏳ 等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
                else:
                    # 清理临时文件
                    if temp_filepath.exists():
                        temp_filepath.unlink()
                    
                    return {
                        'success': False,
                        'error': str(e)
                    }
    
    def batch_download(self, url_list, show_progress=True):
        """
        批量下载(并发)
        
        参数:
            url_list: URL列表
                [
                    {'url': '...', 'filename': '...', 'verify_hash': '...'},
                    ...
                ]
            show_progress: 是否显示总体进度
        
        返回:
            下载结果列表
        """
        results = []
        
        print(f"\n📦 开始批量下载 {len(url_list)} 个文件...")
        print(f"⚙️ 并发数: {self.max_workers}")
        
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # 提交所有下载任务
            future_to_url = {
                executor.submit(
                    self.download,
                    item['url'],
                    item.get('filename'),
                    verify_hash=item.get('verify_hash'),
                    hash_type=item.get('hash_type', 'md5')
                ): item for item in url_list
            }
            
            # 等待完成
            completed = 0
            for future in as_completed(future_to_url):
                item = future_to_url[future]
                
                try:
                    result = future.result()
                    result['url'] = item['url']
                    results.append(result)
                    
                    completed += 1
                    if show_progress:
                        print(f"\n✅ 已完成: {completed}/{len(url_list)}")
                
                except Exception as e:
                    print(f"❌ 下载异常: {item['url']}, 错误: {e}")
                    results.append({
                        'success': False,
                        'url': item['url'],
                        'error': str(e)
                    })
        
        # 统计结果
        success_count = sum(1 for r in results if r['success'])
        failed_count = len(results) - success_count
        
        print(f"\n📊 批量下载完成:")
        print(f"   成功: {success_count}")
        print(f"   失败: {failed_count}")
        
        return results
    
    def _extract_filename(self, url):
        """从URL提取文件名"""
        parsed = urlparse(url)
        filename = os.path.basename(parsed.path)
        
        if not filename:
            # 使用URL哈希作为文件名
            url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
            filename = f"file_{url_hash}.dat"
        
        return filename
    
    def _calculate_hash(self, filepath, hash_type='md5'):
        """
        计算文件哈希
        
        参数:
            filepath: 文件路径
            hash_type: 'md5'或'sha256'
        
        返回:
            哈希值(十六进制字符串)
        """
        if hash_type == 'md5':
            hasher = hashlib.md5()
        elif hash_type == 'sha256':
            hasher = hashlib.sha256()
        else:
            raise ValueError(f"不支持的哈希类型: {hash_type}")
        
        with open(filepath, 'rb') as f:
            for chunk in iter(lambda: f.read(8192), b''):
                hasher.update(chunk)
        
        return hasher.hexdigest()
    
    def _is_already_downloaded(self, url, filepath, expected_hash=None, hash_type='md5'):
        """
        检查文件是否已下载且有效
        
        参数:
            url: URL
            filepath: 文件路径
            expected_hash: 预期哈希值
            hash_type: 哈希类型
        
        返回:
            True/False
        """
        if not filepath.exists():
            return False
        
        # 检查下载历史
        if url in self.download_history:
            history_entry = self.download_history[url]
            
            # 验证哈希
            if expected_hash and history_entry.get('hash') == expected_hash:
                return True
            
            # 如果没有预期哈希,重新计算并对比历史记录
            if not expected_hash:
                actual_hash = self._calculate_hash(filepath, hash_type)
                if actual_hash == history_entry.get('hash'):
                    return True
        
        return False
    
    def _record_download(self, url, filepath, file_hash, hash_type):
        """记录下载历史"""
        self.download_history[url] = {
            'filepath': str(filepath),
            'hash': file_hash,
            'hash_type': hash_type,
            'timestamp': time.time(),
            'size': filepath.stat().st_size
        }
        
        self._save_history()
    
    def get_download_stats(self):
        """获取下载统计"""
        total_files = len(self.download_history)
        total_size = sum(entry['size'] for entry in self.download_history.values())
        
        return {
            'total_files': total_files,
            'total_size_mb': total_size / 1024 / 1024,
            'download_dir': str(self.download_dir)
        }
    
    def clean_old_downloads(self, days=30):
        """
        清理旧下载
        
        参数:
            days: 保留最近N天的下载
        """
        cutoff_time = time.time() - (days * 24 * 3600)
        removed_count url, entry in list(self.download_history.items()):
            if entry['timestamp'] < cutoff_time:
                filepath = Path(entry['filepath'])
                if filepath.exists():
                    filepath.unlink()
                    print(f"🗑️ 删除旧文件: {filepath.name}")
                
                del self.download_history[url]
                removed_count += 1
        
        if removed_count > 0:
            self._save_history()
            print(f"✅ 清理完成,删除了 {removed_count} 个文件")

# 使用示例
downloader = SmartDownloader(download_dir='data/downloads', max_workers=3)

# 示例1:单文件下载
result = downloader.download(
    url='https://example.com/data/sample.csv',
    filename='sample_data.csv'
)

# 示例2:批量下载
urls = [
    {
        'url': 'https://data.worldbank.org/indicator/NY.GDP.MKTP.CD?downloadformat=csv',
        'filename': 'world_gdp.csv'
    },
    {
        'url': 'https://example.com/reports/2024_annual_report.pdf',
        'filename': 'report_2024.pdf'
    },
    {
        'url': 'https://api.example.com/export/data.json',
        'filename': 'api_data.json'
    }
]

results = downloader.batch_download(urls)

# 示例3:查看统计
stats = downloader.get_download_stats()
print(f"\n下载统计:")
print(f"  总文件数: {stats['total_files']}")
print(f"  总大小: {stats['total_size_mb']:.2f} MB")

🔄 多格式解析器

CSV解析器

python 复制代码
import pandas as pd
import csv
from pathlib import Path

class CSVParser:
    """
    CSV解析器
    
    功能:
    1. 自动检测分隔符(逗号/制表符/分号等)
    2. 自动检测编码(UTF-8/GBK/etc)
    3. 处理格式错乱(缺失列/多余列)
    4. 数据类型推断
    5. 缺失值处理
    """
    
    def __init__(self):
        self.supported_encodings = ['utf-8', 'gbk', 'gb2312', 'latin1', 'iso-8859-1']
        self.supported_delimiters = [',', '\t', ';', '|']
    
    def parse(self, filepath, encoding=None, delimiter=None, 
              skip_bad_lines=False, clean_data=True):
        """
        解析CSV文件
        
        参数:
            filepath: 文件路径
            encoding: 编码(如果None,自动检测)
            delimiter: 分隔符(如果None,自动检测)
            skip_bad_lines: 是否跳过错误行
            clean_data: 是否清洗数据
        
        返回:
            pandas DataFrame
        """
        filepath = Path(filepath)
        
        # 自动检测编码
        if not encoding:
            encoding = self._detect_encoding(filepath)
            print(f"🔍 检测编码: {encoding}")
        
        # 自动检测分隔符
        if not delimiter:
            delimiter = self._detect_delimiter(filepath, encoding)
            print(f"🔍 检测分隔符: {repr(delimiter)}")
        
        # 读取CSV
        try:
            df = pd.read_csv(
                filepath,
                encoding=encoding,
                delimiter=delimiter,
                on_bad_lines='skip' if skip_bad_lines else 'error',
                low_memory=False
            )
            
            print(f"✅ 成功读取CSV: {len(df)} 行 x {len(df.columns)} 列")
            
            # 数据清洗
            if clean_data:
                df = self._clean_dataframe(df)
            
            return df
        
        except Exception as e:
            print(f"❌ 解析失败: {e}")
            return None
    
    def _detect_encoding(self, filepath):
        """
        自动检测文件编码
        
        使用chardet库
        """
        import chardet
        
        with open(filepath, 'rb') as f:
            raw_data = f.read(10000)  # 读取前10KB
            result = chardet.detect(raw_data)
        
        detected_encoding = result['encoding']
        confidence = result['confidence']
        
        print(f"   编码置信度: {confidence:.2f}")
        
        return detected_encoding
    
    def _detect_delimiter(self, filepath, encoding):
        """
        自动检测分隔符
        
        方法:读取前几行,统计各种分隔符出现次数
        """
        with open(filepath, 'r', encoding=encoding) as f:
            sample_lines = [f.readline() for _ in range(5)]
        
        # 统计各分隔符出现次数
        delimiter_counts = {}
        
        for delimiter in self.supported_delimiters:
            counts = [line.count(delimiter) for line in sample_lines]
            
            # 如果各行分隔符数量一致,且>0,认为是候选
            if len(set(counts)) == 1 and counts[0] > 0:
                delimiter_counts[delimiter] = counts[0]
        
        # 选择出现次数最多的
        if delimiter_counts:
            detected_delimiter = max(delimiter_counts, key=delimiter_counts.get)
            return detected_delimiter
        
        # 默认逗号
        return ','
    
    def _clean_dataframe(self, df):
        """
        清洗DataFrame
        
        操作:
        1. 去除全空列
        2. 去除全空行
        3. 列名标准化(去除空格、转小写)
        4. 去除重复行
        """
        original_shape = df.shape
        
        # 去除全空列
        df = df.dropna(axis=1, how='all')
        
        # 去除全空行
        df = df.dropna(axis=0, how='all')
        
        # 列名标准化
        df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_')
        
        # 去除重复行
        df = df.drop_duplicates()
        
        new_shape = df.shape
        
        if original_shape != new_shape:
            print(f"🧹 数据清洗: {original_shape} -> {new_shape}")
        
        return df
    
    def convert_to_standard_format(self, df, schema):
        """
        转换为标准格式
        
        参数:
            df: 原始DataFrame
            schema: 标准Schema定义
                {
                    'standard_col_name': {
                        'source_columns': ['col1', 'col2'],  # 可能的源列名
                        'type': 'int/float/str/datetime',
                        'required': True/False
                    }
                }
        
        返回:
            标准化的DataFrame
        """
        standard_df = pd.DataFrame()
        
        for standard_col, config in schema.items():
            source_cols = config['source_columns']
            col_type = config['type']
            required = config.get('required', False)
            
            # 找到匹配的源列
            matched_col = None
            for src_col in source_cols:
                if src_col in df.columns:
                    matched_col = src_col
                    break
            
            if matched_col:
                # 复制并转换类型
                standard_df[standard_col] = df[matched_col]
                
                try:
                    if col_type == 'int':
                        standard_df[standard_col] = pd.to_numeric(
                            standard_df[standard_col], errors='coerce'
                        ).astype('Int64')  # 支持NA的整型
                    
                    elif col_type == 'float':
                        standard_df[standard_col] = pd.to_numeric(
                            standard_df[standard_col], errors='coerce'
                        )
                    
                    elif col_type == 'datetime':
                        standard_df[standard_col] = pd.to_datetime(
                            standard_df[standard_col], errors='coerce'
                        )
                    
                    elif col_type == 'str':
                        standard_df[standard_col] = standard_df[standard_col].astype(str)
                
                except Exception as e:
                    print(f"⚠️ 列 {standard_col} 类型转换失败: {e}")
            
            elif required:
                print(f"⚠️ 缺少必填列: {standard_col}")
                standard_df[standard_col] = None
        
        print(f"✅ 标准化完成: {len(standard_df.columns)} 列")
        
        return standard_df

# 使用示例
parser = CSVParser()

# 解析CSV
df = parser.parse('data/downloads/sample_data.csv')

# 转换为标准格式
schema = {
    'id': {
        'source_columns': ['id', 'ID', '编号'],
        'type': 'int',
        'required': True
    },
    'name': {
        'source_columns': ['name', 'Name', '名称', '姓名'],
        'type': 'str',
        'required': True
    },
    'amount': {
        'source_columns': ['amount', 'value', '金额', '数量'],
        'type': 'float',
        'required': True
    },
    'date': {
        'source_columns': ['date', 'Date', '日期', 'created_at'],
        'type': 'datetime',
        'required': False
    }
}

standard_df = parser.convert_to_standard_format(df, schema)

JSON解析器

python 复制代码
import json
from pathlib import Path
import pandas as pd

class JSONParser:
    """
    JSON解析器
    
    功能:
    1. 解析标准JSON/JSON Lines
    2. 处理嵌套结构(展平)
    3. 提取特定路径数据(JSONPath)
    4. 转换为DataFrame
    """
    
    def __init__(self):
        pass
    
    def parse(self, filepath, json_type='auto', encoding='utf-8'):
        """
        解析JSON文件
        
        参数:
            filepath: 文件路径
            json_type: 'json'(标准JSON) 或 'jsonlines'(每行一个JSON) 或 'auto'(自动检测)
            encoding: 编码
        
        返回:
            解析后的数据(dict/list/DataFrame)
        """
        filepath = Path(filepath)
        
        # 自动检测JSON类型
        if json_type == 'auto':
            json_type = self._detect_json_type(filepath, encoding)
            print(f"🔍 检测JSON类型: {json_type}")
        
        if json_type == 'json':
            # 标准JSON
            with open(filepath, 'r', encoding=encoding) as f:
                data = json.load(f)
            
            print(f"✅ 成功读取JSON")
            return data
        
        elif json_type == 'jsonlines':
            # JSON Lines(每行一个JSON对象)
            records = []
            
            with open(filepath, 'r', encoding=encoding) as f:
                for line in f:
                    if line.strip():
                        try:
                            records.append(json.loads(line))
                        except json.JSONDecodeError as e:
                            print(f"⚠️ 跳过无效行: {e}")
            
            print(f"✅ 成功读取JSON Lines: {len(records)} 条记录")
            return records
    
    def _detect_json_type(self, filepath, encoding):
        """
        检测JSON类型
        
        方法:读取第一行,判断是否为完整JSON
        """
        with open(filepath, 'r', encoding=encoding) as f:
            first_line = f.readline().strip()
        
        try:
            # 尝试解析第一行
            json.loads(first_line)
            
            # 如果成功,检查是否以{或[开头
            if first_line.startswith('{'):
                return 'jsonlines'
            elif first_line.startswith('['):
                return 'json'
        except:
            pass
        
        # 默认标准JSON
        return 'json'
    
    def flatten_nested_json(self, data, separator='_'):
        """
        展平嵌套JSON
        
        参数:
            data: 嵌套的dict或list of dict
            separator: 键分隔符
        
        返回:
            展平后的dict或list of dict
        
        示例:
            输入: {'a': {'b': {'c': 1}}}
            输出: {'a_b_c': 1}
        """
        if isinstance(data, list):
            return [self._flatten_dict(item, separator) for item in data]
        else:
            return self._flatten_dict(data, separator)
    
    def _flatten_dict(self, d, separator='_', parent_key=''):
        """递归展平字典"""
        items = []
        
        for k, v in d.items():
            new_key = f"{parent_key}{separator}{k}" if parent_key else k
            
            if isinstance(v, dict):
                items.extend(self._flatten_dict(v, separator, new_key).items())
            elif isinstance(v, list):
                # 如果列表元素是字典,展平每个元素
                if v and isinstance(v[0], dict):
                    for i, item in enumerate(v):
                        items.extend(
                            self._flatten_dict(item, separator, f"{new_key}_{i}").items()
                        )
                else:
                    items.append((new_key, v))
            else:
                items.append((new_key, v))
        
        return dict(items)
    
    def extract_by_path(self, data, path):
        """
        按路径提取数据(简化版JSONPath)
        
        参数:
            data: JSON数据
            path: 路径,如'data.items[0].name'
        
        返回:
            提取的值
        """
        keys = path.split('.')
        current = data
        
        for key in keys:
            # 处理数组索引
            if '[' in key:
                field_name = key[:key.index('[')]
                index = int(key[key.index('[') + 1:key.index(']')])
                
                if field_name:
                    current = current[field_name]
                
                current = current[index]
            else:
                current = current[key]
        
        return current
    
    def to_dataframe(self, data, normalize=True):
        """
        转换为DataFrame
        
        参数:
            data: JSON数据(list of dict)
            normalize: 是否展平嵌套结构
        
        返回:
            pandas DataFrame
        """
        if not isinstance(data, list):
            # 如果是单个dict,转换为列表
            data = [data]
        
        if normalize:
            # 展平嵌套
            data = self.flatten_nested_json(data)
        
        df = pd.DataFrame(data)
        
        print(f"✅ 转换为DataFrame: {len(df)} 行 x {len(df.columns)} 列")
        
        return df

# 使用示例
json_parser = JSONParser()

# 示例1:解析标准JSON
data1 = json_parser.parse('data/downloads/api_data.json')
df1 = json_parser.to_dataframe(data1)

# 示例2:展平嵌套JSON
nested_data = {
    'user': {
        'id': 1,
        'profile': {
            'name': 'Alice',
            'age': 30,
            'address': {
                'city': 'Beijing',
                'country': 'China'
            }
        }
    }
}

flattened = json_parser.flatten_nested_json(nested_data)
print(f"\n展平结果:")
print(json.dumps(flattened, indent=2))

# 示例3:路径提取
nested_list_data = {
    'data': {
        'items': [
            {'name': 'Item1', 'price': 100},
            {'name': 'Item2', 'price': 200}
        ]
    }
}

extracted = json_parser.extract_by_path(nested_list_data, 'data.items[0].name')
print(f"\n路径提取: {extracted}")

Excel解析器

python 复制代码
import pandas as pd
from pathlib import Path
import openpyxl

class ExcelParser:
    """
    Excel解析器(XLSX/XLS)
    
    功能:
    1. 读取多sheet
    2. 合并多个文件
    3. 处理合并单元格
    4. 提取公式
    5. 读取元数据
    """
    
    def __init__(self):
        pass
    
    def parse(self, filepath, sheet_name=None, header_row=0, 
              skip_rows=None, use_cols=None):
        """
        解析Excel文件
        
        参数:
            filepath: 文件路径
            sheet_name: sheet名称(None读取第一个sheet,'all'读取所有)
            header_row: 表头行号(0-indexed)
            skip_rows: 跳过的行
            use_cols: 使用的列(如'A:D'或[0,1,2,3])
        
        返回:
            DataFrame或dict of DataFrames
        """
        filepath = Path(filepath)
        
        if sheet_name == 'all':
            # 读取所有sheet
            all_sheets = pd.read_excel(
                filepath,
                sheet_name=None,
                header=header_row,
                skiprows=skip_rows,
                usecols=use_cols
            )
            
            print(f"✅ 读取所有sheet: {list(all_sheets.keys())}")
            return all_sheets
        
        else:
            # 读取单个sheet
            df = pd.read_excel(
                filepath,
                sheet_name=sheet_name or 0,
                header=header_row,
                skiprows=skip_rows,
                usecols=use_cols
            )
            
            print(f"✅ 成功读取Excel: {len(df)} 行 x {len(df.columns)} 列")
            return df
    
    def get_sheet_names(self, filepath):
        """获取所有sheet名称"""
        wb = openpyxl.load_workbook(filepath, read_only=True)
        sheet_names = wb.sheetnames
        wb.close()
        
        print(f"📋 Sheet列表: {sheet_names}")
        return sheet_names
    
    def extract_metadata(self, filepath):
        """
        提取Excel元数据
        
        返回:
            {
                'sheet_count': int,
                'sheet_names': list,
                'file_size': int,
                'modified_time': str
            }
        """
        filepath = Path(filepath)
        
        wb = openpyxl.load_workbook(filepath, read_only=True)
        
        metadata = {
            'sheet_count': len(wb.sheetnames),
            'sheet_names': wb.sheetnames,
            'file_size': filepath.stat().st_size,
            'modified_time': filepath.stat().st_mtime
        }
        
        wb.close()
        
        return metadata
    
    def merge_sheets(self, filepath_list, sheet_name=0):
        """
        合并多个Excel文件的同名sheet
        
        参数:
            filepath_list: 文件路径列表
            sheet_name: sheet名称或索引
        
        返回:
            合并后的DataFrame
        """
        dfs = []
        
        for filepath in filepath_list:
            try:
                df = pd.read_excel(filepath, sheet_name=sheet_name)
                dfs.append(df)
                print(f"✅ 读取: {Path(filepath).name}")
            except Exception as e:
                print(f"⚠️ 跳过: {Path(filepath).name}, 错误: {e}")
        
        if dfs:
            merged_df = pd.concat(dfs, ignore_index=True)
            print(f"✅ 合并完成: {len(merged_df)} 行")
            return merged_df
        
        return None
    
    def handle_merged_cells(self, filepath, sheet_name=0):
        """
        处理合并单元格
        
        方法:将合并单元格的值填充到所有单元格
        """
        wb = openpyxl.load_workbook(filepath)
        ws = wb[wb.sheetnames[sheet_name]] if isinstance(sheet_name, int) else wb[sheet_name]
        
        # 遍历合并单元格范围
        for merged_cell_range in ws.merged_cells.ranges:
            # 获取合并单元格的值(左上角单元格)
            min_col, min_row, max_col, max_row = merged_cell_range.bounds
            merged_value = ws.cell(min_row, min_col).value
            
            # 填充到所有单元格
            for row in range(min_row, max_row + 1):
                for col in range(min_col, max_col + 1):
                    ws.cell(row, col).value = merged_value
        
        # 取消合并
        for merged_cell_range in list(ws.merged_cells.ranges):
            ws.unmerge_cells(str(merged_cell_range))
        
        # 保存到新文件
        output_file = filepath.parent / f"{filepath.stem}_unmerged{filepath.suffix}"
        wb.save(output_file)
        wb.close()
        
        print(f"✅ 处理完成,已保存: {output_file.name}")
        
        return output_file

# 使用示例
excel_parser = ExcelParser()

# 示例1:读取Excel
df = excel_parser.parse('data/downloads/report.xlsx')

# 示例2:读取所有sheet
all_sheets = excel_parser.parse('data/downloads/report.xlsx', sheet_name='all')

# 示例3:获取sheet名称
sheet_names = excel_parser.get_sheet_names('data/downloads/report.xlsx')

# 示例4:提取元数据
metadata = excel_parser.extract_metadata('data/downloads/report.xlsx')
print(f"\nExcel元数据:")
print(json.dumps(metadata, indent=2, default=str))

# 示例5:合并多个文件
files = ['data/downloads/report1.xlsx', 'data/downloads/report2.xlsx']
merged_df = excel_parser.merge_sheets(files, sheet_name=0)

PDF解析器

python 复制代码
import pdfplumber
from pathlib import Path
import pandas as pd

class PDFParser:
    """
    PDF解析器
    
    功能:
    1. 提取文本
    2. 提取表格
    3. OCR识别(扫描件)
    4. 提取元数据
    """
    
    def __init__(self):
        pass
    
    def extract_text(self, filepath, page_numbers=None):
        """
        提取文本
        
        参数:
            filepath: PDF文件路径
            page_numbers: 页码列表(None表示所有页)
        
        返回:
            文本字符串
        """
        filepath = Path(filepath)
        text = ""
        
        with pdfplumber.open(filepath) as pdf:
            pages_to_extract = page_numbers or range(len(pdf.pages))
            
            for page_num in pages_to_extract:
                if page_num < len(pdf.pages):
                    page = pdf.pages[page_num]
                    page_text = page.extract_text()
                    
                    if page_text:
                        text += f"--- Page {page_num + 1} ---\n"
                        text += page_text
                        text += "\n\n"
        
        print(f"✅ 提取文本完成: {len(text)} 字符")
        return text
    
    def extract_tables(self, filepath, page_numbers=None):
        """
        提取表格
        
        参数:
            filepath: PDF文件路径
            page_numbers: 页码列表
        
        返回:
            表格列表(list of DataFrames)
        """
        filepath = Path(filepath)
        all_tables = []
        
        with pdfplumber.open(filepath) as pdf:
            pages_to_extract = page_numbers or range(len(pdf.pages))
            
            for page_num in pages_to_extract:
                if page_num < len(pdf.pages):
                    page = pdf.pages[page_num]
                    tables = page.extract_tables()
                    
                    for i, table in enumerate(tables):
                        if table and len(table) > 1:
                            # 转换为DataFrame
                            df = pd.DataFrame(table[1:], columns=table[0])
                            
                            # 添加元数据
                            df.attrs['page'] = page_num + 1
                            df.attrs['table_index'] = i
                            
                            all_tables.append(df)
                            
                            print(f"📊 Page {page_num + 1}, Table {i + 1}: "
                                  f"{len(df)} rows x {len(df.columns)} cols")
        
        print(f"✅ 提取表格完成: {len(all_tables)} 个表格")
        return all_tables
    
    def extract_tables_to_excel(self, filepath, output_file):
        """
        将所有表格导出到Excel(每个表格一个sheet)
        
        参数:
            filepath: PDF文件路径
            output_file: 输出Excel文件路径
        """
        tables = self.extract_tables(filepath)
        
        if not tables:
            print("⚠️ 未找到表格")
            return
        
        with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
            for i, df in enumerate(tables):
                sheet_name = f"Page{df.attrs['page']}_Table{df.attrs['table_index'] + 1}"
                df.to_excel(writer, sheet_name=sheet_name, index=False)
        
        print(f"✅ 已导出到: {output_file}")
    
    def extract_metadata(self, filepath):
        """
        提取PDF元数据
        
        返回:
            元数据字典
        """
        from pypdf import PdfReader
        
        reader = PdfReader(filepath)
        meta = reader.metadata
        
        metadata = {
            'title': meta.title if meta else None,
            'author': meta.author if meta else None,
            'subject': meta.subject if meta else None,
            'creator': meta.creator if meta else None,
            'producer': meta.producer if meta else None,
            'page_count': len(reader.pages),
            'file_size': Path(filepath).stat().st_size
        }
        
        return metadata

# 使用示例
pdf_parser = PDFParser()

# 示例1:提取文本
text = pdf_parser.extract_text('data/downloads/report_2024.pdf')
# 保存文本
with open('data/extracted_text.txt', 'w', encoding='utf-8') as f:
    f.write(text)

# 示例2:提取表格
tables = pdf_parser.extract_tables('data/downloads/report_2024.pdf')

# 示例3:导出到Excel
pdf_parser.extract_tables_to_excel(
    'data/downloads/report_2024.pdf',
    'data/extracted_tables.xlsx'
)

# 示例4:提取元数据
metadata = pdf_parser.extract_metadata('data/downloads/report_2024.pdf')
print(f"\nPDF元数据:")
print(json.dumps(metadata, indent=2, default=str))

🗂️ 统一数据目录索引系统

python 复制代码
import json
import sqlite3
from datetime import datetime
from pathlib import Path
import hashlib

class DataCatalog:
    """
    数据目录索引系统
    
    功能:
    1. 记录所有下载的数据集
    2. 维护元数据(来源、格式、大小、哈希等)
    3. 版本管理
    4. 快速检索
    5. 数据血缘追踪
    """
    
    def __init__(self, db_path='data/catalog.db'):
        """
        初始化目录系统
        
        参数:
            db_path: SQLite数据库路径
        """
        self.db_path = Path(db_path)
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        
        self.conn = sqlite3.connect(self.db_path)
        self.conn.row_factory = sqlite3.Row  # 返回字典形式
        
        self._create_tables()
    
    def _create_tables(self):
        """创建数据表"""
        cursor = self.conn.cursor()
        
        # 数据集表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS datasets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                source_url TEXT,
                source_platform TEXT,
                file_format TEXT NOT NULL,
                filepath TEXT NOT NULL UNIQUE,
                file_size INTEGER,
                file_hash TEXT,
                row_count INTEGER,
                column_count INTEGER,
                schema_json TEXT,
                description TEXT,
                tags TEXT,
                version INTEGER DEFAULT 1,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        
        # 下载历史表
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS download_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                dataset_id INTEGER,
                download_url TEXT,
                download_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                success INTEGER,
                error_message TEXT,
                FOREIGN KEY (dataset_id) REFERENCES datasets(id)
            )
        ''')
        
        # 数据血缘表(记录数据转换关系)
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS data_lineage (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                source_dataset_id INTEGER,
                target_dataset_id INTEGER,
                transformation_type TEXT,
                transformation_config TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (source_dataset_id) REFERENCES datasets(id),
                FOREIGN KEY (target_dataset_id) REFERENCES datasets(id)
            )
        ''')
        
        self.conn.commit()
        print("✅ 数据库初始化完成")
    
    def register_dataset(self, name, filepath, source_url=None, 
                        source_platform=None, description=None, tags=None):
        """
        注册数据集
        
        参数:
            name: 数据集名称
            filepath: 文件路径
            source_url: 来源URL
            source_platform: 来源平台
            description: 描述
            tags: 标签(逗号分隔)
        
        返回:
            dataset_id
        """
        filepath = Path(filepath)
        
        if not filepath.exists():
            print(f"❌ 文件不存在: {filepath}")
            return None
        
        # 提取文件信息
        file_format = filepath.suffix[1:].upper()
        file_size = filepath.stat().st_size
        file_hash = self._calculate_hash(filepath)
        
        # 分析数据(获取行数、列数、Schema)
        row_count, column_count, schema = self._analyze_file(filepath, file_format)
        
        cursor = self.conn.cursor()
        
        # 检查是否已存在
        cursor.execute('SELECT id FROM datasets WHERE filepath = ?', (str(filepath),))
        existing = cursor.fetchone()
        
        if existing:
            # 更新
            dataset_id = existing['id']
            cursor.execute('''
                UPDATE datasets SET
                    name = ?,
                    source_url = ?,
                    source_platform = ?,
                    file_format = ?,
                    file_size = ?,
                    file_hash = ?,
                    row_count = ?,
                    column_count = ?,
                    schema_json = ?,
                    description = ?,
                    tags = ?,
                    version = version + 1,
                    updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
            ''', (name, source_url, source_platform, file_format, file_size,
                  file_hash, row_count, column_count, json.dumps(schema),
                  description, tags, dataset_id))
            
            print(f"🔄 数据集已更新: {name} (ID: {dataset_id})")
        
        else:
            # 插入
            cursor.execute('''
                INSERT INTO datasets (
                    name, source_url, source_platform, file_format, filepath,
                    file_size, file_hash, row_count, column_count, schema_json,
                    description, tags
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ''', (name, source_url, source_platform, file_format, str(filepath),
                  file_size, file_hash, row_count, column_count, json.dumps(schema),
                  description, tags))
            
            dataset_id = cursor.lastrowid
            print(f"✅ 数据集已注册: {name} (ID: {dataset_id})")
        
        self.conn.commit()
        
        return dataset_id
    
    def search(self, keyword=None, file_format=None, source_platform=None, tags=None):
        """
        搜索数据集
        
        参数:
            keyword: 关键词(搜索名称和描述)
            file_format: 文件格式
            source_platform: 来源平台
            tags: 标签
        
        返回:
            数据集列表
        """
        query = 'SELECT * FROM datasets WHERE 1=1'
        params = []
        
        if keyword:
            query += ' AND (name LIKE ? OR description LIKE ?)'
            params.extend([f'%{keyword}%', f'%{keyword}%'])
        
        if file_format:
            query += ' AND file_format = ?'
            params.append(file_format.upper())
        
        if source_platform:
            query += ' AND source_platform = ?'
            params.append(source_platform)
        
        if tags:
            query += ' AND tags LIKE ?'
            params.append(f'%{tags}%')
        
        query += ' ORDER BY updated_at DESC'
        
        cursor = self.conn.cursor()
        cursor.execute(query, params)
        
        results = [dict(row) for row in cursor.fetchall()]
        
        print(f"🔍 找到 {len(results)} 个数据集")
        
        return results
    
    def get_dataset_info(self, dataset_id):
        """获取数据集详情"""
        cursor = self.conn.cursor()
        cursor.execute('SELECT * FROM datasets WHERE id = ?', (dataset_id,))
        
        row = cursor.fetchone()
        
        if row:
            return dict(row)
        
        return None
    
    def list_all(self, limit=100):
        """列出所有数据集"""
        cursor = self.conn.cursor()
        cursor.execute('''
            SELECT id, name, file_format, source_platform, 
                   row_count, column_count, file_size, updated_at
            FROM datasets
            ORDER BY updated_at DESC
            LIMIT ?
        ''', (limit,))
        
        return [dict(row) for row in cursor.fetchall()]
    
    def print_catalog(self, limit=20):
        """打印数据目录"""
        datasets = self.list_all(limit)
        
        print("\n" + "="*100)
        print("📚 数据目录")
        print("="*100)
        print(f"{'ID':<5} {'名称':<30} {'格式':<8} {'行数':<10} {'列数':<8} {'大小':<12} {'更新时间':<20}")
        print("-"*100)
        
        for ds in datasets:
            file_size_mb = (ds['file_size'] or 0) / 1024 / 1024
            
            print(f"{ds['id']:<5} {ds['name'][:28]:<30} {ds['file_format']:<8} "
                  f"{ds['row_count'] or 'N/A':<10} {ds['column_count'] or 'N/A':<8} "
                  f"{file_size_mb:.2f} MB{'':<4} {ds['updated_at']:<20}")
        
        print("="*100 + "\n")
    
    def _calculate_hash(self, filepath):
        """计算文件MD5哈希"""
        hash_md5 = hashlib.md5()
        
        with open(filepath, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_md5.update(chunk)
        
        return hash_md5.hexdigest()
    
    def _analyze_file(self, filepath, file_format):
        """
        分析文件(获取行数、列数、Schema)
        
        返回:
            (row_count, column_count, schema)
        """
        try:
            if file_format == 'CSV':
                df = pd.read_csv(filepath, nrows=1000)  # 只读前1000行分析
                
                return len(df), len(df.columns), {
                    'columns': df.columns.tolist(),
                    'dtypes': df.dtypes.astype(str).to_dict()
                }
            
            elif file_format == 'XLSX':
                df = pd.read_excel(filepath, nrows=1000)
                
                return len(df), len(df.columns), {
                    'columns': df.columns.tolist(),
                    'dtypes': df.dtypes.astype(str).to_dict()
                }
            
            elif file_format == 'JSON':
                with open(filepath, 'r') as f:
                    data = json.load(f)
                
                if isinstance(data, list):
                    return len(data), len(data[0].keys()) if data else 0, {
                        'columns': list(data[0].keys()) if data else []
                    }
                else:
                    return 1, len(data.keys()), {
                        'columns': list(data.keys())
                    }
            
            else:
                return None, None, {}
        
        except Exception as e:
            print(f"⚠️ 文件分析失败: {e}")
            return None, None, {}
    
    def get_statistics(self):
        """获取统计信息"""
        cursor = self.conn.cursor()
        
        # 总数据集数
        cursor.execute('SELECT COUNT(*) as count FROM datasets')
        total_datasets = cursor.fetchone()['count']
        
        # 按格式分组
        cursor.execute('''
            SELECT file_format, COUNT(*) as count
            FROM datasets
            GROUP BY file_format
        ''')
        format_stats = {row['file_format']: row['count'] for row in cursor.fetchall()}
        
        # 总大小
        cursor.execute('SELECT SUM(file_size) as total_size FROM datasets')
        total_size = cursor.fetchone()['total_size'] or 0
        
        return {
            'total_datasets': total_datasets,
            'format_distribution': format_stats,
            'total_size_mb': total_size / 1024 / 1024
        }
    
    def close(self):
        """关闭数据库连接"""
        self.conn.close()

# 使用示例
catalog = DataCatalog()

# 示例1:注册数据集
dataset_id1 = catalog.register_dataset(
    name='World GDP Data 2024',
    filepath='data/downloads/world_gdp.csv',
    source_url='https://data.worldbank.org/indicator/NY.GDP.MKTP.CD',
    source_platform='World Bank',
    description='全球GDP数据(2000-2024)',
    tags='GDP,经济,世界银行'
)

# 示例2:搜索
results = catalog.search(keyword='GDP', file_format='CSV')

# 示例3:打印目录
catalog.print_catalog()

# 示例4:获取统计
stats = catalog.get_statistics()
print(f"\n📊 统计信息:")
print(f"   总数据集: {stats['total_datasets']}")
print(f"   格式分布: {stats['format_distribution']}")
print(f"   总大小: {stats['total_size_mb']:.2f} MB")

🏗️ 完整ETL流程示例

python 复制代码
class DataIngestionPipeline:
    """
    数据入仓完整流程
    
    流程:
    Download -> Parse -> Transform -> Validate -> Load -> Catalog
    """
    
    def __init__(self):
        self.downloader = SmartDownloader()
        self.catalog = DataCatalog()
        
        # 解析器映射
        self.parsers = {
            'CSV': CSVParser(),
            'JSON': JSONParser(),
            'XLSX': ExcelParser(),
            'PDF': PDFParser()
        }
    
    def ingest(self, config):
        """
        执行完整入仓流程
        
        参数:
            config: 配置字典
                {
                    'name': '数据集名称',
                    'source_url': '下载链接',
                    'source_platform': '平台名称',
                    'file_format': 'CSV/JSON/XLSX/PDF',
                    'schema': {...},  # 标准Schema
                    'description': '描述',
                    'tags': '标签'
                }
        
        返回:
            成功返回dataset_id,失败返回None
        """
        print(f"\n{'='*80}")
        print(f"🚀 开始数据入仓: {config['name']}")
        print(f"{'='*80}")
        
        # 步骤1:下载
        print("\n📥 步骤1: 下载数据...")
        download_result = self.downloader.download(
            url=config['source_url'],
            filename=f"{config['name']}.{config['file_format'].lower()}"
        )
        
        if not download_result['success']:
            print(f"❌ 下载失败: {download_result.get('error')}")
            return None
        
        filepath = download_result['filepath']
        
        # 步骤2:解析
        print("\n🔄 步骤2: 解析数据...")
        file_format = config['file_format'].upper()
        
        if file_format not in self.parsers:
            print(f"❌ 不支持的格式: {file_format}")
            return None
        
        parser = self.parsers[file_format]
        
        if file_format in ['CSV', 'XLSX', 'JSON']:
            df = parser.parse(filepath)
            
            if df is None:
                print(f"❌ 解析失败")
                return None
            
            # 步骤3:转换(如果提供了Schema)
            if config.get('schema'):
                print("\n🔧 步骤3: 标准化数据...")
                df = parser.convert_to_standard_format(df, config['schema'])
            
            # 步骤4:验证(示例:检查缺失值)
            print("\n✅ 步骤4: 数据验证...")
            missing_rate = df.isnull().sum().sum() / (len(df) * len(df.columns))
            print(f"   缺失率: {missing_rate * 100:.2f}%")
            
            if missing_rate > 0.3:
                print(f"⚠️ 警告: 缺失率过高({missing_rate * 100:.2f}%)")
            
            # 步骤5:保存处理后的数据
            processed_filepath = Path(filepath).with_stem(f"{Path(filepath).stem}_processed")
            df.to_csv(processed_filepath, index=False)
            
            print(f"\n💾 已保存处理后数据: {processed_filepath.name}")
        
        else:
            processed_filepath = filepath
        
        # 步骤6:注册到目录
        print("\n📚 步骤5: 注册到数据目录...")
        dataset_id = self.catalog.register_dataset(
            name=config['name'],
            filepath=processed_filepath,
            source_url=config['source_url'],
            source_platform=config.get('source_platform'),
            description=config.get('description'),
            tags=config.get('tags')
        )
        
        print(f"\n{'='*80}")
        print(f"✅ 数据入仓完成! Dataset ID: {dataset_id}")
        print(f"{'='*80}\n")
        
        return dataset_id

# 使用示例
pipeline = DataIngestionPipeline()

# 配置1:CSV数据
config_csv = {
    'name': 'US_Unemployment_Data',
    'source_url': 'https://data.bls.gov/timeseries/LNS14000000',
    'source_platform': 'US Bureau of Labor Statistics',
    'file_format': 'CSV',
    'schema': {
        'date': {'source_columns': ['Date', 'date', 'Year'], 'type': 'datetime'},
        'rate': {'source_columns': ['Rate', 'Unemployment Rate'], 'type': 'float'},
    },
    'description': '美国失业率数据(月度)',
    'tags': '失业率,美国,经济指标'
}

# 执行入仓
dataset_id = pipeline.ingest(config_csv)

💡 最佳实践总结

1. 下载策略

场景 推荐方案 原因
大文件(>100MB) 断点续传 + 分块下载 避免重复下载,节省时间
多个小文件 并发下载(线程池) 提升效率
不稳定网络 重试机制 + 指数退避 提高成功率
需要验证 MD5/SHA256校验 确保完整性

2. 解析优化

  • ✅ 使用流式解析(大文件)
  • ✅ 缓存解析结果
  • ✅ 并行处理多个文件
  • ✅ 增量解析(只处理新数据)

3. Schema设计原则

  • ✅ 字段名使用snake_case(小写+下划线)
  • ✅ 必填字段明确标注
  • ✅ 统一时间格式(ISO 8601)
  • ✅ 统一货币单位

📚 本章总结

本章系统讲解了开放式入仓的完整体系:

自动下载系统 : 断点续传、并发下载、重试机制

多格式解析器 : CSV/JSON/XLSX/PDF统一处理

数据目录索引 : 元数据管理、版本控制、快速检索

ETL流程设计: Extract-Transform-Load完整链路

关键要点

  1. 建立统一的数据目录是数据治理的基础
  2. 自动化流程比手工处理效率高100倍
  3. 元数据管理数据血缘追踪至关重要
  4. 容错设计确保系统健壮性

希望这一章能帮你构建生产级的数据采集系统!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!

相关推荐
焦糖夹心1 小时前
python中,怎么同时输出字典的键和值?
开发语言·python
ValhallaCoder2 小时前
hot100-回溯II
数据结构·python·算法·回溯
2401_828890642 小时前
正/余弦位置编码 Sinusoidal Encoding
python·自然语言处理·transformer·embedding
流烟默2 小时前
Python爬虫之下载豆瓣电影图片到本地
爬虫·python
喵手2 小时前
Python爬虫实战:构建“时光机”——网站数据增量监控与差异分析系统!
爬虫·python·爬虫实战·差异分析·零基础python爬虫教学·网站数据增量·网站数据增量监控系统
Katecat996632 小时前
SAR图像火情与烟雾检测:Cascade-Mask-RCNN与RegNetX模型融合详解
python
禁默2 小时前
零基础全面掌握层次分析法(AHP):Python实现+论文加分全攻略
python·数学建模·matlab
深蓝电商API3 小时前
爬虫数据导出 Excel:openpyxl 高级用法
爬虫·python·openpyxl
reasonsummer3 小时前
【教学类-74-05】20260216剪影马(黑色填充图案转黑线条白填充)
python