πŸ“š General & Other

Cleaning Text for SEO Analysis: Whitespace, Punctuation, HTML, Stop Words and Find and Replace

Text copied from web pages, PDFs, forums and tool exports carries invisible baggage: blank lines, stray spaces, HTML and BBCode tags, punctuation, tracking digits and filler words. Analysis run on…

Text copied from web pages, PDFs, forums and tool exports carries invisible baggage: blank lines, stray spaces, HTML and BBCode tags, punctuation, tracking digits and filler words. Analysis run on dirty text β€” word counts, frequency checks, comparisons β€” produces misleading results.

This guide covers SeoWolf's text-cleanup tools: what each removes, what it can accidentally destroy, and the SEO workflows where it matters.

The Tools at a Glance

ToolUse it to
Remove Blank LinesThis deletes empty lines, closing the gaps in a list.
Trim & Remove Empty LinesThis removes leading and trailing whitespace from every line and discards lines left with nothing in them.
Normalize WhitespaceThis collapses runs of whitespace into single spaces and removes leading and trailing space from each line.
Remove PunctuationThis removes punctuation marks, leaving letters, numbers and spaces.
Strip Digits from TextThis deletes numeric characters wherever they appear, leaving the surrounding text in place.
Remove HTML / BBCode TagsThis removes markup from a block of text and leaves the readable content behind.
Remove Stop WordsStop words are the high-frequency structural words that hold sentences together β€” the, and, of, to, is.
Find and Replace TextThis substitutes one string for another everywhere it appears in the input, in a single pass over the entire list.

Remove Blank Lines

What It Does

This deletes empty lines, closing the gaps in a list.

Where blank lines come from

Copying between applications, exports that separate sections, and files that end with several empty lines nobody notices.

Why they cause problems

They count as rows. A list reported as 5,000 entries may contain 4,200 real ones. They also produce empty values when processed β€” and an empty value submitted to a system is frequently accepted as a valid empty record rather than rejected.

Blank is not always empty

A line containing only spaces looks blank and is not. Trim first, then remove empties, or the whitespace-only lines survive.

Empty, Blank, and the Line That Is Not There

grep -v '^$' removes truly empty lines. grep -v '^[[:space:]]*$' also removes whitespace-only ones. The distinction accounts for most cases where blank-line removal appears not to work.

Carriage returns make lines non-empty

In a file with Windows line endings, an "empty" line contains \r. It does not match ^$ and survives the filter. This is the same invisible character that breaks suffix matching, and it produces the same bafflement.

The final newline is not a blank line

A well-formed text file ends with a newline after its last line. That trailing newline is a terminator, not an empty line β€” but tools differ on whether they report a final empty field because of it, which is why a count is sometimes one higher than expected.

sed and awk alternatives

sed '/^$/d' and awk 'NF' both remove empties. awk 'NF' is the neatest: it keeps lines with at least one field, which by default means at least one non-whitespace character β€” so it handles the whitespace-only case without a pattern at all.

In a Real Workflow: The Blank Rows That Corrupt a Bulk Upload

A redirect import, a disavow file or a bulk edit that contains blank rows behaves unpredictably. Some systems skip them, some error, and some create empty records.

The worst outcome

A blank line in a redirect file that gets interpreted as a rule with an empty source or destination. Depending on the platform, that can produce a redirect affecting far more than intended, and it is not obvious in a list of hundreds of rules.

Clean before every upload

Trim, remove blanks, then check the count matches what you expect. Three seconds against the possibility of a malformed rule going live.

Verify after upload too

Compare the number of rows the system reports having accepted against the number you submitted. A mismatch means something was skipped or merged, and finding that immediately is far cheaper than discovering it from a traffic report weeks later.

Try it: Remove Blank Lines on SeoWolf's Notepad.

Trim & Remove Empty Lines

What It Does

This removes leading and trailing whitespace from every line and discards lines left with nothing in them.

Two cleanups in one pass

Trimming fixes values padded with invisible characters. Removing empties clears the gaps that padding and copy-paste leave behind.

Why it belongs first

Almost every other operation behaves better on trimmed input. Comparisons match, filters find what they should, and counts reflect real entries rather than blank rows.

The invisible failure it prevents

A trailing space makes two identical-looking values different to every tool that compares them, with nothing on screen to explain why.

Trim Removes More Than the Space Character

sed 's/^[[:space:]]*//; s/[[:space:]]*$//' trims, then grep -v '^$' drops empties. What counts as space is the part that varies.

The characters that survive a naive trim

Non-breaking space arrives from HTML and PDFs and is not matched by an ASCII space class. The zero-width space is invisible and defeats both trimming and visual inspection. A UTF-8 byte-order mark at the start of a file attaches to the first value and makes exactly one row unmatchable, which is a memorable afternoon to debug.

Order matters

Trim before removing empty lines. A line containing only spaces is not empty until it has been trimmed, so reversing the order leaves whitespace-only rows in the output.

Language trim functions differ

Java's trim() removes characters below U+0020 only; strip() is Unicode-aware. Python's strip() handles Unicode whitespace. JavaScript's trim() does too, including the BOM. Knowing which behaviour you have decides whether the invisible characters actually go away.

In a Real Workflow: The First Thing to Do With Any List You Receive

Data arriving from someone else is never clean. Trailing spaces from manual entry, blank rows where sections were separated, indentation from a copy out of a document.

Make it a reflex

Trim and drop empties before looking at anything else. It costs seconds and prevents a category of problem that is genuinely difficult to diagnose later, because the cause is invisible.

What it changes downstream

Deduplication starts working properly. Counts stop including blank rows. Comparisons against other lists stop returning nothing. Every one of those failures looks like a different problem until you know the cause.

Count before and after

The difference tells you how much of what you received was padding. A file of 5,000 rows that trims to 4,100 real entries was 900 rows of nothing β€” worth knowing before quoting the original figure to anyone.

Try it: Trim & Remove Empty Lines on SeoWolf's Notepad.

Normalize Whitespace

What It Does

This collapses runs of whitespace into single spaces and removes leading and trailing space from each line.

Why irregular spacing accumulates

Copying between applications introduces non-breaking spaces, tabs and double spaces. None are visible, and all of them affect comparison.

The invisible failure

Two entries that look identical but differ by a trailing space will never match. Deduplication misses them, lookups fail, and nothing in the interface explains why.

Normalise before comparing

Any operation involving matching β€” deduplication, set operations, joins β€” should be preceded by whitespace normalisation, or its results are unreliable.

Whitespace Is More Than Space and Tab

tr -s ' ' squeezes repeated spaces and misses most of what causes matching failures in real data.

The characters that break comparisons

U+00A0 non-breaking space is the usual culprit, arriving from HTML and PDFs. There are also en and em spaces, the zero-width space, and the byte-order mark, which frequently appears at the start of the first line of a UTF-8 file and makes the first value mysteriously unmatchable.

Regex whitespace classes vary

\s in a Unicode-aware engine matches the full whitespace category; in a byte-oriented one it matches only ASCII. [[:space:]] in POSIX tools depends on locale. Two implementations of "strip whitespace" can therefore disagree on the same input.

The reliable sequence

Normalise to NFC, replace every Unicode whitespace character with a plain space, collapse runs, then trim the ends. Doing this on ingest β€” rather than at the point of comparison β€” means everything downstream can assume clean input, which is a much easier property to reason about than remembering to normalise before each operation.

In a Real Workflow: The Invisible Reason Two Lists Refuse to Match

You compare two lists that should overlap heavily and get almost no matches. Every value looks correct. The problem is characters you cannot see.

Where they come from

Copying out of a PDF brings non-breaking spaces. Pasting from a web page brings them too. Spreadsheet cells frequently carry trailing spaces from manual entry. Each one is invisible and each one prevents a match.

Fixing it in one pass

Normalise whitespace on both lists before comparing. Match rates typically jump immediately, and the comparison finally reflects the data rather than its formatting.

Do it on arrival

Make normalisation the first thing that happens to any list, before analysis begins. It costs seconds and removes an entire category of problem that is otherwise extremely difficult to diagnose β€” because the evidence is, by definition, invisible.

Try it: Normalize Whitespace on SeoWolf's Notepad.

Remove Punctuation

What It Does

This removes punctuation marks, leaving letters, numbers and spaces.

Why remove it

Punctuation attaches to words and breaks comparisons. shoes, and shoes are different strings, so any matching or counting operation treats them as different terms.

Where it belongs in a sequence

Before tokenising, counting or comparing. Stripping punctuation is part of normalising text for analysis, alongside case folding and whitespace cleanup.

Not for text people will read

Stripped text is for machines. Keep the punctuated original for anything published.

Punctuation Is a Unicode Category, Not a Character List

tr -d '[:punct:]' removes ASCII punctuation. Real text contains a great deal that is not ASCII.

Smart quotes are not quotes

Word processors and CMS editors convert ' to ' and " to ". Those are different code points, outside ASCII, and [:punct:] in a C locale does not match them. Text that appears cleaned still carries typographic quotes, and they break the comparisons the cleaning was meant to fix.

Match by category

A Unicode-aware engine can match \p{P} for all punctuation and \p{S} for symbols. That covers em dashes, ellipsis characters, guillemets and every other mark a real document contains, rather than the sixty-odd ASCII marks.

Decide about the middle cases first

Apostrophes inside words and hyphens inside compounds are the ones where removal changes tokenisation. A common approach is to remove punctuation at word boundaries and keep it word-internally, which preserves don't and state-of-the-art while still stripping trailing commas. Whichever rule you choose, apply it to the whole corpus β€” mixing rules produces counts that cannot be compared.

In a Real Workflow: Why Your Keyword Counts Are Slightly Wrong

A frequency analysis reports shoes 40 times and shoes, 12 times as separate entries. The real count is 52, and every conclusion drawn from the split numbers is understated.

The effect on analysis

Punctuation fragments counts across variants of the same word. On a large text this systematically understates the terms that appear most often β€” precisely the ones the analysis exists to identify.

Normalise before counting

Strip punctuation, fold case, and collapse whitespace before any frequency work. The counts then reflect the text rather than its typography.

Know what you are discarding

Apostrophes and hyphens carry meaning. Stripping them turns don't into dont and state-of-the-art into three words. That is usually acceptable for counting, and it is worth knowing it happened before quoting a figure.

Try it: Remove Punctuation on SeoWolf's Notepad.

Strip Digits from Text

What It Does

This deletes numeric characters wherever they appear, leaving the surrounding text in place.

Different from removing lines

Filters that drop whole lines containing digits remove the entire entry. This removes only the digits and keeps the rest of the line.

Where it is useful

Stripping years from titles to find the underlying pattern, removing IDs from paths to see the URL structure, or clearing quantities from product names to group them.

Watch the gaps left behind

Removing digits from Top 10 Tips leaves Top Tips with a double space. Normalise whitespace afterwards.

Stripping Digits Leaves Artefacts Worth Cleaning

tr -d '0-9' removes ASCII digits. The removal itself is trivial; the state it leaves the text in is where the work is.

Collapsing what is left

Removing digits from /product/12345/reviews gives /product//reviews with an empty path segment. For pattern grouping, replace runs of digits with a single placeholder token instead of deleting them β€” /product/{id}/reviews is both readable and unambiguous, where the deleted version could collide with a genuinely different path.

Unicode digits again

tr -d '0-9' misses Devanagari, Arabic-Indic and full-width numerals. A Unicode-aware \p{Nd} covers the numeric decimal category properly.

Digits inside words

utf8, h1 and covid19 lose meaning when stripped. If the goal is normalising identifiers rather than removing all numbers, target digit runs at path-segment boundaries specifically rather than every digit in the string β€” the broader rule destroys tokens you needed.

In a Real Workflow: Finding the URL Patterns Hidden Behind Product IDs

A crawl of an ecommerce site returns forty thousand URLs, every one carrying a unique product ID. The individual URLs are meaningless; the patterns behind them are the site architecture.

Reducing to structure

Strip the digits and deduplicate. Forty thousand URLs collapse to perhaps twelve distinct patterns β€” the templates the site is actually built from.

What the patterns tell you

Twelve templates means twelve places where a fix applies to thousands of pages. It also exposes patterns nobody knew existed: an old URL structure still generating pages, or a duplicate route serving the same products under two paths.

Count pages per pattern

Group the original URLs by their stripped form and count. A pattern with thirty thousand pages and one with forty are very different problems, and the counts decide which template gets attention first.

Try it: Strip Digits from Text on SeoWolf's Notepad.

Remove HTML / BBCode Tags

What It Does

This removes markup from a block of text and leaves the readable content behind. Copy anything out of a web page, a forum post or a CMS field and the markup usually comes along with it.

What gets removed

Anything between angle brackets in HTML, and the square-bracket equivalents in BBCode. The text between the tags stays.

Why the markup is a problem

Any counting or analysis performed over tagged text is measuring the tags too. Word counts inflate, frequency counts fill up with element names, and readability scores become meaningless.

The usual first step

Strip tags before counting words, measuring length, checking readability, or pasting content into an editor that would otherwise render or escape the markup.

Why You Cannot Strip HTML With a Regex

s/<[^>]*>//g removes anything between angle brackets. It works on well-formed fragments and fails on a great deal of real-world HTML.

Script and style content is not markup

The tags around a <script> block are removed; the JavaScript inside is not. Strip a full page that way and the output is a wall of code interspersed with prose. Those elements must be removed with their contents, which a single tag-matching expression cannot express.

Angle brackets appear in attributes and comments

<img alt="a > b"> terminates the naive pattern at the wrong >, leaving b"> in the output. Conditional comments and CDATA sections break it in similar ways.

Entities survive the strip

Removing tags leaves &amp;, &nbsp; and &#8217; intact. Text that still contains entities is not plain text, and a word count over it is still wrong β€” decoding has to happen as a separate pass after stripping.

The right tool

Parse the document and read its text content. Every language has a parser that handles malformed markup the way browsers do. For a quick one-off paste, a stripping tool is fine; for anything in a pipeline, use the parser.

In a Real Workflow: Getting an Honest Word Count Out of a CMS Export

The CMS reports a blog post as 2,400 words. Reading it, that feels generous. Export the field and the reason appears: the count includes every tag, class name and inline style in the body HTML.

Measuring what readers actually see

Strip the markup, then count. A post that claimed 2,400 words routinely lands nearer 1,400 β€” and 1,400 is the number that matters, because it is what a reader encounters and what a search engine evaluates as content.

Why the inflated figure causes real problems

Content briefs specify word counts. Writers hit them in a system that counts markup, so a brief asking for 2,000 words gets delivered at 1,200 words of actual prose. Everyone believes the spec was met, and the thin-content problem that follows has no obvious cause.

Where else this bites

Meta description fields containing stray entities, product descriptions pasted from a supplier's HTML, and imported content where the markup arrives escaped and renders as visible tags on the live page.

Try it: Remove HTML / BBCode Tags on SeoWolf's Notepad.

Remove Stop Words

What It Does

Stop words are the high-frequency structural words that hold sentences together β€” the, and, of, to, is. Removing them leaves the terms that carry the actual subject matter.

Why remove them at all

They dominate any frequency count without telling you anything. In almost every English text, the same handful of function words occupy the top ten positions, burying the words that distinguish one document from another.

What is left behind

Nouns, verbs and modifiers specific to the topic. That residue is a rough but useful summary of what a piece of text is genuinely about.

What it is not for

Stripped text is unreadable and should never be published. This is an analysis step, not an editing one.

There Is No Standard Stop Word List

Every library ships a different one. NLTK's English list has 179 entries; scikit-learn's has 318; Lucene's is 33. Analyses run with different lists are not comparable, and nothing warns you about it.

Domain words break generic lists

In an SEO corpus, search, page and site appear in nearly every document and carry almost no distinguishing information β€” they are functionally stop words for that corpus and appear on no standard list. Meanwhile a generic list may remove terms you need: not is a stop word almost everywhere, and removing it inverts the meaning of any sentiment work.

Deriving the list from the data

A more defensible approach is to compute document frequency across your own corpus and treat the terms appearing in most documents as stop words. That is essentially what the IDF half of TF-IDF does automatically, which is a good argument for using TF-IDF and skipping explicit stop word removal altogether.

Order relative to other steps

Remove stop words after tokenising and case-folding, and before stemming. Stemming first mangles the function words into forms that no longer match the list, so the removal silently misses them.

In a Real Workflow: Reading What a Competitor's Page Is Really Optimised For

A competitor outranks you and the page looks unremarkable. Rather than guessing at the reason, reduce the page to its vocabulary and look at what it emphasises.

The two-step read

Strip the stop words from their body copy, then run a frequency count on what remains. The top terms are what the page is actually built around β€” which is frequently not what its headline suggests.

What the gap usually shows

Your page covers the topic broadly. Theirs repeats one specific phrasing throughout, in headings and body alike. That consistency is often the whole difference, and it is invisible when reading normally because repetition is something you notice statistically rather than sentence by sentence.

Use it as a diagnosis, not a template

The finding is which intent they committed to. The response is to commit as clearly to yours β€” not to match their term counts, which produces text that reads like it was written for a word-frequency check rather than a reader.

Try it: Remove Stop Words on SeoWolf's Notepad.

Find and Replace Text

What It Does

This substitutes one string for another everywhere it appears in the input, in a single pass over the entire list.

The everyday need

Changing a domain across a list of URLs, correcting a consistently misspelled term, or swapping a placeholder for a real value across hundreds of rows.

Replace and delete are the same operation

Replacing a string with nothing removes it. That is often the simplest way to strip a repeated prefix, a tracking parameter, or an unwanted marker.

Check the count first

Before replacing, filter to see how many lines contain the search term. If the number is far from what you expected, the replacement will affect rows you did not intend.

Replacement Strings Have Their Own Metacharacters

Most people know the search pattern is interpreted. Fewer realise the replacement is too.

The ampersand expands

In sed, an unescaped & in the replacement is substituted with the entire matched text. Replacing a term with AT&T inserts the match where the ampersand was, producing output that looks like a bizarre corruption rather than an escaping error. Escape it as \&.

Backslashes and group references

\1 refers to a capture group. A replacement containing a literal backslash needs doubling, and a Windows path in a replacement string is almost entirely backslashes.

Delimiter collision

sed 's/old/new/' breaks when either side contains /, which URLs always do. Any character can be the delimiter β€” s|old|new| β€” and choosing one absent from both strings is the fix.

Order matters when chaining

Two replacements applied in sequence are not commutative. Replacing a with b and then b with c turns the original a into c. Where that is not intended, replace via a placeholder that cannot appear in the data.

In a Real Workflow: Rewriting 3,000 URLs for a Domain Migration

A rebrand moves the site to a new domain. Every internal link reference, every redirect mapping and every tracked URL in the analytics configuration carries the old domain.

The mechanical part

Replace the old domain with the new one across the whole list. Three thousand rows update in one pass, consistently, which is the only way this gets done without introducing errors.

Build the redirect map from both versions

Keep the original list beside the rewritten one. Old and new side by side is the redirect map, and having it generated rather than typed removes the most common migration failure β€” a mapping that points somewhere subtly wrong.

Where paths changed too

A pure domain swap is the easy case. If the new site also restructured its paths, the replacement gets you the domain and the path mapping still needs doing per section. Do not let the ease of the first part disguise how much of the second remains.

Try it: Find and Replace Text on SeoWolf's Notepad.

The Bottom Line

Clean in a deliberate order β€” structure first (tags, blank lines), then whitespace, then content-level changes like punctuation and stop words β€” and keep the original so you can always check what a cleaning step changed.

Next step: All of these tools are free in SeoWolf's Notepad β€” paste a list and transform it in your browser.