[{"data":1,"prerenderedAt":2575},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet":2546,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet":2554,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet":2559,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet":2560},{"id":4,"title":5,"body":6,"dateModified":2511,"datePublished":2511,"description":2512,"extension":2513,"faq":2514,"keyword":2523,"kind":2524,"meta":2525,"navigation":350,"path":2534,"seo":2535,"slug":2538,"stem":2539,"topics":2540,"type":2544,"__hash__":2545},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet\u002Findex.md","Extract Report Header Fields from an Excel Sheet with Python",{"type":7,"value":8,"toc":2499},"minimark",[9,28,275,280,308,314,648,652,655,678,685,807,810,814,817,1325,1344,1503,1507,1514,1699,1714,1718,1721,1954,1961,1965,1968,2157,2167,2387,2390,2394,2427,2431,2434,2438,2444,2450,2456,2462,2466,2495],[10,11,12,13,17,18,21,22,27],"p",{},"Exports from accounting, ERP and banking systems rarely start with the table. The first rows carry a title, then fields like \"Branch: Leeds\", \"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026\", \"Generated\" with a timestamp, and \"Currency: EUR\" — and only then the column headers. ",[14,15,16],"code",{},"pd.read_excel"," either chokes on that block or, with ",[14,19,20],{},"skiprows",", throws it away, even though those fields are exactly what distinguishes one file from the next. This guide, part of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets",", reads the header fields and the table from the same sheet, wherever the block happens to start.",[29,30,38,39,38,43,38,47,38,55,38,64,38,68,38,75,38,79,38,83,38,87,38,91,38,95,38,100,38,102,38,106,38,108,38,110,38,113,38,117,38,122,38,126,38,128,38,130,38,133,38,136,38,140,38,142,38,145,38,147,38,149,38,152,38,156,38,158,38,161,38,163,38,166,38,168,38,171,38,175,38,177,38,180,38,182,38,185,38,187,38,190,38,194,38,196,38,198,38,200,38,203,38,207,38,211,38,215,38,217,38,220,38,222,38,225,38,228,38,232,38,234,38,238,38,240,38,243,38,245,38,248,38,251,38,255,38,257,38,260,38,262,38,264,38,266,38,269],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"126 0 507 334","img",[34,35],"hf-sheet-t","hf-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:507px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"A report with metadata above the table",[44,45,46],"desc",{"id":35},"The first rows hold a title, a Branch label with its value two cells to the right, a Period written as one text cell, a Generated timestamp and a Currency code. After a blank row, the table header lists Product, Units and Revenue, followed by the data rows.",[48,49],"rect",{"x":50,"y":51,"width":52,"height":53,"fill":54},"126","0","507","334","#ffffff",[48,56],{"x":57,"y":58,"width":59,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"142.7","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[48,65],{"x":66,"y":58,"width":67,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"174.7","225.6",[69,70,74],"text",{"x":71,"y":72,"style":73},"287.5","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,76],{"x":77,"y":58,"width":78,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"400.3","138.9",[69,80,82],{"x":81,"y":72,"style":73},"469.8","B",[48,84],{"x":85,"y":58,"width":86,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"539.3","78.1",[69,88,90],{"x":89,"y":72,"style":73},"578.3","C",[48,92],{"x":57,"y":93,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"40.0","28.0",[69,96,99],{"x":97,"y":98,"style":73},"158.7","59.0","1",[48,101],{"x":66,"y":93,"width":67,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,103,105],{"x":71,"y":98,"style":104},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Sales by Branch",[48,107],{"x":77,"y":93,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,109],{"x":85,"y":93,"width":86,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,111],{"x":57,"y":112,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"68.0",[69,114,116],{"x":97,"y":115,"style":73},"87.0","2",[48,118],{"x":66,"y":112,"width":67,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[69,123,125],{"x":71,"y":115,"style":124},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Branch:",[48,127],{"x":77,"y":112,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,129],{"x":85,"y":112,"width":86,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,131,132],{"x":89,"y":115,"style":124},"York",[48,134],{"x":57,"y":135,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"96.0",[69,137,139],{"x":97,"y":138,"style":73},"115.0","3",[48,141],{"x":66,"y":135,"width":67,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,143,144],{"x":71,"y":138,"style":124},"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026",[48,146],{"x":77,"y":135,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,148],{"x":85,"y":135,"width":86,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,150],{"x":57,"y":151,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"124.0",[69,153,155],{"x":97,"y":154,"style":73},"143.0","4",[48,157],{"x":66,"y":151,"width":67,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,159,160],{"x":71,"y":154,"style":124},"Generated",[48,162],{"x":77,"y":151,"width":78,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,164,165],{"x":81,"y":154,"style":124},"2026-04-02 08:15",[48,167],{"x":85,"y":151,"width":86,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[48,169],{"x":57,"y":170,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"152.0",[69,172,174],{"x":97,"y":173,"style":73},"171.0","5",[48,176],{"x":66,"y":170,"width":67,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,178,179],{"x":71,"y":173,"style":124},"Currency",[48,181],{"x":77,"y":170,"width":78,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[69,183,184],{"x":81,"y":173,"style":124},"EUR",[48,186],{"x":85,"y":170,"width":86,"height":94,"rx":51,"fill":119,"stroke":120,"style":121},[48,188],{"x":57,"y":189,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"180.0",[69,191,193],{"x":97,"y":192,"style":73},"199.0","6",[48,195],{"x":66,"y":189,"width":67,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,197],{"x":77,"y":189,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,199],{"x":85,"y":189,"width":86,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[48,201],{"x":57,"y":202,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"208.0",[69,204,206],{"x":97,"y":205,"style":73},"227.0","7",[48,208],{"x":66,"y":202,"width":67,"height":94,"rx":51,"fill":209,"stroke":210,"style":121},"#f0f4ff","var(--brand,#5b5cf0)",[69,212,214],{"x":71,"y":205,"style":213},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Product",[48,216],{"x":77,"y":202,"width":78,"height":94,"rx":51,"fill":209,"stroke":210,"style":121},[69,218,219],{"x":81,"y":205,"style":213},"Units",[48,221],{"x":85,"y":202,"width":86,"height":94,"rx":51,"fill":209,"stroke":210,"style":121},[69,223,224],{"x":89,"y":205,"style":213},"Revenue",[48,226],{"x":57,"y":227,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"236.0",[69,229,231],{"x":97,"y":230,"style":73},"255.0","8",[48,233],{"x":66,"y":227,"width":67,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,235,237],{"x":71,"y":230,"style":236},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Tea",[48,239],{"x":77,"y":227,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,241,242],{"x":81,"y":230,"style":236},"10",[48,244],{"x":85,"y":227,"width":86,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,246,247],{"x":89,"y":230,"style":236},"120.00",[48,249],{"x":57,"y":250,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"264.0",[69,252,254],{"x":97,"y":253,"style":73},"283.0","9",[48,256],{"x":66,"y":250,"width":67,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,258,259],{"x":71,"y":253,"style":236},"Coffee",[48,261],{"x":77,"y":250,"width":78,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,263,155],{"x":81,"y":253,"style":236},[48,265],{"x":85,"y":250,"width":86,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,267,268],{"x":89,"y":253,"style":236},"88.50",[69,270,274],{"x":271,"y":272,"style":273},"380.0","314.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","gold cells are header fields; blue is the table's own header row",[276,277,279],"h2",{"id":278},"prerequisites","Prerequisites",[281,282,287],"pre",{"className":283,"code":284,"language":285,"meta":286,"style":286},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,288,289],{"__ignoreMap":286},[290,291,294,298,302,305],"span",{"class":292,"line":293},"line",1,[290,295,297],{"class":296},"sMTad","pip",[290,299,301],{"class":300},"srMev"," install",[290,303,304],{"class":300}," pandas",[290,306,307],{"class":300}," openpyxl\n",[10,309,310,311,313],{},"Two sample reports with the same layout, the second shifted down by two rows — the kind of variation that breaks a fixed ",[14,312,20],{},":",[281,315,319],{"className":316,"code":317,"language":318,"meta":286,"style":286},"language-python shiki shiki-themes github-light github-dark-high-contrast","import datetime as dt\nimport re\n\nimport pandas as pd\nfrom openpyxl import Workbook, load_workbook\n\ndef make_report(path, branch, blank_rows_above):\n    wb = Workbook()\n    ws = wb.active\n    for _ in range(blank_rows_above):\n        ws.append([])\n    ws.append([\"Sales by Branch\"])\n    ws.append([\"Branch:\", None, branch])\n    ws.append([\"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026\"])\n    ws.append([\"Generated\", dt.datetime(2026, 4, 2, 8, 15)])\n    ws.append([\"Currency\", \"EUR\"])\n    ws.append([])\n    ws.append([\"Product\", \"Units\", \"Revenue\"])\n    ws.append([\"Tea\", 10, 120.0])\n    ws.append([\"Coffee\", 4, 88.5])\n    wb.save(path)\n\nmake_report(\"leeds.xlsx\", \"Leeds\", 0)\nmake_report(\"york.xlsx\", \"York\", 2)\n","python",[14,320,321,337,345,352,365,379,384,397,409,420,439,445,457,474,484,518,533,539,559,578,597,603,608,629],{"__ignoreMap":286},[290,322,323,327,331,334],{"class":292,"line":293},[290,324,326],{"class":325},"s-kum","import",[290,328,330],{"class":329},"skGVy"," datetime ",[290,332,333],{"class":325},"as",[290,335,336],{"class":329}," dt\n",[290,338,340,342],{"class":292,"line":339},2,[290,341,326],{"class":325},[290,343,344],{"class":329}," re\n",[290,346,348],{"class":292,"line":347},3,[290,349,351],{"emptyLinePlaceholder":350},true,"\n",[290,353,355,357,360,362],{"class":292,"line":354},4,[290,356,326],{"class":325},[290,358,359],{"class":329}," pandas ",[290,361,333],{"class":325},[290,363,364],{"class":329}," pd\n",[290,366,368,371,374,376],{"class":292,"line":367},5,[290,369,370],{"class":325},"from",[290,372,373],{"class":329}," openpyxl ",[290,375,326],{"class":325},[290,377,378],{"class":329}," Workbook, load_workbook\n",[290,380,382],{"class":292,"line":381},6,[290,383,351],{"emptyLinePlaceholder":350},[290,385,387,390,394],{"class":292,"line":386},7,[290,388,389],{"class":325},"def",[290,391,393],{"class":392},"s_Opv"," make_report",[290,395,396],{"class":329},"(path, branch, blank_rows_above):\n",[290,398,400,403,406],{"class":292,"line":399},8,[290,401,402],{"class":329},"    wb ",[290,404,405],{"class":325},"=",[290,407,408],{"class":329}," Workbook()\n",[290,410,412,415,417],{"class":292,"line":411},9,[290,413,414],{"class":329},"    ws ",[290,416,405],{"class":325},[290,418,419],{"class":329}," wb.active\n",[290,421,423,426,429,432,436],{"class":292,"line":422},10,[290,424,425],{"class":325},"    for",[290,427,428],{"class":329}," _ ",[290,430,431],{"class":325},"in",[290,433,435],{"class":434},"sP0c6"," range",[290,437,438],{"class":329},"(blank_rows_above):\n",[290,440,442],{"class":292,"line":441},11,[290,443,444],{"class":329},"        ws.append([])\n",[290,446,448,451,454],{"class":292,"line":447},12,[290,449,450],{"class":329},"    ws.append([",[290,452,453],{"class":300},"\"Sales by Branch\"",[290,455,456],{"class":329},"])\n",[290,458,460,462,465,468,471],{"class":292,"line":459},13,[290,461,450],{"class":329},[290,463,464],{"class":300},"\"Branch:\"",[290,466,467],{"class":329},", ",[290,469,470],{"class":434},"None",[290,472,473],{"class":329},", branch])\n",[290,475,477,479,482],{"class":292,"line":476},14,[290,478,450],{"class":329},[290,480,481],{"class":300},"\"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026\"",[290,483,456],{"class":329},[290,485,487,489,492,495,498,500,502,504,506,508,510,512,515],{"class":292,"line":486},15,[290,488,450],{"class":329},[290,490,491],{"class":300},"\"Generated\"",[290,493,494],{"class":329},", dt.datetime(",[290,496,497],{"class":434},"2026",[290,499,467],{"class":329},[290,501,155],{"class":434},[290,503,467],{"class":329},[290,505,116],{"class":434},[290,507,467],{"class":329},[290,509,231],{"class":434},[290,511,467],{"class":329},[290,513,514],{"class":434},"15",[290,516,517],{"class":329},")])\n",[290,519,521,523,526,528,531],{"class":292,"line":520},16,[290,522,450],{"class":329},[290,524,525],{"class":300},"\"Currency\"",[290,527,467],{"class":329},[290,529,530],{"class":300},"\"EUR\"",[290,532,456],{"class":329},[290,534,536],{"class":292,"line":535},17,[290,537,538],{"class":329},"    ws.append([])\n",[290,540,542,544,547,549,552,554,557],{"class":292,"line":541},18,[290,543,450],{"class":329},[290,545,546],{"class":300},"\"Product\"",[290,548,467],{"class":329},[290,550,551],{"class":300},"\"Units\"",[290,553,467],{"class":329},[290,555,556],{"class":300},"\"Revenue\"",[290,558,456],{"class":329},[290,560,562,564,567,569,571,573,576],{"class":292,"line":561},19,[290,563,450],{"class":329},[290,565,566],{"class":300},"\"Tea\"",[290,568,467],{"class":329},[290,570,242],{"class":434},[290,572,467],{"class":329},[290,574,575],{"class":434},"120.0",[290,577,456],{"class":329},[290,579,581,583,586,588,590,592,595],{"class":292,"line":580},20,[290,582,450],{"class":329},[290,584,585],{"class":300},"\"Coffee\"",[290,587,467],{"class":329},[290,589,155],{"class":434},[290,591,467],{"class":329},[290,593,594],{"class":434},"88.5",[290,596,456],{"class":329},[290,598,600],{"class":292,"line":599},21,[290,601,602],{"class":329},"    wb.save(path)\n",[290,604,606],{"class":292,"line":605},22,[290,607,351],{"emptyLinePlaceholder":350},[290,609,611,614,617,619,622,624,626],{"class":292,"line":610},23,[290,612,613],{"class":329},"make_report(",[290,615,616],{"class":300},"\"leeds.xlsx\"",[290,618,467],{"class":329},[290,620,621],{"class":300},"\"Leeds\"",[290,623,467],{"class":329},[290,625,51],{"class":434},[290,627,628],{"class":329},")\n",[290,630,632,634,637,639,642,644,646],{"class":292,"line":631},24,[290,633,613],{"class":329},[290,635,636],{"class":300},"\"york.xlsx\"",[290,638,467],{"class":329},[290,640,641],{"class":300},"\"York\"",[290,643,467],{"class":329},[290,645,116],{"class":434},[290,647,628],{"class":329},[276,649,651],{"id":650},"step-1-recognise-the-three-kinds-of-header-line","Step 1 — Recognise the three kinds of header line",[10,653,654],{},"The block above the table mixes three shapes:",[656,657,658,666,672],"ul",{},[659,660,661,665],"li",{},[662,663,664],"strong",{},"A label and a value in separate cells"," — \"Branch:\" in A, \"York\" in C, with an empty or merged cell between.",[659,667,668,671],{},[662,669,670],{},"A label and a value in one text cell"," — \"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026\".",[659,673,674,677],{},[662,675,676],{},"A title"," — a single text cell with no colon.",[10,679,680,681,684],{},"Scanning with ",[14,682,683],{},"values_only=True"," and dropping empty cells turns the first shape into a simple pair, whatever the spacing. A regular expression splits the second. Anything else with a single text value is the title.",[281,686,688],{"className":316,"code":687,"language":318,"meta":286,"style":286},"INLINE = re.compile(r\"^\\s*([A-Za-z][A-Za-z ]{0,30}?)\\s*:\\s*(.+?)\\s*$\")\n\nprint(INLINE.match(\"Period: 01\u002F01\u002F2026 - 31\u002F03\u002F2026\").groups())\n# ('Period', '01\u002F01\u002F2026 - 31\u002F03\u002F2026')\nprint(INLINE.match(\"Sales by Branch\"))            # None\n",[14,689,690,761,765,783,789],{"__ignoreMap":286},[290,691,692,695,698,701,704,707,710,713,716,720,723,726,729,732,735,737,739,742,744,747,750,752,754,757,759],{"class":292,"line":293},[290,693,694],{"class":434},"INLINE",[290,696,697],{"class":325}," =",[290,699,700],{"class":329}," re.compile(",[290,702,703],{"class":325},"r",[290,705,706],{"class":300},"\"",[290,708,709],{"class":434},"^\\s",[290,711,712],{"class":325},"*",[290,714,715],{"class":434},"([",[290,717,719],{"class":718},"sSjpA","A-Za-z",[290,721,722],{"class":434},"][",[290,724,725],{"class":718},"A-Za-z ",[290,727,728],{"class":434},"]",[290,730,731],{"class":325},"{0,30}?",[290,733,734],{"class":434},")\\s",[290,736,712],{"class":325},[290,738,313],{"class":300},[290,740,741],{"class":434},"\\s",[290,743,712],{"class":325},[290,745,746],{"class":434},"(.",[290,748,749],{"class":325},"+?",[290,751,734],{"class":434},[290,753,712],{"class":325},[290,755,756],{"class":434},"$",[290,758,706],{"class":300},[290,760,628],{"class":329},[290,762,763],{"class":292,"line":339},[290,764,351],{"emptyLinePlaceholder":350},[290,766,767,770,773,775,778,780],{"class":292,"line":347},[290,768,769],{"class":434},"print",[290,771,772],{"class":329},"(",[290,774,694],{"class":434},[290,776,777],{"class":329},".match(",[290,779,481],{"class":300},[290,781,782],{"class":329},").groups())\n",[290,784,785],{"class":292,"line":354},[290,786,788],{"class":787},"s-wDw","# ('Period', '01\u002F01\u002F2026 - 31\u002F03\u002F2026')\n",[290,790,791,793,795,797,799,801,804],{"class":292,"line":367},[290,792,769],{"class":434},[290,794,772],{"class":329},[290,796,694],{"class":434},[290,798,777],{"class":329},[290,800,453],{"class":300},[290,802,803],{"class":329},"))            ",[290,805,806],{"class":787},"# None\n",[10,808,809],{},"Limiting the label to 30 letters and spaces stops a sentence that happens to contain a colon — a footnote, say — from being taken as a field.",[276,811,813],{"id":812},"step-2-scan-until-the-table-header","Step 2 — Scan until the table header",[10,815,816],{},"Read rows from the top, collecting fields, until you reach the row that contains the table's column headers. Its position is also what pandas needs to read the table:",[281,818,820],{"className":316,"code":819,"language":318,"meta":286,"style":286},"def read_report(path, table_columns=(\"Product\", \"Units\", \"Revenue\"), max_rows=30):\n    ws = load_workbook(path, read_only=True, data_only=True).active\n    fields, title, header_row = {}, None, None\n    for idx, row in enumerate(ws.iter_rows(max_row=max_rows, values_only=True)):\n        values = [v for v in row if v not in (None, \"\")]\n        if not values:\n            continue\n        if set(table_columns) \u003C= {str(v).strip() for v in values}:\n            header_row = idx\n            break\n        first = values[0]\n        if isinstance(first, str) and len(values) == 1:\n            match = INLINE.match(first)\n            if match:\n                fields[match.group(1).lower()] = match.group(2)\n            elif title is None:\n                title = first.strip()\n        elif isinstance(first, str):\n            fields[first.strip().rstrip(\":\").strip().lower()] = values[1]\n    if header_row is None:\n        raise ValueError(f\"{path}: columns {table_columns} not found in the first {max_rows} rows\")\n    table = pd.read_excel(path, header=header_row)\n    return title, fields, table\n\ntitle, fields, table = read_report(\"york.xlsx\")\nprint(title)       # Sales by Branch\nprint(fields)\nprint(table)\n",[14,821,822,857,887,904,937,982,993,998,1029,1039,1044,1059,1092,1105,1113,1132,1148,1158,1171,1190,1204,1253,1271,1279,1283,1298,1309,1317],{"__ignoreMap":286},[290,823,824,826,829,832,834,836,838,840,842,844,846,849,851,854],{"class":292,"line":293},[290,825,389],{"class":325},[290,827,828],{"class":392}," read_report",[290,830,831],{"class":329},"(path, table_columns",[290,833,405],{"class":325},[290,835,772],{"class":329},[290,837,546],{"class":300},[290,839,467],{"class":329},[290,841,551],{"class":300},[290,843,467],{"class":329},[290,845,556],{"class":300},[290,847,848],{"class":329},"), max_rows",[290,850,405],{"class":325},[290,852,853],{"class":434},"30",[290,855,856],{"class":329},"):\n",[290,858,859,861,863,866,870,872,875,877,880,882,884],{"class":292,"line":339},[290,860,414],{"class":329},[290,862,405],{"class":325},[290,864,865],{"class":329}," load_workbook(path, ",[290,867,869],{"class":868},"sa561","read_only",[290,871,405],{"class":325},[290,873,874],{"class":434},"True",[290,876,467],{"class":329},[290,878,879],{"class":868},"data_only",[290,881,405],{"class":325},[290,883,874],{"class":434},[290,885,886],{"class":329},").active\n",[290,888,889,892,894,897,899,901],{"class":292,"line":347},[290,890,891],{"class":329},"    fields, title, header_row ",[290,893,405],{"class":325},[290,895,896],{"class":329}," {}, ",[290,898,470],{"class":434},[290,900,467],{"class":329},[290,902,903],{"class":434},"None\n",[290,905,906,908,911,913,916,919,922,924,927,930,932,934],{"class":292,"line":354},[290,907,425],{"class":325},[290,909,910],{"class":329}," idx, row ",[290,912,431],{"class":325},[290,914,915],{"class":434}," enumerate",[290,917,918],{"class":329},"(ws.iter_rows(",[290,920,921],{"class":868},"max_row",[290,923,405],{"class":325},[290,925,926],{"class":329},"max_rows, ",[290,928,929],{"class":868},"values_only",[290,931,405],{"class":325},[290,933,874],{"class":434},[290,935,936],{"class":329},")):\n",[290,938,939,942,944,947,950,953,955,958,961,963,966,969,972,974,976,979],{"class":292,"line":367},[290,940,941],{"class":329},"        values ",[290,943,405],{"class":325},[290,945,946],{"class":329}," [v ",[290,948,949],{"class":325},"for",[290,951,952],{"class":329}," v ",[290,954,431],{"class":325},[290,956,957],{"class":329}," row ",[290,959,960],{"class":325},"if",[290,962,952],{"class":329},[290,964,965],{"class":325},"not",[290,967,968],{"class":325}," in",[290,970,971],{"class":329}," (",[290,973,470],{"class":434},[290,975,467],{"class":329},[290,977,978],{"class":300},"\"\"",[290,980,981],{"class":329},")]\n",[290,983,984,987,990],{"class":292,"line":381},[290,985,986],{"class":325},"        if",[290,988,989],{"class":325}," not",[290,991,992],{"class":329}," values:\n",[290,994,995],{"class":292,"line":386},[290,996,997],{"class":325},"            continue\n",[290,999,1000,1002,1005,1008,1011,1014,1017,1020,1022,1024,1026],{"class":292,"line":399},[290,1001,986],{"class":325},[290,1003,1004],{"class":434}," set",[290,1006,1007],{"class":329},"(table_columns) ",[290,1009,1010],{"class":325},"\u003C=",[290,1012,1013],{"class":329}," {",[290,1015,1016],{"class":434},"str",[290,1018,1019],{"class":329},"(v).strip() ",[290,1021,949],{"class":325},[290,1023,952],{"class":329},[290,1025,431],{"class":325},[290,1027,1028],{"class":329}," values}:\n",[290,1030,1031,1034,1036],{"class":292,"line":411},[290,1032,1033],{"class":329},"            header_row ",[290,1035,405],{"class":325},[290,1037,1038],{"class":329}," idx\n",[290,1040,1041],{"class":292,"line":422},[290,1042,1043],{"class":325},"            break\n",[290,1045,1046,1049,1051,1054,1056],{"class":292,"line":441},[290,1047,1048],{"class":329},"        first ",[290,1050,405],{"class":325},[290,1052,1053],{"class":329}," values[",[290,1055,51],{"class":434},[290,1057,1058],{"class":329},"]\n",[290,1060,1061,1063,1066,1069,1071,1074,1077,1080,1083,1086,1089],{"class":292,"line":447},[290,1062,986],{"class":325},[290,1064,1065],{"class":434}," isinstance",[290,1067,1068],{"class":329},"(first, ",[290,1070,1016],{"class":434},[290,1072,1073],{"class":329},") ",[290,1075,1076],{"class":325},"and",[290,1078,1079],{"class":434}," len",[290,1081,1082],{"class":329},"(values) ",[290,1084,1085],{"class":325},"==",[290,1087,1088],{"class":434}," 1",[290,1090,1091],{"class":329},":\n",[290,1093,1094,1097,1099,1102],{"class":292,"line":459},[290,1095,1096],{"class":329},"            match ",[290,1098,405],{"class":325},[290,1100,1101],{"class":434}," INLINE",[290,1103,1104],{"class":329},".match(first)\n",[290,1106,1107,1110],{"class":292,"line":476},[290,1108,1109],{"class":325},"            if",[290,1111,1112],{"class":329}," match:\n",[290,1114,1115,1118,1120,1123,1125,1128,1130],{"class":292,"line":486},[290,1116,1117],{"class":329},"                fields[match.group(",[290,1119,99],{"class":434},[290,1121,1122],{"class":329},").lower()] ",[290,1124,405],{"class":325},[290,1126,1127],{"class":329}," match.group(",[290,1129,116],{"class":434},[290,1131,628],{"class":329},[290,1133,1134,1137,1140,1143,1146],{"class":292,"line":520},[290,1135,1136],{"class":325},"            elif",[290,1138,1139],{"class":329}," title ",[290,1141,1142],{"class":325},"is",[290,1144,1145],{"class":434}," None",[290,1147,1091],{"class":329},[290,1149,1150,1153,1155],{"class":292,"line":535},[290,1151,1152],{"class":329},"                title ",[290,1154,405],{"class":325},[290,1156,1157],{"class":329}," first.strip()\n",[290,1159,1160,1163,1165,1167,1169],{"class":292,"line":541},[290,1161,1162],{"class":325},"        elif",[290,1164,1065],{"class":434},[290,1166,1068],{"class":329},[290,1168,1016],{"class":434},[290,1170,856],{"class":329},[290,1172,1173,1176,1179,1182,1184,1186,1188],{"class":292,"line":561},[290,1174,1175],{"class":329},"            fields[first.strip().rstrip(",[290,1177,1178],{"class":300},"\":\"",[290,1180,1181],{"class":329},").strip().lower()] ",[290,1183,405],{"class":325},[290,1185,1053],{"class":329},[290,1187,99],{"class":434},[290,1189,1058],{"class":329},[290,1191,1192,1195,1198,1200,1202],{"class":292,"line":580},[290,1193,1194],{"class":325},"    if",[290,1196,1197],{"class":329}," header_row ",[290,1199,1142],{"class":325},[290,1201,1145],{"class":434},[290,1203,1091],{"class":329},[290,1205,1206,1209,1212,1214,1217,1219,1222,1225,1228,1231,1233,1236,1238,1241,1243,1246,1248,1251],{"class":292,"line":599},[290,1207,1208],{"class":325},"        raise",[290,1210,1211],{"class":434}," ValueError",[290,1213,772],{"class":329},[290,1215,1216],{"class":325},"f",[290,1218,706],{"class":300},[290,1220,1221],{"class":718},"{",[290,1223,1224],{"class":329},"path",[290,1226,1227],{"class":718},"}",[290,1229,1230],{"class":300},": columns ",[290,1232,1221],{"class":718},[290,1234,1235],{"class":329},"table_columns",[290,1237,1227],{"class":718},[290,1239,1240],{"class":300}," not found in the first ",[290,1242,1221],{"class":718},[290,1244,1245],{"class":329},"max_rows",[290,1247,1227],{"class":718},[290,1249,1250],{"class":300}," rows\"",[290,1252,628],{"class":329},[290,1254,1255,1258,1260,1263,1266,1268],{"class":292,"line":605},[290,1256,1257],{"class":329},"    table ",[290,1259,405],{"class":325},[290,1261,1262],{"class":329}," pd.read_excel(path, ",[290,1264,1265],{"class":868},"header",[290,1267,405],{"class":325},[290,1269,1270],{"class":329},"header_row)\n",[290,1272,1273,1276],{"class":292,"line":610},[290,1274,1275],{"class":325},"    return",[290,1277,1278],{"class":329}," title, fields, table\n",[290,1280,1281],{"class":292,"line":631},[290,1282,351],{"emptyLinePlaceholder":350},[290,1284,1286,1289,1291,1294,1296],{"class":292,"line":1285},25,[290,1287,1288],{"class":329},"title, fields, table ",[290,1290,405],{"class":325},[290,1292,1293],{"class":329}," read_report(",[290,1295,636],{"class":300},[290,1297,628],{"class":329},[290,1299,1301,1303,1306],{"class":292,"line":1300},26,[290,1302,769],{"class":434},[290,1304,1305],{"class":329},"(title)       ",[290,1307,1308],{"class":787},"# Sales by Branch\n",[290,1310,1312,1314],{"class":292,"line":1311},27,[290,1313,769],{"class":434},[290,1315,1316],{"class":329},"(fields)\n",[290,1318,1320,1322],{"class":292,"line":1319},28,[290,1321,769],{"class":434},[290,1323,1324],{"class":329},"(table)\n",[10,1326,1327,1328,1331,1332,1334,1335,1338,1339,1343],{},"The York file has two extra blank rows at the top, and it still works: the scan counts every row, and ",[14,1329,1330],{},"header="," takes the same zero-based position. Opening in ",[14,1333,869],{}," mode keeps the scan cheap even on large exports, since only the first rows are read. The error names the file and the missing columns when a report has changed layout, which is far easier to act on than a ",[14,1336,1337],{},"KeyError"," later. Locating header rows by content is covered further in ",[23,1340,1342],{"href":1341},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002F","Find the Header Row in an Excel Sheet with a Shifting Layout",".",[29,1345,38,1351,38,1354,38,1357,38,1361,38,1368,38,1378,38,1384,38,1389,38,1394,38,1399,38,1402,38,1408,38,1412,38,1415,38,1419,38,1422,38,1425,38,1431,38,1435,38,1438,38,1441,38,1445,38,1449,38,1452,38,1455,38,1462,38,1467,38,1470,38,1474,38,1477,38,1482,38,1489,38,1495,38,1499],{"viewBox":1346,"role":32,"ariaLabelledBy":1347,"xmlns":36,"style":1350},"4 0 752 160",[1348,1349],"hf-scan-t","hf-scan-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1352,1353],{"id":1348},"One pass over the top of the sheet",[44,1355,1356],{"id":1349},"Rows are read from the top. Empty rows are skipped, rows with a label and value become fields, a single inline Label colon value cell is split into a field, the first other text is the title, and the scan stops at the row that contains the table's column names, whose index is passed to pandas.",[48,1358],{"x":155,"y":51,"width":1359,"height":1360,"fill":54},"752","160",[48,1362],{"x":1363,"y":58,"width":1364,"height":1365,"rx":1366,"fill":209,"stroke":210,"style":1367},"20.0","115.2","128.0","12","stroke-width:2px",[1369,1370,1372],"g",{"transform":1371},"translate(65.6,46.5) scale(1.000)",[1224,1373],{"d":1374,"fill":1375,"stroke":1376,"style":1377},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[69,1379,1383],{"x":1380,"y":1381,"style":1382},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Read a row",[69,1385,1388],{"x":1380,"y":1386,"style":1387},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","values only",[292,1390],{"x1":1391,"y1":1392,"x2":1393,"y2":1392,"stroke":210,"style":1367},"140.2","80.0","162.2",[1395,1396],"polygon",{"points":1397,"fill":1398},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[48,1400],{"x":1401,"y":58,"width":1364,"height":1365,"rx":1366,"fill":209,"stroke":210,"style":1367},"171.2",[1369,1403,1405],{"transform":1404},"translate(216.8,46.5) scale(1.000)",[1224,1406],{"d":1407,"fill":1375,"stroke":1376,"style":1377},"M3 5h18l-7 8v6l-4 2v-8z",[69,1409,1411],{"x":1410,"y":1381,"style":1382},"228.8","Skip if empty",[69,1413,1414],{"x":1410,"y":1386,"style":1387},"spacer rows",[292,1416],{"x1":1417,"y1":1392,"x2":1418,"y2":1392,"stroke":210,"style":1367},"291.4","313.4",[1395,1420],{"points":1421,"fill":1398},"317.4,80.0 308.4,85.0 308.4,75.0",[48,1423],{"x":1424,"y":58,"width":1364,"height":1365,"rx":1366,"fill":209,"stroke":210,"style":1367},"322.4",[1369,1426,1428],{"transform":1427},"translate(368.0,29.0) scale(1.000)",[1224,1429],{"d":1430,"fill":1375,"stroke":1376,"style":1377},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[69,1432,1434],{"x":271,"y":1433,"style":1382},"75.5","Table",[69,1436,1437],{"x":271,"y":1381,"style":1382},"header?",[69,1439,1440],{"x":271,"y":1386,"style":1387},"stop, keep",[69,1442,1444],{"x":271,"y":1443,"style":1387},"130.0","index",[292,1446],{"x1":1447,"y1":1392,"x2":1448,"y2":1392,"stroke":210,"style":1367},"442.6","464.6",[1395,1450],{"points":1451,"fill":1398},"468.6,80.0 459.6,85.0 459.6,75.0",[48,1453],{"x":1454,"y":58,"width":1364,"height":1365,"rx":1366,"fill":119,"stroke":120,"style":1367},"473.6",[1369,1456,1458],{"transform":1457},"translate(519.2,46.5) scale(1.000)",[1224,1459],{"d":1460,"fill":1375,"stroke":1461,"style":1377},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1","var(--gold-ink,#7a4e06)",[69,1463,1466],{"x":1464,"y":1381,"style":1465},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Pair or inline",[69,1468,1469],{"x":1464,"y":1386,"style":1387},"label → value",[292,1471],{"x1":1472,"y1":1392,"x2":1473,"y2":1392,"stroke":210,"style":1367},"593.8","615.8",[1395,1475],{"points":1476,"fill":1398},"619.8,80.0 610.8,85.0 610.8,75.0",[48,1478],{"x":1479,"y":58,"width":1364,"height":1365,"rx":1366,"fill":1480,"stroke":1481,"style":1367},"624.8","#d9f4f1","var(--teal,#0f9488)",[1369,1483,1485],{"transform":1484},"translate(670.4,37.8) scale(1.000)",[1224,1486],{"d":1487,"fill":1375,"stroke":1488,"style":1377},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--teal-ink,#0b6157)",[69,1490,1494],{"x":1491,"y":1492,"style":1493},"682.4","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","pandas",[69,1496,1498],{"x":1491,"y":1497,"style":1493},"101.8","reads table",[69,1500,1502],{"x":1491,"y":1501,"style":1387},"121.2","header=index",[276,1504,1506],{"id":1505},"step-3-turn-field-text-into-typed-values","Step 3 — Turn field text into typed values",[10,1508,1509,1510,1513],{},"Header fields arrive as text or as whatever type Excel stored. The period is text that needs splitting into two dates; the generated timestamp is already a ",[14,1511,1512],{},"datetime"," because the cell held a real date:",[281,1515,1517],{"className":316,"code":1516,"language":318,"meta":286,"style":286},"def parse_period(text):\n    parts = re.split(r\"\\s+(?:-|–|to)\\s+\", text.strip())\n    if len(parts) != 2:\n        raise ValueError(f\"cannot parse period {text!r}\")\n    return [pd.to_datetime(p, dayfirst=True) for p in parts]\n\nstart, end = parse_period(fields[\"period\"])\nprint(start.date(), end.date())                  # 2026-01-01 2026-03-31\nprint(type(fields[\"generated\"]).__name__)        # datetime\n",[14,1518,1519,1529,1574,1591,1617,1643,1647,1662,1672],{"__ignoreMap":286},[290,1520,1521,1523,1526],{"class":292,"line":293},[290,1522,389],{"class":325},[290,1524,1525],{"class":392}," parse_period",[290,1527,1528],{"class":329},"(text):\n",[290,1530,1531,1534,1536,1539,1541,1543,1545,1548,1551,1554,1557,1560,1562,1565,1567,1569,1571],{"class":292,"line":339},[290,1532,1533],{"class":329},"    parts ",[290,1535,405],{"class":325},[290,1537,1538],{"class":329}," re.split(",[290,1540,703],{"class":325},[290,1542,706],{"class":300},[290,1544,741],{"class":434},[290,1546,1547],{"class":325},"+",[290,1549,1550],{"class":434},"(?:",[290,1552,1553],{"class":300},"-",[290,1555,1556],{"class":325},"|",[290,1558,1559],{"class":300},"–",[290,1561,1556],{"class":325},[290,1563,1564],{"class":300},"to",[290,1566,734],{"class":434},[290,1568,1547],{"class":325},[290,1570,706],{"class":300},[290,1572,1573],{"class":329},", text.strip())\n",[290,1575,1576,1578,1580,1583,1586,1589],{"class":292,"line":347},[290,1577,1194],{"class":325},[290,1579,1079],{"class":434},[290,1581,1582],{"class":329},"(parts) ",[290,1584,1585],{"class":325},"!=",[290,1587,1588],{"class":434}," 2",[290,1590,1091],{"class":329},[290,1592,1593,1595,1597,1599,1601,1604,1606,1608,1611,1613,1615],{"class":292,"line":354},[290,1594,1208],{"class":325},[290,1596,1211],{"class":434},[290,1598,772],{"class":329},[290,1600,1216],{"class":325},[290,1602,1603],{"class":300},"\"cannot parse period ",[290,1605,1221],{"class":718},[290,1607,69],{"class":329},[290,1609,1610],{"class":325},"!r",[290,1612,1227],{"class":718},[290,1614,706],{"class":300},[290,1616,628],{"class":329},[290,1618,1619,1621,1624,1627,1629,1631,1633,1635,1638,1640],{"class":292,"line":367},[290,1620,1275],{"class":325},[290,1622,1623],{"class":329}," [pd.to_datetime(p, ",[290,1625,1626],{"class":868},"dayfirst",[290,1628,405],{"class":325},[290,1630,874],{"class":434},[290,1632,1073],{"class":329},[290,1634,949],{"class":325},[290,1636,1637],{"class":329}," p ",[290,1639,431],{"class":325},[290,1641,1642],{"class":329}," parts]\n",[290,1644,1645],{"class":292,"line":381},[290,1646,351],{"emptyLinePlaceholder":350},[290,1648,1649,1652,1654,1657,1660],{"class":292,"line":386},[290,1650,1651],{"class":329},"start, end ",[290,1653,405],{"class":325},[290,1655,1656],{"class":329}," parse_period(fields[",[290,1658,1659],{"class":300},"\"period\"",[290,1661,456],{"class":329},[290,1663,1664,1666,1669],{"class":292,"line":399},[290,1665,769],{"class":434},[290,1667,1668],{"class":329},"(start.date(), end.date())                  ",[290,1670,1671],{"class":787},"# 2026-01-01 2026-03-31\n",[290,1673,1674,1676,1678,1681,1684,1687,1690,1693,1696],{"class":292,"line":411},[290,1675,769],{"class":434},[290,1677,772],{"class":329},[290,1679,1680],{"class":434},"type",[290,1682,1683],{"class":329},"(fields[",[290,1685,1686],{"class":300},"\"generated\"",[290,1688,1689],{"class":329},"]).",[290,1691,1692],{"class":434},"__name__",[290,1694,1695],{"class":329},")        ",[290,1697,1698],{"class":787},"# datetime\n",[10,1700,1701,1702,1705,1706,1709,1710,1713],{},"Splitting on a dash surrounded by spaces, rather than on any dash, keeps ISO dates such as ",[14,1703,1704],{},"2026-01-01 - 2026-03-31"," intact. ",[14,1707,1708],{},"dayfirst=True"," is essential for European exports: without it, 01\u002F03\u002F2026 would be read as 3 January. If a system writes dates in more than one style, parse with an explicit ",[14,1711,1712],{},"format="," and let it fail loudly.",[276,1715,1717],{"id":1716},"step-4-check-the-fields-you-depend-on","Step 4 — Check the fields you depend on",[10,1719,1720],{},"A report missing its branch or period should stop the run, not produce rows that cannot be traced. Check required fields and their types in one place:",[281,1722,1724],{"className":316,"code":1723,"language":318,"meta":286,"style":286},"REQUIRED = {\"branch\": str, \"period\": str, \"currency\": str}\n\ndef check_fields(path, fields):\n    problems = [f\"missing {k}\" for k in REQUIRED if k not in fields]\n    problems += [f\"{k} is {type(fields[k]).__name__}\" for k, t in REQUIRED.items()\n                 if k in fields and not isinstance(fields[k], t)]\n    if problems:\n        raise ValueError(f\"{path}: \" + \"; \".join(problems))\n\ncheck_fields(\"york.xlsx\", fields)\nprint(\"fields ok\")\n",[14,1725,1726,1763,1767,1777,1824,1871,1892,1899,1929,1933,1943],{"__ignoreMap":286},[290,1727,1728,1731,1733,1735,1738,1741,1743,1745,1747,1749,1751,1753,1756,1758,1760],{"class":292,"line":293},[290,1729,1730],{"class":434},"REQUIRED",[290,1732,697],{"class":325},[290,1734,1013],{"class":329},[290,1736,1737],{"class":300},"\"branch\"",[290,1739,1740],{"class":329},": ",[290,1742,1016],{"class":434},[290,1744,467],{"class":329},[290,1746,1659],{"class":300},[290,1748,1740],{"class":329},[290,1750,1016],{"class":434},[290,1752,467],{"class":329},[290,1754,1755],{"class":300},"\"currency\"",[290,1757,1740],{"class":329},[290,1759,1016],{"class":434},[290,1761,1762],{"class":329},"}\n",[290,1764,1765],{"class":292,"line":339},[290,1766,351],{"emptyLinePlaceholder":350},[290,1768,1769,1771,1774],{"class":292,"line":347},[290,1770,389],{"class":325},[290,1772,1773],{"class":392}," check_fields",[290,1775,1776],{"class":329},"(path, fields):\n",[290,1778,1779,1782,1784,1787,1789,1792,1794,1797,1799,1801,1804,1807,1809,1812,1815,1817,1819,1821],{"class":292,"line":354},[290,1780,1781],{"class":329},"    problems ",[290,1783,405],{"class":325},[290,1785,1786],{"class":329}," [",[290,1788,1216],{"class":325},[290,1790,1791],{"class":300},"\"missing ",[290,1793,1221],{"class":718},[290,1795,1796],{"class":329},"k",[290,1798,1227],{"class":718},[290,1800,706],{"class":300},[290,1802,1803],{"class":325}," for",[290,1805,1806],{"class":329}," k ",[290,1808,431],{"class":325},[290,1810,1811],{"class":434}," REQUIRED",[290,1813,1814],{"class":325}," if",[290,1816,1806],{"class":329},[290,1818,965],{"class":325},[290,1820,968],{"class":325},[290,1822,1823],{"class":329}," fields]\n",[290,1825,1826,1828,1831,1833,1835,1837,1839,1841,1843,1846,1848,1850,1853,1855,1857,1859,1861,1864,1866,1868],{"class":292,"line":367},[290,1827,1781],{"class":329},[290,1829,1830],{"class":325},"+=",[290,1832,1786],{"class":329},[290,1834,1216],{"class":325},[290,1836,706],{"class":300},[290,1838,1221],{"class":718},[290,1840,1796],{"class":329},[290,1842,1227],{"class":718},[290,1844,1845],{"class":300}," is ",[290,1847,1221],{"class":718},[290,1849,1680],{"class":434},[290,1851,1852],{"class":329},"(fields[k]).",[290,1854,1692],{"class":434},[290,1856,1227],{"class":718},[290,1858,706],{"class":300},[290,1860,1803],{"class":325},[290,1862,1863],{"class":329}," k, t ",[290,1865,431],{"class":325},[290,1867,1811],{"class":434},[290,1869,1870],{"class":329},".items()\n",[290,1872,1873,1876,1878,1880,1883,1885,1887,1889],{"class":292,"line":381},[290,1874,1875],{"class":325},"                 if",[290,1877,1806],{"class":329},[290,1879,431],{"class":325},[290,1881,1882],{"class":329}," fields ",[290,1884,1076],{"class":325},[290,1886,989],{"class":325},[290,1888,1065],{"class":434},[290,1890,1891],{"class":329},"(fields[k], t)]\n",[290,1893,1894,1896],{"class":292,"line":386},[290,1895,1194],{"class":325},[290,1897,1898],{"class":329}," problems:\n",[290,1900,1901,1903,1905,1907,1909,1911,1913,1915,1917,1920,1923,1926],{"class":292,"line":399},[290,1902,1208],{"class":325},[290,1904,1211],{"class":434},[290,1906,772],{"class":329},[290,1908,1216],{"class":325},[290,1910,706],{"class":300},[290,1912,1221],{"class":718},[290,1914,1224],{"class":329},[290,1916,1227],{"class":718},[290,1918,1919],{"class":300},": \"",[290,1921,1922],{"class":325}," +",[290,1924,1925],{"class":300}," \"; \"",[290,1927,1928],{"class":329},".join(problems))\n",[290,1930,1931],{"class":292,"line":411},[290,1932,351],{"emptyLinePlaceholder":350},[290,1934,1935,1938,1940],{"class":292,"line":422},[290,1936,1937],{"class":329},"check_fields(",[290,1939,636],{"class":300},[290,1941,1942],{"class":329},", fields)\n",[290,1944,1945,1947,1949,1952],{"class":292,"line":441},[290,1946,769],{"class":434},[290,1948,772],{"class":329},[290,1950,1951],{"class":300},"\"fields ok\"",[290,1953,628],{"class":329},[10,1955,1956,1957,1343],{},"Unknown extra fields are fine — systems add lines like \"Printed by\" — so only the fields the pipeline uses are enforced. Stricter validation of the table itself is covered in ",[23,1958,1960],{"href":1959},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",[276,1962,1964],{"id":1963},"step-5-tag-every-row-and-combine-reports","Step 5 — Tag every row and combine reports",[10,1966,1967],{},"Once each report is read, copy the header fields onto its rows as ordinary columns and stack the files. The branch and period now travel with the data:",[281,1969,1971],{"className":316,"code":1970,"language":318,"meta":286,"style":286},"frames = []\nfor path in [\"leeds.xlsx\", \"york.xlsx\"]:\n    title, fields, table = read_report(path)\n    check_fields(path, fields)\n    start, end = parse_period(fields[\"period\"])\n    frames.append(table.assign(\n        branch=fields[\"branch\"], period_start=start, period_end=end,\n        currency=fields[\"currency\"], source_file=path,\n    ))\ncombined = pd.concat(frames, ignore_index=True)\nprint(combined[[\"branch\", \"Product\", \"Revenue\", \"period_start\"]].values.tolist())\nprint(combined.groupby(\"branch\")[\"Revenue\"].sum().to_dict())   # {'Leeds': 208.5, 'York': 208.5}\n",[14,1972,1973,1983,2003,2013,2018,2031,2036,2067,2088,2093,2112,2137],{"__ignoreMap":286},[290,1974,1975,1978,1980],{"class":292,"line":293},[290,1976,1977],{"class":329},"frames ",[290,1979,405],{"class":325},[290,1981,1982],{"class":329}," []\n",[290,1984,1985,1987,1990,1992,1994,1996,1998,2000],{"class":292,"line":339},[290,1986,949],{"class":325},[290,1988,1989],{"class":329}," path ",[290,1991,431],{"class":325},[290,1993,1786],{"class":329},[290,1995,616],{"class":300},[290,1997,467],{"class":329},[290,1999,636],{"class":300},[290,2001,2002],{"class":329},"]:\n",[290,2004,2005,2008,2010],{"class":292,"line":347},[290,2006,2007],{"class":329},"    title, fields, table ",[290,2009,405],{"class":325},[290,2011,2012],{"class":329}," read_report(path)\n",[290,2014,2015],{"class":292,"line":354},[290,2016,2017],{"class":329},"    check_fields(path, fields)\n",[290,2019,2020,2023,2025,2027,2029],{"class":292,"line":367},[290,2021,2022],{"class":329},"    start, end ",[290,2024,405],{"class":325},[290,2026,1656],{"class":329},[290,2028,1659],{"class":300},[290,2030,456],{"class":329},[290,2032,2033],{"class":292,"line":381},[290,2034,2035],{"class":329},"    frames.append(table.assign(\n",[290,2037,2038,2041,2043,2046,2048,2051,2054,2056,2059,2062,2064],{"class":292,"line":386},[290,2039,2040],{"class":868},"        branch",[290,2042,405],{"class":325},[290,2044,2045],{"class":329},"fields[",[290,2047,1737],{"class":300},[290,2049,2050],{"class":329},"], ",[290,2052,2053],{"class":868},"period_start",[290,2055,405],{"class":325},[290,2057,2058],{"class":329},"start, ",[290,2060,2061],{"class":868},"period_end",[290,2063,405],{"class":325},[290,2065,2066],{"class":329},"end,\n",[290,2068,2069,2072,2074,2076,2078,2080,2083,2085],{"class":292,"line":399},[290,2070,2071],{"class":868},"        currency",[290,2073,405],{"class":325},[290,2075,2045],{"class":329},[290,2077,1755],{"class":300},[290,2079,2050],{"class":329},[290,2081,2082],{"class":868},"source_file",[290,2084,405],{"class":325},[290,2086,2087],{"class":329},"path,\n",[290,2089,2090],{"class":292,"line":411},[290,2091,2092],{"class":329},"    ))\n",[290,2094,2095,2098,2100,2103,2106,2108,2110],{"class":292,"line":422},[290,2096,2097],{"class":329},"combined ",[290,2099,405],{"class":325},[290,2101,2102],{"class":329}," pd.concat(frames, ",[290,2104,2105],{"class":868},"ignore_index",[290,2107,405],{"class":325},[290,2109,874],{"class":434},[290,2111,628],{"class":329},[290,2113,2114,2116,2119,2121,2123,2125,2127,2129,2131,2134],{"class":292,"line":441},[290,2115,769],{"class":434},[290,2117,2118],{"class":329},"(combined[[",[290,2120,1737],{"class":300},[290,2122,467],{"class":329},[290,2124,546],{"class":300},[290,2126,467],{"class":329},[290,2128,556],{"class":300},[290,2130,467],{"class":329},[290,2132,2133],{"class":300},"\"period_start\"",[290,2135,2136],{"class":329},"]].values.tolist())\n",[290,2138,2139,2141,2144,2146,2149,2151,2154],{"class":292,"line":447},[290,2140,769],{"class":434},[290,2142,2143],{"class":329},"(combined.groupby(",[290,2145,1737],{"class":300},[290,2147,2148],{"class":329},")[",[290,2150,556],{"class":300},[290,2152,2153],{"class":329},"].sum().to_dict())   ",[290,2155,2156],{"class":787},"# {'Leeds': 208.5, 'York': 208.5}\n",[10,2158,2159,2160,2162,2163,1343],{},"Keeping ",[14,2161,2082],{}," as well costs one column and answers \"where did this row come from?\" when a number is questioned months later. Combining many files in a folder follows the same pattern; see ",[23,2164,2166],{"href":2165},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One in Python",[29,2168,38,2173,38,2176,38,2179,38,2182,38,2184,38,2188,38,2192,38,2196,38,2199,38,2203,38,2206,38,2209,38,2213,38,2215,38,2217,38,2219,38,2221,38,2223,38,2225,38,2228,38,2231,38,2233,38,2235,38,2237,38,2239,38,2241,38,2243,38,2245,38,2248,38,2250,38,2252,38,2254,38,2256,38,2258,38,2260,38,2263,38,2267,38,2270,38,2274,38,2277,38,2281,38,2284,38,2288,38,2292,38,2294,38,2297,38,2299,38,2302,38,2304,38,2306,38,2308,38,2310,38,2312,38,2314,38,2316,38,2318,38,2320,38,2322,38,2324,38,2326,38,2328,38,2330,38,2332,38,2335,38,2337,38,2339,38,2341,38,2343,38,2345,38,2347,38,2349,38,2351,38,2353,38,2355,38,2361,38,2365,38,2370,38,2373,38,2381],{"viewBox":2169,"role":32,"ariaLabelledBy":2170,"xmlns":36,"style":1350},"4 0 752 164",[2171,2172],"hf-tag-t","hf-tag-d",[40,2174,2175],{"id":2171},"Header fields become columns on every row",[44,2177,2178],{"id":2172},"Each report's table has only Product, Units and Revenue. After tagging, every row also carries the branch, the period start date and the currency taken from the fields above its table, so rows from Leeds and York can be combined and still told apart.",[48,2180],{"x":155,"y":51,"width":1359,"height":2181,"fill":54},"164",[48,2183],{"x":1363,"y":93,"width":59,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},[48,2185],{"x":2186,"y":93,"width":2187,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"52.0","65.5",[69,2189,74],{"x":2190,"y":2191,"style":73},"84.8","57.0",[48,2193],{"x":2194,"y":93,"width":2195,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"117.5","50.5",[69,2197,82],{"x":2198,"y":2191,"style":73},"142.8",[48,2200],{"x":2201,"y":93,"width":2202,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"168.1","70.3",[69,2204,90],{"x":2205,"y":2191,"style":73},"203.2",[48,2207],{"x":1363,"y":2208,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"64.0",[69,2210,99],{"x":2211,"y":2212,"style":73},"36.0","83.0",[48,2214],{"x":2186,"y":2208,"width":2187,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2216,214],{"x":2190,"y":2212,"style":104},[48,2218],{"x":2194,"y":2208,"width":2195,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2220,219],{"x":2198,"y":2212,"style":104},[48,2222],{"x":2201,"y":2208,"width":2202,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2224,224],{"x":2205,"y":2212,"style":104},[48,2226],{"x":1363,"y":2227,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},"92.0",[69,2229,116],{"x":2211,"y":2230,"style":73},"111.0",[48,2232],{"x":2186,"y":2227,"width":2187,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2234,237],{"x":2190,"y":2230,"style":236},[48,2236],{"x":2194,"y":2227,"width":2195,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2238,242],{"x":2198,"y":2230,"style":236},[48,2240],{"x":2201,"y":2227,"width":2202,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2242,247],{"x":2205,"y":2230,"style":236},[48,2244],{"x":1363,"y":575,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},[69,2246,139],{"x":2211,"y":2247,"style":73},"139.0",[48,2249],{"x":2186,"y":575,"width":2187,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2251,259],{"x":2190,"y":2247,"style":236},[48,2253],{"x":2194,"y":575,"width":2195,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2255,155],{"x":2198,"y":2247,"style":236},[48,2257],{"x":2201,"y":575,"width":2202,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2259,268],{"x":2205,"y":2247,"style":236},[48,2261],{"x":2262,"y":93,"width":59,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"421.5",[48,2264],{"x":2265,"y":93,"width":2266,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"453.5","61.1",[69,2268,74],{"x":2269,"y":2191,"style":73},"484.0",[48,2271],{"x":2272,"y":93,"width":2273,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"514.5","65.9",[69,2275,82],{"x":2276,"y":2191,"style":73},"547.5",[48,2278],{"x":2279,"y":93,"width":2280,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"580.4","70.6",[69,2282,90],{"x":2283,"y":2191,"style":73},"615.7",[48,2285],{"x":2286,"y":93,"width":2287,"height":60,"rx":51,"fill":61,"stroke":62,"style":63},"651.0","89.0",[69,2289,2291],{"x":2290,"y":2191,"style":73},"695.5","D",[48,2293],{"x":2262,"y":2208,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},[69,2295,99],{"x":2296,"y":2212,"style":73},"437.5",[48,2298],{"x":2265,"y":2208,"width":2266,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2300,2301],{"x":2269,"y":2212,"style":104},"branch",[48,2303],{"x":2272,"y":2208,"width":2273,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2305,214],{"x":2276,"y":2212,"style":104},[48,2307],{"x":2279,"y":2208,"width":2280,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2309,224],{"x":2283,"y":2212,"style":104},[48,2311],{"x":2286,"y":2208,"width":2287,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2313,2053],{"x":2290,"y":2212,"style":104},[48,2315],{"x":2262,"y":2227,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},[69,2317,116],{"x":2296,"y":2230,"style":73},[48,2319],{"x":2265,"y":2227,"width":2266,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2321,132],{"x":2269,"y":2230,"style":236},[48,2323],{"x":2272,"y":2227,"width":2273,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2325,237],{"x":2276,"y":2230,"style":236},[48,2327],{"x":2279,"y":2227,"width":2280,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2329,247],{"x":2283,"y":2230,"style":236},[48,2331],{"x":2286,"y":2227,"width":2287,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2333,2334],{"x":2290,"y":2230,"style":236},"2026-01-01",[48,2336],{"x":2262,"y":575,"width":59,"height":94,"rx":51,"fill":61,"stroke":62,"style":63},[69,2338,139],{"x":2296,"y":2247,"style":73},[48,2340],{"x":2265,"y":575,"width":2266,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2342,132],{"x":2269,"y":2247,"style":236},[48,2344],{"x":2272,"y":575,"width":2273,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2346,259],{"x":2276,"y":2247,"style":236},[48,2348],{"x":2279,"y":575,"width":2280,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2350,268],{"x":2283,"y":2247,"style":236},[48,2352],{"x":2286,"y":575,"width":2287,"height":94,"rx":51,"fill":54,"stroke":62,"style":63},[69,2354,2334],{"x":2290,"y":2247,"style":236},[69,2356,2360],{"x":2357,"y":2358,"style":2359},"129.2","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[69,2362,2364],{"x":2363,"y":2358,"style":2359},"580.7","after",[292,2366],{"x1":2367,"y1":2368,"x2":2369,"y2":2368,"stroke":210,"style":1367},"248.3","94.0","407.5",[1395,2371],{"points":2372,"fill":1398},"411.5,94.0 402.5,99.0 402.5,89.0",[48,2374],{"x":2375,"y":2376,"width":2377,"height":2378,"rx":2379,"fill":54,"stroke":62,"style":2380},"272.6","61.0","114.5","22.0","11","stroke-width:1.2px",[69,2382,2386],{"x":2383,"y":2384,"style":2385},"329.9","76.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","assign fields",[10,2388,2389],{},"Tagging every row repeats the same few values thousands of times, which is fine for a report of this size and makes filtering trivial. For very large combined datasets, or when the header block has many fields, store one row per report in a separate \"runs\" table — file name, branch, period, generated time — and give the data rows only a run identifier. Written as two sheets or two database tables, that keeps the data compact and makes it easy to answer questions about the runs themselves, such as which branches have not sent this quarter's report.",[276,2391,2393],{"id":2392},"common-pitfalls","Common pitfalls",[656,2395,2396,2402,2408,2417],{},[659,2397,2398,2401],{},[662,2399,2400],{},"Fixed skiprows."," A report that gains one line of header — a new \"Printed by\" field — shifts every column name. Find the table header by content.",[659,2403,2404,2407],{},[662,2405,2406],{},"Two fields on one row."," Some layouts put the title in A1 and the period in the top-right corner of the same row. The scan pairs the first two non-empty cells, so it would read the period as the title's value; for such layouts, walk the row's cells two at a time and check each label against the fields you expect.",[659,2409,2410,2413,2414,2416],{},[662,2411,2412],{},"Merged label cells."," openpyxl returns the value only in the top-left cell of a merge, and ",[14,2415,470],{}," elsewhere; dropping empty values handles that.",[659,2418,2419,2422,2423,2426],{},[662,2420,2421],{},"Formula-driven headers."," If the period is a formula, open with ",[14,2424,2425],{},"data_only=True"," to get its cached result, as the scan does.",[276,2428,2430],{"id":2429},"conclusion","Conclusion",[10,2432,2433],{},"Report header blocks carry the context that tells files apart. Scan the top rows with openpyxl, pairing labels and values across empty cells and splitting \"Label: value\" text; stop at the table's column-header row and pass its index to pandas. Parse periods and dates explicitly, fail on missing required fields, and attach the fields to every row so combined data keeps its source.",[276,2435,2437],{"id":2436},"frequently-asked-questions","Frequently asked questions",[10,2439,2440,2443],{},[662,2441,2442],{},"How do I read the values above the table in an Excel report?","\nScan the first rows with openpyxl, treating a text cell followed by a value as a label-value pair and a single \"Label: value\" cell as an inline pair. Stop when you reach the row that holds the table's column headers.",[10,2445,2446,2449],{},[662,2447,2448],{},"How does pandas skip the header block?","\nPass header= the zero-based index of the table's column-header row, which the scan returns. pandas then reads the table below it and ignores everything above.",[10,2451,2452,2455],{},[662,2453,2454],{},"What if the label and value are separated by empty cells?","\nIgnore empty cells when pairing. Take the first non-empty cell in the row as the label and the next non-empty cell as the value, which handles merged cells and spacer columns.",[10,2457,2458,2461],{},[662,2459,2460],{},"Why attach header fields to every row?","\nOnce several reports are combined, the branch or period printed above each table is the only thing that tells rows apart. Adding them as columns keeps that information with the data.",[276,2463,2465],{"id":2464},"related","Related",[656,2467,2468,2474,2481,2488],{},[659,2469,2470,2471,2473],{},"Up one level: ",[23,2472,26],{"href":25}," — pulling structure out of irregular sheets.",[659,2475,2476,2480],{},[23,2477,2479],{"href":2478},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-multiple-tables-from-one-excel-sheet\u002F","Read Multiple Tables from One Excel Sheet"," — when one sheet holds several blocks.",[659,2482,2483,2487],{},[23,2484,2486],{"href":2485},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fremove-subtotal-and-total-rows-from-an-excel-export\u002F","Remove Subtotal and Total Rows from an Excel Export"," — cleaning the bottom of the same reports.",[659,2489,2490,2494],{},[23,2491,2493],{"href":2492},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-excel-with-multi-row-headers-in-pandas\u002F","Read Excel with Multi-Row Headers in pandas"," — tables whose own header spans rows.",[2496,2497,2498],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":286,"searchDepth":339,"depth":339,"links":2500},[2501,2502,2503,2504,2505,2506,2507,2508,2509,2510],{"id":278,"depth":339,"text":279},{"id":650,"depth":339,"text":651},{"id":812,"depth":339,"text":813},{"id":1505,"depth":339,"text":1506},{"id":1716,"depth":339,"text":1717},{"id":1963,"depth":339,"text":1964},{"id":2392,"depth":339,"text":2393},{"id":2429,"depth":339,"text":2430},{"id":2436,"depth":339,"text":2437},{"id":2464,"depth":339,"text":2465},"2026-09-23","Many exported reports put the branch, period, currency and run date in label-value pairs above the table. Read those fields with openpyxl wherever they sit, find where the table starts, parse the period into real dates and attach the fields to every row before combining files.","md",[2515,2517,2519,2521],{"q":2442,"a":2516},"Scan the first rows with openpyxl, treating a text cell followed by a value as a label-value pair and a single \"Label: value\" cell as an inline pair. Stop when you reach the row that holds the table's column headers.",{"q":2448,"a":2518},"Pass header= the zero-based index of the table's column-header row, which the scan returns. pandas then reads the table below it and ignores everything above.",{"q":2454,"a":2520},"Ignore empty cells when pairing. Take the first non-empty cell in the row as the label and the next non-empty cell as the value, which handles merged cells and spacer columns.",{"q":2460,"a":2522},"Once several reports are combined, the branch or period printed above each table is the only thing that tells rows apart. Adding them as columns keeps that information with the data.","extract report header fields from an excel sheet with python",null,{"breadcrumb":2526},[2527,2530,2533],{"name":2528,"item":2529},"Home","\u002F",{"name":2531,"item":2532},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":26,"item":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet",{"title":2536,"description":2537},"Extract Report Header Fields from an Excel Sheet (Python)","Read metadata above an Excel table with openpyxl and pandas — label-value pairs in separate cells or \"Label: value\" text, a title line, locating the table header, parsing periods, validating required fields and tagging rows from many files.","extract-report-header-fields-from-an-excel-sheet","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet\u002Findex",[2541,2542,2543],"messy-sheets","reading-excel","openpyxl","how-to","tQ03pWEJ0XL-IFvdWJCQQ7Ieade3kpxDv7UmfXiwnpo",[2547,2551],{"title":2548,"path":2549,"stem":2550,"children":-1},"Extract Hyperlink URLs from Excel Cells with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells\u002Findex",{"title":1342,"path":2552,"stem":2553,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002Findex",[2555,2557],{"path":2556,"title":26},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets",{"path":2558,"title":2531},"\u002Fadvanced-data-transformation-and-cleaning",[],[2561,2564,2567,2570,2573,2574],{"path":2562,"title":2563,"kind":2544},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl","Freeze the Header Row in Excel with openpyxl",{"path":2565,"title":2566,"kind":2544},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas","Handle Merged Cells When Reading Excel with pandas",{"path":2568,"title":2569,"kind":2544},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Funmerge-cells-and-fill-values-with-openpyxl","Unmerge Cells and Fill the Values with openpyxl",{"path":2571,"title":2572,"kind":2544},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fcombine-sheets-with-different-layouts-into-one-table","Combine Sheets with Different Layouts into One Table",{"path":2549,"title":2548,"kind":2544},{"path":2552,"title":1342,"kind":2544},1790972090853]