Topic 3.7
Real Text Work: Cleaning and Parsing
In one line
Real text is messy: stray spaces, mixed case, punctuation, odd line endings. This topic puts the whole phase together: split text into lines and words, clean it, count words, wrap it with textwrap, use the string constants, and parse a small log file.
Think of it like this
Sorting a pile of handwritten shop receipts. Some are smudged, some use capitals, some have extra spaces. Before you can add anything up, you tidy each one into the same shape, then read it. Text work always has two stages: clean (make the text uniform) and parse (pull out the parts you care about). Skipping the first stage is the usual reason a parser breaks on the 50th line.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Cleaning
- Making text uniform: trimming, fixing case, removing unwanted characters.
- Parsing
- Reading text of a known shape and pulling out its parts as values.
- Dictionary
- A notebook of key-to-value pairs written with
{}. Phase 4 covers it in full. - Triple-quoted string
- A string written between
"""marks that can span many lines. - Word wrap
- Breaking long text into lines of at most a chosen width.
- Log
- A file or stream of timestamped messages that a program writes about what it is doing.
- Delimiter
- The character that separates fields, such as a comma or
=. - Tokenizing
- Splitting text into small pieces (tokens), such as words.
Step by step
01Start with lines
A triple-quoted string stands in for a file here. splitlines() gives one item per line and no newline characters. A common detail: if the text ends with a newline, splitlines() does not produce an extra empty line at the end, unlike split("\n").
text = """first line
second line
"""
print(text.splitlines()) # ['first line', 'second line']
print(text.split("\n")) # ['first line', 'second line', '']02Clean the text
Normalise before you count. Make everything lower case so "The" and "the" are the same word, delete punctuation, then split. maketrans("", "", ".,!?") builds a table that says "map nothing to anything, delete these characters", and translate applies it in one pass.
text = "The cat sat on the mat. The dog sat!"
cleaned = text.lower().translate(str.maketrans("", "", ".,!?"))
print(cleaned) # the cat sat on the mat the dog sat
print(cleaned.split()) # extra spaces vanish: split() ignores them03Count the words
The dictionary starts empty. For each word, counts.get(w, 0) returns the current count (or 0 for a new word) and we store one more. Printing in sorted order makes the output the same every time.
words = "the cat sat on the mat".split()
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
for w in sorted(counts):
print(w, counts[w]) # cat 1, mat 1, on 1, sat 1, the 204Wrap, shorten and indent with textwrap
textwrap.fill(text, width=30) breaks a long paragraph into lines of at most 30 characters, at word boundaries. shorten collapses whitespace and cuts at a word boundary with a placeholder, perfect for previews. dedent removes the common indentation of a block, which keeps long messages tidy inside indented code.
import textwrap
para = "Python is a programming language that lets you work quickly."
print(textwrap.fill(para, width=30))
print(textwrap.shorten(para, width=30, placeholder="..."))05Split a log line into parts
split(None, 3) means "split on whitespace, at most 3 times", so the first three pieces are date, time and level, and everything else stays together as the fourth. Unpacking into four names only works when the line really has at least that many pieces. Real parsers guard against short lines.
line = "2026-10-09 10:15:07 ERROR user=ravi action=upload"
date, time, level, rest = line.split(None, 3)
print(level) # ERROR
print(rest) # user=ravi action=upload06Parse the key=value fields
rest.split() gives ['user=ravi', 'action=upload']. Each pair goes through partition("="). The underscore _ is a normal variable name that programmers use for "a value I do not need" (here the separator).
rest = "user=ravi action=upload size=2048"
for pair in rest.split():
key, _, value = pair.partition("=")
print(key, "->", value)07Put it together and expect the unexpected
The complete log parser appears in the examples. Real log files contain blank lines, truncated lines and fields in a different order. Topic 7.1 (exceptions) and Topic 3.6 (regular expressions) give you the tools to deal with those, and Topic 7.9 shows how to write logs properly in the first place.
Try it yourself
- 1
Add a line with a problem
Add the line
"bad line"to the log in the second example and run it. The unpackingdate, time, level, rest = ...crashes with aValueError. Think about how a parser should handle a line it cannot read. Topic 7.1 shows how. - 2
Count your own text
Replace the text in the first example with a paragraph from anywhere. Which word is the most common? Does it change if you stop lowercasing?
- 3
Find the top word
After the counting loop, loop through
countsonce more and keep the word with the largest count in two variables,best_wordandbest_count. Start withbest_word = ""andbest_count = 0. When a word's count is bigger thanbest_count, update both variables.
Code & diagrams
A dictionary keeps one count per word. Topic 4.10 shows the shortcut `Counter`.
Expected output
['the', 'cat', 'sat', 'on', 'the', 'mat', 'the', 'dog', 'sat', 'on', 'the', 'log']
total words: 12
cat 1
dog 1
log 1
mat 1
on 2
sat 2
the 4
distinct words: 7Expected output
10:15:01 INFO user=asha, action=login
10:15:07 ERROR user=ravi, action=upload, size=2048
upload size: 2 KB
10:16:30 WARN user=asha, action=retry
lines: 3 errors: 1Expected output
Python is a programming
language that lets you work
quickly and integrate systems
effectively.
Python is a programming language that...
> a
> b
'\nindented\n more\n'
abcdefghijklmnopqrstuvwxyz
ABCDE 0123456789
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
TrueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Unpack a line with too few parts
Parse a log line that has only three words.
line = "2026-10-09 10:15:07 ERROR"
date, time, level, rest = line.split(None, 3)Myth vs fact
Myth
Cleaning text is the boring part you can skip.
Fact
Most real-world text bugs and bad data come from skipping it. A word counter that treats The and the as different words is wrong in a way that no test of the counting loop would catch.
Myth
split("\n") and splitlines() are the same.
Fact
splitlines() handles \r\n and other line endings and does not produce a final empty item for a trailing newline.
Myth
A word count equals a token count.
Fact
Language models count sub-word tokens. The same text can have very different word and token counts.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Real files reach your program as bytes. Decode with an explicit encoding first (Topic 3.4, Topic 7.5), then clean. For large files, iterate line by line instead of reading everything, so memory stays flat.
- ▸
splitlines()also splits on\x0b,\x0c,\x1cto\x1e,\x85,\u2028and\u2029, not only on\nand\r. That is usually what you want for text, but a log line that legitimately contains a form feed will be cut in two; if records are defined by\nalone, iterate the file object or usesplit("\n")after normalising\r\n. Passkeepends=Truewhen you must be able to rebuild the original text exactly. - ▸
str.translatewith a table built bystr.maketransis one pass over the text in C and is much faster than chaining manyreplacecalls for character-level cleaning. For Unicode-aware cleaning, combineunicodedata.normalize(Topic 3.4) withcasefold()first. - ▸
For text pipelines in AI work, keep the original text and store cleaned versions beside it, with offsets if you need to point back to the source. Chunking and citation (Topic 16.9) depend on being able to find the original span again.
Remember this
- 1
Lines first. In real programs text arrives in one big string (read from a file, Topic 7.5, or a network reply).
text.splitlines()cuts it into lines and handles\n,\r\n(Windows) and old Mac\rendings;text.split("\n")does not handle\r\nand leaves stray\rcharacters. Here we use a multi-line string, written with triple quotes, in place of a file. - 2
Cleaning toolkit.
strip()for edges." ".join(text.split())collapses any runs of spaces, tabs and newlines into single spaces.lower()orcasefold()to ignore case.str.translate(str.maketrans("", "", ".,!?"))deletes every listed character in one pass (the walkthrough explains it).replacefor specific fixes. Do the cleaning steps before counting or comparing, and keep the original text if you may need it later. - 3
Counting words. Split the cleaned text with
split()and count each word. The easiest way to keep a count per word is a dictionary: a notebook that maps a key (the word) to a value (its count), written{}.counts[w] = counts.get(w, 0) + 1means "look upw, use 0 if it is new, add one". Phase 4 teaches dictionaries properly (Topic 4.4), and theCounterclass in Topic 4.10 does this job in one line.sorted(counts)gives the keys in alphabetical order. - 4
Standard-library helpers. The standard library is the set of tools that come with Python. The
stringmodule holds ready-made constants:string.ascii_lowercase,ascii_uppercase,digits,punctuationandwhitespace. Thetextwrapmodule reshapes paragraphs:fillwraps text to a width,shortencuts to a width with a placeholder (...),indentadds a prefix to every line anddedentremoves common leading spaces from a block (handy for triple-quoted text inside indented code). - 5
A few older padding tools exist too:
str.ljust,rjust,centerandzfillpad strings, though format specs (Topic 3.3) usually do the same job. - 6
Parsing a log. A log line has a fixed shape:
date time LEVEL key=value key=value.line.split(None, 3)splits on whitespace but stops after three cuts, leaving the rest of the line as the fourth piece. Thenpartition("=")splits eachkey=valuepair safely. Always handle the case where a line does not have the shape you expect, because real files have blank lines and corrupted lines. Topic 3.6's regular expressions are the next step up for irregular formats. - 7
The AI-era link. Cleaning and splitting text is the first step of nearly every language-model pipeline. Before documents are turned into numbers for search, they are cleaned, split into chunks of limited size, and counted (Topic 16.9). Splitting on whitespace is a crude "tokenizer"; real models split into sub-word tokens (Topic 16.4), so a word count is never a token count.
Explain it without notes
Why should you clean text before counting or comparing it?
What is the difference between splitlines() and split("\n")?
How does line.split(None, 3) differ from line.split(), and why is it useful for logs?
When would you use partition instead of split to read key=value text?
Practice
Count the vowels in "Education is the key".
Find the longest word in "the quick brown foxes jumped" with a loop and two variables.
Wrap "one two three four five six seven" to lines of at most 15 characters.
Trade-offs
- ↔
Aggressive cleaning against fidelity. Removing punctuation and case makes counting easy but loses information ("US" and "us"). Clean a copy and keep the original.
- ↔
Hand-written parsers against regular expressions.
splitandpartitionare clear for fixed shapes. Once the format has optional parts or varying order, a regular expression with named groups is usually shorter and safer.
Done when you can
Done when you can turn a block of text into lines, words and fields.
Done when you can clean text with
strip,lower,split/joinandtranslate.Done when you can count words with a dictionary and print the result in a stable order.
Done when you can wrap and shorten text with
textwrap.Done when you can parse a
date time LEVEL key=valuelog line safely.