5
\$\begingroup\$

I'm trying to extract room types and sizes from a descriptive text utilizing the following scripts as an exercise. Any tips on being more pythonic would be amazing!

The script is based off the help I received in this question.

The idea is that I find anything in the following sense: 16m2, 3.90 x 3,00m, 640x390 and that I at least attempt to return what type of room it is referring to (I'm still working on a more complete list of roomtypes).

def extract_rooms(description, printresults=False):
    regexes = []
    regexes.append(r'(\d{1,4}.{0,2}m[.]?[2|xb2])')                                 # Finds 16m2
    regexes.append(r'(\d{1,4}[.,]?\d{0,3}[ ]?[x]?[ ]\d{1,4}[.,]?\d{0,3}[ ?]m)')     # Finds 3.90 x 3,00m & 3.90 x 3,00
    regexes.append(r'(\d{1,4}[ ]?x[ ]?\d{1,4})')                                     # Finds 640x390
    # Split description into sentences
    sentences = re.split('\. |! ', description)  # Extract sentences
    roomtypes =['woonkamer',
                'slaapkamer',
                'slaapkamers',
                'woning',
                'Ouderslaapkamer']
    rooms = {}
    i = 1
    for sentence in sentences:  # Try regex in the order described above
        if printresults:
            print('Working on sentence: ' + sentence)
        for regex in regexes:
            match = re.findall(regex, sentence)
            if len(match) > 0:
                for result in match:
                    beforematch = re.split(result, sentence)[0]
                    roomtype = "Unknown"
                    for word in beforematch.split(' ')[::-1]:  # Search in reverse order of words
                        if word in roomtypes:
                            roomtype = word
                            break
                    rooms[roomtype + str(i)] = result
                    i += 1
                    if printresults:
                        print(roomtype + ' of ' + result + ' found in sentence: ' + sentence)
                    sentence = re.sub(result, "", sentence)  # Remove the match from the sentence
    return rooms

if __name__ == "__main__":
    description1 = r"[u'Schrabber  11 wordt short-stay  verhuurd  voor minimaal  3  maanden. De  woning is  voorzien  van  laminaat,  raambekleding en  deels voorzien van  verlichting en  is beschikbaar per  01  januari  2015.', <br/>, <br/>, u'Schrabber 11 te Den  Haag - Ypenburg', <br/>, <br/>, u'Kenmerken:', <br/>, u'- Bouwjaar is 2005;', <br/>, u'- Keuken is standaard voorzien van kookplaat, oven, afzuigkap;', <br/>, u'- Badkamer, voorzien van ligbad en douche;', <br/>, u'- Mogelijkheid tot het huren van een parkeerplaats;', <br/>, u'- Verwarming en warm water door middel van stadsverwarming;', <br/>, <br/>, u'Indeling:', <br/>, u'Een ruim 4-kamer appartement op de eerste etage. De woning heeft een oppervlakte van 133 m2 (!) met een ruime woonkamer. De woonkamer is zeer riant te noemen met circa 45m2! Verder voorziet het appartement in drie ruime slaapkamers (16m2, 12m2 & 12m2 ). De keuken voorziet in een oven, kookplaat, afzuigkap, koelkast, vriezer en vaatwasser. De badkamer heeft een separate douche, ligbad en tweede toilet. ', <br/>, <br/>, u'In het centrum van Ypenburg worden diverse typen appartementen verhuurd en verkocht inclusief berging en een mogelijke parkeerplaats (niet verplicht). De appartementen zijn gelegen in moderne en kleinschalige complexen met fraaie binnentuinen en uiterst centraal met allerlei voorzieningen in de directe omgeving zoals winkels, het openbaar vervoer (bus, tram en NS-station Ypenburg) en recreatieve voorzieningen als het Balijbos, Dobbeplas en Delftse Hout. Middels de nabij gelegen uitvalswegen A-4, A-12 en A-13 zijn Delft, Rijswijk, Den Haag Centrum, Scheveningen, Nootdorp, Leidschendam en Voorburg zijn allen zeer snel te bereiken. ', <br/>, <br/>, u'De aangeboden woningen vari\xebren in grootte van 69m2 tot 138m2 (2, 3, 4 en 5 \u2013 kamertypen).', <br/>, <br/>, u'Indeling algemeen:', <br/>, u'Afgesloten centrale entree met bellentableau, intercom en brievenbussen. Met lift of trap naar de betreffende woonverdieping cq. afgesloten parkeerkelder met bergingen en parkeerplaatsen.', <br/>, <br/>, u'De voorwaarden die wij stellen aan u als nieuwe huurder van een woning in ons beheer kunt u op onze website terugvinden. Bij het direct beschikbare aanbod staat boven elke woning een kopje \u201chuurvoorwaarden\u201d. Verder is in het hoofdmenu bovenaan de site onder het kopje \u201cinformatie\u201d meer te vinden over de \u201chuurvoorwaarden\u201d.', <br/>, <br/>, u'Dit object is gesitueerd in Ypenburg centrum'];"
    description2 = r"[u'Goed onderhouden, royaal  3-kamer TOPappartement  met 2  balkons en levendig uitzicht. Dit  appartement is  gelegen  op de  3e  woonlaag (dus geen bovenburen) en  ligt op  loopafstand van winkelcentrum De  Luifelbaan met alle grote winkelketens (o.a. Albert Heijn XL) en op slechts 15 autominuten van het strand van Katwijk. Rondom voorzien van kunststof kozijnen met dubbel glas. Er wordt een nieuwe c.v.-installatie aangelegd door verhuurder. Flinke fietsenberging in de onderbouw.', <br/>, <br/>, u'Indeling:', <br/>, u'Hoofdentree via beveiligde centrale hal met intercominstallatie, brievenbussen- en bellentableau. ', <br/>, <br/>, u'Entree appartement: Ruime hal met intercom, nieuwe meterkast, toilet en vaste kastruimte. Royale en lichte doorzon-woonkamer van 31 m\xb2 met toegang tot het zonnige balkon op het zuidwesten. In lichte kleurstellingen uitgevoerde nieuw keukenblok (2014) zonder apparatuur, vaste kast en toegang tot het 2e balkon aan de achterzijde.  Badkamer met wastafel, douche en aansluiting wasapparatuur. ', <br/>, <br/>, u'Ouderslaapkamer (3.90 x 3,00 m) met toegang tot het balkon aan de voorzijde. De tweede slaapkamer (4 x 2,60) bevindt zich aan de achterzijde.', <br/>, <br/>, u'Bijzonderheden:', <br/>, <br/>, u'- De huurprijs is exclusief gas, water en elektra;', <br/>, u'- Huurder is aan verhuurder een waarborgsom van twee maanden verschuldigd als garantiestelling voor de juiste ', <br/>, u'  nakoming van zijn verplichtingen uit hoofde van de huurovereenkomst;', <br/>, u'- De woning dient voor minimaal 1 jaar te worden gehuurd;', <br/>, u'- Huurder dient een bruto jaarinkomen te kunnen overleggen van minimaal 4 x de jaarhuur (incl. servicekosten);', <br/>, u'- Servicekosten bedragen \u20ac 27,-- per maand;', <br/>, u'- Complete nieuwe C.V.-INSTALLATIE met HR combiketel en nieuwe radiatoren wordt aangelegd door verhuurder;', <br/>, u'- Gunstige ligging ten opzicht van scholen, bushalte en de uitvalswegen richting de A4 / A44;', <br/>, u'- Aanvaarding op korte termijn mogelijk.']"
    print extract_rooms(description1, True)
    print extract_rooms(description2, True)
\$\endgroup\$
6
\$\begingroup\$

This is not Pythonic:

regexes = []
regexes.append(r'(\d{1,4}.{0,2}m[.]?[2|xb2])')                                 # Finds 16m2
regexes.append(r'(\d{1,4}[.,]?\d{0,3}[ ]?[x]?[ ]\d{1,4}[.,]?\d{0,3}[ ?]m)')     # Finds 3.90 x 3,00m & 3.90 x 3,00
regexes.append(r'(\d{1,4}[ ]?x[ ]?\d{1,4})')                                     # Finds 640x390

It would be better to use a list literal, like this:

regexes = [
    r'(\d{1,4}.{0,2}m[.]?[2|xb2])',
    r'(\d{1,4}[.,]?\d{0,3}[ ]?[x]?[ ]\d{1,4}[.,]?\d{0,3}[ ?]m)',
    r'(\d{1,4}[ ]?x[ ]?\d{1,4})',
]

This is also not Pythonic:

        match = re.findall(regex, sentence)
        if len(match) > 0:
            for result in match:
                # ...

Since re.findall will always return a list, you can simplify the above:

        for result in re.findall(regex, sentence):
            # ...

If there are no matches, then there will be no iterations, which is exactly the same effect as in your original code, but shorter, simpler, Pythonic.


Although the code is fairly nicely formatted, you don't follow strictly PEP8. For example:

roomtypes =['woonkamer',

Should be:

# put space around '='
roomtypes = ['woonkamer',

The raw string notation r"" is for regular expressions, but these are simple strings:

description1 = r"[u'Schrabber....."
description2 = r"[u'Goed onde....."

So you should use simply "..." instead of r'...'


It's better to move code out of if __name__ == ... into a method, otherwise the variables used there in the global name space may be shadowed inside the methods. Make it a habit to write this way:

def main():
    description1 = r"[u'Schrabber  ..."
    description2 = r"[u'Goed onderh..."
    print(extract_rooms(description1, True))
    print(extract_rooms(description2, True))

if __name__ == "__main__":
    main()
\$\endgroup\$
  • \$\begingroup\$ I wanted to put the regexes on separate rows, but somehow did not think of this one. Thanks! \$\endgroup\$ – MattV Dec 16 '14 at 23:22
3
\$\begingroup\$

You're doing three things here: splitting up the listing into sentences, parsing the listing, and recording the results. You can clean this up by separating these out into separate functions functions or classes. This keeps the code less nested and easier to follow.

To keep things simpler I'm using regexes for the room names as well as for the dimensions. This will actually be a bit slower - "in" is really fast - but it is more flexible (for example, if you have to support plural endings or something else besides string lookups). I'm compiling the regexes using re.compile to make them faster and re.I to make them case-insenstive.

Lastly, I'm returning the results as a list-of-dictionaries, where each dictionary includes the room, dimensions and the sentence from which they came. This lets you keep the printout display separate from the parsing. In practice I would probably use collections.namedtuple instead of a dictionary but I didn't want to get too far off topic

import re

DIMENSIONS = [
    re.compile(r'(\d{1,4}.{0,2}m[.]?[2|xb2])', re.I),
    re.compile(r'(\d{1,4}[.,]?\d{0,3}[ ]?[x]?[ ]\d{1,4}[.,]?\d{0,3}[ ?]m)', re.I),
    re.compile(r'(\d{1,4}[ ]?x[ ]?\d{1,4})', re.I)
]

ROOMS = [
    re.compile('woonkamer', re.I),
    re.compile('slaapkamer', re.I),
    re.compile('slaapkamers', re.I),
    re.compile('woning', re.I),
    re.compile('Ouderslaapkamer', re.I)
]


def sentences(description):
    desc = description.replace("<br/>","")
    desc = desc.replace("\u201", "")
    return re.split('\. |! ', desc)


def parse(sentence):
    def get_matches(regex):
        for item in regex.findall(sentence):
            return item
    dims = None
    for regex in DIMENSIONS:
        dims = get_matches(regex) or dims
    if not dims: 
        return

    desc = None
    for room in ROOMS:
        desc = get_matches(room) or desc
    if desc:
        return {'room': desc, 'dimensions': dims, 'sentence': sentence } 

def listing(text):
    results =  [ parse(s) for s in sentences(text)]
    return [i for i in results if i]


print listing(description1)
\$\endgroup\$
  • \$\begingroup\$ Moving it into separate functions is definitely a nice touch improving readability. Wasn't aware of re.I, and I agree on the regex instead of comparing strings. How would namedtuples be better than a dictionary in this usecase? Thanks for your response, very helpful! \$\endgroup\$ – MattV Dec 17 '14 at 0:31
  • \$\begingroup\$ two things: first, namedtuples are immutable (so you can't accidentally change the contents) and second, namedtuples are easier to type: x.name is easier than x['name'] :) \$\endgroup\$ – theodox Dec 17 '14 at 0:41
1
\$\begingroup\$

Going from theodox's code, I see that the regexes could be vastly improved. Here's how I would do it:

def build_regex(regex):
    regex = regex.format(
        integer=r'\d{1,4}',
        decimal=r'\d{1,4}(?:.,\d{0,3})'
    )

    return re.compile(regex, re.IGNORECASE)


DIMENSIONS = [
    build_regex(r'{integer} ?x ?{integer}'),
    build_regex(r'{decimal} ?x? {decimal}[ ?]m'),
    build_regex(r'{integer}.{{0,2}}m\.?[|bx2]')
]

this makes it obvious that something is amiss. Do you really mean ?x? and not ?x ?? What about [|bx2]? Should this not be (xb)?2? What about [ ?]. Do you mean ??

You can also simplify the parse further by noting that the loops correspond to the next function over iterators:

def parse(sentence):
    def get_match(regex):
        return regex.search(sentence)

    dims_matches = map(get_match, DIMENSIONS)
    desc_matches = map(get_match, ROOMS)

    try:
        dims = next(filter(bool, dims_matches)).group()
        desc = next(filter(bool, desc_matches)).group()
    except StopIteration:
        return

    return {'room': desc, 'dimensions': dims, 'sentence': sentence} 
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.