TweetFollow Us on Twitter

Mac in the Shell: Python Text Parsing

Volume Number: 25
Issue Number: 07
Column Tag: Mac in the Shell

Mac in the Shell: Python Text Parsing

Automating entries through keyword searching

by Edward Marczak

Introduction

We've been covering Python basics over the last several columns. This month, we'll hit something with a little practicality: text processing. While computers are really good with numbers, people are really good with words. More often than not, input from people comes as text. Turns out that Python is pretty good at dealing with text processing and manipulation. Let's have a closer look, shall we?

More To The Story

OK, there's a little bit more to the story. I've dealt with e-mail systems and e-mail processing for a very long time. (Let's just say that I started with sendmail before it used m4, mmmmkay?). Oftentimes, though, we want a program dealing with incoming mail. This may be for the purposes of a mailing list, for auto-response or to parse the e-mail and then put relevant bits into a database.

E-mail is either really complex or really simple, depending on how you look at it. It's complex because it's got headers and encoding and parts. But it's simple, because it's all text. No matter what all of the pieces are, they're all just human readable text. Fortunately, there are many pre-built libraries that help deal with the complexity, allowing you, the script writer, to focus on the task at hand: processing the parts of the message body that you're interested in. Python's "batteries included" philosophy ensures that a good mail processing library ships as part of the core package.

How is any of this Mac-specific? Well, it isn't. Not directly. However, I just mentioned that Python has, by default-no extra installation required-a good e-mail processing library. Python ships standard with OS X. That's part of the equation solved. Then, there's the issue of receiving the mail in the first place.

Just about every contemporary mail system has a method of taking incoming mail and feeding it to a script. Postfix, which ships with OS X, is no exception. By default, an SMTP (Simple Mail Transfer Protocol) server wants to receive mail, decide if the mail is for a valid user on it's system, and to then drop that mail in the user's mailbox. That's it. But what about a list server? Well, you take the same SMTP server, but instead of delivering any mail to an end user's mailbox, you hand all mail off to the list server program. The list server program will determine who to deliver this mail to.

This is also similar to server-side anti-spam. All incoming mail is handed off to an anti-spam program. The mail is analyzed, potentially acted upon (read: dropped), and mail is then fed back into the SMTP server for final delivery.

We're not going to do anything so grand here today, but after finishing up, you'll have the groundwork. If you have an OS X machine acting as a mail relay and really want to test/use this, you're going to need to modify some postfix config files directly.

In /etc/postfix/transport, you'll need to first define a transport. Let's say your main mail server is called mail.example.com. If you want to divert mail to a script, have the mail sent to mproc.example.com, and add the following like to /etc/postfix/transport:

mproc.example.com mproc:

This says, "all mail that arrives for mproc.example.com send it to the transport named mproc." Once a transport is defined, we also need to tell postfix how to connect the dots between the transport named mproc and our script. That happens in /etc/postfix/master.cf. Add the following line to the end of the file:

mproc  unix  -       n       n       -       -       pipe
  flags=DRhu user=mproc argv=/usr/bin/mproc.py

This tells postfix that any mail arriving on the mproc transport should be piped to the mproc.py script. This is, of course, assuming that we store our script in /usr/bin as "mproc.py". Adjust as needed.

Of course, we're going to keep it simple: since the text will be piped into the script, it's easy to simulate. The pipe simply delivers the entire message on stdin.

A Text Processing Script

Again, we said that we're really focusing on processing e-mail as it arrives, so, we're going to look for input via stdin (which the pipe above does for us). Other text processing scripts may want to deal with text already in a file or elsewhere. I'll make sure to cover that in a future column, but that's not the goal of today's exercise. Despite 'keeping it simple,' we'll be covering a few new-to-us concepts.

Here's the assignment: currently, stock information arrives via e-mail where a dedicated person reads the mail and inputs the entries into a database. This person could clearly be doing better things, as this can be automated without changing the backend system that is sending the e-mail message (whether that's a person or a machine is immaterial for this article). These messages will have a strict format: category and value, separated by a colon. The body of a message would look like this:

Company: Cartier

Product: Watch

Model: Original Tank

Number: 12324A332

Price: $4,500

Available: Yes

However, there's a problem when parsing an e-mail message: it's never just the body that you receive. It's headers. And MIME parts. Oy. Fortunately, Python's email library has functions to deal with this.

I say, let's dive right in. Here's the code I'm using, which will be followed by an explanation of the program.

Listing 1: e-mail parsing program, epp.py

#!/usr/bin/env python
import email
import re
import sys
from email.Parser import Parser
# The keywords we're looking for
keys = ['Company', 'Product', 'Model', 'Number', 'Price', 'Available']
# Compile each keyword into a regular expression
keysre = {}
for i in keys:
  keysre[i] = re.compile(i)
# Read stdin into a single string
mystdin = sys.stdin.read()
# Create a parser object and parse the input
p = Parser()
ps = p.parsestr(mystdin)
# Examine each message part for an appripriate plain body
for i in ps.walk():
  if i.get_content_subtype() != "plain":
    continue
  plainbody = i.as_string()
# Break message into lines, based on newline char
plainbody = plainbody.splitlines()
for i in plainbody:
  # Look at each key for a match.
  for k in keys:
    if keysre[k].match(i):
      print i
sys.exit(0)

First thing to notice about the code is the relative brevity-37 lines in total. As usual, the first few lines simply get us set up: she-bang line and relevant imports, including the Python-supplied email module.

#!/usr/bin/env python
import email
import re
import sys
from email.Parser import Parser

There have been a few times in this column that I've mentioned the importance of regular expressions (RE). Python has good support for RE from the re module:

# The keywords we're looking for
keys = ['Company', 'Product', 'Model', 'Number', 'Price', 'Available']
# Compile each keyword into a regular expression
keysre = {}
for i in keys:
  keysre[i] = re.compile(i)

What is happening here is that we define a list of the keywords we're going to be looking for in the message body. Python regular expressions need to be compiled into an object, which is why we define the keysre dictionary. Of course, we could define these objects one at a time, but that's really inelegant and doesn't scale. In the loop, the dictionary is filled with keys that correspond to the words we're going to match, with a value of the compiled RE object.

# Read stdin into a single string
mystdin = sys.stdin.read()
# Create a parser object and parse the input
p = Parser()
ps = p.parsestr(mystdin)

The first part of this section is pretty simple: assign all of stdin to the variable mystdin. Part of the email library is the email parser object. This object allows an e-mail message, headers, MIME parts and all to be parsed, iterated over and picked apart. We're defining a new parser object and then loading the variable ps with a parsed version of the message that's arriving on stdin.

# Examine each message part for an appropriate plain body
for i in ps.walk():
  if i.get_content_subtype() != "plain":
    continue
  plainbody = i.as_string()

This section of the code hands us back the plain part of the message. MIME types are described in two parts, such as "text/html". We're only interested in the plain portion of the message if there are additional parts in the message. The conditional tests if the subpart is not plain. If it is not, we continue and go back to the top of the loop. If it is plain, we fall though and assign the entire subpart, as a string, to the variable plainbody.

# Break message into lines, based on newline char
plainbody = plainbody.splitlines()

The splitlines() string method returns a list, each element a line in the string, split by a separator-by default, the newline character. Now, we can examine each line in turn:

for i in plainbody:
  # Look at each key for a match.
  for k in keys:
    if keysre[k].match(i):
      print i

As we examine each line, an if statement tests for a match of our regular expressions by looping through the keysre dirctionary. If there's a match, we print it out. Naturally, we can take other action here besides printing it out, such as storing it internally, comparing it to some known value or even inserting it into a database. One thing you will likely want to do is to split the matching lines into key/value pairs. The string's split method does this very nicely. For example:

key, value = i.split(':')

The argument to split is the separator to split on. In our case, we know the lines are split by the colon character and that we're expecting back two values. The split method will happily split as many times as needed. In the case where you don't know how many values to expect, you may just want to assign to a list, like so:

values = i.split(':')

From there you can work out how many values were split and returned to you, and what to do with them.

Finally, we exit the program with a 'clean' exit code:

sys.exit(0)

Running the Program

If you don't happen to have any test e-mail sitting around, I've placed one on the MacTech ftp site, under this month's directory (ftp.mactech.com/src/mactech/volume25_2009/25.07.sit ). If you run your own mail server, you can actually just go grab a raw message from the mail spool-your own mail, mind you!

Since the instructions I gave in the first part allow postfix to send incoming mail through a pipe and to the application, we need a more convenient way to test. The command line makes this easy: just pipe it yourself. Don't forget to mark the program as executable:

chmod 770 epp.py

and then pipe away:

cat /path/to/mits_test_mail | ./epp.py

(or, substitute the ./ with the full path to the program, if needed). If you're using the test mail from the MacTech ftp site, you should see the output you expect: the values that we're matching on, with no headers, MIME clutter, etc. Take a look at the original test mail file to see just how much cruft is being left out.

Conclusion

This was a bit of a whirlwind tour of several concepts. I'd encourage you to bulk up an application like this by checking for error conditions and then taking appropriate action. Outside of that, though, it's pretty impressive at how few dedicated commands are needed to process a well-formed e-mail message. The rest are really just 'nuts and bolts' features of the language.

Media of the month: I'd like to think that everyone has some kind of music that they like. Something that reached them, or that reminds them of some period of time. Well, growing up in New York certainly left a musical stamp on me. I just finished "No Wave" by Marc Masters, and I just loved every second of it. I remember the NY scene around that time, but was certainly too young to fully appreciate it. I don't expect everyone to fully enjoy or 'get' No Wave. But sometimes, the best way to enjoy music is by reading about it. So think of the music that inspires you and find the reading material that points out its inspiration. Thanks to Bruce Gerson for inspiring the topic this month.

Next month, we'll expand on some of the concepts covered here and dig deeper into the well that Python has to offer.


Ed Marczak is the Executive Editor of MacTech Magazine. He has written for MacTech since 2004.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Backup and Sync 3.46 - File backup and s...
Backup and Sync (was Google Drive) is a place where you can create, share, collaborate, and keep all of your stuff. Whether you're working with a friend on a joint research project, planning a... Read more
iClock 5.5 - Customizable menu bar clock...
iClock replaces the old Apple's default menu bar clock with more features, customization and increases your productivity. Features: Have your Apple or Google calendar instantly available from the... Read more
Garmin Express 6.18.0.0 - Manage your Ga...
Garmin Express is your essential tool for managing your Garmin devices. Update maps, golf courses and device software. You can even register your device. Update maps Update software Register your... Read more
MarsEdit 4.3.5 - Quick and convenient bl...
MarsEdit is a blog editor for OS X that makes editing your blog like writing email, with spell-checking, drafts, multiple windows, and even AppleScript support. It works with with most blog services... Read more
Xcode 11.0 - Integrated development envi...
Xcode includes everything developers need to create great applications for Mac, iPhone, iPad, and Apple Watch. Xcode provides developers a unified workflow for user interface design, coding, testing... Read more
DaisyDisk 4.8 - $9.99
DaisyDisk allows you to visualize your disk usage and free up disk space by quickly finding and deleting big unused files. The program scans your disk and displays its content as a sector diagram... Read more
VMware Fusion 11.5.0 - Run Windows apps...
VMware Fusion and Fusion Pro - virtualization software for running Windows, Linux, and other systems on a Mac without rebooting. The latest version includes full support for Windows 10, macOS Mojave... Read more
Apple Configurator 2.10 - Configure and...
Apple Configurator makes it easy to deploy iPad, iPhone, iPod touch, and Apple TV devices in your school or business. Use Apple Configurator to quickly configure large numbers of devices connected to... Read more
Spotify 1.1.15.448. - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
MenuMeters 1.9.8 - CPU, memory, disk, an...
MenuMeters is a set of CPU, memory, disk, and network monitoring tools for Mac OS X. Although there are numerous other programs which do the same thing, none had quite the feature set I was looking... Read more

Latest Forum Discussions

See All

Marvel Strike Force is adding Agent Coul...
Marvel Strike Force, the popular squad-based RPG, is set to receive a bunch of new content over the next few weeks. [Read more] | Read more »
Lots of premium games are going free (so...
You may have seen over the past couple weeks a that a bunch of premium games have suddenly become free. This isn’t a mistake, nor is it some last hurrah before Apple Arcade hits, and it’s important to know that these games aren’t actually becoming... | Read more »
Yoozoo Games launches Saint Seiya Awaken...
If you’re into your anime, you’ve probably seen or heard of Saint Seiya. Based on a shonen manga by Masami Kurumada, the series was massively popular in the 1980s – especially in its native Japan. Since then, it’s grown into a franchise of all... | Read more »
Five Nights at Freddy's AR: Special...
Five Nights at Freddy's AR: Special Delivery is a terrifying new nightmare from developer Illumix. Last week, FNAF fans were sent into a frenzy by a short teaser for what we now know to be Special Delivery. Those in the comments were quick to... | Read more »
Rush Rally 3's new live events are...
Last week, Rush Rally 3 got updated with live events, and it’s one of the best things to happen to racing games on mobile. Prior to this update, the game already had multiplayer, but live events are more convenient in the sense that it’s somewhat... | Read more »
Why your free-to-play racer sucks
It’s been this way for a while now, but playing Hot Wheels Infinite Loop really highlights a big issue with free-to-play mobile racing games: They suck. It doesn’t matter if you’re trying going for realism, cart racing, or arcade nonsense, they’re... | Read more »
Steam Link Spotlight - The Banner Saga 3
Steam Link Spotlight is a new feature where we take a look at PC games that play exceptionally well using the Steam Link app. Our last entry talked about Terry Cavanaugh’s incredible Dicey Dungeons. Read about how it’s a great mobile experience... | Read more »
Combo Quest (Games)
Combo Quest 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Combo Quest is an epic, time tap role-playing adventure. In this unique masterpiece, you are a knight on a heroic quest to retrieve... | Read more »
Hero Emblems (Games)
Hero Emblems 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: ** 25% OFF for a limited time to celebrate the release ** ** Note for iPhone 6 user: If it doesn't run fullscreen on your device... | Read more »
Puzzle Blitz (Games)
Puzzle Blitz 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Puzzle Blitz is a frantic puzzle solving race against the clock! Solve as many puzzles as you can, before time runs out! You have... | Read more »

Price Scanner via MacPrices.net

11″ WiFi iPad Pros on sale today for up to $2...
Amazon has new 2018 Apple 11″ WiFi iPad Pros in stock today and on sale for up to $200 off Apple’s MSRP. These are the same iPad Pros sold by Apple in its retail and online stores. Be sure to select... Read more
Select 12″ iPad Pros on sale for $200 off App...
Amazon has select 2018 Apple 12″ iPad Pros in stock today and on sale for $200 off Apple’s MSRP. These are the same iPad Pros sold by Apple in its retail and online stores. Be sure to select Amazon... Read more
Get one of Apple’s new 2019 iPhone 11 models...
Boost Mobile is offering the new 2019 Apple iPhone 11, iPhone 11 Pro, and 11 Pro Max for $100 off MSRP. Their discount reduces the cost of an iPhone 11 to $599 for the 64GB models, $899 for the 64GB... Read more
13″ 1.4GHz Silver MacBook Pros on sale for $1...
B&H Photo has new 2019 13″ 1.4GHz 4-Core Touch Bar Silver MacBook Pros on sale for $100 off Apple’s MSRP. Overnight shipping is free to many addresses in the US. These are the same MacBook Pros... Read more
4-core and 6-core 2018 Mac minis available at...
Apple has Certified Refurbished 2018 Mac minis available on their online store for $120-$170 off the cost of new models. Each mini comes with a new outer case plus a standard Apple one-year warranty... Read more
$250 prepaid Visa card with any Apple iPhone,...
Xfinity Mobile will include a free $250 prepaid Visa card with the purchase of any new iPhone, new line activation, and transfer of phone number to Xfinity Mobile. Offer is valid through October 27,... Read more
Sprint is offering the 64GB Apple iPhone 11 P...
Sprint has the new 64GB iPhone 11 Pro available for $12.50 per month for new customers with an eligible trade-in in of iPhone 7 or newer. That’s down from their standard monthly lease of $41.67. The... Read more
Final week: Apple’s 2019 Back to School Promo...
Purchase a new Mac using Apple’s Education discount, and take up to $400 off MSRP. All teachers, students, and staff of any educational institution with a .edu email address qualify for the discount... Read more
Save $30 on Apple’s AirPods at these reseller...
Amazon is offering discounts on new 2019 Apple AirPods ranging up to $30 off MSRP as part of their Labor Day sale. Shipping is free: – AirPods with Charging Case: $144.95 $15 off MSRP – AirPods with... Read more
Preorder your Apple Watch Series 5 today at A...
Amazon has Apple Watch Series 5 GPS models available for preorder and on sale today for $15 off Apple’s MSRP. Shipping is free and starts on September 20th: – 40mm Apple Watch Series 5 GPS: $384.99 $... Read more

Jobs Board

Systems Analyst ( *Apple* & Android) (Jo...
Systems Analyst ( Apple & Android) (Job ID: 572513) + 11751 Meadowville Ln, Chester, VA 23836, USA + Full-time Company Description Computer Consultants International, Read more
*Apple* Mobile App Developer - eiWorkflow So...
…eiWorkflow Solutions, LLC is currently looking for a consultant for the following role. Apple Mobile App Developer Tasks the role will be performing: ? Mobile App Read more
Essbase Developer - *Apple* - Theorem, LLC...
Job Summary Apple is seeking an experienced, detail-minded Essbase developer to join our worldwide business development and strategy team. If you are someone who Read more
Student Employment (Blue *Apple* Cafe) Spri...
Student Employment (Blue Apple Cafe) Spring 2019 Penn State University Campus/Location: Penn State Brandywine Campus City: Media, PA Date Announced: 12/20/2018 Date Read more
Best Buy *Apple* Computing Master - Best Bu...
**732093BR** **Job Title:** Best Buy Apple Computing Master **Job Category:** Store Associates **Location Number:** 001441-Beaumont-Store **Job Description:** The Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.