最近在使用一个使用python制作的软件时发现软件会报错,但是根据软件的报错无法直接分析出具体是哪里有问题,所以想将exe进行解包分析里面的源代码,并分享一下过程中遇到的问题。
提取 .pyc 文件
pyinstxtractor
下载依赖
pip install pyinstxtractor
如果是内网环境,可以到github官方仓库下载 https://github.com/extremecoders-re/pyinstxtractor
解包
注意,当前环境的python版本要和软件的python版本相同才能解包,否则没有任何输出
python pyinstxtractor.py <filename>
pyinstxtractor-ng
pyinstxtractor-ng是 pyinstxtractor 的一个分支。
pyinstxtractor-ng 使用 xdis 库来解包 Python 字节码,因此不需要使用用于构建可执行文件的相同 Python 版本。
Pyinstxtractor-ng 还支持自动解密加密的 pyinstaller 可执行文件。
下载依赖
pip install pyinstxtractor-ng
github仓库:https://github.com/pyinstxtractor/pyinstxtractor-ng
解包
pyinstxtractor-ng <filename>
在线解包
https://pyinstxtractor-web.netlify.app/
将pyc文件反编译为py源代码
在线反编译
PyLingual:https://pylingual.io/
在线Python pyc文件编译与反编译(3.9以下成功率较高):https://www.lddgo.net/string/pyc-compile-decompile
本地反编译
由于上面两个网站对python新版本支持可能较差,我本次反编译的软件使用的是python 3.12.7 所以上述两个在线网站均无法正常反编译,只能将pyc文件的字节码读取出来扔给claude让它把源代码解析出来。
# disasm.py
import marshal, dis
extracted_dir = "农险地块数据智能整理与质检工具V1.1.exe_extracted"
with open(f"{extracted_dir}/main.pyc", "rb") as f:
f.read(16) # 跳过已有的16字节头部
code = marshal.loads(f.read())
print("✅ 读取成功!")
dis.dis(code)
得到字节码后发给claude,解析出如下源码
"""
========主函数============
主要用于展示各项功能和调用各个功能窗口。
"""
import sys
from typing import Optional
import os
import shutil
from PyQt5 import QtCore, QtGui, QtWidgets
from DC_Admin_Query import AdminQueryWindow
from DC_Combined_Prep import DataPrepWindow, PolicyPrepWindow
from DQ_Quality_Tool import QualityToolWindow
from DC_Shp_Merge import ParcelMergeWindow
from DC_Batch_CRS_Convert import BatchCRSConvertWindow
from DC_Shp_Split import ShpSplitByFieldWindow
from DC_Chart_Join import ShpExcelJoinWindow
from BS_locationBasedPrefill import FillTemplateWindow as LocationPrefillWindow
from BS_ExcelShpJoin import ExcelShpJoinWindow
from DC_Small_shp_merge import SmallShpMergeWindow # ← 新增模块
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setAttribute(QtCore.Qt.WA_QuitOnClose, True)
self.setWindowTitle('农险地块数据智能整理与质检工具V1.1')
self.resize(1300, 800)
self.setMinimumSize(900, 600)
base_font = QtGui.QFont('Microsoft YaHei', 12)
self.setFont(base_font)
# 子窗口引用
self.admin_window = None
self.dataprep_window = None
self.policy_window = None
self.quality_window = None
self.merge_window = None
self.crs_window = None
self.shp_split_window = None
self.location_prefill_window = None
self.person_join_window = None
self.small_merge_window = None # ← 新增
# 主布局
central = QtWidgets.QWidget()
self.setCentralWidget(central)
central.setStyleSheet('QWidget { background:#FFFFFF; }')
root = QtWidgets.QVBoxLayout(central)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
scroller = QtWidgets.QScrollArea()
scroller.setWidgetResizable(True)
scroller.setFrameShape(QtWidgets.QFrame.NoFrame)
root.addWidget(scroller)
content = QtWidgets.QWidget()
scroller.setWidget(content)
outer_v = QtWidgets.QVBoxLayout(content)
outer_v.setContentsMargins(24, 24, 24, 24)
outer_v.setSpacing(0)
outer_v.addStretch(1)
row = QtWidgets.QWidget()
self.h_layout = QtWidgets.QHBoxLayout(row)
self.h_layout.setContentsMargins(0, 0, 0, 0)
self.h_layout.setSpacing(16)
self.h_layout.addStretch(1)
# ── 基本功能 卡片(左列,6个按钮)──
prep_card, prep_body = self._create_card('基本功能')
self.vprep = QtWidgets.QVBoxLayout(prep_body)
self.vprep.setContentsMargins(16, 16, 16, 16)
self.vprep.setSpacing(12)
features_prep = [
('行政区划查询', self.launch_admin_query),
('坐标系统转换', self.launch_crs_convert),
('地块向上合并', self.launch_parcel_merge),
('小地块合并为大地块', self.launch_small_shp_merge), # ← 新增
('地块向下拆分', self.launch_shp_split),
('图表挂接', self.launch_chart_join),
]
for txt, handler in features_prep:
self.vprep.addWidget(self.make_feature_btn(txt, handler, color='#10B981'))
self.vprep.addStretch(1)
prep_card.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
self.h_layout.addWidget(prep_card, 0)
# ── 数据整理 卡片 ──
quality_card, quality_body = self._create_card('数据整理')
self.vq = QtWidgets.QVBoxLayout(quality_body)
self.vq.setContentsMargins(16, 16, 16, 16)
self.vq.setSpacing(12)
self.vq.addWidget(self.make_feature_btn('地块图形shp数据整理', self.launch_data_prep, color='#10B981'))
self.vq.addWidget(self.make_feature_btn('承保业务信息表整理', self.launch_policy_prep, color='#F59E0B'))
self.vq.addStretch(1)
quality_card.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
# ── 业务场景 卡片 ──
biz_card, biz_body = self._create_card('业务场景')
self.vbiz = QtWidgets.QVBoxLayout(biz_body)
self.vbiz.setContentsMargins(16, 16, 16, 16)
self.vbiz.setSpacing(12)
self.vbiz.addWidget(self.make_feature_btn('按地投保预填', self.launch_location_prefill, color='#0EA5E9'))
self.vbiz.addWidget(self.make_feature_btn('按人投保撞库', self.launch_person_join, color='#EC4899'))
self.vbiz.addStretch(1)
biz_card.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
# ── 帮助文档 卡片(新增)──
help_card, help_body = self._create_card('帮助文档')
self.vhelp = QtWidgets.QVBoxLayout(help_body)
self.vhelp.setContentsMargins(16, 16, 16, 16)
self.vhelp.setSpacing(12)
self.vhelp.addWidget(self.make_feature_btn('地块图形和承保表结构', self.download_help_doc, color='#10B981'))
self.vhelp.addStretch(1)
help_card.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
# ── 数据质检 卡片 ──
service_card, service_body = self._create_card('数据质检')
self.vs = QtWidgets.QVBoxLayout(service_body)
self.vs.setContentsMargins(16, 16, 16, 16)
self.vs.setSpacing(12)
self.vs.addWidget(self.make_feature_btn('地块数据质检工具', self.launch_quality_tool, color='#F14842'))
self.vs.addStretch(1)
service_card.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)
# ── 右侧 2×2 网格布局(新版改为 QGridLayout)──
grid = QtWidgets.QWidget()
grid_layout = QtWidgets.QGridLayout(grid)
grid_layout.setContentsMargins(0, 0, 0, 0)
grid_layout.setSpacing(16)
# 第0行:数据整理(0,0) 业务场景(0,1)
grid_layout.addWidget(quality_card, 0, 0)
grid_layout.addWidget(biz_card, 0, 1)
# 第1行:数据质检(1,0) 帮助文档(1,1)
grid_layout.addWidget(service_card, 1, 0)
grid_layout.addWidget(help_card, 1, 1)
grid_layout.setColumnStretch(0, 1)
grid_layout.setColumnStretch(1, 1)
grid_layout.setRowStretch(0, 1)
grid_layout.setRowStretch(1, 1)
self.h_layout.addWidget(grid, 0)
self.h_layout.addStretch(1)
outer_v.addWidget(row)
outer_v.addStretch(1)
self._update_dynamic_spacing()
# ------------------------------------------------------------------ #
# 资源路径(新增)
# ------------------------------------------------------------------ #
def _resource_path(self, relative_path: str) -> str:
base = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, relative_path)
# ------------------------------------------------------------------ #
# 帮助文档下载(新增)
# ------------------------------------------------------------------ #
def download_help_doc(self):
target_dir = QtWidgets.QFileDialog.getExistingDirectory(
self,
'选择保存位置',
os.path.expanduser('~'),
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks,
)
if not target_dir:
return
filename = '地块图形属性表及承保信息表结构.xlsx'
src = self._resource_path(os.path.join('data', filename))
if not os.path.exists(src):
QtWidgets.QMessageBox.critical(
self, '错误',
f'未找到源文件:\n{src}\n\n请确认 data 目录已打包/存在。'
)
return
dst = os.path.join(target_dir, filename)
if os.path.exists(dst):
ret = QtWidgets.QMessageBox.question(
self, '提示',
f'目标位置已存在同名文件:\n{dst}\n\n是否覆盖?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No,
)
if ret != QtWidgets.QMessageBox.Yes:
return
try:
shutil.copy2(src, dst)
QtWidgets.QMessageBox.information(self, '完成', f'已保存到:\n{dst}')
except Exception as e:
QtWidgets.QMessageBox.critical(self, '错误', f'保存失败:\n{e}')
# ------------------------------------------------------------------ #
# 动态间距(新版只管 vprep/vq/vs,去掉了 right_v)
# ------------------------------------------------------------------ #
def _update_dynamic_spacing(self):
w = max(1, self.width())
btn_spacing = max(12, min(32, int(w / 90)))
card_spacing = max(16, min(48, int(w / 60)))
for layout in (
getattr(self, 'vprep', None),
getattr(self, 'vq', None),
getattr(self, 'vs', None),
):
if isinstance(layout, QtWidgets.QBoxLayout):
layout.setSpacing(btn_spacing)
if hasattr(self, 'h_layout'):
self.h_layout.setSpacing(card_spacing)
def resizeEvent(self, event: QtGui.QResizeEvent):
super().resizeEvent(event)
self._update_dynamic_spacing()
# ------------------------------------------------------------------ #
# 按钮 & 卡片工厂
# ------------------------------------------------------------------ #
def make_feature_btn(self, text: str, handler, color: str = '#10B981') -> QtWidgets.QPushButton:
btn = QtWidgets.QPushButton(text)
btn.setFixedWidth(370)
btn.setMinimumHeight(80)
btn.setFont(QtGui.QFont('Microsoft YaHei', 16, QtGui.QFont.Bold))
btn.setCursor(QtCore.Qt.PointingHandCursor)
btn.setStyleSheet(
f'QPushButton {{ padding: 16px 28px; background:{color}; color:white; border-radius:8px; }}'
f'QPushButton:hover{{ background:#059669; }}'
f'QPushButton:disabled{{ background:#A7F3D0; }}'
)
btn.clicked.connect(handler)
return btn
def _create_card(self, title_text: str):
wrap = QtWidgets.QWidget()
wrap.setObjectName('Card')
wrap.setStyleSheet('''
#Card {
background:#FFFFFF;
border:1px solid #E2E8F0;
border-radius:8px;
}
''')
v = QtWidgets.QVBoxLayout(wrap)
v.setContentsMargins(0, 0, 0, 0)
v.setSpacing(0)
header = QtWidgets.QWidget()
header.setObjectName('CardHeader')
header.setStyleSheet('#CardHeader { background:#FFFFFF; border-bottom:1px solid #E2E8F0; }')
hv = QtWidgets.QHBoxLayout(header)
hv.setContentsMargins(16, 12, 16, 12)
lab = QtWidgets.QLabel(title_text)
lab.setFont(QtGui.QFont('Microsoft YaHei', 16, QtGui.QFont.Bold))
lab.setStyleSheet('color:#0F172A; border:0;')
hv.addWidget(lab, 0, QtCore.Qt.AlignCenter)
v.addWidget(header)
body = QtWidgets.QWidget()
v.addWidget(body, 1)
return wrap, body
# ------------------------------------------------------------------ #
# 子窗口管理
# ------------------------------------------------------------------ #
def _open_child(self, w_attr: str, ctor):
if not self.isHidden():
self.hide()
w = ctor()
w.setWindowFlags(w.windowFlags() | QtCore.Qt.Window)
w.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
prev = getattr(self, w_attr)
if prev is not None:
try:
prev.destroyed.disconnect()
except Exception:
pass
setattr(self, w_attr, w)
w.destroyed.connect(lambda: setattr(self, w_attr, None))
w.destroyed.connect(self.show)
try:
w.show()
except Exception as e:
QtWidgets.QMessageBox.critical(self, '错误', f'无法打开窗口:\n{e}')
# ------------------------------------------------------------------ #
# 各功能入口
# ------------------------------------------------------------------ #
def launch_admin_query(self):
self._open_child('admin_window', lambda: AdminQueryWindow())
def launch_data_prep(self):
self._open_child('dataprep_window', lambda: DataPrepWindow())
def launch_policy_prep(self):
self._open_child('policy_window', lambda: PolicyPrepWindow())
def launch_quality_tool(self):
self._open_child('quality_window', lambda: QualityToolWindow())
def launch_parcel_merge(self):
self._open_child('merge_window', lambda: ParcelMergeWindow())
def launch_small_shp_merge(self): # ← 新增
self._open_child('small_merge_window', lambda: SmallShpMergeWindow())
def launch_crs_convert(self):
self._open_child('crs_window', lambda: BatchCRSConvertWindow())
def launch_shp_split(self):
self._open_child('shp_split_window', lambda: ShpSplitByFieldWindow())
def launch_chart_join(self):
self._open_child('chart_join_window', lambda: ShpExcelJoinWindow())
def launch_location_prefill(self):
self._open_child('location_prefill_window', lambda: LocationPrefillWindow())
def launch_person_join(self):
self._open_child('person_join_window', lambda: ExcelShpJoinWindow())
def closeEvent(self, event: QtGui.QCloseEvent):
QtWidgets.QApplication.quit()
event.accept()
def main():
app = QtWidgets.QApplication(sys.argv)
QtWidgets.QApplication.setQuitOnLastWindowClosed(False)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()