I have a function that takes a string as an input and tries to extract the name and surname. It is a combination of NER and regex to try to extract the names present. Is there a better or more efficient way to do it? It struggles mainly with compound names i.e first name + middle name + surname. A user could input John Michael Smith and the code struggles with determining what part belongs to which.
def extract_name(self, text: str):
all_names = []
# Method 1: Named Entity Recognition (NER)
try:
tokens = nltk.word_tokenize(text)
pos_tags = nltk.pos_tag(tokens)
named_entities = nltk.ne_chunk(pos_tags)
for chunk in named_entities:
if hasattr(chunk, 'label') and chunk.label() == 'PERSON':
name = ' '.join([token for token, pos in chunk.leaves()])
all_names.append(name)
except Exception as e:
logger.debug(f"NER extraction failed: {e}")
# Method 2: POS Tagging + Pattern Recognition
# Define action verbs to skip (must match before combining into names)
action_verbs_lower = {
'change', 'remove', 'update', 'delete', 'add', 'create',
'show', 'find', 'get', 'view', 'display', 'list',
'fetch', 'retrieve', 'pull', 'access', 'lookup', 'search', 'locate', 'bring',
'mark', 'set', 'record', 'edit', 'modify', 'alter', 'revise',
'erase', 'drop', 'register', 'enroll'
}
try:
tokens = nltk.word_tokenize(text)
pos_tags = nltk.pos_tag(tokens)
i = 0
while i < len(pos_tags):
if pos_tags[i][1] == 'NNP':
# Skip if this word is an action verb
if pos_tags[i][0].lower() in action_verbs_lower:
i += 1
continue
name_parts = [pos_tags[i][0]]
j = i + 1
while j < len(pos_tags) and pos_tags[j][1] == 'NNP':
# Skip action verbs even in the middle of proper noun sequences
if pos_tags[j][0].lower() not in action_verbs_lower:
name_parts.append(pos_tags[j][0])
j += 1
# Only add if we have actual name parts (not just action verbs)
if len(name_parts) >= 1:
all_names.append(' '.join(name_parts))
i = j
else:
i += 1
except Exception as e:
logger.debug(f"POS extraction failed: {e}")
# Method 3: Regex Pattern Matching
patterns = [
r'\b[A-Z][a-z]+\s+[A-Z][a-z]+\b', # Standard Firstname Lastname
r'\b[A-Z][a-z]+\b', # Single capitalized name
]
for pattern in patterns:
matches = re.findall(pattern, text)
# Filter out action verbs from regex matches
for match in matches:
words = match.split()
# Only add if no word in the match is an action verb
if not any(word.lower() in action_verbs_lower for word in words):
all_names.append(match)
# Filter out common false positives (action verbs and system words)
false_positives = {
# Action verbs
'Change', 'Remove', 'Update', 'Delete', 'Add', 'Create',
'Show', 'Find', 'Get', 'View', 'Display', 'List',
'Fetch', 'Retrieve', 'Pull', 'Access', 'Lookup', 'Search', 'Locate', 'Bring',
'Mark', 'Set', 'Record', 'Edit', 'Modify', 'Alter', 'Revise',
'Erase', 'Drop', 'Register', 'Enroll',
}
filtered_names = []
for name in all_names:
if name not in filtered_names and not any(fp in name for fp in false_positives):
filtered_names.append(name)
# Use voting/frequency to determine most likely name
if filtered_names:
name_counts = Counter(filtered_names)
most_common = name_counts.most_common(1)[0][0]
confidence = name_counts[most_common] / len(all_names) if all_names else 0
return most_common, confidence, filtered_names
return None, 0.0, []