
简介面向能源经济、区域发展、电力规划等领域研究者的省级发电量面板数据覆盖2005至2022年间各省份发电量情况可用于跨区域对比、时序趋势分析及经济关联性研究既适合高校师生课题使用也适合行业分析师快速取得基础数据。资源包共4个文件以xlsx数据表为核心同时包含txt使用说明、pdf文档说明和html数据来源页面整体压缩包仅106KB结构紧凑数据粒度清晰、字段明了便于直接下载与后续处理。文件内除了可读性强的发电量数据表还提供了数据来源网页存档和使用说明能帮助使用者快速掌握字段含义、核对统计口径并正确引用原始出处减少数据清洗与溯源成本。目前已有51人浏览学习适合需要省级能源数据支撑的区域研究、政策分析或学术写作场景。1. 发电量数据 ZIP 包要过的第一关是文件本身拿到“各省份发电量数据2005-2022年.zip”常见的做法是先解压再看但问题往往在解压之后才暴露中文文件名乱码、CSV 内部是 GBK 而脚本按 UTF-8 读、某一年文件缺失、甚至整个压缩包尾部在传输中被截断连 EOCD 都找不到。这些不是业务层的数据质量问题而是文件工程问题通常比统计操作更消耗时间。我一般按“压缩包清单确认 → 安全提取 → 编码对齐 → 结构校验 → 固化加载函数”这条链路处理目标不是解出一个文件夹而是得到一个可复现的 DataFrame 入口。这篇内容适合用 Python 处理跨年省级面板数据、又想减少文件层返工的工程师和数据分析人员。2. 解压前先读 ZIP 元数据文件清单与 EOCD 完整性2.1 用 namelist() 与 unzip -l 建立压缩包内文件清单压缩包本身是一个携带目录结构的文件ZIP 格式在末尾记录了所有条目的中央目录。用 unzip -l 能直接列出条目而不实际解压# -l 只列清单不产生文件 unzip -l power_province_05_22.zip如果系统里没有 unzip或者希望把检查动作一并收进 Python 脚本用 zipfile 模块做相同的事import zipfile zip_path power_province_05_22.zip with zipfile.ZipFile(zip_path) as zf: # infolist 返回完整 ZIP 条目元数据 for info in zf.infolist(): print( f{info.filename:36} fraw{info.file_size:8} fcompressed{info.compress_size:8} )infolist()返回 ZipInfo 对象集合每项带有 filename、file_size、compress_size、CRC 等字段。要在解压前确认的内容是目标条目是否存在、年份文件数量是否覆盖 2005-2022、有没有 0 字节条目。当compressed数值接近raw时文件可能已被单独压缩过或者做了加密处理后续读取策略需要调整。这一步相当于数据处理中的“元数据先行”跳过它直接双击解压遇到Error read zip archive时反而要从头排查。2.2 从文件命名反推数据排列方式unzip -l的输出也揭示了数据切分方式。按年份拆分、按省份拆分、或者单个宽表三种命名风格的合并策略完全不同。命名风格合并策略需要警惕的点2005.csv、2006.csv纵向 concat 并补 year 列某一年缺失时不易察觉beijing.csv、shanghai.csv横向合并并统一指标列不同省份的列顺序可能不一致2005_2022.csv单个新文件直接读入文件过大时内存占用高观察命名规律后把合并策略写进脚本注释。常见的返工场景是把多版本文件当作连续年份纵向拼接导致同名年份被统计两遍。这类错误比列名不一致更难回溯因为大多数聚合函数不会报错只会把数值翻倍。2.3 提前识别截断EOCD 签名与 could not find EOCD“could not find EOCD” 是压缩包处理里常见也容易误判的错误。EOCD 是 End of Central Directory Record 的缩写位于 ZIP 文件最末尾。下载中断、网盘导出失败或磁盘写满丢失的多半正是这一段。通过直接读取文件尾部签名可以判断with open(zip_path, rb) as f: f.seek(-1024, 2) # 从文件末尾向前退 1024 字节 tail f.read() if bPK\x05\x06 in tail: eocd_offset tail.rfind(bPK\x05\x06) print(fEOCD found at offset {eocd_offset}) else: print(EOCD missing: archive truncated)PK\x05\x06是 EOCD 记录的十六进制签名f.seek(-1024, 2)读取文件最后 1KB然后用rfind找到最后一个签名位置。找不到签名就说明中心目录不完整继续尝试解压没有意义直接回到数据源重新获取。超过 4GB 的 Zip64 格式会在 EOCD 之后多一段定位记录但签名检索方式不变只需把读取范围扩大到文件末尾的 2KB 再做同样查找。3. 提取发电量数据压缩包中文文件名的编码修复与按需读取3.1 中文文件名乱码的根源代码页没有被记录ZIP 规范允许文件名使用任意字符集并不强制 UTF-8。很多压缩工具在打包时使用本地代码页 GBK却不在文件头标志位声明 UTF-8Python 的 zipfile 默认按 cp437 解码这类名称于是出现鵦一类乱码。这不是文件损坏而是名称解码方式错误。最常见的修复方式是把乱码字符串反向编码回 cp437 字节再用 GBK 解码def fix_zip_name(name: str) - str: try: return name.encode(cp437).decode(gbk) except (UnicodeEncodeError, UnicodeDecodeError): return name逻辑是乱码是 cp437 解码造成的先把字符串编码成原始字节再交给正确字符集处理。如果文件实际是 UTF-8这一步会抛出解码失败此时返回原名即可。实际压缩包中可能混有不同编码的文件处理时对每个namelist()条目独立调用该函数并输出修复前后的映射表避免覆盖同名文件。3.2 Info-ZIP 的 -O 参数与目录隔离命令行用户可以用 Info-ZIP 的 unzip 直接指定字符集# -O 指定原始文件名编码为 GBK-d 指定输出目录 unzip -O GBK power_province_05_22.zip -d extracted/-O参数把文件名字符集指定为 GBK-d指定输出目录。需要注意macOS 自带的 BSD unzip 不支持-OLinux 上也要确认 unzip 版本。没有-O时回退到 Python 修复方案。-d extracted/这一步看似可有可无但能够避免解出的 README、说明文档和年份数据散落到当前目录干扰后续的glob遍历。若压缩包设置了访问口令只能使用分发方提供的密码不要尝试第三方破解工具。Python 打开加密包时需要显式传入密码# pwd 接受 bytes非法则抛出 RuntimeError zf zipfile.ZipFile(zip_path, pwdbyour-password)3.3 按需读取不落盘直接 read 到内存一整个压缩包可能包含数百 MB 文件但分析往往只需要某几年的数据。在没有必要全部解压时ZipFile.read(name)可以直接将单个条目读入内存import io import pandas as pd wanted 2020.csv with zipfile.ZipFile(zip_path, r) as zf: raw zf.read(wanted) # 返回 bytes df pd.read_csv(io.StringIO(raw.decode(utf-8, errorsreplace)))zf.read()返回 bytes 后经StringIO包装pandas 直接从内存读取省去中间磁盘文件也避免解出的大量临时文件残留。但按需读取依赖一个准确的namelist()结果如果目标文件实际放在final/2020.csv而脚本写死了2020.csv读取就会报错。更稳妥的做法是用文件名后缀匹配[n for n in names if n.endswith(2020.csv)]。4. 加载 2005-2022 年数据编码探测与列结构对齐4.1 自动探测 CSV 编码的优先级序列数据文件在 Excel 里打开正常pandas 一读就抛UnicodeDecodeError多半是文件内容编码为 GBK 而脚本按 UTF-8 读。不同年份文件的编码习惯并不统一写死一种编码永远覆盖不全。按优先级逐个尝试是标准做法尝试顺序编码适用场景失败表现1UTF-8近年默认导出UnicodeDecodeError2GBK2010 年前后常见中文 CSV解码成功但个别列名乱码3GB18030GBK 的超集极少失败作为兜底import pandas as pd def load_csv_auto(path: str) - pd.DataFrame: for enc in (utf-8, gbk, gb18030): try: # 先读 5000 行试探避免全量加载后才发现编码错误 preview pd.read_csv(path, encodingenc, nrows5000) print(floaded with {enc}) return pd.read_csv(path, encodingenc) except (UnicodeDecodeError, pd.errors.ParserError): continue raise RuntimeError(fencoding not supported for {path})nrows5000先用少量行试探成功后再全量读入。gb18030放最后因为它是 GBK 的超集几乎能解任何中文字符但也可能把原本合规的 UTF-8 内容解析出乱码列只在前面都失败时才应该使用。UnicodeDecodeError表示字节序列无法解析抛错时换编码ParserError表示分隔符或引号规则不匹配可与编码无关需要单独查看文件头。4.2 列结构对齐把地区、省份这类列名统一起来逐年文件之间列名写法并不一致早期叫“地区”后期叫“省份”。先清理列名再统一映射df load_csv_auto(2020.csv) # 去掉列名中的首尾空格和中间空格 df.columns [c.strip().replace( , ) for c in df.columns] # 统一映射保证纵向拼接时字段一致 df.rename(columns{地区: 省份}, inplaceTrue)df.columns直接赋值的写法比逐列 rename 更彻底一次性处理了所有列名的空白字符。如果文件内没有年份列纵向拼接之前要补上df[year] 2020。年份即使写在文件名里也不能依赖文件名参与计算因为拼接后行索引会丢失来源信息。4.3 数据载入后的面板形状核验# nunique 统计唯一值快速确认年份和省份范围 print(years:, df[year].nunique()) print(provinces:, df[省份].nunique()) assert 2005 df[year].min() 2020 assert df[year].max() 2022assert在这里只是轻量级护栏。真正的目的是当文件名写了 2005-2022而数据里最小年却是 2010 时尽早发现问题避免分析完成后才发现早期数据一直是空集。数值列如果带有千分位逗号或中文单位需要先转字符串替换再转 floatdf[value] ( df[value].astype(str) .str.replace(,, ) .str.replace(万千瓦时, ) # 去掉单位词统一量纲 .astype(float) )这一步处理的是数值列中夹带的格式字符单位统一到同一量纲后再参与后续计算。5. CRC 校验与空文件把压缩包损坏的准确定位5.1 CRC-32 校验失败的定位思路ZIP 中央目录为每个条目保存了 CRC-32 值解压工具解出内容后重新计算并比对。CRC check failed表示文件内容与目录记录不符通常来自存储介质错误或传输丢包。单个条目失败不代表整个包作废用unzip -t可以让工具只做完整性测试而不实际解压所有文件# -t 逐个校验条目tail 只看末尾汇总 unzip -t power_province_05_22.zip | tail -30-t参数逐个读取包内条目并校验 CRCtail -30只保留结尾部分的汇总输出可以直接看出哪个文件报 bad CRC。如果连中心目录本身都读不完整工具会直接报Error read zip archive此时定位单个文件没有意义应该整体重新获取。区别在于报错时间点如果-t跑了一段才报 CRC 失败说明只有局部数据块损坏可以只针对那一年文件单独替换。5.2 用原始大小总和核对解压产物把infolist()中所有条目的file_size累加再和磁盘上解压目录的实际大小比较是一种低成本的全量核对方式import pathlib # 压缩包里所有文件的原始大小总和 total_raw sum(i.file_size for i in zf.infolist()) # 磁盘上解压目录的实际占用 extracted_size sum( p.stat().st_size for p in pathlib.Path(extracted).rglob(*) if p.is_file() ) print(farchive raw sum: {total_raw}, on disk: {extracted_size}) assert abs(extracted_size - total_raw) 1024差值在 1KB 以内算正常。如果相差几十 MB说明有文件在解压过程中被跳过或写出失败。此时用一个保存了解压前文件名的列表和extracted下的实际路径做差集就能定位缺失的那个年份。5.3 0 字节文件与重复键的识别解压工具对 0 字节文件不报错文件名也正常但加载时读出一个空 DataFrame。最稳妥的防御是在解压前检查条目大小# 找出所有空条目提前拦截 empty [ i.filename for i in zf.infolist() if i.file_size 0 ] print(empty entries:, empty)还要检查合并后是否有重复键。面板数据最常见的问题是同一省份同一年出现在多行# keepFalse 标记全部重复行方便打印样本 dup df.duplicated(subset[省份, year], keepFalse) print(fduplicated rows: {dup.sum()})keepFalse会把所有重复行都标记出来而不是只标记后面的这样打印样本行时能看到重复的完整两侧判断是数据源更新遗留还是拼接逻辑问题。6. 把发电量面板解析包装成带缓存的数据入口6.1 按年份输出 parquet 缓存的加载函数数据稳定读入后不需要每次运行脚本都重复解压、解码、清洗。常见做法是写一个加载函数以 parquet 格式缓存每个年份的解析结果import pathlib import zipfile import io import pandas as pd CACHE_DIR pathlib.Path(./cache) def load_year(zip_path: str, year: int): CACHE_DIR.mkdir(exist_okTrue) cache_file CACHE_DIR / f{year}.parquet # 缓存命中时直接返回跳过解析流程 if cache_file.exists(): return pd.read_parquet(cache_file) with zipfile.ZipFile(zip_path) as zf: names zf.namelist() # endswith 匹配文件名不写死完整路径 matched [n for n in names if n.endswith(f{year}.csv)] if not matched: raise FileNotFoundError(fyear {year} not found) raw zf.read(matched[0]) df pd.read_csv(io.StringIO(raw.decode(utf-8))) df[year] year df.to_parquet(cache_file) return dfendswith(f{year}.csv)避开目录前缀不同的情况year列在写入 parquet 之前补齐这样后面的pd.concat不再需要逐个添加。第一次运行会执行完整解析之后直接读缓存。6.2 在分析会话里合并多年数据years list(range(2005, 2023)) frames [load_year(power_province_05_22.zip, y) for y in years] panel pd.concat(frames, ignore_indexTrue)ignore_indexTrue重新生成整数索引避免年份之间索引重叠。提示源压缩包更新后清空cache/目录再重跑即可覆盖旧缓存。缓存文件名里最好带上数据来源版本例如2020_v2.parquet避免不同版本数据混用。这个加载入口把解压、编码、列对齐都收拢到一个函数后续做电源结构对比、分省汇总或年度增长分析时都只需要从这个入口读取已标准化的 panel 数据。本文还有配套的精品资源点击获取