news 2026/9/16 20:52:40

Python处理ZIP压缩包:发电量数据解压与编码修复全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python处理ZIP压缩包:发电量数据解压与编码修复全指南

简介:面向能源经济、区域发展、电力规划等领域研究者的省级发电量面板数据,覆盖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.csv2006.csv纵向 concat 并补 year 列某一年缺失时不易察觉
beijing.csvshanghai.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 读。不同年份文件的编码习惯并不统一,写死一种编码永远覆盖不全。按优先级逐个尝试是标准做法:

尝试顺序编码适用场景失败表现
1UTF-8近年默认导出UnicodeDecodeError
2GBK2010 年前后常见中文 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, 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() <= 2022

assert在这里只是轻量级护栏。真正的目的是:当文件名写了 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 df

endswith(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 数据。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 20:52:24

PHP反序列化与mt_rand种子爆破:从CTF抽奖题看伪随机数漏洞

前阵子复盘 GWCTF 2019 的 Web 题&#xff0c;有一道叫“枯燥的抽奖”的题目让我印象很深。名字听起来像个休闲小游戏&#xff0c;实际考的是 PHP 反序列化加 mt_rand 种子爆破&#xff0c;两步都是 Web 安全里很经典但不少人容易卡住的点。对新手来说&#xff0c;这道题是很好…

作者头像 李华
网站建设 2026/9/16 20:51:42

CPRI与eCPRI前传协议帧结构解析:从Wireshark抓包到丢包排查实战

上周在实验室帮同事看一台分布式小站的eCPRI前传抓包&#xff0c;抓到一大堆以太网帧&#xff0c;同事问我&#xff1a;这里边怎么看不到IQ数据&#xff1f;我说你往协议树下面翻&#xff0c;找到EtherType 0x22EE那一条&#xff0c;eCPRI的IQ数据全藏在以太网帧的载荷里。他恍…

作者头像 李华
网站建设 2026/9/16 20:51:05

CMake入门:从编译痛点到底层构建逻辑,一文讲透C++项目构建

很多写C的朋友第一次接触CMake&#xff0c;都是因为项目从单个cpp文件变成了十几个文件&#xff0c;又或者是从GitHub上拉了一个项目下来&#xff0c;发现根本没有Visual Studio的.sln&#xff0c;也没有Makefile&#xff0c;只有一堆CMakeLists.txt。网上教程一搜一大把&#…

作者头像 李华
网站建设 2026/9/16 20:49:32

宝塔邮局25端口被封?465端口中继配置全指南

1. 为什么25端口突然“失联”&#xff1f;这不是故障&#xff0c;是行业常态你刚在宝塔面板里点开邮局管理器&#xff0c;填好域名、设置好用户&#xff0c;信心满满地点击“发送测试邮件”&#xff0c;结果日志里赫然跳出一行红字&#xff1a;Connection refused或timeout on …

作者头像 李华