最近在使用一个使用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()