简介:面向能源经济、区域发展、电力规划等领域研究者的省级发电量面板数据,覆盖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} " f"raw={info.file_size:>8} " f"compressed={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 b"PK\x05\x06" in tail: eocd_offset = tail.rfind(b"PK\x05\x06") print(f"EOCD 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-8;Python 的 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 不支持-O,Linux 上也要确认 unzip 版本。没有-O时回退到 Python 修复方案。-d extracted/这一步看似可有可无,但能够避免解出的 README、说明文档和年份数据散落到当前目录,干扰后续的glob遍历。
若压缩包设置了访问口令,只能使用分发方提供的密码,不要尝试第三方破解工具。Python 打开加密包时需要显式传入密码:
# pwd 接受 bytes,非法则抛出 RuntimeError zf = zipfile.ZipFile(zip_path, pwd=b"your-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", errors="replace")))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 读。不同年份文件的编码习惯并不统一,写死一种编码永远覆盖不全。按优先级逐个尝试是标准做法:
| 尝试顺序 | 编码 | 适用场景 | 失败表现 |
|---|---|---|---|
| 1 | UTF-8 | 近年默认导出 | UnicodeDecodeError |
| 2 | GBK | 2010 年前后常见中文 CSV | 解码成功但个别列名乱码 |
| 3 | GB18030 | GBK 的超集 | 极少失败,作为兜底 |
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, encoding=enc, nrows=5000) print(f"loaded with {enc}") return pd.read_csv(path, encoding=enc) except (UnicodeDecodeError, pd.errors.ParserError): continue raise RuntimeError(f"encoding not supported for {path}")nrows=5000先用少量行试探,成功后再全量读入。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={"地区": "省份"}, inplace=True)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 时,尽早发现问题,避免分析完成后才发现早期数据一直是空集。数值列如果带有千分位逗号或中文单位,需要先转字符串替换再转 float:
df["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参数逐个读取包内条目并校验 CRC,tail -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(f"archive 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)还要检查合并后是否有重复键。面板数据最常见的问题是同一省份同一年出现在多行:
# keep=False 标记全部重复行,方便打印样本 dup = df.duplicated(subset=["省份", "year"], keep=False) print(f"duplicated rows: {dup.sum()}")keep=False会把所有重复行都标记出来,而不是只标记后面的,这样打印样本行时能看到重复的完整两侧,判断是数据源更新遗留还是拼接逻辑问题。
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_ok=True) 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(f"year {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_index=True)ignore_index=True重新生成整数索引,避免年份之间索引重叠。
提示:源压缩包更新后,清空
cache/目录再重跑,即可覆盖旧缓存。
缓存文件名里最好带上数据来源版本,例如2020_v2.parquet,避免不同版本数据混用。这个加载入口把解压、编码、列对齐都收拢到一个函数,后续做电源结构对比、分省汇总或年度增长分析时,都只需要从这个入口读取已标准化的 panel 数据。
本文还有配套的精品资源,点击获取