You paste AI-drafted text into a page, run a few find-and-replace rules to strip the leftovers, and later notice a missing image or a sentence glued to a file extension. The rules removed the junk, and some of your content with it.
[Image of …], empty tags), convert formatting instead of stripping it, and test every rule against lines you want to keep. In one test of a typical 15-rule cleanup set, 5 rules did their job and 6 changed legitimate content: one deleted a real image, one deleted a source citation, one turned the .htaccess file into the.htaccess file.Table of Contents
Why a cleanup rule that looks right still breaks pages
A typical cleanup set for AI-drafted product or course descriptions, cleaned in a spreadsheet before import, covers seven jobs: remove image placeholders, remove stray captions, strip leftover markdown, delete empty tags, protect shortcodes, tidy whitespace, then check. Each rule is usually written while looking at the junk it should remove, so it passes that test by construction.
The missing test is the other direction: does the rule leave good content alone? To find out, the 15 patterns of such a set were run one at a time against 18 synthetic lines. Eight were artifacts an AI draft really leaves behind ([Image of a laptop], **bold** markers, an empty <p>). The other ten were legitimate lines a product page really contains (an image with alt text, a cited source, a file extension, an offer with a footnote star). Every line a rule changed was recorded.
A cleanup rule isn’t tested until it has run on text you want to keep.
The five that did their job
These matched only artifacts and left every legitimate line alone:
\[Image of[^\]]*\]and\[Image:[^\]]*\]: the bracketed image descriptions chat assistants put where a picture would go.<p>\s*</p>and<h[1-6]>\s*</h[1-6]>: empty paragraphs and headings.- the inline-code rule, which removes a pair of backticks and keeps the words between them.
What they have in common is that the thing they match can’t be legitimate. Nobody writes [Image of …] on purpose.
The six that damaged real content
“Empty or broken” image tags. The pattern was <img[^>]*alt="[^"]*"[^>]*\/?>. It matches any image that has an alt attribute, which is every accessible image on the page. It deleted the sample course cover outright. An image is broken when its file is missing, and no pattern can see that. Alt text is a separate question; even an empty alt="" is correct for a decorative image.
Caption lines. ^(Image credit:|Source:|Figure\s+\d+:|Diagram showing).*$ was meant to catch captions orphaned by a removed image. It also deleted Source: Bureau of Labor Statistics, Occupational Outlook Handbook, 2025. and Figure 1: Enrollment by month (our own data). A line starting with “Source:” is usually how you credit data.
Space before punctuation. \s+([.,;:]) → $1 fixes word , but treats a leading dot as punctuation: Upload the .htaccess file and convert images to .webp format. became Upload the.htaccess file and convert images to.webp format., and .NET and .csv went the same way.
Italic markers. \*(.*?)\* → $1 removed the footnote stars from Free trial* for 7 days, *card required., the multiplication signs from width * height * depth, and the double stars from f(**kwargs). Three legitimate lines out of ten, the most of any rule.
Bold markers. \*\*(.*?)\*\* → $1 also turned f(**kwargs) and g(**options) into f(kwargs) and g(options). Code in prose is exactly where AI drafts put double stars on purpose.
Double line breaks. (<br\s*\/?>\s*){2,} replaced with nothing was meant to collapse extra breaks. Replacing with nothing deletes them, so Line one<br><br>Line two became Line oneLine two.
Six of the fifteen rules changed lines they should have left alone.
One left debris, one won’t run in Sheets
The markdown-image pattern !\[[^\]]*\]\([^\)]*\) stops at the first ). A Wikipedia image URL such as …/Logo_(2019).png has its own brackets, so the rule removed most of the line and left .png) behind.
The rule for __bold__ used (?<=\s) and (?=\s), a lookbehind and a lookahead. Google Sheets uses the RE2 regular-expression engine, which doesn’t support lookaround, so in a spreadsheet, where this rule set was meant to run, that pattern can’t be used at all. It matched nothing in the sample in Python either.
A safer rule set to clean AI-generated HTML
The safer set is built on two ideas: match only what can’t be legitimate, and convert formatting to its HTML equivalent rather than throwing it away. The bold and italic rules now need a space, a tag or punctuation on both sides, so f(**kwargs) and width * height no longer qualify, and double line breaks are left alone, because they’re often meant. Run in this order, twice, the nine rules below turned all 8 artifacts into the expected output and left all 10 legitimate lines byte-for-byte unchanged. They also passed 6 edge cases added afterwards: an italic word, stray spaces before punctuation, two bold words in one line, two bold runs separated by a single space, 2**10 inside a code block, and footnote stars after numbers.
1. Image placeholders \[(?:Image|Figure|Diagram)(?: of|:)[^\]]*\] -> (nothing)
2. Markdown images !\[[^\]\n]*\]\((?:[^()\s]|\([^()\s]*\))*\) -> (nothing)
3. Empty paragraphs <p>\s*</p> -> (nothing)
4. Empty headings <h[1-6]>\s*</h[1-6]> -> (nothing)
5. Bold (^|[\s>])\*\*([^*\s][^*\n]*?[^*\s]|[^*\s])\*\*([\s<.,;:!?)]|$) -> $1<strong>$2</strong>$3
6. Italic (^|[\s>(])\*([^*\s][^*\n]*[^*\s]|[^*\s])\*([\s<.,;:!?)]|$) -> $1<em>$2</em>$3
7. Inline code `([^`\n]+)` -> <code>$1</code>
8. Space before , ; : [ \t]+([,;:]) -> $1
9. Space before a sentence-ending full stop [ \t]+\.([ \t]|$) -> .$1Code language: HTML, XML (xml)
None of them uses lookaround or a backreference inside the pattern, so all nine run in RE2. The price is the second pass: without lookaround, a bold or italic run that follows another one with a single space between them shares that space and is skipped the first time (**a** **b** needs two passes). Running the set again changes nothing that’s already converted. In Google Sheets, put each rule in REGEXREPLACE, which uses $1 for a captured group. In Python, write \1 instead.
Two of the old rules have no safe regex version, so they’re gone: broken images need someone to check the file exists, and an orphaned caption needs someone to see that its image is missing. Those lines go to a review list, not to a delete.
Detect instead of replace when you can
For text that matters, a safer alternative to auto-correcting is to flag residue and let a person fix it. A check that reads each published page and fails it on things that can’t be legitimate (chat citation markers, utm_source=chatgpt.com on links, an As an AI disclaimer, [Insert …] placeholders) never deletes a paragraph by mistake. It’s slower than a replace-all. The reasoning matches render bugs that pass every check: automated checks catch structure, and someone still has to read the result. The corrections no linter catches are the other half of that review.
Where you do clean in bulk, remember that cleaning later is the expensive end of the job. If the drafts come from a prompt, asking it not to emit image placeholders and markdown at all is cheaper than finding them afterwards.
The short version
A typical set of 15 regex rules for cleaning AI-generated HTML was tested against 10 legitimate lines. Five rules removed only artifacts. Six damaged real content: they deleted a real image and a source citation, merged lines, stripped footnote and multiplication stars, and glued words to file extensions. A rewritten set of nine rules passed all 18 lines and 6 edge cases. Delete only what can’t be legitimate, convert formatting instead of stripping it, keep a file of lines that must never change, and run every rule against it before you trust it.
Frequently Asked Questions
How do I remove image placeholders from AI-generated text?
Find \[(?:Image|Figure|Diagram)(?: of|:)[^\]]*\] and replace it with nothing. It matches the bracketed descriptions chat assistants insert where an image would go, such as [Image of a laptop] or [Image: chart], and leaves WordPress shortcodes like [caption] and [gallery] alone. It works in Google Sheets and in most regex engines.
Why does my regex work in Python but not in Google Sheets?
Google Sheets uses the RE2 engine, which leaves out some features other engines have. The usual culprits are lookbehind and lookahead ((?<=…), (?=…)) and backreferences inside the pattern. Rewrite the rule to capture the surrounding character in a group and put it back in the replacement with $1.