[{"data":1,"prerenderedAt":2331},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python":2295,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python":2304,"children:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python":2309,"related:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python":2310},{"id":4,"title":5,"body":6,"dateModified":2262,"datePublished":2262,"description":2263,"extension":2264,"faq":2265,"keyword":2274,"kind":2275,"meta":2276,"navigation":288,"path":2284,"seo":2285,"slug":2288,"stem":2289,"topics":2290,"type":2293,"__hash__":2294},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python\u002Findex.md","Convert Word Document Tables to Excel with Python",{"type":7,"value":8,"toc":2247},"minimark",[9,24,29,60,82,239,243,260,368,375,379,386,528,549,731,735,742,933,959,963,966,1266,1292,1296,1539,1765,1769,1772,1929,1943,1947,1953,2085,2108,2111,2115,2157,2161,2171,2175,2182,2186,2192,2198,2204,2210,2214,2243],[10,11,12,13,17,18,23],"p",{},"Price lists, rate cards, contract schedules and survey results often live in Word documents as tables. When the numbers are needed in a spreadsheet — to compare suppliers, load into a system, or check against invoices — copying and pasting table by table loses formatting, merges cells in odd ways and turns every number into text. python-docx reads a ",[14,15,16],"code",{},".docx"," file's tables directly, cell by cell, so a short script can move every table into its own sheet and convert the values on the way. This page is part of ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install python-docx pandas openpyxl\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," python-docx",[39,55,56],{"class":49}," pandas",[39,58,59],{"class":49}," openpyxl\n",[10,61,62,63,66,67,70,71,74,75,78,79,23],{},"python-docx is installed as ",[14,64,65],{},"python-docx"," and imported as ",[14,68,69],{},"docx",". The example document, ",[14,72,73],{},"price_list.docx",", has a title, an introductory paragraph and two tables, each under its own heading. The first table has a note row merged across all four columns, a description that wraps onto a second line, prices like ",[14,76,77],{},"£1,200.00"," and discounts like ",[14,80,81],{},"2.5%",[83,84,92,93,92,97,92,101,92,109,92,119,92,130,92,137,92,142,92,146,92,152,92,157,92,161,92,168,92,173,92,176,92,179,92,183,92,186,92,189,92,195,92,200,92,204,92,208,92,211,92,216,92,223,92,229,92,232,92,235],"svg",{"viewBox":85,"role":86,"ariaLabelledBy":87,"xmlns":90,"style":91},"4 0 752 160","img",[88,89],"docx-flow-t","docx-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[94,95,96],"title",{"id":88},"From a Word document to a workbook",[98,99,100],"desc",{"id":89},"The document body is walked in order, each table's cells are read with merged duplicates removed, values are converted from text, and each table is written to its own sheet named after the heading above it.",[102,103],"rect",{"x":104,"y":105,"width":106,"height":107,"fill":108},"4","0","752","160","#ffffff",[102,110],{"x":111,"y":112,"width":113,"height":114,"rx":115,"fill":116,"stroke":117,"style":118},"20.0","16.0","153.0","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[120,121,123],"g",{"transform":122},"translate(84.5,37.8) scale(1.000)",[124,125],"path",{"d":126,"fill":127,"stroke":128,"style":129},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[131,132,136],"text",{"x":133,"y":134,"style":135},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Walk the body",[131,138,141],{"x":133,"y":139,"style":140},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","headings and tables in",[131,143,145],{"x":133,"y":144,"style":140},"121.2","order",[41,147],{"x1":148,"y1":149,"x2":150,"y2":149,"stroke":151,"style":118},"178.0","80.0","200.0","var(--brand,#5b5cf0)",[153,154],"polygon",{"points":155,"fill":156},"204.0,80.0 195.0,85.0 195.0,75.0","#5b5cf0",[102,158],{"x":159,"y":112,"width":113,"height":114,"rx":115,"fill":160,"stroke":151,"style":118},"209.0","#f0f4ff",[120,162,164],{"transform":163},"translate(273.5,37.8) scale(1.000)",[124,165],{"d":166,"fill":127,"stroke":167,"style":129},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--brand-strong,#4338ca)",[131,169,172],{"x":170,"y":134,"style":171},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Read cells",[131,174,175],{"x":170,"y":139,"style":140},"skip merged",[131,177,178],{"x":170,"y":144,"style":140},"duplicates",[41,180],{"x1":181,"y1":149,"x2":182,"y2":149,"stroke":151,"style":118},"367.0","389.0",[153,184],{"points":185,"fill":156},"393.0,80.0 384.0,85.0 384.0,75.0",[102,187],{"x":188,"y":112,"width":113,"height":114,"rx":115,"fill":160,"stroke":151,"style":118},"398.0",[120,190,192],{"transform":191},"translate(462.5,46.5) scale(1.000)",[124,193],{"d":194,"fill":127,"stroke":167,"style":129},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1",[131,196,199],{"x":197,"y":198,"style":171},"474.5","93.0","Convert text",[131,201,203],{"x":197,"y":202,"style":140},"112.5","'£1,200.00' → 1200.0",[41,205],{"x1":206,"y1":149,"x2":207,"y2":149,"stroke":151,"style":118},"556.0","578.0",[153,209],{"points":210,"fill":156},"582.0,80.0 573.0,85.0 573.0,75.0",[102,212],{"x":213,"y":112,"width":113,"height":114,"rx":115,"fill":214,"stroke":215,"style":118},"587.0","#d9f4f1","var(--teal,#0f9488)",[120,217,219],{"transform":218},"translate(651.5,29.0) scale(1.000)",[124,220],{"d":221,"fill":127,"stroke":222,"style":129},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--teal-ink,#0b6157)",[131,224,228],{"x":225,"y":226,"style":227},"663.5","75.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","One sheet per",[131,230,231],{"x":225,"y":198,"style":227},"table",[131,233,234],{"x":225,"y":202,"style":140},"named after its",[131,236,238],{"x":225,"y":237,"style":140},"130.0","heading",[25,240,242],{"id":241},"step-1-read-the-tables-as-they-are","Step 1 — Read the tables as they are",[10,244,245,248,249,252,253,256,257,259],{},[14,246,247],{},"Document(path).tables"," lists every top-level table. Each table has ",[14,250,251],{},"rows",", each row has ",[14,254,255],{},"cells",", and each cell has ",[14,258,131],{},":",[30,261,265],{"className":262,"code":263,"language":264,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from docx import Document\n\ndoc = Document(\"price_list.docx\")\nprint(len(doc.tables), \"tables\")\nfor row in doc.tables[0].rows:\n    print([cell.text for cell in row.cells])\n","python",[14,266,267,283,290,308,329,349],{"__ignoreMap":35},[39,268,269,273,277,280],{"class":41,"line":42},[39,270,272],{"class":271},"s-kum","from",[39,274,276],{"class":275},"skGVy"," docx ",[39,278,279],{"class":271},"import",[39,281,282],{"class":275}," Document\n",[39,284,286],{"class":41,"line":285},2,[39,287,289],{"emptyLinePlaceholder":288},true,"\n",[39,291,293,296,299,302,305],{"class":41,"line":292},3,[39,294,295],{"class":275},"doc ",[39,297,298],{"class":271},"=",[39,300,301],{"class":275}," Document(",[39,303,304],{"class":49},"\"price_list.docx\"",[39,306,307],{"class":275},")\n",[39,309,311,315,318,321,324,327],{"class":41,"line":310},4,[39,312,314],{"class":313},"sP0c6","print",[39,316,317],{"class":275},"(",[39,319,320],{"class":313},"len",[39,322,323],{"class":275},"(doc.tables), ",[39,325,326],{"class":49},"\"tables\"",[39,328,307],{"class":275},[39,330,332,335,338,341,344,346],{"class":41,"line":331},5,[39,333,334],{"class":271},"for",[39,336,337],{"class":275}," row ",[39,339,340],{"class":271},"in",[39,342,343],{"class":275}," doc.tables[",[39,345,105],{"class":313},[39,347,348],{"class":275},"].rows:\n",[39,350,352,355,358,360,363,365],{"class":41,"line":351},6,[39,353,354],{"class":313},"    print",[39,356,357],{"class":275},"([cell.text ",[39,359,334],{"class":271},[39,361,362],{"class":275}," cell ",[39,364,340],{"class":271},[39,366,367],{"class":275}," row.cells])\n",[10,369,370,371,374],{},"Two things stand out. The description cell for ",[14,372,373],{},"HC-140"," contains a newline, because it was typed as two lines. And the merged note row appears four times: python-docx returns one cell object per grid column, and for a merged cell every position it covers returns the same content. Written straight to Excel, that row would show the note in four adjacent cells.",[25,376,378],{"id":377},"step-2-detect-merged-cells","Step 2 — Detect merged cells",[10,380,381,382,385],{},"The cells a merged cell covers all point at the same underlying XML element, ",[14,383,384],{},"cell._tc",". Keep a cell's text only the first time its element appears in the row:",[30,387,389],{"className":262,"code":388,"language":264,"meta":35,"style":35},"def row_values(row):\n    values, seen = [], []\n    for cell in row.cells:\n        if any(cell._tc is tc for tc in seen):\n            values.append(None)          # covered by a merge to the left\n        else:\n            seen.append(cell._tc)\n            values.append(\" \".join(cell.text.split()))\n    return values\n\nfor row in doc.tables[0].rows:\n    print(row_values(row))\n",[14,390,391,403,413,425,451,466,474,480,491,500,505,520],{"__ignoreMap":35},[39,392,393,396,400],{"class":41,"line":42},[39,394,395],{"class":271},"def",[39,397,399],{"class":398},"s_Opv"," row_values",[39,401,402],{"class":275},"(row):\n",[39,404,405,408,410],{"class":41,"line":285},[39,406,407],{"class":275},"    values, seen ",[39,409,298],{"class":271},[39,411,412],{"class":275}," [], []\n",[39,414,415,418,420,422],{"class":41,"line":292},[39,416,417],{"class":271},"    for",[39,419,362],{"class":275},[39,421,340],{"class":271},[39,423,424],{"class":275}," row.cells:\n",[39,426,427,430,433,436,439,442,444,446,448],{"class":41,"line":310},[39,428,429],{"class":271},"        if",[39,431,432],{"class":313}," any",[39,434,435],{"class":275},"(cell._tc ",[39,437,438],{"class":271},"is",[39,440,441],{"class":275}," tc ",[39,443,334],{"class":271},[39,445,441],{"class":275},[39,447,340],{"class":271},[39,449,450],{"class":275}," seen):\n",[39,452,453,456,459,462],{"class":41,"line":331},[39,454,455],{"class":275},"            values.append(",[39,457,458],{"class":313},"None",[39,460,461],{"class":275},")          ",[39,463,465],{"class":464},"s-wDw","# covered by a merge to the left\n",[39,467,468,471],{"class":41,"line":351},[39,469,470],{"class":271},"        else",[39,472,473],{"class":275},":\n",[39,475,477],{"class":41,"line":476},7,[39,478,479],{"class":275},"            seen.append(cell._tc)\n",[39,481,483,485,488],{"class":41,"line":482},8,[39,484,455],{"class":275},[39,486,487],{"class":49},"\" \"",[39,489,490],{"class":275},".join(cell.text.split()))\n",[39,492,494,497],{"class":41,"line":493},9,[39,495,496],{"class":271},"    return",[39,498,499],{"class":275}," values\n",[39,501,503],{"class":41,"line":502},10,[39,504,289],{"emptyLinePlaceholder":288},[39,506,508,510,512,514,516,518],{"class":41,"line":507},11,[39,509,334],{"class":271},[39,511,337],{"class":275},[39,513,340],{"class":271},[39,515,343],{"class":275},[39,517,105],{"class":313},[39,519,348],{"class":275},[39,521,523,525],{"class":41,"line":522},12,[39,524,354],{"class":313},[39,526,527],{"class":275},"(row_values(row))\n",[10,529,530,531,533,534,537,538,541,542,544,545,548],{},"Now the note appears once, in the first column, with ",[14,532,458],{}," in the positions the merge covered — the same shape Excel itself uses for merged cells, where only the top-left cell holds a value. ",[14,535,536],{},"\" \".join(cell.text.split())"," collapses the line break and any repeated spaces into single spaces, so ",[14,539,540],{},"Hinge, stainless (marine grade)"," fits in one cell. Compare the elements with ",[14,543,438],{}," rather than ",[14,546,547],{},"id()",": python-docx creates new wrapper objects on every access, and the identity of the XML element is the reliable test. The same check catches cells merged vertically, where the covered positions sit in the rows below.",[83,550,92,556,92,559,92,562,92,567,92,574,92,578,92,584,92,587,92,591,92,594,92,598,92,602,92,607,92,609,92,613,92,615,92,618,92,620,92,623,92,626,92,630,92,635,92,639,92,641,92,643,92,645,92,647,92,650,92,653,92,656,92,660,92,663,92,667,92,670,92,672,92,675,92,677,92,679,92,681,92,683,92,685,92,687,92,689,92,691,92,693,92,696,92,698,92,700,92,706,92,710,92,714,92,717,92,725],{"viewBox":551,"role":86,"ariaLabelledBy":552,"xmlns":90,"style":555},"15 0 729 136",[553,554],"docx-merge-t","docx-merge-d","width:100%;max-width:729px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[94,557,558],{"id":553},"A merged row repeated by python-docx, and after de-duplication",[98,560,561],{"id":554},"On the left the note row as python-docx returns it, with the note text repeated in every cell. On the right the same row with the note once in the first cell and the other cells empty.",[102,563],{"x":564,"y":105,"width":565,"height":566,"fill":108},"15","729","136",[102,568],{"x":569,"y":570,"width":571,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"31.9","40.0","32.0","24.0","stroke-width:1px",[102,575],{"x":576,"y":570,"width":577,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"63.9","79.1",[131,579,583],{"x":580,"y":581,"style":582},"103.4","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[102,585],{"x":586,"y":570,"width":577,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"143.0",[131,588,590],{"x":589,"y":581,"style":582},"182.5","B",[102,592],{"x":593,"y":570,"width":577,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"222.0",[131,595,597],{"x":596,"y":581,"style":582},"261.5","C",[102,599],{"x":569,"y":600,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"64.0","28.0",[131,603,606],{"x":604,"y":605,"style":582},"47.9","83.0","1",[102,608],{"x":576,"y":600,"width":577,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,610,612],{"x":580,"y":605,"style":611},"font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Part",[102,614],{"x":586,"y":600,"width":577,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,616,617],{"x":589,"y":605,"style":611},"Price",[102,619],{"x":593,"y":600,"width":577,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,621,622],{"x":596,"y":605,"style":611},"Disc.",[102,624],{"x":569,"y":625,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"92.0",[131,627,629],{"x":604,"y":628,"style":582},"111.0","2",[102,631],{"x":576,"y":625,"width":577,"height":601,"rx":105,"fill":632,"stroke":633,"style":634},"#fce9e9","var(--danger,#dc2626)","stroke-width:1.5px",[131,636,638],{"x":580,"y":628,"style":637},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","excl. VAT",[102,640],{"x":586,"y":625,"width":577,"height":601,"rx":105,"fill":632,"stroke":633,"style":634},[131,642,638],{"x":589,"y":628,"style":637},[102,644],{"x":593,"y":625,"width":577,"height":601,"rx":105,"fill":632,"stroke":633,"style":634},[131,646,638],{"x":596,"y":628,"style":637},[102,648],{"x":649,"y":570,"width":571,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"511.5",[102,651],{"x":652,"y":570,"width":577,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"543.5",[131,654,583],{"x":655,"y":581,"style":582},"583.0",[102,657],{"x":658,"y":570,"width":659,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"622.5","53.0",[131,661,590],{"x":662,"y":581,"style":582},"649.0",[102,664],{"x":665,"y":570,"width":666,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"675.5","52.6",[131,668,597],{"x":669,"y":581,"style":582},"701.8",[102,671],{"x":649,"y":600,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},[131,673,606],{"x":674,"y":605,"style":582},"527.5",[102,676],{"x":652,"y":600,"width":577,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,678,612],{"x":655,"y":605,"style":611},[102,680],{"x":658,"y":600,"width":659,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,682,617],{"x":662,"y":605,"style":611},[102,684],{"x":665,"y":600,"width":666,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,686,622],{"x":669,"y":605,"style":611},[102,688],{"x":649,"y":625,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},[131,690,629],{"x":674,"y":628,"style":582},[102,692],{"x":652,"y":625,"width":577,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,694,638],{"x":655,"y":628,"style":695},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[102,697],{"x":658,"y":625,"width":659,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[102,699],{"x":665,"y":625,"width":666,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,701,705],{"x":702,"y":703,"style":704},"166.5","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[131,707,709],{"x":708,"y":703,"style":704},"619.8","after",[41,711],{"x1":712,"y1":149,"x2":713,"y2":149,"stroke":151,"style":118},"311.1","497.5",[153,715],{"points":716,"fill":156},"501.5,80.0 492.5,85.0 492.5,75.0",[102,718],{"x":719,"y":720,"width":721,"height":722,"rx":723,"fill":108,"stroke":117,"style":724},"338.1","47.0","136.3","22.0","11","stroke-width:1.2px",[131,726,730],{"x":727,"y":728,"style":729},"406.3","62.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","cell._tc is seen",[25,732,734],{"id":733},"step-3-name-each-table-after-its-heading","Step 3 — Name each table after its heading",[10,736,737,738,741],{},"A document with several tables usually introduces each one with a heading. ",[14,739,740],{},"doc.iter_inner_content()"," yields paragraphs and tables in the order they appear, so the most recent heading before a table is its title:",[30,743,745],{"className":262,"code":744,"language":264,"meta":35,"style":35},"import pandas as pd\nfrom docx.table import Table\n\ndef tables_with_titles(doc):\n    title, out = \"Table\", []\n    for block in doc.iter_inner_content():\n        if isinstance(block, Table):\n            rows = [row_values(r) for r in block.rows]\n            out.append((title, pd.DataFrame(rows[1:], columns=rows[0])))\n        elif block.style.name.startswith(\"Heading\"):\n            title = block.text.strip()\n    return out\n\nfound = tables_with_titles(doc)\nfor title, frame in found:\n    print(title, frame.shape)\n",[14,746,747,760,772,776,786,799,811,821,841,865,879,889,896,901,912,925],{"__ignoreMap":35},[39,748,749,751,754,757],{"class":41,"line":42},[39,750,279],{"class":271},[39,752,753],{"class":275}," pandas ",[39,755,756],{"class":271},"as",[39,758,759],{"class":275}," pd\n",[39,761,762,764,767,769],{"class":41,"line":285},[39,763,272],{"class":271},[39,765,766],{"class":275}," docx.table ",[39,768,279],{"class":271},[39,770,771],{"class":275}," Table\n",[39,773,774],{"class":41,"line":292},[39,775,289],{"emptyLinePlaceholder":288},[39,777,778,780,783],{"class":41,"line":310},[39,779,395],{"class":271},[39,781,782],{"class":398}," tables_with_titles",[39,784,785],{"class":275},"(doc):\n",[39,787,788,791,793,796],{"class":41,"line":331},[39,789,790],{"class":275},"    title, out ",[39,792,298],{"class":271},[39,794,795],{"class":49}," \"Table\"",[39,797,798],{"class":275},", []\n",[39,800,801,803,806,808],{"class":41,"line":351},[39,802,417],{"class":271},[39,804,805],{"class":275}," block ",[39,807,340],{"class":271},[39,809,810],{"class":275}," doc.iter_inner_content():\n",[39,812,813,815,818],{"class":41,"line":476},[39,814,429],{"class":271},[39,816,817],{"class":313}," isinstance",[39,819,820],{"class":275},"(block, Table):\n",[39,822,823,826,828,831,833,836,838],{"class":41,"line":482},[39,824,825],{"class":275},"            rows ",[39,827,298],{"class":271},[39,829,830],{"class":275}," [row_values(r) ",[39,832,334],{"class":271},[39,834,835],{"class":275}," r ",[39,837,340],{"class":271},[39,839,840],{"class":275}," block.rows]\n",[39,842,843,846,848,851,855,857,860,862],{"class":41,"line":493},[39,844,845],{"class":275},"            out.append((title, pd.DataFrame(rows[",[39,847,606],{"class":313},[39,849,850],{"class":275},":], ",[39,852,854],{"class":853},"sa561","columns",[39,856,298],{"class":271},[39,858,859],{"class":275},"rows[",[39,861,105],{"class":313},[39,863,864],{"class":275},"])))\n",[39,866,867,870,873,876],{"class":41,"line":502},[39,868,869],{"class":271},"        elif",[39,871,872],{"class":275}," block.style.name.startswith(",[39,874,875],{"class":49},"\"Heading\"",[39,877,878],{"class":275},"):\n",[39,880,881,884,886],{"class":41,"line":507},[39,882,883],{"class":275},"            title ",[39,885,298],{"class":271},[39,887,888],{"class":275}," block.text.strip()\n",[39,890,891,893],{"class":41,"line":522},[39,892,496],{"class":271},[39,894,895],{"class":275}," out\n",[39,897,899],{"class":41,"line":898},13,[39,900,289],{"emptyLinePlaceholder":288},[39,902,904,907,909],{"class":41,"line":903},14,[39,905,906],{"class":275},"found ",[39,908,298],{"class":271},[39,910,911],{"class":275}," tables_with_titles(doc)\n",[39,913,915,917,920,922],{"class":41,"line":914},15,[39,916,334],{"class":271},[39,918,919],{"class":275}," title, frame ",[39,921,340],{"class":271},[39,923,924],{"class":275}," found:\n",[39,926,928,930],{"class":41,"line":927},16,[39,929,354],{"class":313},[39,931,932],{"class":275},"(title, frame.shape)\n",[10,934,935,938,939,942,943,946,947,950,951,954,955,958],{},[14,936,937],{},"iter_inner_content"," arrived in python-docx 1.0; with older versions, walk ",[14,940,941],{},"doc.element.body"," and wrap ",[14,944,945],{},"w:tbl"," and ",[14,948,949],{},"w:p"," elements yourself. Headings are recognised by their paragraph style — ",[14,952,953],{},"Heading 1",", ",[14,956,957],{},"Heading 2"," and so on. Documents where people used bold text instead of real headings need a different rule, for example \"the last non-empty paragraph before the table\".",[25,960,962],{"id":961},"step-4-convert-text-to-numbers","Step 4 — Convert text to numbers",[10,964,965],{},"Every cell value is a string. Convert prices and percentages into numbers, and turn the merged note row into something useful — here it is a footnote, so move it out of the data:",[30,967,969],{"className":262,"code":968,"language":264,"meta":35,"style":35},"def to_number(series):\n    cleaned = series.str.replace(r\"[£$€,\\s]\", \"\", regex=True)\n    pct = cleaned.str.endswith(\"%\")\n    nums = pd.to_numeric(cleaned.str.rstrip(\"%\"), errors=\"coerce\")\n    return nums.where(~pct, nums \u002F 100)\n\nsheets = {}\nfor title, frame in found:\n    note_rows = frame.iloc[:, 1:].isna().all(axis=1)\n    notes = frame.loc[note_rows, frame.columns[0]].tolist()\n    frame = frame.loc[~note_rows].copy()\n    for col in frame.columns[1:]:\n        converted = to_number(frame[col])\n        if converted.notna().all():\n            frame[col] = converted\n    sheets[title[:31]] = frame\n    print(title, \"notes:\", notes)\n    print(frame.to_string(index=False), \"\\n\")\n",[14,970,971,981,1026,1041,1066,1087,1091,1101,1111,1135,1150,1165,1182,1192,1199,1209,1225,1239],{"__ignoreMap":35},[39,972,973,975,978],{"class":41,"line":42},[39,974,395],{"class":271},[39,976,977],{"class":398}," to_number",[39,979,980],{"class":275},"(series):\n",[39,982,983,986,988,991,994,997,1000,1004,1007,1009,1011,1014,1016,1019,1021,1024],{"class":41,"line":285},[39,984,985],{"class":275},"    cleaned ",[39,987,298],{"class":271},[39,989,990],{"class":275}," series.str.replace(",[39,992,993],{"class":271},"r",[39,995,996],{"class":49},"\"",[39,998,999],{"class":313},"[",[39,1001,1003],{"class":1002},"sSjpA","£$€,",[39,1005,1006],{"class":313},"\\s]",[39,1008,996],{"class":49},[39,1010,954],{"class":275},[39,1012,1013],{"class":49},"\"\"",[39,1015,954],{"class":275},[39,1017,1018],{"class":853},"regex",[39,1020,298],{"class":271},[39,1022,1023],{"class":313},"True",[39,1025,307],{"class":275},[39,1027,1028,1031,1033,1036,1039],{"class":41,"line":292},[39,1029,1030],{"class":275},"    pct ",[39,1032,298],{"class":271},[39,1034,1035],{"class":275}," cleaned.str.endswith(",[39,1037,1038],{"class":49},"\"%\"",[39,1040,307],{"class":275},[39,1042,1043,1046,1048,1051,1053,1056,1059,1061,1064],{"class":41,"line":310},[39,1044,1045],{"class":275},"    nums ",[39,1047,298],{"class":271},[39,1049,1050],{"class":275}," pd.to_numeric(cleaned.str.rstrip(",[39,1052,1038],{"class":49},[39,1054,1055],{"class":275},"), ",[39,1057,1058],{"class":853},"errors",[39,1060,298],{"class":271},[39,1062,1063],{"class":49},"\"coerce\"",[39,1065,307],{"class":275},[39,1067,1068,1070,1073,1076,1079,1082,1085],{"class":41,"line":331},[39,1069,496],{"class":271},[39,1071,1072],{"class":275}," nums.where(",[39,1074,1075],{"class":271},"~",[39,1077,1078],{"class":275},"pct, nums ",[39,1080,1081],{"class":271},"\u002F",[39,1083,1084],{"class":313}," 100",[39,1086,307],{"class":275},[39,1088,1089],{"class":41,"line":351},[39,1090,289],{"emptyLinePlaceholder":288},[39,1092,1093,1096,1098],{"class":41,"line":476},[39,1094,1095],{"class":275},"sheets ",[39,1097,298],{"class":271},[39,1099,1100],{"class":275}," {}\n",[39,1102,1103,1105,1107,1109],{"class":41,"line":482},[39,1104,334],{"class":271},[39,1106,919],{"class":275},[39,1108,340],{"class":271},[39,1110,924],{"class":275},[39,1112,1113,1116,1118,1121,1123,1126,1129,1131,1133],{"class":41,"line":493},[39,1114,1115],{"class":275},"    note_rows ",[39,1117,298],{"class":271},[39,1119,1120],{"class":275}," frame.iloc[:, ",[39,1122,606],{"class":313},[39,1124,1125],{"class":275},":].isna().all(",[39,1127,1128],{"class":853},"axis",[39,1130,298],{"class":271},[39,1132,606],{"class":313},[39,1134,307],{"class":275},[39,1136,1137,1140,1142,1145,1147],{"class":41,"line":502},[39,1138,1139],{"class":275},"    notes ",[39,1141,298],{"class":271},[39,1143,1144],{"class":275}," frame.loc[note_rows, frame.columns[",[39,1146,105],{"class":313},[39,1148,1149],{"class":275},"]].tolist()\n",[39,1151,1152,1155,1157,1160,1162],{"class":41,"line":507},[39,1153,1154],{"class":275},"    frame ",[39,1156,298],{"class":271},[39,1158,1159],{"class":275}," frame.loc[",[39,1161,1075],{"class":271},[39,1163,1164],{"class":275},"note_rows].copy()\n",[39,1166,1167,1169,1172,1174,1177,1179],{"class":41,"line":522},[39,1168,417],{"class":271},[39,1170,1171],{"class":275}," col ",[39,1173,340],{"class":271},[39,1175,1176],{"class":275}," frame.columns[",[39,1178,606],{"class":313},[39,1180,1181],{"class":275},":]:\n",[39,1183,1184,1187,1189],{"class":41,"line":898},[39,1185,1186],{"class":275},"        converted ",[39,1188,298],{"class":271},[39,1190,1191],{"class":275}," to_number(frame[col])\n",[39,1193,1194,1196],{"class":41,"line":903},[39,1195,429],{"class":271},[39,1197,1198],{"class":275}," converted.notna().all():\n",[39,1200,1201,1204,1206],{"class":41,"line":914},[39,1202,1203],{"class":275},"            frame[col] ",[39,1205,298],{"class":271},[39,1207,1208],{"class":275}," converted\n",[39,1210,1211,1214,1217,1220,1222],{"class":41,"line":927},[39,1212,1213],{"class":275},"    sheets[title[:",[39,1215,1216],{"class":313},"31",[39,1218,1219],{"class":275},"]] ",[39,1221,298],{"class":271},[39,1223,1224],{"class":275}," frame\n",[39,1226,1228,1230,1233,1236],{"class":41,"line":1227},17,[39,1229,354],{"class":313},[39,1231,1232],{"class":275},"(title, ",[39,1234,1235],{"class":49},"\"notes:\"",[39,1237,1238],{"class":275},", notes)\n",[39,1240,1242,1244,1247,1250,1252,1255,1257,1259,1262,1264],{"class":41,"line":1241},18,[39,1243,354],{"class":313},[39,1245,1246],{"class":275},"(frame.to_string(",[39,1248,1249],{"class":853},"index",[39,1251,298],{"class":271},[39,1253,1254],{"class":313},"False",[39,1256,1055],{"class":275},[39,1258,996],{"class":49},[39,1260,1261],{"class":1002},"\\n",[39,1263,996],{"class":49},[39,1265,307],{"class":275},[10,1267,1268,1269,1272,1273,946,1276,1279,1280,1282,1283,1286,1287,1291],{},"The conversion is applied only when every value in the column parses, so the ",[14,1270,1271],{},"Description"," column stays text while ",[14,1274,1275],{},"Unit price",[14,1277,1278],{},"Discount"," become numbers. ",[14,1281,81],{}," becomes ",[14,1284,1285],{},"0.025",", which Excel displays as a percentage once the column has a percent format. Sheet names are cut to 31 characters, Excel's limit — see ",[19,1288,1290],{"href":1289},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-invalid-worksheet-name-errors\u002F","Fix Invalid Worksheet Name Errors"," for the characters that are also forbidden.",[25,1293,1295],{"id":1294},"step-5-write-one-sheet-per-table","Step 5 — Write one sheet per table",[30,1297,1299],{"className":262,"code":1298,"language":264,"meta":35,"style":35},"with pd.ExcelWriter(\"price_list.xlsx\", engine=\"xlsxwriter\") as xw:\n    money = xw.book.add_format({\"num_format\": \"£#,##0.00\"})\n    pct = xw.book.add_format({\"num_format\": \"0.0%\"})\n    for name, frame in sheets.items():\n        frame.to_excel(xw, sheet_name=name, index=False)\n        ws = xw.sheets[name]\n        for i, col in enumerate(frame.columns):\n            fmt = pct if col == \"Discount\" else money if frame[col].dtype == \"float64\" else None\n            ws.set_column(i, i, max(12, len(col) + 2, int(frame[col].astype(str).str.len().max()) + 2), fmt)\nwith pd.ExcelFile(\"price_list.xlsx\") as xl:\n    print(xl.sheet_names)\n",[14,1300,1301,1330,1352,1369,1381,1402,1412,1428,1470,1516,1532],{"__ignoreMap":35},[39,1302,1303,1306,1309,1312,1314,1317,1319,1322,1325,1327],{"class":41,"line":42},[39,1304,1305],{"class":271},"with",[39,1307,1308],{"class":275}," pd.ExcelWriter(",[39,1310,1311],{"class":49},"\"price_list.xlsx\"",[39,1313,954],{"class":275},[39,1315,1316],{"class":853},"engine",[39,1318,298],{"class":271},[39,1320,1321],{"class":49},"\"xlsxwriter\"",[39,1323,1324],{"class":275},") ",[39,1326,756],{"class":271},[39,1328,1329],{"class":275}," xw:\n",[39,1331,1332,1335,1337,1340,1343,1346,1349],{"class":41,"line":285},[39,1333,1334],{"class":275},"    money ",[39,1336,298],{"class":271},[39,1338,1339],{"class":275}," xw.book.add_format({",[39,1341,1342],{"class":49},"\"num_format\"",[39,1344,1345],{"class":275},": ",[39,1347,1348],{"class":49},"\"£#,##0.00\"",[39,1350,1351],{"class":275},"})\n",[39,1353,1354,1356,1358,1360,1362,1364,1367],{"class":41,"line":292},[39,1355,1030],{"class":275},[39,1357,298],{"class":271},[39,1359,1339],{"class":275},[39,1361,1342],{"class":49},[39,1363,1345],{"class":275},[39,1365,1366],{"class":49},"\"0.0%\"",[39,1368,1351],{"class":275},[39,1370,1371,1373,1376,1378],{"class":41,"line":310},[39,1372,417],{"class":271},[39,1374,1375],{"class":275}," name, frame ",[39,1377,340],{"class":271},[39,1379,1380],{"class":275}," sheets.items():\n",[39,1382,1383,1386,1389,1391,1394,1396,1398,1400],{"class":41,"line":331},[39,1384,1385],{"class":275},"        frame.to_excel(xw, ",[39,1387,1388],{"class":853},"sheet_name",[39,1390,298],{"class":271},[39,1392,1393],{"class":275},"name, ",[39,1395,1249],{"class":853},[39,1397,298],{"class":271},[39,1399,1254],{"class":313},[39,1401,307],{"class":275},[39,1403,1404,1407,1409],{"class":41,"line":351},[39,1405,1406],{"class":275},"        ws ",[39,1408,298],{"class":271},[39,1410,1411],{"class":275}," xw.sheets[name]\n",[39,1413,1414,1417,1420,1422,1425],{"class":41,"line":476},[39,1415,1416],{"class":271},"        for",[39,1418,1419],{"class":275}," i, col ",[39,1421,340],{"class":271},[39,1423,1424],{"class":313}," enumerate",[39,1426,1427],{"class":275},"(frame.columns):\n",[39,1429,1430,1433,1435,1438,1441,1443,1446,1449,1452,1455,1457,1460,1462,1465,1467],{"class":41,"line":482},[39,1431,1432],{"class":275},"            fmt ",[39,1434,298],{"class":271},[39,1436,1437],{"class":275}," pct ",[39,1439,1440],{"class":271},"if",[39,1442,1171],{"class":275},[39,1444,1445],{"class":271},"==",[39,1447,1448],{"class":49}," \"Discount\"",[39,1450,1451],{"class":271}," else",[39,1453,1454],{"class":275}," money ",[39,1456,1440],{"class":271},[39,1458,1459],{"class":275}," frame[col].dtype ",[39,1461,1445],{"class":271},[39,1463,1464],{"class":49}," \"float64\"",[39,1466,1451],{"class":271},[39,1468,1469],{"class":313}," None\n",[39,1471,1472,1475,1478,1480,1482,1484,1486,1489,1492,1495,1497,1500,1503,1506,1509,1511,1513],{"class":41,"line":493},[39,1473,1474],{"class":275},"            ws.set_column(i, i, ",[39,1476,1477],{"class":313},"max",[39,1479,317],{"class":275},[39,1481,115],{"class":313},[39,1483,954],{"class":275},[39,1485,320],{"class":313},[39,1487,1488],{"class":275},"(col) ",[39,1490,1491],{"class":271},"+",[39,1493,1494],{"class":313}," 2",[39,1496,954],{"class":275},[39,1498,1499],{"class":313},"int",[39,1501,1502],{"class":275},"(frame[col].astype(",[39,1504,1505],{"class":313},"str",[39,1507,1508],{"class":275},").str.len().max()) ",[39,1510,1491],{"class":271},[39,1512,1494],{"class":313},[39,1514,1515],{"class":275},"), fmt)\n",[39,1517,1518,1520,1523,1525,1527,1529],{"class":41,"line":502},[39,1519,1305],{"class":271},[39,1521,1522],{"class":275}," pd.ExcelFile(",[39,1524,1311],{"class":49},[39,1526,1324],{"class":275},[39,1528,756],{"class":271},[39,1530,1531],{"class":275}," xl:\n",[39,1533,1534,1536],{"class":41,"line":507},[39,1535,354],{"class":313},[39,1537,1538],{"class":275},"(xl.sheet_names)\n",[83,1540,92,1546,92,1549,92,1552,92,1557,92,1563,92,1568,92,1574,92,1580,92,1583,92,1586,92,1590,92,1594,92,1597,92,1601,92,1604,92,1608,92,1612,92,1615,92,1619,92,1621,92,1624,92,1626,92,1628,92,1630,92,1632,92,1634,92,1636,92,1639,92,1642,92,1644,92,1648,92,1650,92,1653,92,1655,92,1658,92,1660,92,1663,92,1666,92,1670,92,1672,92,1674,92,1676,92,1678,92,1680,92,1683,92,1685,92,1687,92,1690,92,1693,92,1695,92,1698,92,1700,92,1703,92,1705,92,1708,92,1710,92,1713,92,1716,92,1720,92,1722,92,1725,92,1727,92,1730,92,1732,92,1735,92,1737,92,1739,92,1744,92,1750,92,1756,92,1760],{"viewBox":1541,"role":86,"ariaLabelledBy":1542,"xmlns":90,"style":1545},"119 0 521 250",[1543,1544],"docx-result-t","docx-result-d","width:100%;max-width:521px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[94,1547,1548],{"id":1543},"The Standard parts sheet in price_list.xlsx",[98,1550,1551],{"id":1544},"The first table written to Excel. Unit price is a currency number and Discount a percentage; the note row has been removed.",[102,1553],{"x":1554,"y":105,"width":1555,"height":1556,"fill":108},"119","521","250",[102,1558],{"x":1559,"y":112,"width":1560,"height":1561,"rx":1562,"fill":116,"stroke":117,"style":634},"135.7","488.5","30.0","8",[102,1564],{"x":1559,"y":1565,"width":1560,"height":1566,"rx":105,"fill":116,"stroke":117,"style":1567},"38.0","8.0","stroke-width:0px",[120,1569,1571],{"transform":1570},"translate(145.7,23.0) scale(0.667)",[124,1572],{"d":221,"fill":127,"stroke":222,"style":1573},"stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[131,1575,1579],{"x":1576,"y":1577,"style":1578},"167.7","36.0","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start","price_list.xlsx",[102,1581],{"x":1559,"y":1582,"width":571,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"46.0",[102,1584],{"x":1576,"y":1582,"width":1585,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"73.0",[131,1587,583],{"x":1588,"y":1589,"style":582},"204.2","63.0",[102,1591],{"x":1592,"y":1582,"width":1593,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"240.7","216.7",[131,1595,590],{"x":1596,"y":1589,"style":582},"349.1",[102,1598],{"x":1599,"y":1582,"width":1600,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"457.4","87.8",[131,1602,597],{"x":1603,"y":1589,"style":582},"501.3",[102,1605],{"x":1606,"y":1582,"width":1607,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"545.2","79.0",[131,1609,1611],{"x":1610,"y":1589,"style":582},"584.7","D",[102,1613],{"x":1559,"y":1614,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"70.0",[131,1616,606],{"x":1617,"y":1618,"style":582},"151.7","89.0",[102,1620],{"x":1576,"y":1614,"width":1585,"height":601,"rx":105,"fill":160,"stroke":151,"style":634},[131,1622,612],{"x":1588,"y":1618,"style":1623},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[102,1625],{"x":1592,"y":1614,"width":1593,"height":601,"rx":105,"fill":160,"stroke":151,"style":634},[131,1627,1271],{"x":1596,"y":1618,"style":1623},[102,1629],{"x":1599,"y":1614,"width":1600,"height":601,"rx":105,"fill":160,"stroke":151,"style":634},[131,1631,1275],{"x":1603,"y":1618,"style":1623},[102,1633],{"x":1606,"y":1614,"width":1607,"height":601,"rx":105,"fill":160,"stroke":151,"style":634},[131,1635,1278],{"x":1610,"y":1618,"style":1623},[102,1637],{"x":1559,"y":1638,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"98.0",[131,1640,629],{"x":1617,"y":1641,"style":582},"117.0",[102,1643],{"x":1576,"y":1638,"width":1585,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1645,1647],{"x":1588,"y":1641,"style":1646},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","HC-100",[102,1649],{"x":1592,"y":1638,"width":1593,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1651,1652],{"x":1596,"y":1641,"style":1646},"Hinge, steel",[102,1654],{"x":1599,"y":1638,"width":1600,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1656,77],{"x":1603,"y":1641,"style":1657},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[102,1659],{"x":1606,"y":1638,"width":1607,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1661,1662],{"x":1610,"y":1641,"style":1657},"5.0%",[102,1664],{"x":1559,"y":1665,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"126.0",[131,1667,1669],{"x":1617,"y":1668,"style":582},"145.0","3",[102,1671],{"x":1576,"y":1665,"width":1585,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1673,373],{"x":1588,"y":1668,"style":1646},[102,1675],{"x":1592,"y":1665,"width":1593,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1677,540],{"x":1596,"y":1668,"style":1646},[102,1679],{"x":1599,"y":1665,"width":1600,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1681,1682],{"x":1603,"y":1668,"style":1657},"£1,480.50",[102,1684],{"x":1606,"y":1665,"width":1607,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1686,1662],{"x":1610,"y":1668,"style":1657},[102,1688],{"x":1559,"y":1689,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"154.0",[131,1691,104],{"x":1617,"y":1692,"style":582},"173.0",[102,1694],{"x":1576,"y":1689,"width":1585,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1696,1697],{"x":1588,"y":1692,"style":1646},"HC-210",[102,1699],{"x":1592,"y":1689,"width":1593,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1701,1702],{"x":1596,"y":1692,"style":1646},"Bracket, small",[102,1704],{"x":1599,"y":1689,"width":1600,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1706,1707],{"x":1603,"y":1692,"style":1657},"£85.50",[102,1709],{"x":1606,"y":1689,"width":1607,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1711,1712],{"x":1610,"y":1692,"style":1657},"0.0%",[102,1714],{"x":1559,"y":1715,"width":571,"height":601,"rx":105,"fill":116,"stroke":117,"style":573},"182.0",[131,1717,1719],{"x":1617,"y":1718,"style":582},"201.0","5",[102,1721],{"x":1576,"y":1715,"width":1585,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1723,1724],{"x":1588,"y":1718,"style":1646},"HC-220",[102,1726],{"x":1592,"y":1715,"width":1593,"height":601,"rx":105,"fill":108,"stroke":117,"style":573},[131,1728,1729],{"x":1596,"y":1718,"style":1646},"Bracket, large",[102,1731],{"x":1599,"y":1715,"width":1600,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1733,1734],{"x":1603,"y":1718,"style":1657},"£112.00",[102,1736],{"x":1606,"y":1715,"width":1607,"height":601,"rx":105,"fill":214,"stroke":215,"style":634},[131,1738,81],{"x":1610,"y":1718,"style":1657},[102,1740],{"x":1741,"y":1742,"width":1743,"height":572,"rx":105,"fill":108,"stroke":117,"style":573},"143.7","210.0","111.6",[41,1745],{"x1":1746,"y1":1747,"x2":1748,"y2":1747,"stroke":215,"style":1749},"145.7","233.0","253.3","stroke-width:3px",[131,1751,1755],{"x":1752,"y":1753,"style":1754},"199.5","226.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Standard parts",[102,1757],{"x":1758,"y":1742,"width":1759,"height":572,"rx":105,"fill":116,"stroke":117,"style":573},"257.3","120.3",[131,1761,1764],{"x":1762,"y":1753,"style":1763},"317.5","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Delivery charges",[25,1766,1768],{"id":1767},"step-6-a-folder-of-documents","Step 6 — A folder of documents",[10,1770,1771],{},"Contracts from several suppliers, each with a \"Schedule of rates\" table, combine into one comparison sheet by adding the file name as a column:",[30,1773,1775],{"className":262,"code":1774,"language":264,"meta":35,"style":35},"from pathlib import Path\n\nframes = []\nfor path in sorted(Path(\"contracts\").glob(\"*.docx\")):\n    for title, frame in tables_with_titles(Document(path)):\n        if title == \"Schedule of rates\":\n            frames.append(frame.assign(Supplier=path.stem))\nrates = pd.concat(frames, ignore_index=True)\nrates[\"Day rate\"] = to_number(rates[\"Day rate\"])\nprint(rates.sort_values(\"Day rate\").to_string(index=False))\n",[14,1776,1777,1789,1793,1803,1830,1841,1855,1868,1887,1908],{"__ignoreMap":35},[39,1778,1779,1781,1784,1786],{"class":41,"line":42},[39,1780,272],{"class":271},[39,1782,1783],{"class":275}," pathlib ",[39,1785,279],{"class":271},[39,1787,1788],{"class":275}," Path\n",[39,1790,1791],{"class":41,"line":285},[39,1792,289],{"emptyLinePlaceholder":288},[39,1794,1795,1798,1800],{"class":41,"line":292},[39,1796,1797],{"class":275},"frames ",[39,1799,298],{"class":271},[39,1801,1802],{"class":275}," []\n",[39,1804,1805,1807,1810,1812,1815,1818,1821,1824,1827],{"class":41,"line":310},[39,1806,334],{"class":271},[39,1808,1809],{"class":275}," path ",[39,1811,340],{"class":271},[39,1813,1814],{"class":313}," sorted",[39,1816,1817],{"class":275},"(Path(",[39,1819,1820],{"class":49},"\"contracts\"",[39,1822,1823],{"class":275},").glob(",[39,1825,1826],{"class":49},"\"*.docx\"",[39,1828,1829],{"class":275},")):\n",[39,1831,1832,1834,1836,1838],{"class":41,"line":331},[39,1833,417],{"class":271},[39,1835,919],{"class":275},[39,1837,340],{"class":271},[39,1839,1840],{"class":275}," tables_with_titles(Document(path)):\n",[39,1842,1843,1845,1848,1850,1853],{"class":41,"line":351},[39,1844,429],{"class":271},[39,1846,1847],{"class":275}," title ",[39,1849,1445],{"class":271},[39,1851,1852],{"class":49}," \"Schedule of rates\"",[39,1854,473],{"class":275},[39,1856,1857,1860,1863,1865],{"class":41,"line":476},[39,1858,1859],{"class":275},"            frames.append(frame.assign(",[39,1861,1862],{"class":853},"Supplier",[39,1864,298],{"class":271},[39,1866,1867],{"class":275},"path.stem))\n",[39,1869,1870,1873,1875,1878,1881,1883,1885],{"class":41,"line":482},[39,1871,1872],{"class":275},"rates ",[39,1874,298],{"class":271},[39,1876,1877],{"class":275}," pd.concat(frames, ",[39,1879,1880],{"class":853},"ignore_index",[39,1882,298],{"class":271},[39,1884,1023],{"class":313},[39,1886,307],{"class":275},[39,1888,1889,1892,1895,1898,1900,1903,1905],{"class":41,"line":493},[39,1890,1891],{"class":275},"rates[",[39,1893,1894],{"class":49},"\"Day rate\"",[39,1896,1897],{"class":275},"] ",[39,1899,298],{"class":271},[39,1901,1902],{"class":275}," to_number(rates[",[39,1904,1894],{"class":49},[39,1906,1907],{"class":275},"])\n",[39,1909,1910,1912,1915,1917,1920,1922,1924,1926],{"class":41,"line":502},[39,1911,314],{"class":313},[39,1913,1914],{"class":275},"(rates.sort_values(",[39,1916,1894],{"class":49},[39,1918,1919],{"class":275},").to_string(",[39,1921,1249],{"class":853},[39,1923,298],{"class":271},[39,1925,1254],{"class":313},[39,1927,1928],{"class":275},"))\n",[10,1930,1931,1932,1935,1936,1939,1940,1942],{},"Old binary ",[14,1933,1934],{},".doc"," files cannot be opened by python-docx. Convert them first with LibreOffice — ",[14,1937,1938],{},"soffice --headless --convert-to docx *.doc"," — and then read the ",[14,1941,16],{}," copies.",[25,1944,1946],{"id":1945},"step-7-check-that-nothing-was-missed","Step 7 — Check that nothing was missed",[10,1948,1949,1952],{},[14,1950,1951],{},"doc.tables"," returns only tables in the main body at the top level. A table nested inside another table's cell, or placed in a page header or footer, is skipped without any warning — and supplier documents do use both, for letterhead contact blocks and for sub-tables inside a schedule. Count what is there before trusting the conversion:",[30,1954,1956],{"className":262,"code":1955,"language":264,"meta":35,"style":35},"def audit(doc):\n    nested = sum(len(cell.tables) for t in doc.tables for row in t.rows for cell in row.cells)\n    in_headers = sum(len(s.header.tables) + len(s.footer.tables) for s in doc.sections)\n    return {\"body\": len(doc.tables), \"nested\": nested, \"headers\u002Ffooters\": in_headers}\n\nprint(audit(doc))\n",[14,1957,1958,1967,2012,2046,2074,2078],{"__ignoreMap":35},[39,1959,1960,1962,1965],{"class":41,"line":42},[39,1961,395],{"class":271},[39,1963,1964],{"class":398}," audit",[39,1966,785],{"class":275},[39,1968,1969,1972,1974,1977,1979,1981,1984,1986,1989,1991,1994,1996,1998,2000,2003,2005,2007,2009],{"class":41,"line":285},[39,1970,1971],{"class":275},"    nested ",[39,1973,298],{"class":271},[39,1975,1976],{"class":313}," sum",[39,1978,317],{"class":275},[39,1980,320],{"class":313},[39,1982,1983],{"class":275},"(cell.tables) ",[39,1985,334],{"class":271},[39,1987,1988],{"class":275}," t ",[39,1990,340],{"class":271},[39,1992,1993],{"class":275}," doc.tables ",[39,1995,334],{"class":271},[39,1997,337],{"class":275},[39,1999,340],{"class":271},[39,2001,2002],{"class":275}," t.rows ",[39,2004,334],{"class":271},[39,2006,362],{"class":275},[39,2008,340],{"class":271},[39,2010,2011],{"class":275}," row.cells)\n",[39,2013,2014,2017,2019,2021,2023,2025,2028,2030,2033,2036,2038,2041,2043],{"class":41,"line":292},[39,2015,2016],{"class":275},"    in_headers ",[39,2018,298],{"class":271},[39,2020,1976],{"class":313},[39,2022,317],{"class":275},[39,2024,320],{"class":313},[39,2026,2027],{"class":275},"(s.header.tables) ",[39,2029,1491],{"class":271},[39,2031,2032],{"class":313}," len",[39,2034,2035],{"class":275},"(s.footer.tables) ",[39,2037,334],{"class":271},[39,2039,2040],{"class":275}," s ",[39,2042,340],{"class":271},[39,2044,2045],{"class":275}," doc.sections)\n",[39,2047,2048,2050,2053,2056,2058,2060,2062,2065,2068,2071],{"class":41,"line":310},[39,2049,496],{"class":271},[39,2051,2052],{"class":275}," {",[39,2054,2055],{"class":49},"\"body\"",[39,2057,1345],{"class":275},[39,2059,320],{"class":313},[39,2061,323],{"class":275},[39,2063,2064],{"class":49},"\"nested\"",[39,2066,2067],{"class":275},": nested, ",[39,2069,2070],{"class":49},"\"headers\u002Ffooters\"",[39,2072,2073],{"class":275},": in_headers}\n",[39,2075,2076],{"class":41,"line":331},[39,2077,289],{"emptyLinePlaceholder":288},[39,2079,2080,2082],{"class":41,"line":351},[39,2081,314],{"class":313},[39,2083,2084],{"class":275},"(audit(doc))\n",[10,2086,2087,2088,2091,2092,2095,2096,2099,2100,2103,2104,2107],{},"For this document all the tables are in the body. When ",[14,2089,2090],{},"nested"," or ",[14,2093,2094],{},"headers\u002Ffooters"," is not zero, decide whether those tables matter: nested tables are reached through ",[14,2097,2098],{},"cell.tables"," and can be read with the same ",[14,2101,2102],{},"row_values"," function, and header tables through ",[14,2105,2106],{},"section.header.tables",". Logging the audit alongside each converted file also makes it easy to spot the one document in a batch that was laid out differently.",[10,2109,2110],{},"It is also worth comparing a few values against the document by eye the first time a new template comes through — a price in the wrong column is a plausible number, and no type check will catch it.",[25,2112,2114],{"id":2113},"common-pitfalls","Common pitfalls",[2116,2117,2118,2126,2137,2145,2151],"ul",{},[2119,2120,2121,2125],"li",{},[2122,2123,2124],"strong",{},"Merged cells repeated."," python-docx returns the merged content in every position it covers; de-duplicate by the cell's XML element.",[2119,2127,2128,2131,2132,2134,2135,23],{},[2122,2129,2130],{},"Nested tables."," A table inside a cell is not in ",[14,2133,1951],{},"; reach it through ",[14,2136,2098],{},[2119,2138,2139,2142,2143,23],{},[2122,2140,2141],{},"Tables in headers, footers and text boxes."," These are outside the document body and not returned by ",[14,2144,1951],{},[2119,2146,2147,2150],{},[2122,2148,2149],{},"Numbers left as text."," Every value arrives as a string; convert columns explicitly, and only when every value parses.",[2119,2152,2153,2156],{},[2122,2154,2155],{},"Tracked changes."," python-docx reads the XML as stored and does not interpret revisions, so a document that is mid-review can produce text that matches neither the old nor the new version. Accept or reject the changes in Word first.",[25,2158,2160],{"id":2159},"performance-notes","Performance notes",[10,2162,2163,2164,2167,2168,2170],{},"python-docx loads the whole document into memory, but Word documents are small compared with workbooks, and a typical price list or contract opens in a fraction of a second. The slow part of a batch job is usually opening thousands of files, which parallelises well with a process pool. ",[14,2165,2166],{},"cell.text"," is recomputed from the XML on every access, so read it once per cell, as ",[14,2169,2102],{}," does, rather than repeatedly in a loop.",[25,2172,2174],{"id":2173},"conclusion","Conclusion",[10,2176,2177,2178,2181],{},"Walk the document body with ",[14,2179,2180],{},"iter_inner_content()"," to pair each table with the heading above it, read cells row by row while skipping positions that belong to a merge, collapse line breaks, and convert money and percentage columns only when every value parses. Write one sheet per table with suitable number formats, and loop over a folder to build comparisons across documents.",[25,2183,2185],{"id":2184},"frequently-asked-questions","Frequently asked questions",[10,2187,2188,2191],{},[2122,2189,2190],{},"How do I extract tables from a Word document with Python?","\nInstall python-docx, open the file with Document(\"file.docx\") and loop over doc.tables; each table has rows, each row has cells, and each cell has text. Build a DataFrame from the rows and write it with to_excel.",[10,2193,2194,2197],{},[2122,2195,2196],{},"Why does merged cell text appear several times?","\npython-docx returns one cell per grid column, and every position a merged cell covers returns the same content. Compare each cell's underlying element (cell._tc) with the ones already seen in the row and keep the text only once.",[10,2199,2200,2203],{},[2122,2201,2202],{},"Can python-docx read .doc files?","\nNo, only .docx. Convert .doc files to .docx with LibreOffice in headless mode, or with Word through COM on Windows, and read the converted copies.",[10,2205,2206,2209],{},[2122,2207,2208],{},"How do I keep the table titles when converting to Excel?","\nIterate over doc.iter_inner_content(), which yields paragraphs and tables in document order, and remember the last heading paragraph seen before each table. Use it as the sheet name, cut to 31 characters.",[25,2211,2213],{"id":2212},"related","Related",[2116,2215,2216,2222,2229,2236],{},[2119,2217,2218,2219,2221],{},"Up one level: ",[19,2220,22],{"href":21}," — every format that ends up in a workbook.",[2119,2223,2224,2228],{},[19,2225,2227],{"href":2226},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows\u002F","Generate Word Documents from Excel Rows"," — the opposite direction.",[2119,2230,2231,2235],{},[19,2232,2234],{"href":2233},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fextract-tables-from-a-pdf-into-excel-with-python\u002F","Extract Tables from a PDF into Excel with Python"," — when the document arrives as a PDF.",[2119,2237,2238,2242],{},[19,2239,2241],{"href":2240},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas\u002F","Convert Percentage Strings to Numbers with pandas"," — the percentage rules in detail.",[2244,2245,2246],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":285,"depth":285,"links":2248},[2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261],{"id":27,"depth":285,"text":28},{"id":241,"depth":285,"text":242},{"id":377,"depth":285,"text":378},{"id":733,"depth":285,"text":734},{"id":961,"depth":285,"text":962},{"id":1294,"depth":285,"text":1295},{"id":1767,"depth":285,"text":1768},{"id":1945,"depth":285,"text":1946},{"id":2113,"depth":285,"text":2114},{"id":2159,"depth":285,"text":2160},{"id":2173,"depth":285,"text":2174},{"id":2184,"depth":285,"text":2185},{"id":2212,"depth":285,"text":2213},"2026-09-29","Pull every table out of a .docx file into a workbook with python-docx and pandas: one sheet per table named after the heading above it, merged cells that python-docx repeats, line breaks inside cells, prices and percentages stored as text, a whole folder of documents, and old .doc files.","md",[2266,2268,2270,2272],{"q":2190,"a":2267},"Install python-docx, open the file with Document(\"file.docx\") and loop over doc.tables; each table has rows, each row has cells, and each cell has text. Build a DataFrame from the rows and write it with to_excel.",{"q":2196,"a":2269},"python-docx returns one cell per grid column, and every position a merged cell covers returns the same content. Compare each cell's underlying element (cell._tc) with the ones already seen in the row and keep the text only once.",{"q":2202,"a":2271},"No, only .docx. Convert .doc files to .docx with LibreOffice in headless mode, or with Word through COM on Windows, and read the converted copies.",{"q":2208,"a":2273},"Iterate over doc.iter_inner_content(), which yields paragraphs and tables in document order, and remember the last heading paragraph seen before each table. Use it as the sheet name, cut to 31 characters.","convert word document tables to excel with python",null,{"breadcrumb":2277},[2278,2280,2283],{"name":2279,"item":1081},"Home",{"name":2281,"item":2282},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":22,"item":21},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python",{"title":2286,"description":2287},"Convert Word (.docx) Tables to Excel with Python","Read tables from Word documents with python-docx, detect merged cells by their shared XML element, name sheets after the preceding heading with iter_inner_content, convert \"£1,200.00\" to numbers and write every table to Excel.","convert-word-document-tables-to-excel-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-word-document-tables-to-excel-with-python\u002Findex",[2291,2292],"file-formats","writing-excel","how-to","kdT-MQWLd4L_FUeKY30HgMLExZKyCJhWpncJokTB8cQ",[2296,2300],{"title":2297,"path":2298,"stem":2299,"children":-1},"Convert Fixed-Width Text Files to Excel with pandas","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-fixed-width-text-files-to-excel-with-pandas","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-fixed-width-text-files-to-excel-with-pandas\u002Findex",{"title":2301,"path":2302,"stem":2303,"children":-1},"Convert .xls to .xlsx with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002Findex",[2305,2307],{"path":2306,"title":22},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions",{"path":2308,"title":2281},"\u002Fgetting-started-with-python-excel-automation",[],[2311,2314,2317,2321,2324,2328],{"path":2312,"title":2313,"kind":2293},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python","Convert Excel Files to Parquet with Python",{"path":2315,"title":2316,"kind":2293},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python","Convert Excel to CSV with Python",{"path":2318,"title":2319,"kind":2320},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","Excel vs CSV vs Parquet for Python Data Pipelines","compare",{"path":2322,"title":2323,"kind":2320},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods",{"path":2325,"title":2326,"kind":2327},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffix-garbled-characters-in-excel-data","Fix Garbled Characters Like \"CafÃ©\" and \"â€™\" in Excel Data","fix",{"path":2329,"title":2330,"kind":2293},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file","Extract VBA Macro Code from an Excel File",1790972059181]