ai newsDi., 3. Nov.
EilmeldungGoogle will switch off Gemini for school students unless schools opt in

Convert Word (DOCX) to Markdown With Python (Standard Library Only) for LLM Prompts and Chatbot Knowledge

A lot of company knowledge still lives in Word files: refund policies, onboarding guides, SOPs, proposal templates. If you want that content in a prompt, a RAG index or a chatbot's knowledge base, Markdown is a better format than the .docx itself or a flat text dump. It keeps headings, lists, table

dev.toDienstag 16:56 · 03.11.7 Min. Lesezeit
Bild: dev.to

A lot of company knowledge still lives in Word files: refund policies, onboarding guides, SOPs, proposal templates. If you want that content in a prompt, a RAG index or a chatbot's knowledge base, Markdown is a better format than the.docx itself or a flat text dump. It keeps headings, lists, tables and links, and it is easy to diff, chunk and review.

This post builds a DOCX-to-Markdown converter with Python's standard library only ( zipfile and xml.etree.ElementTree ), so it runs in a CI job, a small container or a serverless function without a pip install. It handles headings, bold and italic, links, nested bullet and numbered lists, tables, line breaks, images and tracked changes.

Disclosure: Appie.run publishes this post. Appie has a free DOCX to Markdown converter and a few other Word tools, mentioned near the end. The script below does not depend on them.

Earlier posts in this series cover converting CSV and JSON to Markdown tables and converting PDFs to Markdown for RAG.

A.docx file is a ZIP archive of XML files. Rename one to.zip and open it, and you will find:

So a converter has four jobs: read the body in order, look up styles to find headings, look up numbering to tell bullets from numbers, and resolve relationships for links and images.

Save this as docx2md.py. It was tested with Python 3.13.5 on the files described below.

python3 docx2md.py policy.docx > policy.md # images referenced by file name python3 docx2md.py policy.docx media > policy.md # also extracts images into./media Enter fullscreen mode Exit fullscreen mode How the pieces fit _load_styles maps each style ID to a heading level, using the style name ( title, heading 1 to heading 6 ) or an outline level. It also records list styles such as "List Bullet" that carry their own numbering, and treats "List Bullet 2" or "List Number 3" as one or two levels deeper. _load_numbering resolves each numId and level to a format. Word stores list formats in two hops ( num points to abstractNum ), which is why there are two loops. runs walks a paragraph and yields (text, bold, italic, link) segments.

It follows hyperlinks, tracked insertions ( w:ins ) and content controls. Deleted text lives in w:delText, which it never reads, so tracked deletions drop out. inline merges neighbouring segments with identical formatting, then puts the ** and * markers inside any leading or trailing spaces, because ** bold** is not valid Markdown emphasis. paragraph decides between heading, list item, code block and plain paragraph. Numbered items keep a counter per list and level, and deeper counters reset when a shallower item appears. convert walks the body in order, so text and tables stay in their original sequence, and puts list items on consecutive lines so they render as one list.

Try it on a test document We built a small test file with python-docx (used only to create the fixture; the converter does not need it). It has a Title, Heading 1 and Heading 2, a paragraph with a bold run split in two, an italic run, a bold external link and a run with bold explicitly turned off, a numbered list, a bullet list with a "List Bullet 2" item, a table with a pipe in a cell and a two-paragraph cell, a soft line break, and an image with alt text.

python3 docx2md.py sample.docx media > sample.md Enter fullscreen mode Exit fullscreen mode Output:

# Refund policy Last updated in the Word file. This intro has an empty paragraph after it. # Who can ask for a refund Customers on **annual plans** can ask within *30 days*. Read the [ **full terms** ]( https://example.com/terms ) first.

Not bold ## Steps 1. Open Billing 2. Click Request refund 3.

Pick a reason What you need: - Order ID - Account email - Found in the receipt footer - Reason ## Plans | Plan | Window | Notes | | --- | --- | --- | | Monthly | 7 days | Pro \| Team | | Annual | 30 days | Prorated <br> Second line in cell | Line one Line two after a soft break![ Refund flow diagram ]( media/image1.png ) Enter fullscreen mode Exit fullscreen mode The split bold run came out as one **annual plans**, "Not bold" stayed plain, the nested bullet is indented under its parent, the pipe in "Pro | Team" stayed inside its cell, and the image was written to media/image1.png with its alt text.

For a shared drive export, convert everything in one pass and keep the folder structure:

from pathlib import Path from docx2md import Docx src, dst = Path ( " docs " ), Path ( " markdown " ) for path in sorted ( src. rglob ( " *.docx " )): if path. name. startswith ( " ~$ " ): # Word lock files continue out = dst / path. relative_to ( src ). with_suffix ( ".md " ) out. parent. mkdir ( parents = True, exist_ok = True ) media = out. parent / ( out. stem + " _media " ) out. write_text ( Docx ( path, media ). convert (), encoding = " utf-8 " ) print ( f " { path } -> { out } " ) Enter fullscreen mode Exit fullscreen mode Each Markdown file gets its own <name>_media folder, and image links are written relative to the.md file, so the output can be moved as a unit. The ~$ check skips the lock files Word leaves next to open documents.

Both are mature and handle more of the edge cases above. mammoth is built to produce clean HTML from Word files; its own documentation says its Markdown output is deprecated and recommends converting to HTML and then to Markdown (checked 2 Nov 2026). If you can install dependencies, that route (mammoth, then an HTML-to-Markdown step) or pandoc -f docx -t gfm is a sound choice. The standard-library script makes sense when you cannot add dependencies, want to read and change every rule, or only need the structure that matters for LLM input.

Usually yes. Headings give chunkers natural split points, lists and tables keep their shape, and the file is small and easy to review before it goes live. Two habits help: split on ## headings and keep the source document name with each chunk so answers can cite it, and read the converted file once before you index it. A converter cannot tell you that the refund policy in a 2023 Word file is out of date.

Appie has free browser tools for Word files. Each run asks for a Cloudflare Turnstile check:

If the end goal is a website chatbot, Appie's AI Chatbots app takes DOCX, PDF, CSV and text uploads as knowledge directly, so you may not need the conversion step at all. If you work from Claude Code or Cursor, Appie's MCP server can add text content to a chatbot (its add_chatbot_content tool takes text, URLs, sitemaps or Content Writer articles, not files), which is where the Markdown from this script fits.

Does this work with Google Docs exports? It should, since Google Docs can download a doc as.docx (File > Download > Microsoft Word (.docx)), but we did not test a Google Docs export for this post. Convert one file and check the headings and lists before running a whole folder.

Why are my headings coming out as plain paragraphs? The author probably made text big and bold instead of applying a Heading style. The script only sees styles. Apply Heading styles in Word, or add a rule that treats short, fully bold paragraphs as headings.

Can I keep underline, strikethrough or colours? Markdown has no underline or colour. Strikethrough ( ~~text~~ ) is supported by GitHub-flavored Markdown; read w:strike in runs the same way as w:b.

How do I split the output for a RAG index? Split on lines starting with ##, keep the # title with each chunk, and store the source file path as metadata.

If you want a website chatbot trained on your documents, you can try Appie's AI Chatbots on the Free plan (500 credits, no credit card) at appie.run.

Quelle: dev.to (dev.to). Original öffnen