[{"data":1,"prerenderedAt":2450},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl":2415,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl":2424,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl":2430,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl":2431},{"id":4,"title":5,"body":6,"dateModified":2380,"datePublished":2380,"description":2381,"extension":2382,"faq":2383,"keyword":2392,"kind":2393,"meta":2394,"navigation":323,"path":2403,"seo":2404,"slug":2407,"stem":2408,"topics":2409,"type":2413,"__hash__":2414},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl\u002Findex.md","Read Cell Colours and Font Styles as Data with openpyxl",{"type":7,"value":8,"toc":2366},"minimark",[9,24,242,247,275,282,651,655,662,773,792,796,810,1096,1107,1221,1225,1252,1392,1396,1399,1610,1613,1704,1708,1719,1757,1764,1768,1775,2031,2038,2042,2166,2181,2185,2191,2276,2280,2291,2295,2302,2308,2314,2320,2324,2362],[10,11,12,13,17,18,23],"p",{},"Plenty of real trackers never got a Status column. Instead, somebody shades a task green when it is\ndone, red when it is blocked and yellow while it waits for sign-off, and strikes through the owner's\nname when that person has left. To everyone who opens the file that is perfectly clear. To\n",[14,15,16],"code",{},"pd.read_excel()"," it is invisible, because pandas reads values and throws formatting away. This guide,\npart of ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002F","Extracting Data from Messy Excel Sheets",",\nreads that formatting with openpyxl and turns it into columns you can filter, count and report on.",[25,26,34,35,34,39,34,43,34,50,34,57,34,66,34,70,34,76,34,80,34,84,34,88,34,92,34,95,34,99,34,101,34,105,34,107,34,110,34,112,34,115,34,117,34,121,34,125,34,129,34,131,34,135,34,137,34,140,34,146,34,151,34,154,34,158,34,162,34,166,34,168,34,171,34,173,34,176,34,179,34,183,34,186,34,190,34,194,34,198,34,200,34,203,34,205,34,208,34,211,34,215,34,218,34,222,34,224,34,227,34,229,34,232,34,234,34,237],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 556 284","img",[30,31],"ccs-tracker-t","ccs-tracker-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:556px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"A tracker where colour carries the status",[40,41,42],"desc",{"id":31},"Task names in column A are shaded green, red or amber to mean done, blocked or waiting, and one owner name is bold with strikethrough to mean the person has left.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","556","284","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"278","32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","the status lives in the formatting, not in a cell",[44,58],{"x":59,"y":60,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"20","50","36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[44,67],{"x":68,"y":60,"width":69,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"56","120",[51,71,75],{"x":72,"y":73,"style":74},"116","70","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[44,77],{"x":78,"y":60,"width":79,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"176","110",[51,81,83],{"x":82,"y":73,"style":74},"231","B",[44,85],{"x":86,"y":60,"width":87,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"286","100",[51,89,91],{"x":90,"y":73,"style":74},"336","C",[44,93],{"x":59,"y":94,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"80",[51,96,98],{"x":97,"y":87,"style":74},"38","1",[44,100],{"x":68,"y":94,"width":69,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},[51,102,104],{"x":72,"y":87,"style":103},"font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Task",[44,106],{"x":78,"y":94,"width":79,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},[51,108,109],{"x":82,"y":87,"style":103},"Owner",[44,111],{"x":86,"y":94,"width":87,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},[51,113,114],{"x":90,"y":87,"style":103},"Due",[44,116],{"x":59,"y":79,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},[51,118,120],{"x":97,"y":119,"style":74},"130","2",[44,122],{"x":68,"y":79,"width":69,"height":62,"rx":46,"fill":123,"stroke":124,"style":65},"#d9f4f1","var(--teal,#0f9488)",[51,126,128],{"x":72,"y":119,"style":127},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","Invoice run",[44,130],{"x":78,"y":79,"width":79,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,132,134],{"x":82,"y":119,"style":133},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Ana",[44,136],{"x":86,"y":79,"width":87,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,138,139],{"x":90,"y":119,"style":133},"2026-09-01",[44,141],{"x":142,"y":143,"width":119,"height":144,"rx":145,"fill":123,"stroke":124,"style":65},"406","112","26","8",[51,147,150],{"x":148,"y":149,"style":127},"471","129","green = done",[44,152],{"x":59,"y":153,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"140",[51,155,157],{"x":97,"y":156,"style":74},"160","3",[44,159],{"x":68,"y":153,"width":69,"height":62,"rx":46,"fill":160,"stroke":161,"style":65},"#fee8f2","var(--accent,#f43f8f)",[51,163,165],{"x":72,"y":156,"style":164},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","Audit prep",[44,167],{"x":78,"y":153,"width":79,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,169,170],{"x":82,"y":156,"style":133},"Ben",[44,172],{"x":86,"y":153,"width":87,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,174,175],{"x":90,"y":156,"style":133},"2026-09-05",[44,177],{"x":142,"y":178,"width":119,"height":144,"rx":145,"fill":160,"stroke":161,"style":65},"142",[51,180,182],{"x":148,"y":181,"style":164},"159","red = blocked",[44,184],{"x":59,"y":185,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"170",[51,187,189],{"x":97,"y":188,"style":74},"190","4",[44,191],{"x":68,"y":185,"width":69,"height":62,"rx":46,"fill":192,"stroke":193,"style":65},"#fdefd8","var(--gold,#b4740a)",[51,195,197],{"x":72,"y":188,"style":196},"font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Close books",[44,199],{"x":78,"y":185,"width":79,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},[51,201,202],{"x":82,"y":188,"style":103},"Cara (struck)",[44,204],{"x":86,"y":185,"width":87,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,206,207],{"x":90,"y":188,"style":133},"2026-09-10",[44,209],{"x":142,"y":210,"width":119,"height":144,"rx":145,"fill":192,"stroke":193,"style":65},"172",[51,212,214],{"x":148,"y":213,"style":196},"189","amber = waiting",[44,216],{"x":59,"y":217,"width":61,"height":62,"rx":46,"fill":63,"stroke":64,"style":65},"200",[51,219,221],{"x":97,"y":220,"style":74},"220","5",[44,223],{"x":68,"y":217,"width":69,"height":62,"rx":46,"fill":123,"stroke":124,"style":65},[51,225,226],{"x":72,"y":220,"style":127},"Payroll",[44,228],{"x":78,"y":217,"width":79,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,230,231],{"x":82,"y":220,"style":133},"Dev",[44,233],{"x":86,"y":217,"width":87,"height":62,"rx":46,"fill":49,"stroke":64,"style":65},[51,235,236],{"x":90,"y":220,"style":133},"2026-09-12",[51,238,241],{"x":53,"y":239,"style":240},"262","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","pandas returns the text in these cells and nothing else",[243,244,246],"h2",{"id":245},"prerequisites","Prerequisites",[248,249,254],"pre",{"className":250,"code":251,"language":252,"meta":253,"style":253},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[14,255,256],{"__ignoreMap":253},[257,258,261,265,269,272],"span",{"class":259,"line":260},"line",1,[257,262,264],{"class":263},"sMTad","pip",[257,266,268],{"class":267},"srMev"," install",[257,270,271],{"class":267}," openpyxl",[257,273,274],{"class":267}," pandas\n",[10,276,277,278,281],{},"This builds ",[14,279,280],{},"tracker.xlsx"," with the same fills and fonts as the diagram, using Excel's own \"Good\",\n\"Bad\" and \"Neutral\" cell-style colours:",[248,283,287],{"className":284,"code":285,"language":286,"meta":253,"style":253},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill\n\nwb = Workbook()\nws = wb.active\nws.title = \"Tracker\"\nws.append([\"Task\", \"Owner\", \"Due\"])\nfor row in [(\"Invoice run\", \"Ana\", \"2026-09-01\"), (\"Audit prep\", \"Ben\", \"2026-09-05\"),\n            (\"Close books\", \"Cara\", \"2026-09-10\"), (\"Payroll\", \"Dev\", \"2026-09-12\")]:\n    ws.append(row)\n\nws[\"A2\"].fill = PatternFill(\"solid\", start_color=\"C6EFCE\")   # green\nws[\"A3\"].fill = PatternFill(\"solid\", start_color=\"FFC7CE\")   # red\nws[\"A4\"].fill = PatternFill(\"solid\", start_color=\"FFEB9C\")   # amber\nws[\"A5\"].fill = PatternFill(\"solid\", start_color=\"C6EFCE\")   # green\nws[\"B4\"].font = Font(bold=True, strike=True)\nwb.save(\"tracker.xlsx\")\n","python",[14,288,289,305,318,325,337,348,359,382,429,466,472,477,515,545,575,603,640],{"__ignoreMap":253},[257,290,291,295,299,302],{"class":259,"line":260},[257,292,294],{"class":293},"s-kum","from",[257,296,298],{"class":297},"skGVy"," openpyxl ",[257,300,301],{"class":293},"import",[257,303,304],{"class":297}," Workbook\n",[257,306,308,310,313,315],{"class":259,"line":307},2,[257,309,294],{"class":293},[257,311,312],{"class":297}," openpyxl.styles ",[257,314,301],{"class":293},[257,316,317],{"class":297}," Font, PatternFill\n",[257,319,321],{"class":259,"line":320},3,[257,322,324],{"emptyLinePlaceholder":323},true,"\n",[257,326,328,331,334],{"class":259,"line":327},4,[257,329,330],{"class":297},"wb ",[257,332,333],{"class":293},"=",[257,335,336],{"class":297}," Workbook()\n",[257,338,340,343,345],{"class":259,"line":339},5,[257,341,342],{"class":297},"ws ",[257,344,333],{"class":293},[257,346,347],{"class":297}," wb.active\n",[257,349,351,354,356],{"class":259,"line":350},6,[257,352,353],{"class":297},"ws.title ",[257,355,333],{"class":293},[257,357,358],{"class":267}," \"Tracker\"\n",[257,360,362,365,368,371,374,376,379],{"class":259,"line":361},7,[257,363,364],{"class":297},"ws.append([",[257,366,367],{"class":267},"\"Task\"",[257,369,370],{"class":297},", ",[257,372,373],{"class":267},"\"Owner\"",[257,375,370],{"class":297},[257,377,378],{"class":267},"\"Due\"",[257,380,381],{"class":297},"])\n",[257,383,385,388,391,394,397,400,402,405,407,410,413,416,418,421,423,426],{"class":259,"line":384},8,[257,386,387],{"class":293},"for",[257,389,390],{"class":297}," row ",[257,392,393],{"class":293},"in",[257,395,396],{"class":297}," [(",[257,398,399],{"class":267},"\"Invoice run\"",[257,401,370],{"class":297},[257,403,404],{"class":267},"\"Ana\"",[257,406,370],{"class":297},[257,408,409],{"class":267},"\"2026-09-01\"",[257,411,412],{"class":297},"), (",[257,414,415],{"class":267},"\"Audit prep\"",[257,417,370],{"class":297},[257,419,420],{"class":267},"\"Ben\"",[257,422,370],{"class":297},[257,424,425],{"class":267},"\"2026-09-05\"",[257,427,428],{"class":297},"),\n",[257,430,432,435,438,440,443,445,448,450,453,455,458,460,463],{"class":259,"line":431},9,[257,433,434],{"class":297},"            (",[257,436,437],{"class":267},"\"Close books\"",[257,439,370],{"class":297},[257,441,442],{"class":267},"\"Cara\"",[257,444,370],{"class":297},[257,446,447],{"class":267},"\"2026-09-10\"",[257,449,412],{"class":297},[257,451,452],{"class":267},"\"Payroll\"",[257,454,370],{"class":297},[257,456,457],{"class":267},"\"Dev\"",[257,459,370],{"class":297},[257,461,462],{"class":267},"\"2026-09-12\"",[257,464,465],{"class":297},")]:\n",[257,467,469],{"class":259,"line":468},10,[257,470,471],{"class":297},"    ws.append(row)\n",[257,473,475],{"class":259,"line":474},11,[257,476,324],{"emptyLinePlaceholder":323},[257,478,480,483,486,489,491,494,497,499,503,505,508,511],{"class":259,"line":479},12,[257,481,482],{"class":297},"ws[",[257,484,485],{"class":267},"\"A2\"",[257,487,488],{"class":297},"].fill ",[257,490,333],{"class":293},[257,492,493],{"class":297}," PatternFill(",[257,495,496],{"class":267},"\"solid\"",[257,498,370],{"class":297},[257,500,502],{"class":501},"sa561","start_color",[257,504,333],{"class":293},[257,506,507],{"class":267},"\"C6EFCE\"",[257,509,510],{"class":297},")   ",[257,512,514],{"class":513},"s-wDw","# green\n",[257,516,518,520,523,525,527,529,531,533,535,537,540,542],{"class":259,"line":517},13,[257,519,482],{"class":297},[257,521,522],{"class":267},"\"A3\"",[257,524,488],{"class":297},[257,526,333],{"class":293},[257,528,493],{"class":297},[257,530,496],{"class":267},[257,532,370],{"class":297},[257,534,502],{"class":501},[257,536,333],{"class":293},[257,538,539],{"class":267},"\"FFC7CE\"",[257,541,510],{"class":297},[257,543,544],{"class":513},"# red\n",[257,546,548,550,553,555,557,559,561,563,565,567,570,572],{"class":259,"line":547},14,[257,549,482],{"class":297},[257,551,552],{"class":267},"\"A4\"",[257,554,488],{"class":297},[257,556,333],{"class":293},[257,558,493],{"class":297},[257,560,496],{"class":267},[257,562,370],{"class":297},[257,564,502],{"class":501},[257,566,333],{"class":293},[257,568,569],{"class":267},"\"FFEB9C\"",[257,571,510],{"class":297},[257,573,574],{"class":513},"# amber\n",[257,576,578,580,583,585,587,589,591,593,595,597,599,601],{"class":259,"line":577},15,[257,579,482],{"class":297},[257,581,582],{"class":267},"\"A5\"",[257,584,488],{"class":297},[257,586,333],{"class":293},[257,588,493],{"class":297},[257,590,496],{"class":267},[257,592,370],{"class":297},[257,594,502],{"class":501},[257,596,333],{"class":293},[257,598,507],{"class":267},[257,600,510],{"class":297},[257,602,514],{"class":513},[257,604,606,608,611,614,616,619,622,624,628,630,633,635,637],{"class":259,"line":605},16,[257,607,482],{"class":297},[257,609,610],{"class":267},"\"B4\"",[257,612,613],{"class":297},"].font ",[257,615,333],{"class":293},[257,617,618],{"class":297}," Font(",[257,620,621],{"class":501},"bold",[257,623,333],{"class":293},[257,625,627],{"class":626},"sP0c6","True",[257,629,370],{"class":297},[257,631,632],{"class":501},"strike",[257,634,333],{"class":293},[257,636,627],{"class":626},[257,638,639],{"class":297},")\n",[257,641,643,646,649],{"class":259,"line":642},17,[257,644,645],{"class":297},"wb.save(",[257,647,648],{"class":267},"\"tracker.xlsx\"",[257,650,639],{"class":297},[243,652,654],{"id":653},"step-1-load-the-workbook-with-styles-intact","Step 1 — Load the workbook with styles intact",[10,656,657,658,661],{},"openpyxl keeps every cell's style when it loads a workbook normally. The one mode to avoid is\n",[14,659,660],{},"read_only=True",": it is fast because it streams cells, but the objects it yields carry a much thinner\nstyle model, and relying on it for colours is asking for surprises.",[248,663,665],{"className":284,"code":664,"language":286,"meta":253,"style":253},"from openpyxl import load_workbook\n\nwb = load_workbook(\"tracker.xlsx\")          # not read_only\nws = wb[\"Tracker\"]\n\ncell = ws[\"A2\"]\nprint(cell.value)                  # Invoice run\nprint(cell.fill.fill_type)         # solid\nprint(cell.fill.fgColor.type)      # rgb\nprint(cell.fill.fgColor.rgb)       # 00C6EFCE\n",[14,666,667,678,682,699,714,718,732,743,753,763],{"__ignoreMap":253},[257,668,669,671,673,675],{"class":259,"line":260},[257,670,294],{"class":293},[257,672,298],{"class":297},[257,674,301],{"class":293},[257,676,677],{"class":297}," load_workbook\n",[257,679,680],{"class":259,"line":307},[257,681,324],{"emptyLinePlaceholder":323},[257,683,684,686,688,691,693,696],{"class":259,"line":320},[257,685,330],{"class":297},[257,687,333],{"class":293},[257,689,690],{"class":297}," load_workbook(",[257,692,648],{"class":267},[257,694,695],{"class":297},")          ",[257,697,698],{"class":513},"# not read_only\n",[257,700,701,703,705,708,711],{"class":259,"line":327},[257,702,342],{"class":297},[257,704,333],{"class":293},[257,706,707],{"class":297}," wb[",[257,709,710],{"class":267},"\"Tracker\"",[257,712,713],{"class":297},"]\n",[257,715,716],{"class":259,"line":339},[257,717,324],{"emptyLinePlaceholder":323},[257,719,720,723,725,728,730],{"class":259,"line":350},[257,721,722],{"class":297},"cell ",[257,724,333],{"class":293},[257,726,727],{"class":297}," ws[",[257,729,485],{"class":267},[257,731,713],{"class":297},[257,733,734,737,740],{"class":259,"line":361},[257,735,736],{"class":626},"print",[257,738,739],{"class":297},"(cell.value)                  ",[257,741,742],{"class":513},"# Invoice run\n",[257,744,745,747,750],{"class":259,"line":384},[257,746,736],{"class":626},[257,748,749],{"class":297},"(cell.fill.fill_type)         ",[257,751,752],{"class":513},"# solid\n",[257,754,755,757,760],{"class":259,"line":431},[257,756,736],{"class":626},[257,758,759],{"class":297},"(cell.fill.fgColor.type)      ",[257,761,762],{"class":513},"# rgb\n",[257,764,765,767,770],{"class":259,"line":468},[257,766,736],{"class":626},[257,768,769],{"class":297},"(cell.fill.fgColor.rgb)       ",[257,771,772],{"class":513},"# 00C6EFCE\n",[10,774,775,776,780,781,784,785,787,788,791],{},"Two parts of that output need explaining. A solid fill stores its colour as the ",[777,778,779],"em",{},"foreground"," colour of\nthe pattern, so the attribute to read is ",[14,782,783],{},"fgColor"," (aliased as ",[14,786,502],{},"), not ",[14,789,790],{},"bgColor",". And the\ncolour comes back as eight hex digits — an alpha byte followed by red, green and blue — so compare on\nthe last six characters.",[243,793,795],{"id":794},"step-2-handle-the-three-kinds-of-colour","Step 2 — Handle the three kinds of colour",[10,797,798,801,802,805,806,809],{},[14,799,800],{},"fgColor.type"," tells you how the colour was stored, and each kind needs different handling. Explicit\nRGB is the easy case. A theme colour is an index into the workbook's palette plus a tint, and asking\nfor its ",[14,803,804],{},".rgb"," returns an error string rather than a hex value. An indexed colour points into Excel's\nlegacy 64-colour table, which you mostly meet in files that started life as ",[14,807,808],{},".xls",".",[248,811,813],{"className":284,"code":812,"language":286,"meta":253,"style":253},"from openpyxl.styles.colors import COLOR_INDEX\n\ndef fill_key(cell) -> str | None:\n    \"\"\"Return a stable identifier for a cell's fill, or None when it is unfilled.\"\"\"\n    fill = cell.fill\n    if fill is None or fill.fill_type is None:\n        return None\n    color = fill.fgColor\n    if color.type == \"rgb\":\n        return color.rgb[-6:].upper()\n    if color.type == \"theme\":\n        return f\"theme{color.theme}{color.tint:+.2f}\"\n    if color.type == \"indexed\" and color.indexed \u003C len(COLOR_INDEX):\n        return COLOR_INDEX[color.indexed][-6:].upper()\n    return None\n\nfor row in ws.iter_rows(min_row=2, max_col=1):\n    print(row[0].value, fill_key(row[0]))\n",[14,814,815,827,831,855,860,870,895,903,913,928,944,957,989,1021,1037,1044,1048,1077],{"__ignoreMap":253},[257,816,817,819,822,824],{"class":259,"line":260},[257,818,294],{"class":293},[257,820,821],{"class":297}," openpyxl.styles.colors ",[257,823,301],{"class":293},[257,825,826],{"class":626}," COLOR_INDEX\n",[257,828,829],{"class":259,"line":307},[257,830,324],{"emptyLinePlaceholder":323},[257,832,833,836,840,843,846,849,852],{"class":259,"line":320},[257,834,835],{"class":293},"def",[257,837,839],{"class":838},"s_Opv"," fill_key",[257,841,842],{"class":297},"(cell) -> ",[257,844,845],{"class":626},"str",[257,847,848],{"class":293}," |",[257,850,851],{"class":626}," None",[257,853,854],{"class":297},":\n",[257,856,857],{"class":259,"line":327},[257,858,859],{"class":267},"    \"\"\"Return a stable identifier for a cell's fill, or None when it is unfilled.\"\"\"\n",[257,861,862,865,867],{"class":259,"line":339},[257,863,864],{"class":297},"    fill ",[257,866,333],{"class":293},[257,868,869],{"class":297}," cell.fill\n",[257,871,872,875,878,881,883,886,889,891,893],{"class":259,"line":350},[257,873,874],{"class":293},"    if",[257,876,877],{"class":297}," fill ",[257,879,880],{"class":293},"is",[257,882,851],{"class":626},[257,884,885],{"class":293}," or",[257,887,888],{"class":297}," fill.fill_type ",[257,890,880],{"class":293},[257,892,851],{"class":626},[257,894,854],{"class":297},[257,896,897,900],{"class":259,"line":361},[257,898,899],{"class":293},"        return",[257,901,902],{"class":626}," None\n",[257,904,905,908,910],{"class":259,"line":384},[257,906,907],{"class":297},"    color ",[257,909,333],{"class":293},[257,911,912],{"class":297}," fill.fgColor\n",[257,914,915,917,920,923,926],{"class":259,"line":431},[257,916,874],{"class":293},[257,918,919],{"class":297}," color.type ",[257,921,922],{"class":293},"==",[257,924,925],{"class":267}," \"rgb\"",[257,927,854],{"class":297},[257,929,930,932,935,938,941],{"class":259,"line":468},[257,931,899],{"class":293},[257,933,934],{"class":297}," color.rgb[",[257,936,937],{"class":293},"-",[257,939,940],{"class":626},"6",[257,942,943],{"class":297},":].upper()\n",[257,945,946,948,950,952,955],{"class":259,"line":474},[257,947,874],{"class":293},[257,949,919],{"class":297},[257,951,922],{"class":293},[257,953,954],{"class":267}," \"theme\"",[257,956,854],{"class":297},[257,958,959,961,964,967,971,974,977,980,983,986],{"class":259,"line":479},[257,960,899],{"class":293},[257,962,963],{"class":293}," f",[257,965,966],{"class":267},"\"theme",[257,968,970],{"class":969},"sSjpA","{",[257,972,973],{"class":297},"color.theme",[257,975,976],{"class":969},"}{",[257,978,979],{"class":297},"color.tint",[257,981,982],{"class":293},":+.2f",[257,984,985],{"class":969},"}",[257,987,988],{"class":267},"\"\n",[257,990,991,993,995,997,1000,1003,1006,1009,1012,1015,1018],{"class":259,"line":517},[257,992,874],{"class":293},[257,994,919],{"class":297},[257,996,922],{"class":293},[257,998,999],{"class":267}," \"indexed\"",[257,1001,1002],{"class":293}," and",[257,1004,1005],{"class":297}," color.indexed ",[257,1007,1008],{"class":293},"\u003C",[257,1010,1011],{"class":626}," len",[257,1013,1014],{"class":297},"(",[257,1016,1017],{"class":626},"COLOR_INDEX",[257,1019,1020],{"class":297},"):\n",[257,1022,1023,1025,1028,1031,1033,1035],{"class":259,"line":547},[257,1024,899],{"class":293},[257,1026,1027],{"class":626}," COLOR_INDEX",[257,1029,1030],{"class":297},"[color.indexed][",[257,1032,937],{"class":293},[257,1034,940],{"class":626},[257,1036,943],{"class":297},[257,1038,1039,1042],{"class":259,"line":577},[257,1040,1041],{"class":293},"    return",[257,1043,902],{"class":626},[257,1045,1046],{"class":259,"line":605},[257,1047,324],{"emptyLinePlaceholder":323},[257,1049,1050,1052,1054,1056,1059,1062,1064,1066,1068,1071,1073,1075],{"class":259,"line":642},[257,1051,387],{"class":293},[257,1053,390],{"class":297},[257,1055,393],{"class":293},[257,1057,1058],{"class":297}," ws.iter_rows(",[257,1060,1061],{"class":501},"min_row",[257,1063,333],{"class":293},[257,1065,120],{"class":626},[257,1067,370],{"class":297},[257,1069,1070],{"class":501},"max_col",[257,1072,333],{"class":293},[257,1074,98],{"class":626},[257,1076,1020],{"class":297},[257,1078,1080,1083,1086,1088,1091,1093],{"class":259,"line":1079},18,[257,1081,1082],{"class":626},"    print",[257,1084,1085],{"class":297},"(row[",[257,1087,46],{"class":626},[257,1089,1090],{"class":297},"].value, fill_key(row[",[257,1092,46],{"class":626},[257,1094,1095],{"class":297},"]))\n",[10,1097,1098,1099,1102,1103,1106],{},"Resolving a theme colour to actual RGB means reading the theme XML and applying the tint yourself,\nwhich is rarely worth it. The key only needs to be ",[777,1100,1101],{},"stable",": once you have seen that ",[14,1104,1105],{},"theme5+0.40"," is\nwhat this workbook uses for \"done\", map that key to a status exactly as you would a hex code.",[25,1108,34,1114,34,1117,34,1120,34,1124,34,1132,34,1136,34,1139,34,1143,34,1146,34,1151,34,1156,34,1161,34,1166,34,1170,34,1174,34,1176,34,1179,34,1182,34,1186,34,1189,34,1192,34,1195,34,1197,34,1200,34,1203,34,1207,34,1211,34,1214,34,1217],{"viewBox":1109,"role":28,"ariaLabelledBy":1110,"xmlns":32,"style":1113},"0 0 760 282",[1111,1112],"ccs-kinds-t","ccs-kinds-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,1115,1116],{"id":1111},"Choosing how to read a fill colour from its type",[40,1118,1119],{"id":1112},"If the colour type is rgb, read the last six hex digits. If it is theme, combine the theme index and tint into a key. If it is indexed, look it up in the legacy colour table.",[44,1121],{"x":46,"y":46,"width":1122,"height":1123,"fill":49},"760","282",[44,1125],{"x":188,"y":59,"width":1126,"height":1127,"rx":1128,"fill":1129,"stroke":1130,"style":1131},"380","52","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[51,1133,1135],{"x":1126,"y":1127,"style":1134},"font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","what is cell.fill.fgColor.type?",[259,1137],{"x1":1126,"y1":1138,"x2":1126,"y2":87,"stroke":1130,"style":1131},"72",[259,1140],{"x1":1141,"y1":87,"x2":1142,"y2":87,"stroke":1130,"style":1131},"133.3","626.7",[259,1144],{"x1":1141,"y1":87,"x2":1141,"y2":1145,"stroke":1130,"style":1131},"124",[1147,1148],"polygon",{"points":1149,"fill":1150},"133.3,127 128.3,118 138.3,118","#5b5cf0",[44,1152],{"x":59,"y":1153,"width":1154,"height":1155,"rx":1128,"fill":123,"stroke":124,"style":1131},"128","226.7","104",[51,1157,1160],{"x":1141,"y":1158,"style":1159},"150","font-size:11px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","type == \"rgb\"",[51,1162,1165],{"x":1141,"y":1163,"style":1164},"174","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","hex digits",[51,1167,1169],{"x":1141,"y":1168,"style":133},"196","rgb[-6:]",[51,1171,1173],{"x":1141,"y":1172,"style":133},"216","e.g. C6EFCE",[259,1175],{"x1":1126,"y1":87,"x2":1126,"y2":1145,"stroke":1130,"style":1131},[1147,1177],{"points":1178,"fill":1150},"380,127 375,118 385,118",[44,1180],{"x":1181,"y":1153,"width":1154,"height":1155,"rx":1128,"fill":1129,"stroke":1130,"style":1131},"266.7",[51,1183,1185],{"x":1126,"y":1158,"style":1184},"font-size:11px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle","type == \"theme\"",[51,1187,1188],{"x":1126,"y":1163,"style":1134},"theme + tint",[51,1190,1191],{"x":1126,"y":1168,"style":133},"palette slot",[51,1193,1194],{"x":1126,"y":1172,"style":133},"e.g. theme5+0.40",[259,1196],{"x1":1142,"y1":87,"x2":1142,"y2":1145,"stroke":1130,"style":1131},[1147,1198],{"points":1199,"fill":1150},"626.7,127 621.7,118 631.7,118",[44,1201],{"x":1202,"y":1153,"width":1154,"height":1155,"rx":1128,"fill":192,"stroke":193,"style":1131},"513.3",[51,1204,1206],{"x":1142,"y":1158,"style":1205},"font-size:11px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","type == \"indexed\"",[51,1208,1210],{"x":1142,"y":1163,"style":1209},"font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","legacy table",[51,1212,1213],{"x":1142,"y":1168,"style":133},"COLOR_INDEX lookup",[51,1215,1216],{"x":1142,"y":1172,"style":133},"old .xls files",[51,1218,1220],{"x":1126,"y":1219,"style":240},"266","a stable key is enough; exact RGB is rarely needed",[243,1222,1224],{"id":1223},"step-3-read-font-styles-the-same-way","Step 3 — Read font styles the same way",[10,1226,1227,1228,1231,1232,1235,1236,1239,1240,1243,1244,1247,1248,1251],{},"Fonts are simpler because the properties that carry meaning are booleans. ",[14,1229,1230],{},"font.b"," is bold, ",[14,1233,1234],{},"font.i","\nitalic, ",[14,1237,1238],{},"font.strike"," strikethrough and ",[14,1241,1242],{},"font.u"," the underline style. Font colour works exactly like fill\ncolour, through ",[14,1245,1246],{},"font.color",", which can be ",[14,1249,1250],{},"None"," when the cell uses the default.",[248,1253,1255],{"className":284,"code":1254,"language":286,"meta":253,"style":253},"def font_flags(cell) -> dict:\n    font = cell.font\n    color = font.color.rgb[-6:] if font.color is not None and font.color.type == \"rgb\" else None\n    return {\"bold\": bool(font.b), \"struck\": bool(font.strike), \"font_color\": color}\n\nprint(font_flags(ws[\"B4\"]))   # {'bold': True, 'struck': True, 'font_color': None}\nprint(font_flags(ws[\"B2\"]))   # {'bold': False, 'struck': False, 'font_color': None}\n",[14,1256,1257,1271,1281,1324,1359,1363,1378],{"__ignoreMap":253},[257,1258,1259,1261,1264,1266,1269],{"class":259,"line":260},[257,1260,835],{"class":293},[257,1262,1263],{"class":838}," font_flags",[257,1265,842],{"class":297},[257,1267,1268],{"class":626},"dict",[257,1270,854],{"class":297},[257,1272,1273,1276,1278],{"class":259,"line":307},[257,1274,1275],{"class":297},"    font ",[257,1277,333],{"class":293},[257,1279,1280],{"class":297}," cell.font\n",[257,1282,1283,1285,1287,1290,1292,1294,1297,1300,1303,1305,1308,1310,1312,1315,1317,1319,1322],{"class":259,"line":320},[257,1284,907],{"class":297},[257,1286,333],{"class":293},[257,1288,1289],{"class":297}," font.color.rgb[",[257,1291,937],{"class":293},[257,1293,940],{"class":626},[257,1295,1296],{"class":297},":] ",[257,1298,1299],{"class":293},"if",[257,1301,1302],{"class":297}," font.color ",[257,1304,880],{"class":293},[257,1306,1307],{"class":293}," not",[257,1309,851],{"class":626},[257,1311,1002],{"class":293},[257,1313,1314],{"class":297}," font.color.type ",[257,1316,922],{"class":293},[257,1318,925],{"class":267},[257,1320,1321],{"class":293}," else",[257,1323,902],{"class":626},[257,1325,1326,1328,1331,1334,1337,1340,1343,1346,1348,1350,1353,1356],{"class":259,"line":327},[257,1327,1041],{"class":293},[257,1329,1330],{"class":297}," {",[257,1332,1333],{"class":267},"\"bold\"",[257,1335,1336],{"class":297},": ",[257,1338,1339],{"class":626},"bool",[257,1341,1342],{"class":297},"(font.b), ",[257,1344,1345],{"class":267},"\"struck\"",[257,1347,1336],{"class":297},[257,1349,1339],{"class":626},[257,1351,1352],{"class":297},"(font.strike), ",[257,1354,1355],{"class":267},"\"font_color\"",[257,1357,1358],{"class":297},": color}\n",[257,1360,1361],{"class":259,"line":339},[257,1362,324],{"emptyLinePlaceholder":323},[257,1364,1365,1367,1370,1372,1375],{"class":259,"line":350},[257,1366,736],{"class":626},[257,1368,1369],{"class":297},"(font_flags(ws[",[257,1371,610],{"class":267},[257,1373,1374],{"class":297},"]))   ",[257,1376,1377],{"class":513},"# {'bold': True, 'struck': True, 'font_color': None}\n",[257,1379,1380,1382,1384,1387,1389],{"class":259,"line":361},[257,1381,736],{"class":626},[257,1383,1369],{"class":297},[257,1385,1386],{"class":267},"\"B2\"",[257,1388,1374],{"class":297},[257,1390,1391],{"class":513},"# {'bold': False, 'struck': False, 'font_color': None}\n",[243,1393,1395],{"id":1394},"step-4-build-a-dataframe-with-the-formatting-as-columns","Step 4 — Build a DataFrame with the formatting as columns",[10,1397,1398],{},"Now combine the two: walk the rows once, keep the values, and add one column per piece of formatting\nthat carries meaning. Map the colour keys to readable statuses in a dictionary, and let anything\nunexpected come through as its raw key so that a new colour is visible rather than silently dropped.",[248,1400,1402],{"className":284,"code":1401,"language":286,"meta":253,"style":253},"import pandas as pd\n\nSTATUS_BY_FILL = {\"C6EFCE\": \"done\", \"FFC7CE\": \"blocked\", \"FFEB9C\": \"waiting\"}\n\nrecords = []\nfor task, owner, due in ws.iter_rows(min_row=2, max_col=3):\n    key = fill_key(task)\n    records.append({\n        \"task\": task.value,\n        \"owner\": owner.value,\n        \"due\": pd.to_datetime(due.value),\n        \"status\": STATUS_BY_FILL.get(key, key or \"not started\"),\n        \"owner_left\": bool(owner.font.strike),\n    })\n\ntracker = pd.DataFrame(records)\nprint(tracker)\nprint(tracker[\"status\"].value_counts())\n",[14,1403,1404,1417,1421,1459,1463,1473,1500,1510,1515,1523,1531,1539,1559,1571,1576,1580,1590,1597],{"__ignoreMap":253},[257,1405,1406,1408,1411,1414],{"class":259,"line":260},[257,1407,301],{"class":293},[257,1409,1410],{"class":297}," pandas ",[257,1412,1413],{"class":293},"as",[257,1415,1416],{"class":297}," pd\n",[257,1418,1419],{"class":259,"line":307},[257,1420,324],{"emptyLinePlaceholder":323},[257,1422,1423,1426,1429,1431,1433,1435,1438,1440,1442,1444,1447,1449,1451,1453,1456],{"class":259,"line":320},[257,1424,1425],{"class":626},"STATUS_BY_FILL",[257,1427,1428],{"class":293}," =",[257,1430,1330],{"class":297},[257,1432,507],{"class":267},[257,1434,1336],{"class":297},[257,1436,1437],{"class":267},"\"done\"",[257,1439,370],{"class":297},[257,1441,539],{"class":267},[257,1443,1336],{"class":297},[257,1445,1446],{"class":267},"\"blocked\"",[257,1448,370],{"class":297},[257,1450,569],{"class":267},[257,1452,1336],{"class":297},[257,1454,1455],{"class":267},"\"waiting\"",[257,1457,1458],{"class":297},"}\n",[257,1460,1461],{"class":259,"line":327},[257,1462,324],{"emptyLinePlaceholder":323},[257,1464,1465,1468,1470],{"class":259,"line":339},[257,1466,1467],{"class":297},"records ",[257,1469,333],{"class":293},[257,1471,1472],{"class":297}," []\n",[257,1474,1475,1477,1480,1482,1484,1486,1488,1490,1492,1494,1496,1498],{"class":259,"line":350},[257,1476,387],{"class":293},[257,1478,1479],{"class":297}," task, owner, due ",[257,1481,393],{"class":293},[257,1483,1058],{"class":297},[257,1485,1061],{"class":501},[257,1487,333],{"class":293},[257,1489,120],{"class":626},[257,1491,370],{"class":297},[257,1493,1070],{"class":501},[257,1495,333],{"class":293},[257,1497,157],{"class":626},[257,1499,1020],{"class":297},[257,1501,1502,1505,1507],{"class":259,"line":361},[257,1503,1504],{"class":297},"    key ",[257,1506,333],{"class":293},[257,1508,1509],{"class":297}," fill_key(task)\n",[257,1511,1512],{"class":259,"line":384},[257,1513,1514],{"class":297},"    records.append({\n",[257,1516,1517,1520],{"class":259,"line":431},[257,1518,1519],{"class":267},"        \"task\"",[257,1521,1522],{"class":297},": task.value,\n",[257,1524,1525,1528],{"class":259,"line":468},[257,1526,1527],{"class":267},"        \"owner\"",[257,1529,1530],{"class":297},": owner.value,\n",[257,1532,1533,1536],{"class":259,"line":474},[257,1534,1535],{"class":267},"        \"due\"",[257,1537,1538],{"class":297},": pd.to_datetime(due.value),\n",[257,1540,1541,1544,1546,1548,1551,1554,1557],{"class":259,"line":479},[257,1542,1543],{"class":267},"        \"status\"",[257,1545,1336],{"class":297},[257,1547,1425],{"class":626},[257,1549,1550],{"class":297},".get(key, key ",[257,1552,1553],{"class":293},"or",[257,1555,1556],{"class":267}," \"not started\"",[257,1558,428],{"class":297},[257,1560,1561,1564,1566,1568],{"class":259,"line":517},[257,1562,1563],{"class":267},"        \"owner_left\"",[257,1565,1336],{"class":297},[257,1567,1339],{"class":626},[257,1569,1570],{"class":297},"(owner.font.strike),\n",[257,1572,1573],{"class":259,"line":547},[257,1574,1575],{"class":297},"    })\n",[257,1577,1578],{"class":259,"line":577},[257,1579,324],{"emptyLinePlaceholder":323},[257,1581,1582,1585,1587],{"class":259,"line":605},[257,1583,1584],{"class":297},"tracker ",[257,1586,333],{"class":293},[257,1588,1589],{"class":297}," pd.DataFrame(records)\n",[257,1591,1592,1594],{"class":259,"line":642},[257,1593,736],{"class":626},[257,1595,1596],{"class":297},"(tracker)\n",[257,1598,1599,1601,1604,1607],{"class":259,"line":1079},[257,1600,736],{"class":626},[257,1602,1603],{"class":297},"(tracker[",[257,1605,1606],{"class":267},"\"status\"",[257,1608,1609],{"class":297},"].value_counts())\n",[10,1611,1612],{},"From this point the sheet behaves like any other data source. You can filter blocked tasks, count work\nstill assigned to people who have left, or write the result back as a proper table with a real Status\ncolumn — which is often the most useful outcome, because it retires the colour convention for good.",[25,1614,34,1619,34,1622,34,1625,34,1628,34,1631,34,1636,34,1639,34,1644,34,1647,34,1650,34,1654,34,1657,34,1661,34,1665,34,1668,34,1671,34,1675,34,1678,34,1681,34,1685,34,1688,34,1691,34,1695,34,1698,34,1701],{"viewBox":1615,"role":28,"ariaLabelledBy":1616,"xmlns":32,"style":1113},"0 0 760 156",[1617,1618],"ccs-pipeline-t","ccs-pipeline-d",[36,1620,1621],{"id":1617},"Turning formatting into columns",[40,1623,1624],{"id":1618},"Load the workbook with openpyxl, compute a fill key and font flags for each row, map fill keys to status names, and assemble a DataFrame with status and owner_left columns.",[44,1626],{"x":46,"y":46,"width":1122,"height":1627,"fill":49},"156",[44,1629],{"x":59,"y":59,"width":1158,"height":1630,"rx":1128,"fill":63,"stroke":64,"style":1131},"86",[51,1632,1635],{"x":1633,"y":60,"style":1634},"95","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","load_workbook",[51,1637,1638],{"x":1633,"y":1138,"style":103},"styles kept",[259,1640],{"x1":1641,"y1":1642,"x2":1643,"y2":1642,"stroke":1130,"style":1131},"175","63","204",[1147,1645],{"points":1646,"fill":1150},"207,63 198,58 198,68",[44,1648],{"x":1649,"y":59,"width":1158,"height":1630,"rx":1128,"fill":1129,"stroke":1130,"style":1131},"210",[51,1651,1653],{"x":1652,"y":60,"style":1134},"285","fill_key()",[51,1655,1656],{"x":1652,"y":1138,"style":133},"rgb, theme",[51,1658,1660],{"x":1652,"y":1659,"style":133},"92","or indexed",[259,1662],{"x1":1663,"y1":1642,"x2":1664,"y2":1642,"stroke":1130,"style":1131},"365","394",[1147,1666],{"points":1667,"fill":1150},"397,63 388,58 388,68",[44,1669],{"x":1670,"y":59,"width":1158,"height":1630,"rx":1128,"fill":1129,"stroke":1130,"style":1131},"400",[51,1672,1674],{"x":1673,"y":60,"style":1134},"475","map to status",[51,1676,1677],{"x":1673,"y":1138,"style":133},"dict lookup,",[51,1679,1680],{"x":1673,"y":1659,"style":133},"unknown kept",[259,1682],{"x1":1683,"y1":1642,"x2":1684,"y2":1642,"stroke":1130,"style":1131},"555","584",[1147,1686],{"points":1687,"fill":1150},"587,63 578,58 578,68",[44,1689],{"x":1690,"y":59,"width":1158,"height":1630,"rx":1128,"fill":123,"stroke":124,"style":1131},"590",[51,1692,1694],{"x":1693,"y":60,"style":1164},"665","DataFrame",[51,1696,1697],{"x":1693,"y":1138,"style":133},"status and",[51,1699,1700],{"x":1693,"y":1659,"style":133},"owner_left",[51,1702,1703],{"x":1126,"y":153,"style":240},"unmapped colours surface as raw keys, never as silence",[243,1705,1707],{"id":1706},"step-5-watch-for-conditional-formatting","Step 5 — Watch for conditional formatting",[10,1709,1710,1711,1714,1715,1718],{},"This is the trap that catches almost everyone. A cell shaded by a ",[777,1712,1713],{},"conditional format"," rule has no fill\nof its own; the colour is computed by Excel at display time from a rule stored on the worksheet. openpyxl\nreads the rule but does not evaluate it, so ",[14,1716,1717],{},"cell.fill"," reports the cell as unfilled even though it is\nbright red on screen.",[248,1720,1722],{"className":284,"code":1721,"language":286,"meta":253,"style":253},"for cf_range in ws.conditional_formatting:\n    for rule in cf_range.rules:\n        print(cf_range.sqref, rule.type, rule.operator, rule.formula)\n",[14,1723,1724,1736,1749],{"__ignoreMap":253},[257,1725,1726,1728,1731,1733],{"class":259,"line":260},[257,1727,387],{"class":293},[257,1729,1730],{"class":297}," cf_range ",[257,1732,393],{"class":293},[257,1734,1735],{"class":297}," ws.conditional_formatting:\n",[257,1737,1738,1741,1744,1746],{"class":259,"line":307},[257,1739,1740],{"class":293},"    for",[257,1742,1743],{"class":297}," rule ",[257,1745,393],{"class":293},[257,1747,1748],{"class":297}," cf_range.rules:\n",[257,1750,1751,1754],{"class":259,"line":320},[257,1752,1753],{"class":626},"        print",[257,1755,1756],{"class":297},"(cf_range.sqref, rule.type, rule.operator, rule.formula)\n",[10,1758,1759,1760,809],{},"If that loop prints anything covering your columns, the colours you care about are derived, and the\nright move is to re-implement the rule's condition in pandas instead of reading colours at all. The rule\nis usually simple — a threshold, a date comparison, a duplicate check — and reproducing it gives you a\ncolumn that is correct even for rows added after the rule was written. The rule types themselves are\ndescribed in ",[19,1761,1763],{"href":1762},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Applying Conditional Formatting with openpyxl",[243,1765,1767],{"id":1766},"step-6-write-the-decoded-status-back-as-a-real-column","Step 6 — Write the decoded status back as a real column",[10,1769,1770,1771,1774],{},"Reading colours is a rescue operation; the lasting fix is a workbook where the status is a value. Add a\nStatus column next to the tasks, fill it from the decoded frame, and — if people still want the colour —\nlet a conditional format paint it from that column. The file then looks the same to its readers, and the\nnext script that reads it can use ",[14,1772,1773],{},"pd.read_excel"," like any other sheet.",[248,1776,1778],{"className":284,"code":1777,"language":286,"meta":253,"style":253},"from openpyxl.formatting.rule import FormulaRule\n\nws.insert_cols(2)\nws[\"B1\"] = \"Status\"\nfor offset, status in enumerate(tracker[\"status\"], start=2):\n    ws.cell(row=offset, column=2, value=status)\n\nlast = ws.max_row\nfor word, color in ((\"done\", \"C6EFCE\"), (\"blocked\", \"FFC7CE\"), (\"waiting\", \"FFEB9C\")):\n    ws.conditional_formatting.add(\n        f\"A2:A{last}\",\n        FormulaRule(formula=[f'$B2=\"{word}\"'], fill=PatternFill(\"solid\", start_color=color)),\n    )\nwb.save(\"tracker_with_status.xlsx\")\nprint(pd.read_excel(\"tracker_with_status.xlsx\").head())\n",[14,1779,1780,1792,1796,1805,1820,1848,1878,1882,1892,1929,1934,1955,2005,2010,2019],{"__ignoreMap":253},[257,1781,1782,1784,1787,1789],{"class":259,"line":260},[257,1783,294],{"class":293},[257,1785,1786],{"class":297}," openpyxl.formatting.rule ",[257,1788,301],{"class":293},[257,1790,1791],{"class":297}," FormulaRule\n",[257,1793,1794],{"class":259,"line":307},[257,1795,324],{"emptyLinePlaceholder":323},[257,1797,1798,1801,1803],{"class":259,"line":320},[257,1799,1800],{"class":297},"ws.insert_cols(",[257,1802,120],{"class":626},[257,1804,639],{"class":297},[257,1806,1807,1809,1812,1815,1817],{"class":259,"line":327},[257,1808,482],{"class":297},[257,1810,1811],{"class":267},"\"B1\"",[257,1813,1814],{"class":297},"] ",[257,1816,333],{"class":293},[257,1818,1819],{"class":267}," \"Status\"\n",[257,1821,1822,1824,1827,1829,1832,1834,1836,1839,1842,1844,1846],{"class":259,"line":339},[257,1823,387],{"class":293},[257,1825,1826],{"class":297}," offset, status ",[257,1828,393],{"class":293},[257,1830,1831],{"class":626}," enumerate",[257,1833,1603],{"class":297},[257,1835,1606],{"class":267},[257,1837,1838],{"class":297},"], ",[257,1840,1841],{"class":501},"start",[257,1843,333],{"class":293},[257,1845,120],{"class":626},[257,1847,1020],{"class":297},[257,1849,1850,1853,1856,1858,1861,1864,1866,1868,1870,1873,1875],{"class":259,"line":350},[257,1851,1852],{"class":297},"    ws.cell(",[257,1854,1855],{"class":501},"row",[257,1857,333],{"class":293},[257,1859,1860],{"class":297},"offset, ",[257,1862,1863],{"class":501},"column",[257,1865,333],{"class":293},[257,1867,120],{"class":626},[257,1869,370],{"class":297},[257,1871,1872],{"class":501},"value",[257,1874,333],{"class":293},[257,1876,1877],{"class":297},"status)\n",[257,1879,1880],{"class":259,"line":361},[257,1881,324],{"emptyLinePlaceholder":323},[257,1883,1884,1887,1889],{"class":259,"line":384},[257,1885,1886],{"class":297},"last ",[257,1888,333],{"class":293},[257,1890,1891],{"class":297}," ws.max_row\n",[257,1893,1894,1896,1899,1901,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926],{"class":259,"line":431},[257,1895,387],{"class":293},[257,1897,1898],{"class":297}," word, color ",[257,1900,393],{"class":293},[257,1902,1903],{"class":297}," ((",[257,1905,1437],{"class":267},[257,1907,370],{"class":297},[257,1909,507],{"class":267},[257,1911,412],{"class":297},[257,1913,1446],{"class":267},[257,1915,370],{"class":297},[257,1917,539],{"class":267},[257,1919,412],{"class":297},[257,1921,1455],{"class":267},[257,1923,370],{"class":297},[257,1925,569],{"class":267},[257,1927,1928],{"class":297},")):\n",[257,1930,1931],{"class":259,"line":468},[257,1932,1933],{"class":297},"    ws.conditional_formatting.add(\n",[257,1935,1936,1939,1942,1944,1947,1949,1952],{"class":259,"line":474},[257,1937,1938],{"class":293},"        f",[257,1940,1941],{"class":267},"\"A2:A",[257,1943,970],{"class":969},[257,1945,1946],{"class":297},"last",[257,1948,985],{"class":969},[257,1950,1951],{"class":267},"\"",[257,1953,1954],{"class":297},",\n",[257,1956,1957,1960,1963,1965,1968,1971,1974,1976,1979,1981,1984,1986,1989,1991,1994,1996,1998,2000,2002],{"class":259,"line":479},[257,1958,1959],{"class":297},"        FormulaRule(",[257,1961,1962],{"class":501},"formula",[257,1964,333],{"class":293},[257,1966,1967],{"class":297},"[",[257,1969,1970],{"class":293},"f",[257,1972,1973],{"class":267},"'$B2=\"",[257,1975,970],{"class":969},[257,1977,1978],{"class":297},"word",[257,1980,985],{"class":969},[257,1982,1983],{"class":267},"\"'",[257,1985,1838],{"class":297},[257,1987,1988],{"class":501},"fill",[257,1990,333],{"class":293},[257,1992,1993],{"class":297},"PatternFill(",[257,1995,496],{"class":267},[257,1997,370],{"class":297},[257,1999,502],{"class":501},[257,2001,333],{"class":293},[257,2003,2004],{"class":297},"color)),\n",[257,2006,2007],{"class":259,"line":517},[257,2008,2009],{"class":297},"    )\n",[257,2011,2012,2014,2017],{"class":259,"line":547},[257,2013,645],{"class":297},[257,2015,2016],{"class":267},"\"tracker_with_status.xlsx\"",[257,2018,639],{"class":297},[257,2020,2021,2023,2026,2028],{"class":259,"line":577},[257,2022,736],{"class":626},[257,2024,2025],{"class":297},"(pd.read_excel(",[257,2027,2016],{"class":267},[257,2029,2030],{"class":297},").head())\n",[10,2032,2033,2034,2037],{},"The hand-applied fills are still present on the copied cells, so clear them if you want the rule to be\nthe only source of colour: assign ",[14,2035,2036],{},"PatternFill(fill_type=None)"," to each task cell before saving.",[243,2039,2041],{"id":2040},"common-pitfalls","Common pitfalls",[2043,2044,2045,2061],"table",{},[2046,2047,2048],"thead",{},[2049,2050,2051,2055,2058],"tr",{},[2052,2053,2054],"th",{},"Symptom",[2052,2056,2057],{},"Cause",[2052,2059,2060],{},"Fix",[2062,2063,2064,2088,2104,2115,2131,2155],"tbody",{},[2049,2065,2066,2073,2079],{},[2067,2068,2069,2070],"td",{},"Every cell reports ",[14,2071,2072],{},"00000000",[2067,2074,2075,2076,2078],{},"Reading ",[14,2077,790],{},", or the colour comes from conditional formatting",[2067,2080,2081,2082,2084,2085],{},"Read ",[14,2083,783],{},"; check ",[14,2086,2087],{},"ws.conditional_formatting",[2049,2089,2090,2095,2098],{},[2067,2091,2092,2094],{},[14,2093,804],{}," returns an error message string",[2067,2096,2097],{},"The colour is a theme colour",[2067,2099,2100,2101,2103],{},"Branch on ",[14,2102,800],{}," as in Step 2",[2049,2105,2106,2109,2112],{},[2067,2107,2108],{},"Colours differ slightly between rows",[2067,2110,2111],{},"Somebody picked \"a green\" from the palette each time",[2067,2113,2114],{},"Group near-identical hex codes, or map every key you find",[2049,2116,2117,2120,2126],{},[2067,2118,2119],{},"Formatting missing entirely",[2067,2121,2122,2123,2125],{},"Loaded with ",[14,2124,660],{}," or read through pandas",[2067,2127,2128,2129],{},"Load normally with ",[14,2130,1635],{},[2049,2132,2133,2138,2144],{},[2067,2134,2135,2136],{},"Colour visible in Excel, missing in ",[14,2137,808],{},[2067,2139,2140,2141],{},"xlrd only reads formatting with ",[14,2142,2143],{},"formatting_info=True",[2067,2145,2146,2147,2150,2151],{},"Convert to ",[14,2148,2149],{},".xlsx"," first, as in ",[19,2152,2154],{"href":2153},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002F","Convert .xls to .xlsx with Python",[2049,2156,2157,2160,2163],{},[2067,2158,2159],{},"Status wrong on merged cells",[2067,2161,2162],{},"Only the top-left cell of a merge carries the style",[2067,2164,2165],{},"Read the style from the merge's anchor cell",[10,2167,2168,2169,2172,2173,2176,2177,2180],{},"Before writing the mapping dictionary, it pays to inventory what is actually in the file. A one-liner\nover the column — ",[14,2170,2171],{},"Counter(fill_key(c) for c in ws[\"A\"][1:])"," from ",[14,2174,2175],{},"collections"," — lists every distinct\nkey and how often it appears, which catches the near-identical greens before they turn into a row\nlabelled ",[14,2178,2179],{},"C6EFCF"," in your report.",[243,2182,2184],{"id":2183},"performance-and-scale-notes","Performance and scale notes",[10,2186,2187,2188,2190],{},"Styles are the expensive part of an ",[14,2189,2149],{}," to load, and there is no streaming mode that keeps them in\nfull. For a tracker of a few thousand rows a normal load takes well under a second. For a very large\nsheet, read the values with pandas or calamine for speed and use openpyxl only for the one or two columns\nwhose formatting you need, or pre-convert the colours once and store the result.",[25,2192,34,2198,34,2201,34,2204,34,2208,34,2212,34,2216,34,2222,34,2226,34,2232,34,2237,34,2240,34,2244,34,2247,34,2250,34,2254,34,2257,34,2261,34,2264,34,2268,34,2271],{"viewBox":2193,"role":28,"ariaLabelledBy":2194,"xmlns":32,"style":2197},"0 0 720 212",[2195,2196],"ccs-cost-t","ccs-cost-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,2199,2200],{"id":2195},"Relative load cost with and without styles",[40,2202,2203],{"id":2196},"A calamine read of values only is fastest, openpyxl read_only mode is close, and a full openpyxl load that keeps styles is several times slower on the same large sheet.",[44,2205],{"x":46,"y":46,"width":2206,"height":2207,"fill":49},"720","212",[51,2209,2211],{"x":59,"y":62,"style":2210},"font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative time to load a 100,000-row sheet",[44,2213],{"x":59,"y":2214,"width":185,"height":2215,"rx":940,"fill":49,"stroke":64,"style":65},"46","28",[51,2217,2221],{"x":2218,"y":2219,"style":2220},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","calamine values",[44,2223],{"x":1643,"y":2224,"width":2225,"height":144,"rx":940,"fill":63,"stroke":64,"style":65},"47","330",[44,2227],{"x":2228,"y":2229,"width":2230,"height":2231,"rx":221,"fill":123,"stroke":124,"style":65},"205","48","37.6","24",[51,2233,2236],{"x":2234,"y":2219,"style":2235},"546","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","values only",[44,2238],{"x":59,"y":2239,"width":185,"height":2215,"rx":940,"fill":49,"stroke":64,"style":65},"90",[51,2241,2243],{"x":2218,"y":2242,"style":2220},"109","openpyxl read_only",[44,2245],{"x":1643,"y":2246,"width":2225,"height":144,"rx":940,"fill":63,"stroke":64,"style":65},"91",[44,2248],{"x":2228,"y":1659,"width":2249,"height":2231,"rx":221,"fill":1129,"stroke":1130,"style":65},"97",[51,2251,2253],{"x":2234,"y":2242,"style":2252},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","thin styles",[44,2255],{"x":59,"y":2256,"width":185,"height":2215,"rx":940,"fill":49,"stroke":64,"style":65},"134",[51,2258,2260],{"x":2218,"y":2259,"style":2220},"153","openpyxl full load",[44,2262],{"x":1643,"y":2263,"width":2225,"height":144,"rx":940,"fill":63,"stroke":64,"style":65},"135",[44,2265],{"x":2228,"y":2266,"width":2267,"height":2231,"rx":221,"fill":192,"stroke":193,"style":65},"136","295",[51,2269,1638],{"x":2234,"y":2259,"style":2270},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[51,2272,2275],{"x":2273,"y":2274,"style":240},"360","194","pay for styles only on the columns that need them",[243,2277,2279],{"id":2278},"conclusion","Conclusion",[10,2281,2282,2283,2286,2287,2290],{},"Formatting is data whenever people use it that way, and openpyxl gives you everything needed to read\nit: ",[14,2284,2285],{},"fill.fgColor"," for shading, with a branch for RGB, theme and indexed colours, and the boolean\nproperties of ",[14,2288,2289],{},"font"," for bold and strikethrough. Build a stable key per style, map it to a meaningful\nvalue, keep unknown keys visible, and check for conditional formatting before trusting any colour you\nread. Then write the result back with a real column so the next reader never has to decode colours again.",[243,2292,2294],{"id":2293},"frequently-asked-questions","Frequently asked questions",[10,2296,2297,2301],{},[2298,2299,2300],"strong",{},"Can pandas read cell colours?","\nNo. read_excel returns values only. You need openpyxl (or xlrd with formatting_info=True for old .xls files) to read fills and fonts, then merge those columns into your DataFrame.",[10,2303,2304,2307],{},[2298,2305,2306],{},"Why does a red cell show as unfilled in openpyxl?","\nBecause the red comes from a conditional formatting rule, which Excel evaluates when it displays the sheet. openpyxl stores the rule but does not evaluate it, so the cell's own fill is empty.",[10,2309,2310,2313],{},[2298,2311,2312],{},"How do I get the actual RGB value of a theme colour?","\nYou have to read the workbook's theme XML, find the palette slot and apply the tint. For classification a key built from the theme index and tint is enough and far simpler.",[10,2315,2316,2319],{},[2298,2317,2318],{},"Can I write the status back as colours in a new file?","\nYes — assign PatternFill objects in openpyxl, or better, add a conditional format driven by the new Status column so the colour stays in step with the data.",[243,2321,2323],{"id":2322},"related","Related",[2325,2326,2327,2334,2341,2348,2355],"ul",{},[2328,2329,2330,2331,2333],"li",{},"Up one level: ",[19,2332,22],{"href":21}," — getting reliable data out of sheets built for people.",[2328,2335,2336,2340],{},[19,2337,2339],{"href":2338},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fskip-hidden-and-filtered-rows-when-reading-excel\u002F","Skip Hidden and Filtered Rows When Reading Excel"," — another piece of meaning stored outside the cell values.",[2328,2342,2343,2347],{},[19,2344,2346],{"href":2345},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — the write side of the same style objects.",[2328,2349,2350,2354],{},[19,2351,2353],{"href":2352},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl\u002F","Highlight Cells Above a Threshold with openpyxl"," — replacing hand-applied colour with a rule.",[2328,2356,2357,2361],{},[19,2358,2360],{"href":2359},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl\u002F","Iterate over Rows and Columns with openpyxl"," — the row-walking patterns used above.",[2363,2364,2365],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":253,"searchDepth":307,"depth":307,"links":2367},[2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379],{"id":245,"depth":307,"text":246},{"id":653,"depth":307,"text":654},{"id":794,"depth":307,"text":795},{"id":1223,"depth":307,"text":1224},{"id":1394,"depth":307,"text":1395},{"id":1706,"depth":307,"text":1707},{"id":1766,"depth":307,"text":1767},{"id":2040,"depth":307,"text":2041},{"id":2183,"depth":307,"text":2184},{"id":2278,"depth":307,"text":2279},{"id":2293,"depth":307,"text":2294},{"id":2322,"depth":307,"text":2323},"2026-09-17","When a spreadsheet uses green for done and red for blocked, the colour is the data. Read fill colours, bold and strikethrough with openpyxl and turn them into real columns.","md",[2384,2386,2388,2390],{"q":2300,"a":2385},"No. read_excel returns values only. You need openpyxl (or xlrd with formatting_info=True for old .xls files) to read fills and fonts, then merge those columns into your DataFrame.",{"q":2306,"a":2387},"Because the red comes from a conditional formatting rule, which Excel evaluates when it displays the sheet. openpyxl stores the rule but does not evaluate it, so the cell's own fill is empty.",{"q":2312,"a":2389},"You have to read the workbook's theme XML, find the palette slot and apply the tint. For classification a key built from the theme index and tint is enough and far simpler.",{"q":2318,"a":2391},"Yes — assign PatternFill objects in openpyxl, or better, add a conditional format driven by the new Status column so the colour stays in step with the data.","read cell colours and font styles as data with openpyxl",null,{"breadcrumb":2395},[2396,2399,2402],{"name":2397,"item":2398},"Home","\u002F",{"name":2400,"item":2401},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl",{"title":2405,"description":2406},"Read Excel Cell Colours and Font Styles as Data with openpyxl","Extract Excel cell fill colours, theme colours, bold and strikethrough with openpyxl, map them to status values in pandas, and avoid the conditional formatting trap.","read-cell-colours-and-font-styles-as-data-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-cell-colours-and-font-styles-as-data-with-openpyxl\u002Findex",[2410,2411,2412],"messy-sheets","reading-excel","openpyxl","how-to","_rkZ-V06VqvMdd0LBFb4U4KDqzHanURCKvHHzarjvh8",[2416,2420],{"title":2417,"path":2418,"stem":2419,"children":-1},"Read an Excel Sheet Where Records Run Across Columns","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-a-sheet-where-records-run-across-columns","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-a-sheet-where-records-run-across-columns\u002Findex",{"title":2421,"path":2422,"stem":2423,"children":-1},"Read Dropdown Lists and Validation Rules from an Existing Workbook","\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","advanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook\u002Findex",[2425,2427],{"path":2426,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets",{"path":2428,"title":2429},"\u002Fadvanced-data-transformation-and-cleaning","Advanced Data Transformation and Cleaning for Excel Automation",[],[2432,2435,2438,2441,2444,2447],{"path":2433,"title":2434,"kind":2413},"\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":2436,"title":2437,"kind":2413},"\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":2439,"title":2440,"kind":2413},"\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":2442,"title":2443,"kind":2413},"\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":2445,"title":2446,"kind":2413},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-hyperlink-urls-from-excel-cells","Extract Hyperlink URLs from Excel Cells with openpyxl",{"path":2448,"title":2449,"kind":2413},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fextract-report-header-fields-from-an-excel-sheet","Extract Report Header Fields from an Excel Sheet with Python",1790972091007]