Kanji
・Cloud Engineer / Freelance ・Geboren 1993 ・Aus Ehime / Wohnhaft in Shibuya, Tokio ・5 Jahre AWS-Erfahrung Profildetails
Inhaltsverzeichnis
– Das Python-Skript in diesem Artikel folgt PEP8. - Die Syntaxprüfung erfolgt mit flake8, die automatische Formatierung erfolgt mit black. – Für Code, der aus Verarbeitungsgründen Prüfungen überspringen muss, fügen Sie „# noqa“ hinzu. - Die maximale Zeilenlänge („max-line-length“) ist auf 200 Zeichen pro Zeile festgelegt.
– Der Betrieb wurde unter macOS nicht überprüft.Während es möglicherweise in Umgebungen funktioniert, in denen Office für Mac installiert ist, ist dieses Skript für die Verwendung unter Windows gedacht .
– Die folgende Erweiterung wird zum Bearbeiten von VBA in Visual Studio Code verwendet: - VBA – Visual Basic für Anwendungen
– Fügen Sie Folgendes zu „.vscode/extensions.json“ hinzu, um die VBA-Erweiterung beim Öffnen von VBA-Dateien in Visual Studio Code automatisch zu aktivieren:
{ "recommendations": [ "serkonda7.vscode-vba" ] }
Fügen Sie die folgenden Einstellungen zu Ihrer Visual Studio Code-Datei „settings.json“ hinzu, um VBA-Dateien mit Shift-JIS-Codierung zu öffnen. – Python- und JSON-Dateien sind so eingestellt, dass sie mit UTF-8-Kodierung geöffnet werden.
{ "files.encoding": "shiftjis", "[python]": { "files.encoding": "utf8" }, "[json]": { "files.encoding": "utf8" } }
– Das folgende Python-Skript extrahiert und schreibt VBA-Code aus/in Excel-Dateien.Speichern Sie es unter einem geeigneten Dateinamen (z. B. „sync_vba.py“).
import argparse import difflib import json import os import shutil import sys import zipfile from datetime import datetime import openpyxl import xlwings as xw from oletools.olevba import VBA_Parser CONFIG_FILE = "config.json" CACHE_DIR = ".cache" BACKUP_KEEP_COUNT = 3 def create_config(): config = {"xlsm_file_path": "your_xlsm_file_path_here.xlsm"} with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=4, ensure_ascii=False) print(f"{CONFIG_FILE} has been created. Please edit xlsm_file_path.") def load_config(): if not os.path.exists(CONFIG_FILE): print( f"{CONFIG_FILE} not found. Please initialize with the --init option." ) sys.exit(1) with open(CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f) def extract_vba(xlsm_file_path, output_dir="vba", force_master=False): if not os.path.exists(xlsm_file_path): sys.exit(1) if not os.path.exists(output_dir): os.makedirs(output_dir) if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) with zipfile.ZipFile(xlsm_file_path, "r") as z: vba_bin_path = None for f in z.namelist(): if f == "xl/vbaProject.bin": vba_bin_path = f break if not vba_bin_path: sys.exit(1) tmp_bin = os.path.join(CACHE_DIR, "vbaProject.bin") with z.open(vba_bin_path) as src, open(tmp_bin, "wb") as dst: dst.write(src.read()) vba_parser = VBA_Parser(tmp_bin) if not vba_parser.detect_vba_macros(): return for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros(): if vba_code: filtered_code = "\n".join( line for line in vba_code.splitlines() if not line.strip().startswith("Attribute VB_") ) cache_path = os.path.join(CACHE_DIR, vba_filename) with open(cache_path, "w", encoding="shift-jis") as f: f.write(filtered_code) vba_path = os.path.join(output_dir, vba_filename) if os.path.exists(vba_path): try: with open(vba_path, "r", encoding="shift-jis") as f1: old = f1.readlines() except UnicodeDecodeError: with open(vba_path, "r", encoding="utf-8") as f1: content = f1.read() with open(vba_path, "w", encoding="shift-jis") as f1: f1.write(content) old = content.splitlines(keepends=True) with open(cache_path, "r", encoding="shift-jis") as f2: new = f2.readlines() diff = list(difflib.unified_diff(old, new, fromfile=vba_path, tofile=cache_path)) if diff: print(f"\nDiff for {vba_filename}:") print("") print("".join(diff)) print("") print("* + : New content extracted from xlsm file, - : Content in current file)") if force_master: print(f"--force option: {vba_filename} will keep the current file as master.") continue while True: sel = input(f"Difference found in {vba_filename}. Which do you want to keep as master? [k] Keep current file/[n] Overwrite with new extracted content > ").strip().lower() if sel == "k": break elif sel == "n": with open(vba_path, "w", encoding="shift-jis") as f: f.write(filtered_code) break else: with open(vba_path, "w", encoding="shift-jis") as f: f.write(filtered_code) def write_vba_to_xlsm(xlsm_file_path, vba_dir="vba"): abs_path = os.path.abspath(xlsm_file_path) if not os.path.exists(abs_path): print("The specified xlsm file does not exist. Please check the path.") sys.exit(1) app = xw.App(visible=False) try: wb = xw.Book(abs_path) vba_modules = {mod.Name: mod for mod in wb.api.VBProject.VBComponents} for fname in os.listdir(vba_dir): if fname.startswith("."): continue fpath = os.path.join(vba_dir, fname) if not os.path.isfile(fpath): continue mod_name, ext = os.path.splitext(fname) import re if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", mod_name): print(f"Invalid module name: {mod_name} (Must start with a letter or _, and only contain alphanumeric and _)") sys.exit(1) if mod_name in vba_modules: with open(fpath, "r", encoding="shift-jis") as f: code = f.read() code_module = vba_modules[mod_name].CodeModule code_module.DeleteLines(1, code_module.CountOfLines) code_module.AddFromString(code) else: if ext.lower() == ".bas": kind = 1 elif ext.lower() == ".cls": kind = 2 elif ext.lower() == ".frm": kind = 3 else: continue with open(fpath, "r", encoding="shift-jis") as f: code = f.read() mod = wb.api.VBProject.VBComponents.Add(kind) mod.Name = mod_name mod.CodeModule.AddFromString(code) wb.save() wb.close() except Exception as e: print("Error opening workbook:", e) raise finally: app.quit() def backup_xlsm(xlsm_file_path): if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) base = os.path.basename(xlsm_file_path) dt = datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"{base}.{dt}.bak" backup_path = os.path.join(CACHE_DIR, backup_name) shutil.copy2(xlsm_file_path, backup_path) backups = sorted( [f for f in os.listdir(CACHE_DIR) if f.startswith(base) and f.endswith(".bak")], reverse=True ) for old in backups[BACKUP_KEEP_COUNT:]: try: os.remove(os.path.join(CACHE_DIR, old)) except Exception: pass def main(force_master=False): config = load_config() xlsm_file_path = config.get("xlsm_file_path") if not xlsm_file_path: print("xlsm_file_path is not set in config.json.") sys.exit(1) backup_xlsm(xlsm_file_path) extract_vba(xlsm_file_path, force_master=force_master) write_vba_to_xlsm(xlsm_file_path) print("Completed successfully.") if __name__ == "__main__": parser = argparse.ArgumentParser( description="VBA module sync tool" ) parser.add_argument("--init", action="store_true", help="Initialize config.json.") parser.add_argument("--force", action="store_true", help="Keep existing vba files as master even if there are differences.") args = parser.parse_args() if args.init: create_config() sys.exit(0) main(force_master=args.force)
Im Python-Skript werden folgende Module verwendet:
Standardbibliotheken:
„argparse“: Parsen von Befehlszeilenargumenten
difflib : Unterschiede anzeigen
difflib
„json“: Konfigurationsdateien lesen und schreiben
„os“, „shutil“, „sys“, „zipfile“: Dateioperationen
„datetime“: Datums- und Uhrzeitoperationen
Externe Bibliotheken:
„openpyxl“: Excel-Dateien lesen und schreiben
„xlwings“: Bearbeiten von Excel VBA
„oletools“: VBA-Code extrahieren
Um die erforderlichen Module zu installieren, führen Sie den folgenden Befehl aus:
pip install openpyxl xlwings oletools
python sync_vba.py --init
{ "xlsm_file_path": "your_xlsm_file_path_here.xlsm" }
python sync_vba.py
> python .\sync_vba.py Completed successfully.
> python .\sync_vba.py Diff for Sheet1.cls: --- vba\Sheet1.cls +++ .cache\Sheet1.cls @@ -6,6 +6,7 @@ ActiveWindow.ScrollRow = 1 ActiveWindow.ScrollColumn = 1 Next ws + Debug.Print ("TEST") ThisWorkbook.Worksheets(1).Activate ThisWorkbook.Worksheets(1).Range("A1").Select End Sub * + : New content extracted from xlsm file, - : Content in current file) Difference found in Sheet1.cls. Which do you want to keep as master? [k] Keep current file/[n] Overwrite with new extracted content >
python sync_vba.py --force
# Files created as described in Coding Rules and Prerequisites .vscode ├── extensions.json ├── settings.json # Directory where the extracted VBA code is saved vba ├── Sheet1.cls ├── ThisWorkbook.cls ├── Module1.bas # Directory for temporary files (stores VBA code extracted from the Excel file and backup files) .cache ├── Sheet1.cls ├── ThisWorkbook.cls ├── Module1.bas ├── your_xlsm_file_path_here.xlsm.${datetime}.bak # Configuration file generated with the --init option config.json # Main Python script for extracting/writing VBA code sync_vba.py
„sync_vba.py“ ist ein Skript, das VBA-Code aus einer Excel-Datei extrahiert und in einem angegebenen Verzeichnis speichert.
Das Skript bietet die folgenden Funktionen: – Erzeugt die anfängliche Konfigurationsdatei „config.json“ mit der Option „–init“. – Behält die aktuellen Dateien als Master bei, wenn mithilfe der Option „–force“ Unterschiede festgestellt werden
Extrahiert VBA-Code aus der Excel-Datei und speichert ihn im angegebenen Verzeichnis
Schreibt den extrahierten VBA-Code zurück in die Excel-Datei – Wenn Unterschiede gefunden werden, wird der Benutzer aufgefordert, auszuwählen, welche Version als Master beibehalten werden soll
Das Skript verarbeitet die folgenden Schritte der Reihe nach:
def create_config(): config = {"xlsm_file_path": "your_xlsm_file_path_here.xlsm"} with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=4, ensure_ascii=False) print(f"{CONFIG_FILE} has been created. Please edit xlsm_file_path.")
– Lädt die Konfigurationsdatei „config.json“ und gibt sie als Wörterbuch zurück. – Wenn die Konfigurationsdatei nicht vorhanden ist, wird eine Fehlermeldung angezeigt und der Vorgang beendet.
def load_config(): if not os.path.exists(CONFIG_FILE): print( f"{CONFIG_FILE} not found. Please initialize with the --init option." ) sys.exit(1) with open(CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f)
– Extrahiert VBA-Code aus der angegebenen Excel-Datei und speichert ihn im angegebenen Verzeichnis. - „xlsm_file_path“: Pfad zur Ziel-Excel-Datei - „output_dir“: Verzeichnis zum Speichern des extrahierten VBA-Codes (Standard ist „vba“) - „force_master“: Ob die aktuelle Datei als Master beibehalten werden soll, wenn Unterschiede gefunden werden (Standard ist „False“) - Wenn die Excel-Datei nicht vorhanden ist, wird eine Fehlermeldung angezeigt und der Prozess wird beendet. - Der extrahierte VBA-Code wird im Verzeichnis „output_dir“ gespeichert. - Bei Unterschieden wird der Benutzer aufgefordert, auszuwählen, welche Version als Master beibehalten werden soll.
def extract_vba(xlsm_file_path, output_dir="vba", force_master=False): if not os.path.exists(xlsm_file_path): sys.exit(1) if not os.path.exists(output_dir): os.makedirs(output_dir) if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) with zipfile.ZipFile(xlsm_file_path, "r") as z: vba_bin_path = None for f in z.namelist(): if f == "xl/vbaProject.bin": vba_bin_path = f break if not vba_bin_path: sys.exit(1) tmp_bin = os.path.join(CACHE_DIR, "vbaProject.bin") with z.open(vba_bin_path) as src, open(tmp_bin, "wb") as dst: dst.write(src.read()) vba_parser = VBA_Parser(tmp_bin) if not vba_parser.detect_vba_macros(): return for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros(): if vba_code: filtered_code = "\n".join( line for line in vba_code.splitlines() if not line.strip().startswith("Attribute VB_") ) cache_path = os.path.join(CACHE_DIR, vba_filename) with open(cache_path, "w", encoding="shift-jis") as f: f.write(filtered_code) vba_path = os.path.join(output_dir, vba_filename) if os.path.exists(vba_path): try: with open(vba_path, "r", encoding="shift-jis") as f1: old = f1.readlines() except UnicodeDecodeError: with open(vba_path, "r", encoding="utf-8") as f1: content = f1.read() with open(vba_path, "w", encoding="shift-jis") as f1: f1.write(content) old = content.splitlines(keepends=True) with open(cache_path, "r", encoding="shift-jis") as f2: new = f2.readlines() diff = list(difflib.unified_diff(old, new, fromfile=vba_path, tofile=cache_path)) if diff: print(f"\nDiff for {vba_filename}:") print("") print("".join(diff)) print("") print("* + : New content extracted from xlsm file, - : Content in current file)") if force_master: print(f"--force option: {vba_filename} will keep the current file as master.") continue while True: sel = input(f"Difference found in {vba_filename}. Which do you want to keep as master? [k] Keep current file/[n] Overwrite with new extracted content > ").strip().lower() if sel == "k": break elif sel == "n": with open(vba_path, "w", encoding="shift-jis") as f: f.write(filtered_code) break else: with open(vba_path, "w", encoding="shift-jis") as f: f.write(filtered_code)
def write_vba_to_xlsm(xlsm_file_path, vba_dir="vba"): abs_path = os.path.abspath(xlsm_file_path) if not os.path.exists(abs_path): print("The specified xlsm file does not exist. Please check the path.") sys.exit(1) app = xw.App(visible=False) try: wb = xw.Book(abs_path) vba_modules = {mod.Name: mod for mod in wb.api.VBProject.VBComponents} for fname in os.listdir(vba_dir): if fname.startswith("."): continue fpath = os.path.join(vba_dir, fname) if not os.path.isfile(fpath): continue mod_name, ext = os.path.splitext(fname) import re if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", mod_name): print(f"Invalid module name: {mod_name} (Must start with a letter or _, and only contain alphanumeric and _)") sys.exit(1) if mod_name in vba_modules: with open(fpath, "r", encoding="shift-jis") as f: code = f.read() code_module = vba_modules[mod_name].CodeModule code_module.DeleteLines(1, code_module.CountOfLines) code_module.AddFromString(code) else: if ext.lower() == ".bas": kind = 1 elif ext.lower() == ".cls": kind = 2 elif ext.lower() == ".frm": kind = 3 else: continue with open(fpath, "r", encoding="shift-jis") as f: code = f.read() mod = wb.api.VBProject.VBComponents.Add(kind) mod.Name = mod_name mod.CodeModule.AddFromString(code) wb.save() wb.close() except Exception as e: print("Error opening workbook:", e) raise finally: app.quit()
def backup_xlsm(xlsm_file_path): if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) base = os.path.basename(xlsm_file_path) dt = datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"{base}.{dt}.bak" backup_path = os.path.join(CACHE_DIR, backup_name) shutil.copy2(xlsm_file_path, backup_path) backups = sorted( [f for f in os.listdir(CACHE_DIR) if f.startswith(base) and f.endswith(".bak")], reverse=True ) for old in backups[BACKUP_KEEP_COUNT:]: try: os.remove(os.path.join(CACHE_DIR, old)) except Exception: pass
– Führt den Hauptprozess des Skripts aus. - „force_master“: Ob die aktuelle Datei als Master beibehalten werden soll, wenn Unterschiede gefunden werden (Standard ist „False“). - Lädt die Konfigurationsdatei und ruft den Pfad zur Excel-Datei ab. - Erstellt eine Sicherungskopie der Excel-Datei, extrahiert den VBA-Code und schreibt ihn zurück. - Wenn der Vorgang erfolgreich abgeschlossen wird, wird eine Abschlussmeldung angezeigt.
def main(force_master=False): config = load_config() xlsm_file_path = config.get("xlsm_file_path") if not xlsm_file_path: print("xlsm_file_path is not set in config.json.") sys.exit(1) backup_xlsm(xlsm_file_path) extract_vba(xlsm_file_path, force_master=force_master) write_vba_to_xlsm(xlsm_file_path) print("Completed successfully.")
– Der Einstiegspunkt, wenn das Skript direkt ausgeführt wird, ist der Block „if name == „ main “:“. – Das Modul „argparse“ wird zum Parsen von Befehlszeilenargumenten verwendet. - „–init“: Option zum Generieren der Konfigurationsdatei. - „–force“: Option, um die aktuelle Datei als Master beizubehalten, wenn Unterschiede gefunden werden. - Nach dem Parsen der Befehlszeilenargumente wird die Konfigurationsdatei generiert, wenn die Option „–init“ angegeben ist. - Andernfalls wird die Funktion „main()“ aufgerufen, um den Prozess auszuführen.
if __name__ == "__main__": parser = argparse.ArgumentParser( description="VBA module sync tool" ) parser.add_argument("--init", action="store_true", help="Initialize config.json.") parser.add_argument("--force", action="store_true", help="Keep existing vba files as master even if there are differences.") args = parser.parse_args() if args.init: create_config() sys.exit(0) main(force_master=args.force)
– Sie können Visual Studio Code so konfigurieren, dass das Python-Skript automatisch ausgeführt wird und VBA-Code in die Excel-Datei zurückgeschrieben wird, wenn Sie eine VBA-Datei speichern. – Durch die Verwendung der Erweiterung „Run On Save“ für Visual Studio Code können Sie Befehle automatisch ausführen, wenn Dateien gespeichert werden. - Um diese Erweiterung zu verwenden, erstellen Sie die folgenden zwei Dateien in Ihrem Arbeitsbereich (dem Verzeichnis, in dem sich Ihre Excel-Datei befindet).
– Dies ist eine empfohlene Erweiterungsdatei für Visual Studio Code.Speichern Sie den folgenden Inhalt als „.vscode/extensions.json“. - Wenn Sie eine VBA-Datei öffnen, werden die VBA-Erweiterung und die Run On Save-Erweiterung automatisch aktiviert.
{ "recommendations": [ "emeraldwalk.RunOnSave" ] }
– Dies ist die Visual Studio Code-Einstellungsdatei.Speichern Sie den folgenden Inhalt als „.vscode/settings.json“. – Diese Konfiguration führt das Python-Skript automatisch aus, wenn Sie eine VBA-Datei speichern.
{ "emeraldwalk.runonsave": { "commands": [ { "match": ".*\\.(cls|bas)$", "isAsync": false, "cmd": "python .\\sync_vba.py --force" } ] } }