def parse_name(parts, main_names, common_names, debug=0):
  """ 
    Parse all name(s) from a Backpage ad.
      parts -> The backpage ad's posting_body, separated into substrings
      main_names -> "Regular" names (jessica, gabriel, etc.) that can be trusted as names simply by its existence
      common_names -> Names such as "pleasure" or "sexy" that should only be parsed if surrounded by an "intro"
    main_names and common_names can both be either sets or dictionaries. Current version uses dictionaries.
  """

  lowercase_parts = [re.sub(r'(in|out)call', '', p.lower()) for p in parts]

  start = time.time()

  # Intros to common names
  intros = {
    'pre': ['my name is', 'i am', 'call me', 'call', 'text', 'my names', 'my name', 'known as', 'go by', 'Intro',
            'ask for', 'call for', 'ask', 'this is', 'one and only', 'prevphonespothti', 'called'],
    'post': ['is back', 'is here', 'in town', 'prevphonespothti', 'is ready', 'is available']
  }
  spanish_intros = ['hola soy', 'me llamo', 'mi nombre es', 'yo soy', 'pregunta por', 'encantada de conocerlo', 'hola papi']
  intros['pre'].extend(spanish_intros)

  # Regex intros to common names
  rgx_intros = {
    'pre': [r'\b(?:it\'s)\b', r'\b(?:it s)\b', r'\b(?:its)\b', r'\b(?:soy)\b', r'\b(?:es)\b', r'\b(?:hola)\b', r'\b(?:y?o?ur girl)\b', r'\b(?:i\'m)\b',
            r'\b(?:im)\b', r'\b(?:y?o?ur favorite girl)\b', r'\b(?:y?o?ur most favorite girl)\b', r'\bmy ?fr(?:i|e)(?:i|e)nd\b', r'\bm(?:s|z)\.',
            r'\bmi(?:s{1,2}|z{1,2})'],
    'post': [r'\b(?:here)\b', r'\b(?:(?:i|\')s (?:the|my) name)\b']
  }

  # These words shouldn't follow common name matched from an intro
  false_positives = set(['complexion', 'skin', 'hair', 'locks', 'eyes', 'st', 'ave', 'street', 'avenue', 'blvd', 'boulevard',
                         'highway', 'circle', 'hwy', 'road', 'rd'])
  vowels_with_y = set(list('aeiouy'))

  uniques = set([])

  for p in lowercase_parts:
    part = p.lower()
    part = re.sub(r"(^| )i ?'? ?m ", " Intro ", part).strip()
    part = part.replace('<br>', ' ').replace('&amp;', ' and ')
    part = re.sub(r'\.+', ' ', part)
    part = re.sub(r'x+', 'x', part)
    part = re.sub(r'y+', 'y', part)
    # Retain 'part' to be used for separating comma-separated names
    part = re.sub(r',+', ' ', part)
    part = re.sub(r' +', ' ', part)

    builder = []
    for pt in part.split():
      if len(pt) > 2:
        lastc = pt[len(pt)-1]
        # Convert names that have repeated last letters and the last letters aren't "E" and aren't two consonants following a vowel
        if lastc == pt[len(pt)-2] and not (lastc == 'e' or (pt[len(pt)-3] in vowels_with_y and lastc not in vowels_with_y)):
          builder.append(pt[:len(pt)-1])
        else:
          builder.append(pt)
      else:
        builder.append(pt)
    part = ' '.join(builder)

    # Check if the part is entirely just a common word
    ageless_title = re.sub(r' - \d\d', '', part.lower())
    ageless_title = re.sub(r'\W+', '', ageless_title)
    if ageless_title in common_names or ageless_title in main_names:
      uniques.add(ageless_title)
      continue;

    # Find common names that come immediately before or after a "both-side intro"
    for k in intros:
      for intro in intros[k]:
        if intro in part:
          pts = part.split(intro)
          for i in range(1, len(pts)):
            if k == 'post':
              # Check left side of intro
              ptl = re.sub(r'\W', ' ', pts[i-1])
              ptl = re.sub(r' +', ' ', ptl)
              tokenized = ptl.split()
              if tokenized and tokenized[len(tokenized)-1] and tokenized[len(tokenized)-1] in common_names:
                uniques.add(tokenized[len(tokenized)-1])
                break
            else:
              # Check right side of intro
              ptr = re.sub(r'\W', ' ', pts[i])
              ptr = re.sub(r' +', ' ', ptr)
              tokenized = ptr.split()
              if tokenized and tokenized[0] in common_names:
                if not (len(tokenized) > 1 and tokenized[1] in false_positives or (len(tokenized) > 2 and tokenized[2] in false_positives)):
                  # Next 2 words are not false positives
                  uniques.add(tokenized[0])
                  break

    # Check intros that include regexes
    for k in rgx_intros:
      for intro in rgx_intros[k]:
        matches = list(re.findall(intro, part))
        for match in matches:
          pts = part.split(match)
          for i in range(1, len(pts)):
            if k == 'post':
              # Check left side of intro
              ptl = re.sub(r'\W', ' ', pts[i-1])
              ptl = re.sub(r' +', ' ', ptl)
              tokenized = ptl.split()
              if tokenized and tokenized[len(tokenized)-1] and tokenized[len(tokenized)-1] in common_names:
                uniques.add(tokenized[len(tokenized)-1])
                break
            else:
              # Check right side of intro
              ptr = re.sub(r'\W', ' ', pts[i])
              ptr = re.sub(r' +', ' ', ptr)
              tokenized = ptr.split()
              if tokenized and tokenized[0] in common_names:
                if not (len(tokenized) > 1 and tokenized[1] in false_positives or (len(tokenized) > 2 and tokenized[2] in false_positives)):
                  # Next 2 words are not false positives
                  uniques.add(tokenized[0])
                  break

    # Find regular names
    tokens = list(re.split(r'\W+', part))
    for i in range(len(tokens)):
      if not tokens[i]:
        continue
      curr = tokens[i]
      # Check if current token has an 's' at the end (ex: "brittanys beautiful body")
      if curr not in main_names and curr[len(curr)-1] == 's' and curr[:-1] in main_names:
        curr = curr[:-1]
      if curr in main_names:
        # Check if name is a two-part name
        if i > 0 and (''.join([tokens[i-1], curr]) in main_names or ''.join([tokens[i-1], curr]) in common_names):
          # Prev token was a prefix to current
          uniques.add(' '.join([tokens[i-1], curr]))
          uniques.discard(tokens[i-1])
        elif (i < len(tokens)-1 and tokens[i+1] and (''.join([tokens[i], tokens[i+1]]) in main_names or
              ''.join([tokens[i], tokens[i+1]]) in common_names)):
          # Current token has a suffix
          uniques.add(' '.join([tokens[i], tokens[i+1]]))
        elif (i < len(tokens)-1 and tokens[i+1] and tokens[i+1][len(tokens[i+1])-1] == 's' and (''.join([tokens[i], tokens[i+1][:-1]]) in main_names or
              ''.join([tokens[i], tokens[i+1][:-1]]) in common_names)):
          # Current token has a suffix with plural ending ('s')
          uniques.add(' '.join([tokens[i], tokens[i+1][:-1]]))
        else:
          # Only single-word name
          uniques.add(curr)

    # Find common words that are part of "pairing" phrases, paired with names that we found already
    pairings = set(['and', 'plus', 'with'])
    for i in range(len(tokens)):
      if tokens[i] not in uniques and tokens[i] in common_names:
        if i > 1 and tokens[i-2] in uniques and tokens[i-1] in pairings:
          # ex: "jessica and diamond"
          uniques.add(tokens[i])
        elif i < len(tokens)-2 and tokens[i+2] in uniques and tokens[i+1] in pairings:
          # ex: "diamond and jessica"
          uniques.add(tokens[i])

  # Odd cases
  if ('mary' in uniques or 'jane' in uniques or 'mary jane' in uniques) and re.search(r'mary\W+jane', part):
    uniques.discard('jane')
    uniques.discard('mary')
    uniques.discard('mary jane')
  if 'crystal' in uniques and re.search(r'crystal ?(blue|spa|massage|parlor|city|stone)', part):
    uniques.discard('crystal')
      
  # Remove names that are substrings of larger names
  names_final = set([])
  if isinstance(main_names, set):
    # Name datasets are raw sets of names
    for match in uniques:
      if not any(match in name for name in [v for v in uniques if v != match]) and match:
        names_final.add(match.strip())
  else:
    # Name datasets are misspelled names mapped to properly spelled names
    for match in uniques:
      nosp_match = match.replace(' ', '')
      if not any(nosp_match in name for name in [v for v in uniques if v != nosp_match]) and nosp_match:
        # add the parsed name, not the converted one (ex: don't change "mickey" to "mikey")
        names_final.add(nosp_match)

  if debug == 1:
    print 'parse_name time taken: {} seconds'.format(time.time() - start)
  return list(names_final)
def parse_name ( parts , main_names , common_names , debug = 0 ) : lowercase_parts = [ re . sub ( r'(in|out)call' , '' , p . lower ( ) ) for p in parts ] start = time . time ( ) intros = { 'pre' : [ 'my name is' , 'i am' , 'call me' , 'call' , 'text' , 'my names' , 'my name' , 'known as' , 'go by' , 'Intro' , 'ask for' , 'call for' , 'ask' , 'this is' , 'one and only' , 'prevphonespothti' , 'called' ] , 'post' : [ 'is back' , 'is here' , 'in town' , 'prevphonespothti' , 'is ready' , 'is available' ] } spanish_intros = [ 'hola soy' , 'me llamo' , 'mi nombre es' , 'yo soy' , 'pregunta por' , 'encantada de conocerlo' , 'hola papi' ] intros [ 'pre' ] . extend ( spanish_intros ) rgx_intros = { 'pre' : [ r'\b(?:it\'s)\b' , r'\b(?:it s)\b' , r'\b(?:its)\b' , r'\b(?:soy)\b' , r'\b(?:es)\b' , r'\b(?:hola)\b' , r'\b(?:y?o?ur girl)\b' , r'\b(?:i\'m)\b' , r'\b(?:im)\b' , r'\b(?:y?o?ur favorite girl)\b' , r'\b(?:y?o?ur most favorite girl)\b' , r'\bmy ?fr(?:i|e)(?:i|e)nd\b' , r'\bm(?:s|z)\.' , r'\bmi(?:s{1,2}|z{1,2})' ] , 'post' : [ r'\b(?:here)\b' , r'\b(?:(?:i|\')s (?:the|my) name)\b' ] } false_positives = set ( [ 'complexion' , 'skin' , 'hair' , 'locks' , 'eyes' , 'st' , 'ave' , 'street' , 'avenue' , 'blvd' , 'boulevard' , 'highway' , 'circle' , 'hwy' , 'road' , 'rd' ] ) vowels_with_y = set ( list ( 'aeiouy' ) ) uniques = set ( [ ] ) for p in lowercase_parts : part = p . lower ( ) part = re . sub ( r"(^| )i ?'? ?m " , " Intro " , part ) . strip ( ) part = part . replace ( '<br>' , ' ' ) . replace ( '&amp;' , ' and ' ) part = re . sub ( r'\.+' , ' ' , part ) part = re . sub ( r'x+' , 'x' , part ) part = re . sub ( r'y+' , 'y' , part ) part = re . sub ( r',+' , ' ' , part ) part = re . sub ( r' +' , ' ' , part ) builder = [ ] for pt in part . split ( ) : if len ( pt ) > 2 : lastc = pt [ len ( pt ) - 1 ] if lastc == pt [ len ( pt ) - 2 ] and not ( lastc == 'e' or ( pt [ len ( pt ) - 3 ] in vowels_with_y and lastc not in vowels_with_y ) ) : builder . append ( pt [ : len ( pt ) - 1 ] ) else : builder . append ( pt ) else : builder . append ( pt ) part = ' ' . join ( builder ) ageless_title = re . sub ( r' - \d\d' , '' , part . lower ( ) ) ageless_title = re . sub ( r'\W+' , '' , ageless_title ) if ageless_title in common_names or ageless_title in main_names : uniques . add ( ageless_title ) continue ; for k in intros : for intro in intros [ k ] : if intro in part : pts = part . split ( intro ) for i in range ( 1 , len ( pts ) ) : if k == 'post' : ptl = re . sub ( r'\W' , ' ' , pts [ i - 1 ] ) ptl = re . sub ( r' +' , ' ' , ptl ) tokenized = ptl . split ( ) if tokenized and tokenized [ len ( tokenized ) - 1 ] and tokenized [ len ( tokenized ) - 1 ] in common_names : uniques . add ( tokenized [ len ( tokenized ) - 1 ] ) break else : ptr = re . sub ( r'\W' , ' ' , pts [ i ] ) ptr = re . sub ( r' +' , ' ' , ptr ) tokenized = ptr . split ( ) if tokenized and tokenized [ 0 ] in common_names : if not ( len ( tokenized ) > 1 and tokenized [ 1 ] in false_positives or ( len ( tokenized ) > 2 and tokenized [ 2 ] in false_positives ) ) : uniques . add ( tokenized [ 0 ] ) break for k in rgx_intros : for intro in rgx_intros [ k ] : matches = list ( re . findall ( intro , part ) ) for match in matches : pts = part . split ( match ) for i in range ( 1 , len ( pts ) ) : if k == 'post' : ptl = re . sub ( r'\W' , ' ' , pts [ i - 1 ] ) ptl = re . sub ( r' +' , ' ' , ptl ) tokenized = ptl . split ( ) if tokenized and tokenized [ len ( tokenized ) - 1 ] and tokenized [ len ( tokenized ) - 1 ] in common_names : uniques . add ( tokenized [ len ( tokenized ) - 1 ] ) break else : ptr = re . sub ( r'\W' , ' ' , pts [ i ] ) ptr = re . sub ( r' +' , ' ' , ptr ) tokenized = ptr . split ( ) if tokenized and tokenized [ 0 ] in common_names : if not ( len ( tokenized ) > 1 and tokenized [ 1 ] in false_positives or ( len ( tokenized ) > 2 and tokenized [ 2 ] in false_positives ) ) : uniques . add ( tokenized [ 0 ] ) break tokens = list ( re . split ( r'\W+' , part ) ) for i in range ( len ( tokens ) ) : if not tokens [ i ] : continue curr = tokens [ i ] if curr not in main_names and curr [ len ( curr ) - 1 ] == 's' and curr [ : - 1 ] in main_names : curr = curr [ : - 1 ] if curr in main_names : if i > 0 and ( '' . join ( [ tokens [ i - 1 ] , curr ] ) in main_names or '' . join ( [ tokens [ i - 1 ] , curr ] ) in common_names ) : uniques . add ( ' ' . join ( [ tokens [ i - 1 ] , curr ] ) ) uniques . discard ( tokens [ i - 1 ] ) elif ( i < len ( tokens ) - 1 and tokens [ i + 1 ] and ( '' . join ( [ tokens [ i ] , tokens [ i + 1 ] ] ) in main_names or '' . join ( [ tokens [ i ] , tokens [ i + 1 ] ] ) in common_names ) ) : uniques . add ( ' ' . join ( [ tokens [ i ] , tokens [ i + 1 ] ] ) ) elif ( i < len ( tokens ) - 1 and tokens [ i + 1 ] and tokens [ i + 1 ] [ len ( tokens [ i + 1 ] ) - 1 ] == 's' and ( '' . join ( [ tokens [ i ] , tokens [ i + 1 ] [ : - 1 ] ] ) in main_names or '' . join ( [ tokens [ i ] , tokens [ i + 1 ] [ : - 1 ] ] ) in common_names ) ) : uniques . add ( ' ' . join ( [ tokens [ i ] , tokens [ i + 1 ] [ : - 1 ] ] ) ) else : uniques . add ( curr ) pairings = set ( [ 'and' , 'plus' , 'with' ] ) for i in range ( len ( tokens ) ) : if tokens [ i ] not in uniques and tokens [ i ] in common_names : if i > 1 and tokens [ i - 2 ] in uniques and tokens [ i - 1 ] in pairings : uniques . add ( tokens [ i ] ) elif i < len ( tokens ) - 2 and tokens [ i + 2 ] in uniques and tokens [ i + 1 ] in pairings : uniques . add ( tokens [ i ] ) if ( 'mary' in uniques or 'jane' in uniques or 'mary jane' in uniques ) and re . search ( r'mary\W+jane' , part ) : uniques . discard ( 'jane' ) uniques . discard ( 'mary' ) uniques . discard ( 'mary jane' ) if 'crystal' in uniques and re . search ( r'crystal ?(blue|spa|massage|parlor|city|stone)' , part ) : uniques . discard ( 'crystal' ) names_final = set ( [ ] ) if isinstance ( main_names , set ) : for match in uniques : if not any ( match in name for name in [ v for v in uniques if v != match ] ) and match : names_final . add ( match . strip ( ) ) else : for match in uniques : nosp_match = match . replace ( ' ' , '' ) if not any ( nosp_match in name for name in [ v for v in uniques if v != nosp_match ] ) and nosp_match : names_final . add ( nosp_match ) if debug == 1 : print 'parse_name time taken: {} seconds' . format ( time . time ( ) - start ) return list ( names_final )
Parse all name(s) from a Backpage ad.
      parts -> The backpage ad's posting_body, separated into substrings
      main_names -> "Regular" names (jessica, gabriel, etc.) that can be trusted as names simply by its existence
      common_names -> Names such as "pleasure" or "sexy" that should only be parsed if surrounded by an "intro"
    main_names and common_names can both be either sets or dictionaries. Current version uses dictionaries.