[{"data":1,"prerenderedAt":2268},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file":2236,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file":2245,"children:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file":2250,"related:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file":2251},{"id":4,"title":5,"body":6,"dateModified":2203,"datePublished":2203,"description":2204,"extension":2205,"faq":2206,"keyword":2215,"kind":2216,"meta":2217,"navigation":341,"path":2225,"seo":2226,"slug":2229,"stem":2230,"topics":2231,"type":2234,"__hash__":2235},"docs\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file\u002Findex.md","Extract VBA Macro Code from an Excel File",{"type":7,"value":8,"toc":2190},"minimark",[9,32,37,65,96,272,276,291,483,495,499,514,735,747,926,930,933,1103,1120,1124,1127,1425,1439,1443,1467,1542,1576,1740,1744,1747,2044,2052,2056,2091,2095,2125,2129,2135,2141,2147,2153,2157,2186],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"Macro-enabled workbooks accumulate code that nobody has read in years: the month-end file with a button that \"does the import\", the template whose ",[14,15,16],"code",{},"Workbook_Open"," runs something before anyone sees the sheet. Migrating those macros to Python, reviewing them for security, or simply putting them under version control all start with getting the source code out. Excel stores VBA inside the workbook in a compressed binary container, ",[14,19,20],{},"vbaProject.bin",", and the ",[14,23,24],{},"oletools"," package can decompress and read it on any operating system, without Excel and without running a single line of the macros. This page is part of ",[27,28,30],"a",{"href":29},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install oletools openpyxl\n","bash","",[14,45,46],{"__ignoreMap":43},[47,48,51,55,59,62],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sMTad","pip",[47,56,58],{"class":57},"srMev"," install",[47,60,61],{"class":57}," oletools",[47,63,64],{"class":57}," openpyxl\n",[10,66,67,68,71,72,75,76,79,80,83,84,87,88,91,92,95],{},"The ",[14,69,70],{},"inbox\u002F"," folder holds two macro-enabled workbooks and one ordinary ",[14,73,74],{},".xlsx",". The macro workbooks contain a small VBA project with a standard module, ",[14,77,78],{},"Module1",", holding a ",[14,81,82],{},"say_hello"," procedure, and the class modules Excel creates for the workbook and each sheet. oletools reads ",[14,85,86],{},".xlsm",", ",[14,89,90],{},".xlsb",", legacy ",[14,93,94],{},".xls"," and even Word files the same way.",[97,98,106,107,106,111,106,115,106,123,106,133,106,144,106,151,106,156,106,159,106,165,106,170,106,174,106,181,106,185,106,188,106,191,106,195,106,198,106,201,106,207,106,211,106,213,106,216,106,220,106,223,106,228,106,235,106,241,106,245,106,249,106,252,106,257,106,264,106,269],"svg",{"viewBox":99,"role":100,"ariaLabelledBy":101,"xmlns":104,"style":105},"4 0 752 142","img",[102,103],"vb-flow-t","vb-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[108,109,110],"title",{"id":102},"From a workbook to reviewable source files",[112,113,114],"desc",{"id":103},"The workbook is opened as a zip or OLE container, the vbaProject.bin part is found, oletools decompresses each module's source, empty document modules are skipped, and each remaining module is written to a .bas or .cls file.",[116,117],"rect",{"x":118,"y":119,"width":120,"height":121,"fill":122},"4","0","752","142","#ffffff",[116,124],{"x":125,"y":126,"width":127,"height":128,"rx":129,"fill":130,"stroke":131,"style":132},"20.0","16.0","115.2","110.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[134,135,137],"g",{"transform":136},"translate(65.6,29.0) scale(1.000)",[138,139],"path",{"d":140,"fill":141,"stroke":142,"style":143},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[145,146,150],"text",{"x":147,"y":148,"style":149},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Workbook",[145,152,155],{"x":147,"y":153,"style":154},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",".xlsm \u002F .xlsb \u002F",[145,157,94],{"x":147,"y":158,"style":154},"112.5",[49,160],{"x1":161,"y1":162,"x2":163,"y2":162,"stroke":164,"style":132},"140.2","71.2","162.2","var(--brand,#5b5cf0)",[166,167],"polygon",{"points":168,"fill":169},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[116,171],{"x":172,"y":126,"width":127,"height":128,"rx":129,"fill":173,"stroke":164,"style":132},"171.2","#f0f4ff",[134,175,177],{"transform":176},"translate(216.8,29.0) scale(1.000)",[138,178],{"d":179,"fill":141,"stroke":180,"style":143},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--brand-strong,#4338ca)",[145,182,20],{"x":183,"y":148,"style":184},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[145,186,187],{"x":183,"y":153,"style":154},"compressed",[145,189,190],{"x":183,"y":158,"style":154},"OLE part",[49,192],{"x1":193,"y1":162,"x2":194,"y2":162,"stroke":164,"style":132},"291.4","313.4",[166,196],{"points":197,"fill":169},"317.4,71.2 308.4,76.2 308.4,66.2",[116,199],{"x":200,"y":126,"width":127,"height":128,"rx":129,"fill":173,"stroke":164,"style":132},"322.4",[134,202,204],{"transform":203},"translate(368.0,29.0) scale(1.000)",[138,205],{"d":206,"fill":141,"stroke":180,"style":143},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[145,208,210],{"x":209,"y":148,"style":184},"380.0","Decompress",[145,212,24],{"x":209,"y":153,"style":154},[145,214,215],{"x":209,"y":158,"style":154},"VBA_Parser",[49,217],{"x1":218,"y1":162,"x2":219,"y2":162,"stroke":164,"style":132},"442.6","464.6",[166,221],{"points":222,"fill":169},"468.6,71.2 459.6,76.2 459.6,66.2",[116,224],{"x":225,"y":126,"width":127,"height":128,"rx":129,"fill":226,"stroke":227,"style":132},"473.6","#d9f4f1","var(--teal,#0f9488)",[134,229,231],{"transform":230},"translate(519.2,37.8) scale(1.000)",[138,232],{"d":233,"fill":141,"stroke":234,"style":143},"M3 5h18l-7 8v6l-4 2v-8z","var(--teal-ink,#0b6157)",[145,236,240],{"x":237,"y":238,"style":239},"531.2","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Skip empty",[145,242,244],{"x":237,"y":243,"style":154},"103.8","attributes only",[49,246],{"x1":247,"y1":162,"x2":248,"y2":162,"stroke":164,"style":132},"593.8","615.8",[166,250],{"points":251,"fill":169},"619.8,71.2 610.8,76.2 610.8,66.2",[116,253],{"x":254,"y":126,"width":127,"height":128,"rx":129,"fill":255,"stroke":256,"style":132},"624.8","#fdefd8","var(--gold,#b4740a)",[134,258,260],{"transform":259},"translate(670.4,37.8) scale(1.000)",[138,261],{"d":262,"fill":141,"stroke":263,"style":143},"M6 3h9l4 4v14H6z M15 3v4h4","var(--gold-ink,#7a4e06)",[145,265,268],{"x":266,"y":238,"style":267},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",".bas \u002F .cls",[145,270,271],{"x":266,"y":243,"style":154},"diffable text",[33,273,275],{"id":274},"step-1-does-the-workbook-have-macros","Step 1 — Does the workbook have macros?",[10,277,278,279,281,282,284,285,287,288,290],{},"For ",[14,280,86],{}," and ",[14,283,90],{}," files the VBA project is a part of the zip archive; its presence is a quick first test. ",[14,286,215],{}," gives the definitive answer for every format, including old ",[14,289,94],{}," files that are not zip archives:",[38,292,296],{"className":293,"code":294,"language":295,"meta":43,"style":43},"language-python shiki shiki-themes github-light github-dark-high-contrast","import zipfile\nfrom pathlib import Path\nfrom oletools.olevba import VBA_Parser\n\nfor path in sorted(Path(\"inbox\").iterdir()):\n    in_zip = zipfile.is_zipfile(path) and \"xl\u002FvbaProject.bin\" in zipfile.ZipFile(path).namelist()\n    parser = VBA_Parser(str(path))\n    print(f\"{path.name:18} container={parser.type:9} vbaProject.bin={in_zip!s:5} macros={parser.detect_vba_macros()}\")\n    parser.close()\n","python",[14,297,298,308,322,336,343,367,391,408,477],{"__ignoreMap":43},[47,299,300,304],{"class":49,"line":50},[47,301,303],{"class":302},"s-kum","import",[47,305,307],{"class":306},"skGVy"," zipfile\n",[47,309,311,314,317,319],{"class":49,"line":310},2,[47,312,313],{"class":302},"from",[47,315,316],{"class":306}," pathlib ",[47,318,303],{"class":302},[47,320,321],{"class":306}," Path\n",[47,323,325,327,330,332],{"class":49,"line":324},3,[47,326,313],{"class":302},[47,328,329],{"class":306}," oletools.olevba ",[47,331,303],{"class":302},[47,333,335],{"class":334},"sP0c6"," VBA_Parser\n",[47,337,339],{"class":49,"line":338},4,[47,340,342],{"emptyLinePlaceholder":341},true,"\n",[47,344,346,349,352,355,358,361,364],{"class":49,"line":345},5,[47,347,348],{"class":302},"for",[47,350,351],{"class":306}," path ",[47,353,354],{"class":302},"in",[47,356,357],{"class":334}," sorted",[47,359,360],{"class":306},"(Path(",[47,362,363],{"class":57},"\"inbox\"",[47,365,366],{"class":306},").iterdir()):\n",[47,368,370,373,376,379,382,385,388],{"class":49,"line":369},6,[47,371,372],{"class":306},"    in_zip ",[47,374,375],{"class":302},"=",[47,377,378],{"class":306}," zipfile.is_zipfile(path) ",[47,380,381],{"class":302},"and",[47,383,384],{"class":57}," \"xl\u002FvbaProject.bin\"",[47,386,387],{"class":302}," in",[47,389,390],{"class":306}," zipfile.ZipFile(path).namelist()\n",[47,392,394,397,399,402,405],{"class":49,"line":393},7,[47,395,396],{"class":306},"    parser ",[47,398,375],{"class":302},[47,400,401],{"class":306}," VBA_Parser(",[47,403,404],{"class":334},"str",[47,406,407],{"class":306},"(path))\n",[47,409,411,414,417,420,423,427,430,433,436,439,441,444,447,449,452,454,457,460,462,465,467,470,472,474],{"class":49,"line":410},8,[47,412,413],{"class":334},"    print",[47,415,416],{"class":306},"(",[47,418,419],{"class":302},"f",[47,421,422],{"class":57},"\"",[47,424,426],{"class":425},"sSjpA","{",[47,428,429],{"class":306},"path.name",[47,431,432],{"class":302},":18",[47,434,435],{"class":425},"}",[47,437,438],{"class":57}," container=",[47,440,426],{"class":425},[47,442,443],{"class":306},"parser.type",[47,445,446],{"class":302},":9",[47,448,435],{"class":425},[47,450,451],{"class":57}," vbaProject.bin=",[47,453,426],{"class":425},[47,455,456],{"class":306},"in_zip",[47,458,459],{"class":302},"!s:5",[47,461,435],{"class":425},[47,463,464],{"class":57}," macros=",[47,466,426],{"class":425},[47,468,469],{"class":306},"parser.detect_vba_macros()",[47,471,435],{"class":425},[47,473,422],{"class":57},[47,475,476],{"class":306},")\n",[47,478,480],{"class":49,"line":479},9,[47,481,482],{"class":306},"    parser.close()\n",[10,484,485,486,488,489,491,492,494],{},"The file extension is only a hint — an ",[14,487,74],{}," renamed to ",[14,490,86],{}," has no macros, and a macro file renamed to ",[14,493,74],{}," still does, though Excel refuses to open it with the wrong extension. Always check the content. Neither check runs any code: they read the file’s structure, which makes them safe to run on files from unknown senders.",[33,496,498],{"id":497},"step-2-extract-the-modules","Step 2 — Extract the modules",[10,500,501,504,505,509,510,513],{},[14,502,503],{},"extract_macros()"," yields one tuple per module: the container file, the stream path, the module's file name and its source code. Many modules are ",[506,507,508],"em",{},"document"," modules that Excel creates for the workbook and each sheet; when nobody wrote code in them they contain only ",[14,511,512],{},"Attribute"," lines. Keep the ones with real code:",[38,515,517],{"className":293,"code":516,"language":295,"meta":43,"style":43},"def modules(path):\n    parser = VBA_Parser(str(path))\n    try:\n        for _, stream, filename, code in parser.extract_macros():\n            body = [l for l in code.splitlines() if l.strip() and not l.startswith(\"Attribute \")]\n            yield filename, code, len(body)\n    finally:\n        parser.close()\n\nfor filename, code, lines in modules(\"inbox\u002Fmonth_end.xlsm\"):\n    print(f\"{filename:18} {lines} line(s) of code\")\nprint()\nprint(next(code for filename, code, lines in modules(\"inbox\u002Fmonth_end.xlsm\") if filename == \"Module1.bas\"))\n",[14,518,519,531,543,551,564,604,618,625,630,634,653,686,695],{"__ignoreMap":43},[47,520,521,524,528],{"class":49,"line":50},[47,522,523],{"class":302},"def",[47,525,527],{"class":526},"s_Opv"," modules",[47,529,530],{"class":306},"(path):\n",[47,532,533,535,537,539,541],{"class":49,"line":310},[47,534,396],{"class":306},[47,536,375],{"class":302},[47,538,401],{"class":306},[47,540,404],{"class":334},[47,542,407],{"class":306},[47,544,545,548],{"class":49,"line":324},[47,546,547],{"class":302},"    try",[47,549,550],{"class":306},":\n",[47,552,553,556,559,561],{"class":49,"line":338},[47,554,555],{"class":302},"        for",[47,557,558],{"class":306}," _, stream, filename, code ",[47,560,354],{"class":302},[47,562,563],{"class":306}," parser.extract_macros():\n",[47,565,566,569,571,574,576,579,581,584,587,590,592,595,598,601],{"class":49,"line":345},[47,567,568],{"class":306},"            body ",[47,570,375],{"class":302},[47,572,573],{"class":306}," [l ",[47,575,348],{"class":302},[47,577,578],{"class":306}," l ",[47,580,354],{"class":302},[47,582,583],{"class":306}," code.splitlines() ",[47,585,586],{"class":302},"if",[47,588,589],{"class":306}," l.strip() ",[47,591,381],{"class":302},[47,593,594],{"class":302}," not",[47,596,597],{"class":306}," l.startswith(",[47,599,600],{"class":57},"\"Attribute \"",[47,602,603],{"class":306},")]\n",[47,605,606,609,612,615],{"class":49,"line":369},[47,607,608],{"class":302},"            yield",[47,610,611],{"class":306}," filename, code, ",[47,613,614],{"class":334},"len",[47,616,617],{"class":306},"(body)\n",[47,619,620,623],{"class":49,"line":393},[47,621,622],{"class":302},"    finally",[47,624,550],{"class":306},[47,626,627],{"class":49,"line":410},[47,628,629],{"class":306},"        parser.close()\n",[47,631,632],{"class":49,"line":479},[47,633,342],{"emptyLinePlaceholder":341},[47,635,637,639,642,644,647,650],{"class":49,"line":636},10,[47,638,348],{"class":302},[47,640,641],{"class":306}," filename, code, lines ",[47,643,354],{"class":302},[47,645,646],{"class":306}," modules(",[47,648,649],{"class":57},"\"inbox\u002Fmonth_end.xlsm\"",[47,651,652],{"class":306},"):\n",[47,654,656,658,660,662,664,666,669,671,673,676,679,681,684],{"class":49,"line":655},11,[47,657,413],{"class":334},[47,659,416],{"class":306},[47,661,419],{"class":302},[47,663,422],{"class":57},[47,665,426],{"class":425},[47,667,668],{"class":306},"filename",[47,670,432],{"class":302},[47,672,435],{"class":425},[47,674,675],{"class":425}," {",[47,677,678],{"class":306},"lines",[47,680,435],{"class":425},[47,682,683],{"class":57}," line(s) of code\"",[47,685,476],{"class":306},[47,687,689,692],{"class":49,"line":688},12,[47,690,691],{"class":334},"print",[47,693,694],{"class":306},"()\n",[47,696,698,700,702,705,708,710,712,714,716,718,721,723,726,729,732],{"class":49,"line":697},13,[47,699,691],{"class":334},[47,701,416],{"class":306},[47,703,704],{"class":334},"next",[47,706,707],{"class":306},"(code ",[47,709,348],{"class":302},[47,711,641],{"class":306},[47,713,354],{"class":302},[47,715,646],{"class":306},[47,717,649],{"class":57},[47,719,720],{"class":306},") ",[47,722,586],{"class":302},[47,724,725],{"class":306}," filename ",[47,727,728],{"class":302},"==",[47,730,731],{"class":57}," \"Module1.bas\"",[47,733,734],{"class":306},"))\n",[10,736,737,740,741,743,744,746],{},[14,738,739],{},"Module1.bas"," holds the only real code — the ",[14,742,82],{}," procedure — while the workbook and sheet modules are empty apart from their attributes. The ",[14,745,512],{}," lines are metadata Excel writes into every module: its name, and for document modules the link to the object they belong to. They are needed to re-import a module into Excel, but not for reading it.",[97,748,106,754,106,757,106,760,106,765,106,771,106,775,106,781,106,785,106,789,106,793,106,797,106,801,106,806,106,809,106,813,106,815,106,818,106,820,106,823,106,826,106,830,106,832,106,835,106,837,106,840,106,842,106,845,106,848,106,851,106,853,106,857,106,859,106,862,106,864,106,866,106,869,106,872,106,874,106,877,106,879,106,882,106,884,106,886,106,889,106,893,106,895,106,898,106,900,106,902,106,904,106,906,106,909,106,913,106,915,106,918,106,920,106,922,106,924],{"viewBox":749,"role":100,"ariaLabelledBy":750,"xmlns":104,"style":753},"123 0 513 224",[751,752],"vb-modules-t","vb-modules-d","width:100%;max-width:513px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[108,755,756],{"id":751},"The modules oletools finds in the project",[112,758,759],{"id":752},"A table of the VBA modules in the workbook with their type and the number of lines of real code. The standard module Module1 has three lines; the workbook and sheet document modules have none.",[116,761],{"x":762,"y":119,"width":763,"height":764,"fill":122},"123","513","224",[116,766],{"x":767,"y":126,"width":768,"height":769,"rx":119,"fill":130,"stroke":131,"style":770},"139.9","32.0","24.0","stroke-width:1px",[116,772],{"x":773,"y":126,"width":774,"height":769,"rx":119,"fill":130,"stroke":131,"style":770},"171.9","142.2",[145,776,780],{"x":777,"y":778,"style":779},"242.9","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[116,782],{"x":783,"y":126,"width":784,"height":769,"rx":119,"fill":130,"stroke":131,"style":770},"314.0","199.9",[145,786,788],{"x":787,"y":778,"style":779},"414.0","B",[116,790],{"x":791,"y":126,"width":792,"height":769,"rx":119,"fill":130,"stroke":131,"style":770},"513.9","106.2",[145,794,796],{"x":795,"y":778,"style":779},"567.0","C",[116,798],{"x":767,"y":799,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"40.0","28.0",[145,802,805],{"x":803,"y":804,"style":779},"155.9","59.0","1",[116,807],{"x":773,"y":799,"width":774,"height":800,"rx":119,"fill":173,"stroke":164,"style":808},"stroke-width:1.5px",[145,810,812],{"x":777,"y":804,"style":811},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Module",[116,814],{"x":783,"y":799,"width":784,"height":800,"rx":119,"fill":173,"stroke":164,"style":808},[145,816,817],{"x":787,"y":804,"style":811},"Kind",[116,819],{"x":791,"y":799,"width":792,"height":800,"rx":119,"fill":173,"stroke":164,"style":808},[145,821,822],{"x":795,"y":804,"style":811},"Lines of code",[116,824],{"x":767,"y":825,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"68.0",[145,827,829],{"x":803,"y":828,"style":779},"87.0","2",[116,831],{"x":773,"y":825,"width":774,"height":800,"rx":119,"fill":226,"stroke":227,"style":808},[145,833,739],{"x":777,"y":828,"style":834},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[116,836],{"x":783,"y":825,"width":784,"height":800,"rx":119,"fill":226,"stroke":227,"style":808},[145,838,839],{"x":787,"y":828,"style":834},"standard module",[116,841],{"x":791,"y":825,"width":792,"height":800,"rx":119,"fill":226,"stroke":227,"style":808},[145,843,844],{"x":795,"y":828,"style":834},"3",[116,846],{"x":767,"y":847,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"96.0",[145,849,844],{"x":803,"y":850,"style":779},"115.0",[116,852],{"x":773,"y":847,"width":774,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,854,856],{"x":777,"y":850,"style":855},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","ThisWorkbook.cls",[116,858],{"x":783,"y":847,"width":784,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,860,861],{"x":787,"y":850,"style":855},"workbook document module",[116,863],{"x":791,"y":847,"width":792,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,865,119],{"x":795,"y":850,"style":855},[116,867],{"x":767,"y":868,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"124.0",[145,870,118],{"x":803,"y":871,"style":779},"143.0",[116,873],{"x":773,"y":868,"width":774,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,875,876],{"x":777,"y":871,"style":855},"Sheet1.cls",[116,878],{"x":783,"y":868,"width":784,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,880,881],{"x":787,"y":871,"style":855},"sheet document module",[116,883],{"x":791,"y":868,"width":792,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,885,119],{"x":795,"y":871,"style":855},[116,887],{"x":767,"y":888,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"152.0",[145,890,892],{"x":803,"y":891,"style":779},"171.0","5",[116,894],{"x":773,"y":888,"width":774,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,896,897],{"x":777,"y":891,"style":855},"Sheet2.cls",[116,899],{"x":783,"y":888,"width":784,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,901,881],{"x":787,"y":891,"style":855},[116,903],{"x":791,"y":888,"width":792,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,905,119],{"x":795,"y":891,"style":855},[116,907],{"x":767,"y":908,"width":768,"height":800,"rx":119,"fill":130,"stroke":131,"style":770},"180.0",[145,910,912],{"x":803,"y":911,"style":779},"199.0","6",[116,914],{"x":773,"y":908,"width":774,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,916,917],{"x":777,"y":911,"style":855},"ThisWorkbook1.cls",[116,919],{"x":783,"y":908,"width":784,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,921,861],{"x":787,"y":911,"style":855},[116,923],{"x":791,"y":908,"width":792,"height":800,"rx":119,"fill":122,"stroke":131,"style":770},[145,925,119],{"x":795,"y":911,"style":855},[33,927,929],{"id":928},"step-3-save-them-as-files-for-version-control-and-review","Step 3 — Save them as files for version control and review",[10,931,932],{},"Write each module with real code to its own file, named after the workbook, so the macros can be committed, diffed and reviewed like any other code:",[38,934,936],{"className":293,"code":935,"language":295,"meta":43,"style":43},"def export_modules(path, out_root=\"vba_src\"):\n    out = Path(out_root) \u002F Path(path).stem\n    out.mkdir(parents=True, exist_ok=True)\n    written = []\n    for filename, code, lines in modules(path):\n        if lines:\n            (out \u002F filename).write_text(code.replace(\"\\r\\n\", \"\\n\"), encoding=\"utf-8\")\n            written.append(str(out \u002F filename))\n    return written\n\nprint(export_modules(\"inbox\u002Fmonth_end.xlsm\"))\n",[14,937,938,955,971,996,1006,1018,1026,1065,1080,1088,1092],{"__ignoreMap":43},[47,939,940,942,945,948,950,953],{"class":49,"line":50},[47,941,523],{"class":302},[47,943,944],{"class":526}," export_modules",[47,946,947],{"class":306},"(path, out_root",[47,949,375],{"class":302},[47,951,952],{"class":57},"\"vba_src\"",[47,954,652],{"class":306},[47,956,957,960,962,965,968],{"class":49,"line":310},[47,958,959],{"class":306},"    out ",[47,961,375],{"class":302},[47,963,964],{"class":306}," Path(out_root) ",[47,966,967],{"class":302},"\u002F",[47,969,970],{"class":306}," Path(path).stem\n",[47,972,973,976,980,982,985,987,990,992,994],{"class":49,"line":324},[47,974,975],{"class":306},"    out.mkdir(",[47,977,979],{"class":978},"sa561","parents",[47,981,375],{"class":302},[47,983,984],{"class":334},"True",[47,986,87],{"class":306},[47,988,989],{"class":978},"exist_ok",[47,991,375],{"class":302},[47,993,984],{"class":334},[47,995,476],{"class":306},[47,997,998,1001,1003],{"class":49,"line":338},[47,999,1000],{"class":306},"    written ",[47,1002,375],{"class":302},[47,1004,1005],{"class":306}," []\n",[47,1007,1008,1011,1013,1015],{"class":49,"line":345},[47,1009,1010],{"class":302},"    for",[47,1012,641],{"class":306},[47,1014,354],{"class":302},[47,1016,1017],{"class":306}," modules(path):\n",[47,1019,1020,1023],{"class":49,"line":369},[47,1021,1022],{"class":302},"        if",[47,1024,1025],{"class":306}," lines:\n",[47,1027,1028,1031,1033,1036,1038,1041,1043,1045,1047,1050,1052,1055,1058,1060,1063],{"class":49,"line":393},[47,1029,1030],{"class":306},"            (out ",[47,1032,967],{"class":302},[47,1034,1035],{"class":306}," filename).write_text(code.replace(",[47,1037,422],{"class":57},[47,1039,1040],{"class":425},"\\r\\n",[47,1042,422],{"class":57},[47,1044,87],{"class":306},[47,1046,422],{"class":57},[47,1048,1049],{"class":425},"\\n",[47,1051,422],{"class":57},[47,1053,1054],{"class":306},"), ",[47,1056,1057],{"class":978},"encoding",[47,1059,375],{"class":302},[47,1061,1062],{"class":57},"\"utf-8\"",[47,1064,476],{"class":306},[47,1066,1067,1070,1072,1075,1077],{"class":49,"line":410},[47,1068,1069],{"class":306},"            written.append(",[47,1071,404],{"class":334},[47,1073,1074],{"class":306},"(out ",[47,1076,967],{"class":302},[47,1078,1079],{"class":306}," filename))\n",[47,1081,1082,1085],{"class":49,"line":479},[47,1083,1084],{"class":302},"    return",[47,1086,1087],{"class":306}," written\n",[47,1089,1090],{"class":49,"line":636},[47,1091,342],{"emptyLinePlaceholder":341},[47,1093,1094,1096,1099,1101],{"class":49,"line":655},[47,1095,691],{"class":334},[47,1097,1098],{"class":306},"(export_modules(",[47,1100,649],{"class":57},[47,1102,734],{"class":306},[10,1104,1105,1106,1108,1109,1111,1112,1114,1115,1119],{},"Line endings are normalised to ",[14,1107,1049],{}," — VBA stores them as ",[14,1110,1040],{}," — so git does not show every line as changed between Windows and Linux checkouts. Running the export as a pre-commit hook, or in CI whenever an ",[14,1113,86],{}," changes, keeps a readable history of the macros next to the binary workbook — the same idea as ",[27,1116,1118],{"href":1117},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver\u002F","Diff Excel Files in Git with a textconv Driver"," for cell contents.",[33,1121,1123],{"id":1122},"list-the-procedures-in-each-module","List the procedures in each module",[10,1125,1126],{},"For migration planning, the procedure names and their signatures are more useful than the full text: they are the \"things the workbook does\". A regular expression over the extracted code lists them:",[38,1128,1130],{"className":293,"code":1129,"language":295,"meta":43,"style":43},"import re\n\nPROC = re.compile(r\"^\\s*(?:(Public|Private|Friend)\\s+)?(?:Static\\s+)?(Sub|Function|Property\\s+(?:Get|Let|Set))\\s+(\\w+)\\s*\\(([^)]*)\\)\",\n                  re.IGNORECASE | re.MULTILINE)\n\ndef procedures(path):\n    out = []\n    for filename, code, lines in modules(path):\n        for scope, kind, name, args in PROC.findall(code):\n            out.append({\"module\": filename, \"kind\": kind.title(), \"name\": name,\n                        \"scope\": scope or \"Public\", \"arguments\": args.strip() or \"—\"})\n    return out\n\nprint(procedures(\"inbox\u002Fmonth_end.xlsm\"))\n",[14,1131,1132,1139,1143,1284,1303,1307,1316,1324,1334,1349,1372,1402,1409,1413],{"__ignoreMap":43},[47,1133,1134,1136],{"class":49,"line":50},[47,1135,303],{"class":302},[47,1137,1138],{"class":306}," re\n",[47,1140,1141],{"class":49,"line":310},[47,1142,342],{"emptyLinePlaceholder":341},[47,1144,1145,1148,1151,1154,1157,1159,1162,1165,1168,1171,1174,1177,1179,1182,1185,1188,1191,1194,1197,1200,1203,1205,1207,1209,1211,1214,1216,1219,1221,1224,1226,1228,1230,1233,1235,1238,1240,1243,1246,1248,1251,1253,1255,1257,1261,1264,1267,1269,1272,1274,1276,1279,1281],{"class":49,"line":324},[47,1146,1147],{"class":334},"PROC",[47,1149,1150],{"class":302}," =",[47,1152,1153],{"class":306}," re.compile(",[47,1155,1156],{"class":302},"r",[47,1158,422],{"class":57},[47,1160,1161],{"class":334},"^\\s",[47,1163,1164],{"class":302},"*",[47,1166,1167],{"class":334},"(?:(",[47,1169,1170],{"class":57},"Public",[47,1172,1173],{"class":302},"|",[47,1175,1176],{"class":57},"Private",[47,1178,1173],{"class":302},[47,1180,1181],{"class":57},"Friend",[47,1183,1184],{"class":334},")\\s",[47,1186,1187],{"class":302},"+",[47,1189,1190],{"class":334},")",[47,1192,1193],{"class":302},"?",[47,1195,1196],{"class":334},"(?:",[47,1198,1199],{"class":57},"Static",[47,1201,1202],{"class":334},"\\s",[47,1204,1187],{"class":302},[47,1206,1190],{"class":334},[47,1208,1193],{"class":302},[47,1210,416],{"class":334},[47,1212,1213],{"class":57},"Sub",[47,1215,1173],{"class":302},[47,1217,1218],{"class":57},"Function",[47,1220,1173],{"class":302},[47,1222,1223],{"class":57},"Property",[47,1225,1202],{"class":334},[47,1227,1187],{"class":302},[47,1229,1196],{"class":334},[47,1231,1232],{"class":57},"Get",[47,1234,1173],{"class":302},[47,1236,1237],{"class":57},"Let",[47,1239,1173],{"class":302},[47,1241,1242],{"class":57},"Set",[47,1244,1245],{"class":334},"))\\s",[47,1247,1187],{"class":302},[47,1249,1250],{"class":334},"(\\w",[47,1252,1187],{"class":302},[47,1254,1184],{"class":334},[47,1256,1164],{"class":302},[47,1258,1260],{"class":1259},"s_b0D","\\(",[47,1262,1263],{"class":334},"([",[47,1265,1266],{"class":302},"^",[47,1268,1190],{"class":425},[47,1270,1271],{"class":334},"]",[47,1273,1164],{"class":302},[47,1275,1190],{"class":334},[47,1277,1278],{"class":1259},"\\)",[47,1280,422],{"class":57},[47,1282,1283],{"class":306},",\n",[47,1285,1286,1289,1292,1295,1298,1301],{"class":49,"line":338},[47,1287,1288],{"class":306},"                  re.",[47,1290,1291],{"class":334},"IGNORECASE",[47,1293,1294],{"class":302}," |",[47,1296,1297],{"class":306}," re.",[47,1299,1300],{"class":334},"MULTILINE",[47,1302,476],{"class":306},[47,1304,1305],{"class":49,"line":345},[47,1306,342],{"emptyLinePlaceholder":341},[47,1308,1309,1311,1314],{"class":49,"line":369},[47,1310,523],{"class":302},[47,1312,1313],{"class":526}," procedures",[47,1315,530],{"class":306},[47,1317,1318,1320,1322],{"class":49,"line":393},[47,1319,959],{"class":306},[47,1321,375],{"class":302},[47,1323,1005],{"class":306},[47,1325,1326,1328,1330,1332],{"class":49,"line":410},[47,1327,1010],{"class":302},[47,1329,641],{"class":306},[47,1331,354],{"class":302},[47,1333,1017],{"class":306},[47,1335,1336,1338,1341,1343,1346],{"class":49,"line":479},[47,1337,555],{"class":302},[47,1339,1340],{"class":306}," scope, kind, name, args ",[47,1342,354],{"class":302},[47,1344,1345],{"class":334}," PROC",[47,1347,1348],{"class":306},".findall(code):\n",[47,1350,1351,1354,1357,1360,1363,1366,1369],{"class":49,"line":636},[47,1352,1353],{"class":306},"            out.append({",[47,1355,1356],{"class":57},"\"module\"",[47,1358,1359],{"class":306},": filename, ",[47,1361,1362],{"class":57},"\"kind\"",[47,1364,1365],{"class":306},": kind.title(), ",[47,1367,1368],{"class":57},"\"name\"",[47,1370,1371],{"class":306},": name,\n",[47,1373,1374,1377,1380,1383,1386,1388,1391,1394,1396,1399],{"class":49,"line":655},[47,1375,1376],{"class":57},"                        \"scope\"",[47,1378,1379],{"class":306},": scope ",[47,1381,1382],{"class":302},"or",[47,1384,1385],{"class":57}," \"Public\"",[47,1387,87],{"class":306},[47,1389,1390],{"class":57},"\"arguments\"",[47,1392,1393],{"class":306},": args.strip() ",[47,1395,1382],{"class":302},[47,1397,1398],{"class":57}," \"—\"",[47,1400,1401],{"class":306},"})\n",[47,1403,1404,1406],{"class":49,"line":688},[47,1405,1084],{"class":302},[47,1407,1408],{"class":306}," out\n",[47,1410,1411],{"class":49,"line":697},[47,1412,342],{"emptyLinePlaceholder":341},[47,1414,1416,1418,1421,1423],{"class":49,"line":1415},14,[47,1417,691],{"class":334},[47,1419,1420],{"class":306},"(procedures(",[47,1422,649],{"class":57},[47,1424,734],{"class":306},[10,1426,1427,1429,1430,1432,1433,87,1435,1438],{},[14,1428,82],{}," is the workbook's one procedure: a public ",[14,1431,1213],{}," with no arguments, which in VBA also means it appears in Excel's macro list and can be attached to a button. Event handlers show up the same way — ",[14,1434,16],{},[14,1436,1437],{},"Worksheet_Change"," — and are worth marking, because they run without anyone choosing to run them. Across a folder of workbooks, this list becomes the backlog of a migration: each procedure is a function to rewrite, test and retire.",[33,1440,1442],{"id":1441},"step-4-flag-suspicious-code-before-anyone-enables-it","Step 4 — Flag suspicious code before anyone enables it",[10,1444,1445,1446,1449,1450,1453,1454,87,1457,1459,1460,87,1463,1466],{},"Workbooks from outside the organisation deserve a look before anyone clicks ",[506,1447,1448],{},"Enable Content",". ",[14,1451,1452],{},"analyze_macros()"," scans the code for keywords that matter: procedures that run automatically (",[14,1455,1456],{},"AutoOpen",[14,1458,16],{},"), calls that can reach outside Excel (",[14,1461,1462],{},"Shell",[14,1464,1465],{},"CreateObject",", file and network access) and obfuscation tricks:",[38,1468,1470],{"className":293,"code":1469,"language":295,"meta":43,"style":43},"def review(path):\n    parser = VBA_Parser(str(path))\n    try:\n        return [(kind, keyword) for kind, keyword, _ in parser.analyze_macros()]\n    finally:\n        parser.close()\n\nprint(review(\"inbox\u002Fmonth_end.xlsm\"))\n",[14,1471,1472,1481,1493,1499,1517,1523,1527,1531],{"__ignoreMap":43},[47,1473,1474,1476,1479],{"class":49,"line":50},[47,1475,523],{"class":302},[47,1477,1478],{"class":526}," review",[47,1480,530],{"class":306},[47,1482,1483,1485,1487,1489,1491],{"class":49,"line":310},[47,1484,396],{"class":306},[47,1486,375],{"class":302},[47,1488,401],{"class":306},[47,1490,404],{"class":334},[47,1492,407],{"class":306},[47,1494,1495,1497],{"class":49,"line":324},[47,1496,547],{"class":302},[47,1498,550],{"class":306},[47,1500,1501,1504,1507,1509,1512,1514],{"class":49,"line":338},[47,1502,1503],{"class":302},"        return",[47,1505,1506],{"class":306}," [(kind, keyword) ",[47,1508,348],{"class":302},[47,1510,1511],{"class":306}," kind, keyword, _ ",[47,1513,354],{"class":302},[47,1515,1516],{"class":306}," parser.analyze_macros()]\n",[47,1518,1519,1521],{"class":49,"line":345},[47,1520,622],{"class":302},[47,1522,550],{"class":306},[47,1524,1525],{"class":49,"line":369},[47,1526,629],{"class":306},[47,1528,1529],{"class":49,"line":393},[47,1530,342],{"emptyLinePlaceholder":341},[47,1532,1533,1535,1538,1540],{"class":49,"line":410},[47,1534,691],{"class":334},[47,1536,1537],{"class":306},"(review(",[47,1539,649],{"class":57},[47,1541,734],{"class":306},[10,1543,1544,1545,1548,1549,1551,1552,281,1555,1558,1559,87,1561,1563,1564,1567,1568,1571,1572,1575],{},"The only finding here is ",[506,1546,1547],{},"Hex Strings"," — raised by the GUIDs in the modules' ",[14,1550,512],{}," lines, which look like hexadecimal data. It is a false positive, and a reminder of how to read the output: ",[14,1553,1554],{},"AutoExec",[14,1556,1557],{},"Suspicious"," findings such as ",[14,1560,1462],{},[14,1562,1465],{}," or ",[14,1565,1566],{},"URLDownloadToFile"," in code that also runs automatically are the combination to worry about; a hex string alone is not. ",[14,1569,1570],{},"olevba"," on the command line gives the same analysis as a table, and ",[14,1573,1574],{},"mraptor"," from the same package gives a simple verdict for automated screening.",[97,1577,106,1582,106,1585,106,1588,106,1591,106,1595,106,1600,106,1603,106,1607,106,1611,106,1614,106,1617,106,1625,106,1630,106,1634,106,1640,106,1644,106,1648,106,1651,106,1654,106,1658,106,1661,106,1664,106,1670,106,1674,106,1678,106,1682,106,1685,106,1688,106,1692,106,1695,106,1700,106,1707,106,1711,106,1714,106,1716,106,1719,106,1723,106,1726,106,1729,106,1734,106,1737],{"viewBox":1578,"role":100,"ariaLabelledBy":1579,"xmlns":104,"style":105},"4 0 752 290",[1580,1581],"vb-review-t","vb-review-d",[108,1583,1584],{"id":1580},"Reading the analysis",[112,1586,1587],{"id":1581},"A decision diagram for olevba findings. No macros means nothing to review. Macros with no AutoExec and no suspicious calls are low risk. AutoExec alone deserves a read. AutoExec together with Shell, CreateObject or download calls means do not enable the macros until reviewed.",[116,1589],{"x":118,"y":119,"width":120,"height":1590,"fill":122},"290",[116,1592],{"x":1593,"y":126,"width":1594,"height":148,"rx":129,"fill":173,"stroke":164,"style":132},"233.8","292.4",[134,1596,1597],{"transform":203},[138,1598],{"d":1599,"fill":141,"stroke":180,"style":143},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[145,1601,1602],{"x":209,"y":148,"style":184},"What did analyze_macros report?",[49,1604],{"x1":209,"y1":1605,"x2":209,"y2":1606,"stroke":164,"style":132},"91.5","111.5",[49,1608],{"x1":1609,"y1":1606,"x2":1610,"y2":1606,"stroke":164,"style":132},"104.0","656.0",[49,1612],{"x1":1609,"y1":1606,"x2":1609,"y2":1613,"stroke":164,"style":132},"157.5",[166,1615],{"points":1616,"fill":169},"104.0,161.5 99.0,152.5 109.0,152.5",[116,1618],{"x":1619,"y":1620,"width":1621,"height":1622,"rx":1623,"fill":122,"stroke":131,"style":1624},"72.4","121.5","63.3","21.0","10","stroke-width:1.2px",[145,1626,1629],{"x":1609,"y":1627,"style":1628},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","nothing",[116,1631],{"x":125,"y":1632,"width":1633,"height":128,"rx":129,"fill":226,"stroke":227,"style":132},"163.5","168.0",[134,1635,1637],{"transform":1636},"translate(92.0,185.2) scale(1.000)",[138,1638],{"d":1639,"fill":141,"stroke":234,"style":143},"M5 12.5l4.5 4.5L19 7",[145,1641,1643],{"x":1609,"y":1642,"style":239},"231.8","Low risk",[145,1645,1647],{"x":1609,"y":1646,"style":154},"251.2","read if migrating",[49,1649],{"x1":1650,"y1":1606,"x2":1650,"y2":1613,"stroke":164,"style":132},"288.0",[166,1652],{"points":1653,"fill":169},"288.0,161.5 283.0,152.5 293.0,152.5",[116,1655],{"x":1656,"y":1620,"width":1657,"height":1622,"rx":1623,"fill":122,"stroke":131,"style":1624},"236.3","103.3",[145,1659,1660],{"x":1650,"y":1627,"style":1628},"AutoExec only",[116,1662],{"x":1663,"y":1632,"width":1633,"height":128,"rx":129,"fill":173,"stroke":164,"style":132},"204.0",[134,1665,1667],{"transform":1666},"translate(276.0,176.5) scale(1.000)",[138,1668],{"d":1669,"fill":141,"stroke":180,"style":143},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z",[145,1671,1673],{"x":1650,"y":1672,"style":184},"223.0","Read the open",[145,1675,1677],{"x":1650,"y":1676,"style":184},"240.5","handler",[145,1679,1681],{"x":1650,"y":1680,"style":154},"260.0","runs on open",[49,1683],{"x1":1684,"y1":1606,"x2":1684,"y2":1613,"stroke":164,"style":132},"472.0",[166,1686],{"points":1687,"fill":169},"472.0,161.5 467.0,152.5 477.0,152.5",[116,1689],{"x":1690,"y":1620,"width":1691,"height":1622,"rx":1623,"fill":122,"stroke":131,"style":1624},"381.0","182.0",[145,1693,1694],{"x":1684,"y":1627,"style":1628},"AutoExec + Shell\u002FDownload",[116,1696],{"x":1697,"y":1632,"width":1633,"height":128,"rx":129,"fill":1698,"stroke":1699,"style":132},"388.0","#fce9e9","var(--danger,#dc2626)",[134,1701,1703],{"transform":1702},"translate(460.0,185.2) scale(1.000)",[138,1704],{"d":1705,"fill":141,"stroke":1706,"style":143},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[145,1708,1710],{"x":1684,"y":1642,"style":1709},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Do not enable",[145,1712,1713],{"x":1684,"y":1646,"style":154},"review first",[49,1715],{"x1":1610,"y1":1606,"x2":1610,"y2":1613,"stroke":164,"style":132},[166,1717],{"points":1718,"fill":169},"656.0,161.5 651.0,152.5 661.0,152.5",[116,1720],{"x":1721,"y":1620,"width":1722,"height":1622,"rx":1623,"fill":122,"stroke":131,"style":1624},"599.9","112.2",[145,1724,1725],{"x":1610,"y":1627,"style":1628},"hex strings only",[116,1727],{"x":1728,"y":1632,"width":1633,"height":128,"rx":129,"fill":255,"stroke":256,"style":132},"572.0",[134,1730,1732],{"transform":1731},"translate(644.0,185.2) scale(1.000)",[138,1733],{"d":233,"fill":141,"stroke":263,"style":143},[145,1735,1736],{"x":1610,"y":1642,"style":267},"Usually harmless",[145,1738,1739],{"x":1610,"y":1646,"style":154},"GUIDs, constants",[33,1741,1743],{"id":1742},"step-5-inventory-a-folder-of-workbooks","Step 5 — Inventory a folder of workbooks",[10,1745,1746],{},"Before a migration or an audit, it helps to know which files have macros, how much code, and whether any run on open. One row per workbook:",[38,1748,1750],{"className":293,"code":1749,"language":295,"meta":43,"style":43},"import pandas as pd\n\ndef inventory(folder):\n    rows = []\n    for path in sorted(Path(folder).glob(\"*.xls*\")):\n        parser = VBA_Parser(str(path))\n        has = parser.detect_vba_macros()\n        parser.close()\n        mods = [(f, n) for f, _, n in modules(path)] if has else []\n        findings = review(path) if has else []\n        rows.append({\"file\": path.name, \"macros\": has,\n                     \"modules with code\": sum(1 for _, n in mods if n),\n                     \"lines\": sum(n for _, n in mods),\n                     \"runs on open\": any(k == \"AutoExec\" for k, _ in findings)})\n    return pd.DataFrame(rows)\n\ninv = inventory(\"inbox\")\nprint(inv.to_string(index=False))\ninv.to_excel(\"macro_inventory.xlsx\", index=False)\n",[14,1751,1752,1765,1769,1779,1788,1807,1820,1830,1834,1864,1882,1899,1930,1951,1979,1987,1992,2007,2025],{"__ignoreMap":43},[47,1753,1754,1756,1759,1762],{"class":49,"line":50},[47,1755,303],{"class":302},[47,1757,1758],{"class":306}," pandas ",[47,1760,1761],{"class":302},"as",[47,1763,1764],{"class":306}," pd\n",[47,1766,1767],{"class":49,"line":310},[47,1768,342],{"emptyLinePlaceholder":341},[47,1770,1771,1773,1776],{"class":49,"line":324},[47,1772,523],{"class":302},[47,1774,1775],{"class":526}," inventory",[47,1777,1778],{"class":306},"(folder):\n",[47,1780,1781,1784,1786],{"class":49,"line":338},[47,1782,1783],{"class":306},"    rows ",[47,1785,375],{"class":302},[47,1787,1005],{"class":306},[47,1789,1790,1792,1794,1796,1798,1801,1804],{"class":49,"line":345},[47,1791,1010],{"class":302},[47,1793,351],{"class":306},[47,1795,354],{"class":302},[47,1797,357],{"class":334},[47,1799,1800],{"class":306},"(Path(folder).glob(",[47,1802,1803],{"class":57},"\"*.xls*\"",[47,1805,1806],{"class":306},")):\n",[47,1808,1809,1812,1814,1816,1818],{"class":49,"line":369},[47,1810,1811],{"class":306},"        parser ",[47,1813,375],{"class":302},[47,1815,401],{"class":306},[47,1817,404],{"class":334},[47,1819,407],{"class":306},[47,1821,1822,1825,1827],{"class":49,"line":393},[47,1823,1824],{"class":306},"        has ",[47,1826,375],{"class":302},[47,1828,1829],{"class":306}," parser.detect_vba_macros()\n",[47,1831,1832],{"class":49,"line":410},[47,1833,629],{"class":306},[47,1835,1836,1839,1841,1844,1846,1849,1851,1854,1856,1859,1862],{"class":49,"line":479},[47,1837,1838],{"class":306},"        mods ",[47,1840,375],{"class":302},[47,1842,1843],{"class":306}," [(f, n) ",[47,1845,348],{"class":302},[47,1847,1848],{"class":306}," f, _, n ",[47,1850,354],{"class":302},[47,1852,1853],{"class":306}," modules(path)] ",[47,1855,586],{"class":302},[47,1857,1858],{"class":306}," has ",[47,1860,1861],{"class":302},"else",[47,1863,1005],{"class":306},[47,1865,1866,1869,1871,1874,1876,1878,1880],{"class":49,"line":636},[47,1867,1868],{"class":306},"        findings ",[47,1870,375],{"class":302},[47,1872,1873],{"class":306}," review(path) ",[47,1875,586],{"class":302},[47,1877,1858],{"class":306},[47,1879,1861],{"class":302},[47,1881,1005],{"class":306},[47,1883,1884,1887,1890,1893,1896],{"class":49,"line":655},[47,1885,1886],{"class":306},"        rows.append({",[47,1888,1889],{"class":57},"\"file\"",[47,1891,1892],{"class":306},": path.name, ",[47,1894,1895],{"class":57},"\"macros\"",[47,1897,1898],{"class":306},": has,\n",[47,1900,1901,1904,1907,1910,1912,1914,1917,1920,1922,1925,1927],{"class":49,"line":688},[47,1902,1903],{"class":57},"                     \"modules with code\"",[47,1905,1906],{"class":306},": ",[47,1908,1909],{"class":334},"sum",[47,1911,416],{"class":306},[47,1913,805],{"class":334},[47,1915,1916],{"class":302}," for",[47,1918,1919],{"class":306}," _, n ",[47,1921,354],{"class":302},[47,1923,1924],{"class":306}," mods ",[47,1926,586],{"class":302},[47,1928,1929],{"class":306}," n),\n",[47,1931,1932,1935,1937,1939,1942,1944,1946,1948],{"class":49,"line":697},[47,1933,1934],{"class":57},"                     \"lines\"",[47,1936,1906],{"class":306},[47,1938,1909],{"class":334},[47,1940,1941],{"class":306},"(n ",[47,1943,348],{"class":302},[47,1945,1919],{"class":306},[47,1947,354],{"class":302},[47,1949,1950],{"class":306}," mods),\n",[47,1952,1953,1956,1958,1961,1964,1966,1969,1971,1974,1976],{"class":49,"line":1415},[47,1954,1955],{"class":57},"                     \"runs on open\"",[47,1957,1906],{"class":306},[47,1959,1960],{"class":334},"any",[47,1962,1963],{"class":306},"(k ",[47,1965,728],{"class":302},[47,1967,1968],{"class":57}," \"AutoExec\"",[47,1970,1916],{"class":302},[47,1972,1973],{"class":306}," k, _ ",[47,1975,354],{"class":302},[47,1977,1978],{"class":306}," findings)})\n",[47,1980,1982,1984],{"class":49,"line":1981},15,[47,1983,1084],{"class":302},[47,1985,1986],{"class":306}," pd.DataFrame(rows)\n",[47,1988,1990],{"class":49,"line":1989},16,[47,1991,342],{"emptyLinePlaceholder":341},[47,1993,1995,1998,2000,2003,2005],{"class":49,"line":1994},17,[47,1996,1997],{"class":306},"inv ",[47,1999,375],{"class":302},[47,2001,2002],{"class":306}," inventory(",[47,2004,363],{"class":57},[47,2006,476],{"class":306},[47,2008,2010,2012,2015,2018,2020,2023],{"class":49,"line":2009},18,[47,2011,691],{"class":334},[47,2013,2014],{"class":306},"(inv.to_string(",[47,2016,2017],{"class":978},"index",[47,2019,375],{"class":302},[47,2021,2022],{"class":334},"False",[47,2024,734],{"class":306},[47,2026,2028,2031,2034,2036,2038,2040,2042],{"class":49,"line":2027},19,[47,2029,2030],{"class":306},"inv.to_excel(",[47,2032,2033],{"class":57},"\"macro_inventory.xlsx\"",[47,2035,87],{"class":306},[47,2037,2017],{"class":978},[47,2039,375],{"class":302},[47,2041,2022],{"class":334},[47,2043,476],{"class":306},[10,2045,2046,2047,2051],{},"The inventory is the starting point for ",[27,2048,2050],{"href":2049},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python\u002F","Translate Common VBA Macros to Python",": the files with the most lines and the ones that run on open are where a migration should start.",[33,2053,2055],{"id":2054},"common-pitfalls","Common pitfalls",[2057,2058,2059,2067,2073,2079,2085],"ul",{},[2060,2061,2062,2066],"li",{},[2063,2064,2065],"strong",{},"Trusting the extension."," Check for a VBA project in the content; extensions lie.",[2060,2068,2069,2072],{},[2063,2070,2071],{},"Opening untrusted files in Excel to read their macros."," oletools reads the code without running it; Excel may run it.",[2060,2074,2075,2078],{},[2063,2076,2077],{},"Treating every finding as malicious."," Hex strings and GUIDs are normal; look for automatic execution combined with outside access.",[2060,2080,2081,2084],{},[2063,2082,2083],{},"Losing the attributes."," Keep them in exported files if the modules will be imported back into Excel.",[2060,2086,2087,2090],{},[2063,2088,2089],{},"Password-protected VBA projects."," The protection only hides the code in Excel's editor; oletools can usually still extract it, which is worth knowing both for migrations and for anyone relying on it as security.",[33,2092,2094],{"id":2093},"conclusion","Conclusion",[10,2096,2097,2098,2100,2101,87,2103,281,2105,2107,2108,2111,2112,2114,2115,2117,2118,281,2121,2124],{},"Use oletools' ",[14,2099,215],{}," to detect and extract VBA from ",[14,2102,86],{},[14,2104,90],{},[14,2106,94],{}," files without Excel: ",[14,2109,2110],{},"detect_vba_macros()"," to find macro files regardless of extension, ",[14,2113,503],{}," for each module's source, skipping document modules that hold only attributes, and ",[14,2116,1452],{}," to flag automatic execution and outside access before anyone enables content. Export real modules as ",[14,2119,2120],{},".bas",[14,2122,2123],{},".cls"," files for version control, and inventory whole folders to plan a migration.",[33,2126,2128],{"id":2127},"frequently-asked-questions","Frequently asked questions",[10,2130,2131,2134],{},[2063,2132,2133],{},"How can I read VBA code from an Excel file without opening Excel?","\nInstall oletools and use olevba on the command line or VBA_Parser in Python; extract_macros() returns each module's name and source code. It works on Windows, macOS and Linux and does not run the macros.",[10,2136,2137,2140],{},[2063,2138,2139],{},"Can openpyxl read VBA macros?","\nNo. openpyxl can keep the VBA project when saving with keep_vba=True, but it does not decompress or expose the code. Use oletools to read it.",[10,2142,2143,2146],{},[2063,2144,2145],{},"Why are some extracted modules empty?","\nExcel creates a document module for the workbook and for each sheet. If no code was written in them, they contain only Attribute lines. Skip them when exporting for review.",[10,2148,2149,2152],{},[2063,2150,2151],{},"How do I check a macro workbook for malicious code?","\nRun olevba or VBA_Parser.analyze_macros() and look for AutoExec keywords such as Workbook_Open combined with Suspicious calls such as Shell, CreateObject or download functions. Hex strings on their own are common and usually harmless.",[33,2154,2156],{"id":2155},"related","Related",[2057,2158,2159,2165,2172,2179],{},[2060,2160,2161,2162,2164],{},"Up one level: ",[27,2163,30],{"href":29}," — driving Excel and its macros from Python.",[2060,2166,2167,2171],{},[27,2168,2170],{"href":2169},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fadd-a-vba-macro-to-a-workbook-with-xlsxwriter\u002F","Add a VBA Macro to a Workbook with xlsxwriter"," — the other direction.",[2060,2173,2174,2178],{},[27,2175,2177],{"href":2176},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002F","Work with Macro-Enabled .xlsm Files in openpyxl"," — editing cells without losing the macros.",[2060,2180,2181,2185],{},[27,2182,2184],{"href":2183},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fread-untrusted-excel-files-safely-in-python\u002F","Read Untrusted Excel Files Safely in Python"," — the rest of handling files from outside.",[2187,2188,2189],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":43,"searchDepth":310,"depth":310,"links":2191},[2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202],{"id":35,"depth":310,"text":36},{"id":274,"depth":310,"text":275},{"id":497,"depth":310,"text":498},{"id":928,"depth":310,"text":929},{"id":1122,"depth":310,"text":1123},{"id":1441,"depth":310,"text":1442},{"id":1742,"depth":310,"text":1743},{"id":2054,"depth":310,"text":2055},{"id":2093,"depth":310,"text":2094},{"id":2127,"depth":310,"text":2128},{"id":2155,"depth":310,"text":2156},"2026-09-29","Read the VBA inside .xlsm, .xlsb and .xls files without opening Excel: detect whether a workbook has macros at all, extract every module with oletools, save them as .bas and .cls files that can be diffed and reviewed in git, inventory the macros across a folder, and flag suspicious code before anyone enables it.","md",[2207,2209,2211,2213],{"q":2133,"a":2208},"Install oletools and use olevba on the command line or VBA_Parser in Python; extract_macros() returns each module's name and source code. It works on Windows, macOS and Linux and does not run the macros.",{"q":2139,"a":2210},"No. openpyxl can keep the VBA project when saving with keep_vba=True, but it does not decompress or expose the code. Use oletools to read it.",{"q":2145,"a":2212},"Excel creates a document module for the workbook and for each sheet. If no code was written in them, they contain only Attribute lines. Skip them when exporting for review.",{"q":2151,"a":2214},"Run olevba or VBA_Parser.analyze_macros() and look for AutoExec keywords such as Workbook_Open combined with Suspicious calls such as Shell, CreateObject or download functions. Hex strings on their own are common and usually harmless.","extract vba macro code from an excel file",null,{"breadcrumb":2218},[2219,2221,2224],{"name":2220,"item":967},"Home",{"name":2222,"item":2223},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":30,"item":29},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file",{"title":2227,"description":2228},"Extract VBA Macros from Excel Files with Python (oletools)","Extract VBA macro source from .xlsm, .xlsb and .xls files in Python with oletools VBA_Parser: detect macros, save modules as .bas\u002F.cls, skip empty document modules, analyse for AutoOpen and Shell, and build a macro inventory of a folder.","extract-vba-macro-code-from-an-excel-file","getting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file\u002Findex",[2232,2233],"macros-and-vba","file-formats","how-to","GUjyd6O1CoolXhXVLRtED5sVjsFA5Mcgd29vrFb6fls",[2237,2241],{"title":2238,"path":2239,"stem":2240,"children":-1},"Close Excel Cleanly and Avoid Orphan COM Processes","\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fclose-excel-cleanly-and-avoid-orphan-com-processes","getting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fclose-excel-cleanly-and-avoid-orphan-com-processes\u002Findex",{"title":2242,"path":2243,"stem":2244,"children":-1},"Handle COM Errors and Excel Dialog Prompts in Python","\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fhandle-com-errors-and-excel-dialog-prompts-in-python","getting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fhandle-com-errors-and-excel-dialog-prompts-in-python\u002Findex",[2246,2248],{"path":2247,"title":30},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32",{"path":2249,"title":2222},"\u002Fgetting-started-with-python-excel-automation",[],[2252,2255,2258,2260,2262,2265],{"path":2253,"title":2254,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf","Call Python from Excel with an xlwings UDF",{"path":2256,"title":2257,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example","xlwings: Run a VBA Macro From Python (Example)",{"path":2259,"title":2050,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python",{"path":2261,"title":2170,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fadd-a-vba-macro-to-a-workbook-with-xlsxwriter",{"path":2263,"title":2264,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com","Run an Excel Macro from Python with win32com",{"path":2266,"title":2267,"kind":2234},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frun-python-from-an-excel-button-with-xlwings","Run Python from an Excel Button with xlwings RunPython",1790972055385]