views:

235

answers:

6

I'd like to be able to parse out the city, state or zip from a string in python. So, if I entered

Boulder, Co
80303
Boulder, Colorado
Boulder, Co 80303
...

any variation of these it would return the city, state or zip.

This is all going to be user inputted data and inputted in one text field.

+1  A: 

I would use ([^\d]+)?(\d{5})? as my regular expression, and use match so it only matches the beginning of the string. This way it won't fail on bad input and will make its best guess as to what was intended. Then you can split the first capture group on ",".

danben
That works for the zip, (won't \d{5} suffice?) but you'll need another pattern for the city/state. How about [a-zA-Z ]+,\s*[a-zA-Z]{5}?
Sam Post
No, you misunderstood the regex. The first capture group will match all non-digit characters.
danben
Ah you're right I see it now , your pattern is pretty neat now that I think about it.
Sam Post
any chance you could give an example on how to use this. I am not good with regex.
Joe
+3  A: 

Just ask for their zip only, then give a (short) list of applicable cities by using a geocode database. That way you get nice clean 5-digit input, they save time, and you all go home happy.

If you already have the data, look just for the zip, find a list of possible cities (there will only be one applicable state) and match for the city name after making everything lowercase.

Peter
This may not be a GUI app -- it could be a batch process also.
Jim Garrison
@Jim hence the second paragraph.
Peter
I don't want to just ask for the zip. Not everyone knows the zip for Chicago, IL or LA, CA but they might want to search those places.
Joe
ah, that's completely reasonable - a different scenario to the one I guessed from your question.
Peter
A: 

This code seems to do what you want:

text = """
Boulder, Co
80303
Boulder, Colorado
Boulder, Co 80303
"""

lines = text.splitlines()

ABBREV = dict(co="Colorado", ca="California")
STATES = ABBREV.values()

def parse_addr(line):
    addr = {}
    # normalize commas
    parts = line.replace(",", " ").split()
    for part in parts:
        if part.capitalize() in STATES:
            addr["state"] = part
        elif part.lower() in ABBREV:
            addr["state"] = ABBREV[part.lower()]
        else:
            try:
                zip = int(part)
                addr["zip"] = part
            except ValueError:
                addr["city"] = part
    return addr

for line in lines:
    print line, parse_addr(line)

Output:

Boulder, Co {'city': 'Boulder', 'state': 'Colorado'}
80303 {'zip': '80303'}
Boulder, Colorado {'city': 'Boulder', 'state': 'Colorado'}
Boulder, Co 80303 {'city': 'Boulder', 'state': 'Colorado', 'zip': '80303'}

Handling of "South Dakota" and other two-word states/cities left as an exercise for the reader :)

As the other posters suggested, you can get smart and use the zip code to narrow in on the city/state as well.

Ryan Ginstrom
There are a lot of ways to break this. If the city name has one or more spaces in it, for instance, this code will ignore all but the last part. Also, if the zip code is malformed such that it can't be represented as an int, it will overwirte the city.
danben
@danben -- Of course it will break easily. A truly robust address parser is a full program in itself -- and that's just for US addresses!
Ryan Ginstrom
Every US address has a zip+4 address as well, all of which would fail in this snippet. This code sample really *only* works on the specific sample data given. It's not a good generalized solution.
Travis Bradshaw
Like I said, a generalized solution is an entire application in and of itself. It's not a code snippet that you can post on SO. You can do it the quick and dirty way to parse semi-dirty data, or you can use one of the many apps out there to do it the right way. A massive regular expression is the path to madness in this case.
Ryan Ginstrom
+3  A: 

You could use a geocoding web service or something similar. For example, on the Yahoo geocoding API page, it shows how you can specify the address in a number of ways:

This free field lets users enter any of the following:
    city, state
    city, state, zip
    zip
    street, city, state
    street, city, state, zip
    street, zip

and the XML results provide the parsed address, for example with this test URL specified on the page.

ars
A: 
  1. easy_install ngram

  2. build file with all the city and state names one per line, place in citystate.data Redwood City, CA Redwood, VA etc

  3. Experiment ( the .2 threshold is a little lax )


import string
import ngram
cityStateParser = ngram.NGram(
  items = (line.strip() for line in open('citystate.data')) ,
  N=3, iconv=string.lower, qconv=string.lower,  threshold=.2
)

Example:

cityStateParser.search('redwood')
[('Redwood VA', 0.5),
('Redwood NY', 0.5),
('Redwood MS', 0.5),
('Redwood City CA', 0.36842105263157893),
...
]

Notes: Because these are NGrams you might get overmatch when the state is part of a ngram in the city i.e. search for "washington" would yield Washington IN with a bette score than "Washington OK"

You might also want read Using Superimposed Coding Of N-Gram Lists For Efficient Inexact Matching (PDF Download)

Jason Culverhouse
A: 

Expect that some users might type a ZIP+4.

The pitfalls of asking for city/state/ZIP without an address are described at http://semaphorecorp.com/cgi/zip5.html

joe snyder