Customizing the parser¶
Every piece of nameparser configuration sorts into one of three places
by asking what it varies with: vocabulary varies by language
(Lexicon), behavior varies by data source or
application (Policy), and presentation varies by
output destination (a rendering argument). See How the parser works for
why the split is drawn there.
Vocabulary: Lexicon¶
Adding and removing words¶
>>> from nameparser import Lexicon, Parser
>>> lex = Lexicon.default().add(titles={"dean"})
>>> Parser(lexicon=lex).parse("Dean Robert Johns").title
'Dean'
add() and remove()
both return a new Lexicon — the one you started
from (here, Lexicon.default()) is never mutated. Every field
accepts a plain set of lowercase words, keyword by field name (titles
above; particles, suffix_words, and the rest work the same
way) — see API reference for the full field list.
Removing works the same way, and drops the word from recognition:
>>> lean = Lexicon.default().remove(titles={"professor"})
>>> Parser(lexicon=lean).parse("Professor Robert Johns").title
''
A few fields mark a subset of another — given_name_titles over
titles, particles_ambiguous over particles,
suffix_acronyms_ambiguous over suffix_acronyms. Entries must
appear in the base field too, so add to both and remove from the marker
first; anything else raises ValueError naming the orphans rather
than leaving a marker entry that no rule will ever consult.
Turning title detection off¶
The subset rule matters most when clearing a field wholesale. Emptying
titles alone orphans every given_name_titles entry, so the two
go together:
>>> d = Lexicon.default()
>>> lean = d.remove(titles=set(d.titles),
... given_name_titles=set(d.given_name_titles))
>>> Parser(lexicon=lean).parse("Hon Solo").given
'Hon'
Emptying the vocabulary does not switch titles off entirely, though. A
leading word that ends in a period is read as a title structurally,
without consulting titles at all — that is what lets unfamiliar
ranks and abbreviations work (see Titles you didn’t configure):
>>> bare = Parser(lexicon=Lexicon.empty())
>>> bare.parse("Professor John Smith").title # vocabulary gone
''
>>> bare.parse("Dr. John Smith").title # structural, stays
'Dr.'
Combining two lexicons¶
Whole lexicons compose with |, which unions field by field — handy
for keeping a shared house vocabulary separate from a per-source one
and combining them at parser construction:
>>> house = Lexicon.empty().add(titles={"dean"})
>>> per_source = Lexicon.empty().add(titles={"provost"})
>>> sorted((house | per_source).titles)
['dean', 'provost']
Fixing the case of a particular word¶
capitalization_exceptions is the one pair-valued field — each entry
maps a lowercase key to its exact-cased replacement ("phd" →
"PhD"), so it isn’t a fit for add()/remove(). Change it with
dataclasses.replace() instead, and pass the result to
capitalized():
>>> import dataclasses
>>> from nameparser import parse
>>> str(parse("jane smith dds").capitalized())
'Jane Smith Dds'
>>> default = Lexicon.default()
>>> lex = dataclasses.replace(
... default,
... capitalization_exceptions=tuple(default.capitalization_exceptions)
... + (("dds", "DDS"),))
>>> str(parse("jane smith dds").capitalized(lex))
'Jane Smith DDS'
Note the tuple(...) + ...: assigning a bare (("dds", "DDS"),)
would replace the default exceptions rather than extend them, so
"phd" and the rest would stop being fixed.
The key is matched against the token with punctuation normalized away,
not against the raw text, so one "phd" entry covers "phd",
"Phd", and "Ph.D." alike — you don’t need a separate key for
each way a source might punctuate it.
Words that are also ordinary names¶
Two fields — suffix_acronyms_ambiguous and particles_ambiguous
— mark entries from suffix_acronyms and particles that are also
plausible as ordinary name words on their own (an acronym suffix that
doubles as a nickname, a particle that doubles as a given name). They
don’t add new vocabulary by themselves; they narrow how an existing
entry is read when it appears alone. If you’re not sure whether a word
you’re adding is one of these ambiguous cases, leave it out — an
unrecognized word usually still parses reasonably, while a wrongly
disambiguated one silently picks the less likely reading. (That
conservatism is why dean above isn’t in the default vocabulary in
the first place: “Dean” is also a common given name, and a default
that swallowed it as a title would misparse “Dean Martin” for
everyone.)
ma is a shipped example. It is both a credential and a common
surname, so it is listed in suffix_acronyms_ambiguous and counts as
a suffix only when written with periods:
>>> parse("Jack Ma").family
'Ma'
>>> parse("Jack M.A.").suffix
'M.A.'
particles_ambiguous is the same idea for surname particles. A
particle listed there may also be a given name, so a name that starts
with one keeps its given name; a particle not listed there is never a
given name, so a name starting with it has no given name at all — the
whole thing is the surname:
>>> parse("van Gogh").given # 'van' may be a given name
'van'
>>> parse("de Mesnil").given # 'de' may not
''
>>> parse("de Mesnil").family
'de Mesnil'
If your data never uses Van as a given name, take it out of the
ambiguous set and leading van becomes part of the surname:
>>> lex = Lexicon.default().remove(particles_ambiguous={"van"})
>>> Parser(lexicon=lex).parse("van Gogh").family
'van Gogh'
Bound given names¶
bound_given_names holds given-name prefixes that attach to the
following word to form one given name — abdul, abu, umm and
their Arabic-script spellings (عبد, أبو, أم) among them:
>>> parse("abdul salam ahmed salem").given
'abdul salam'
Add your own, or empty the set to switch the behavior off entirely:
>>> lex = Lexicon.default().add(bound_given_names={"mohamad"})
>>> Parser(lexicon=lex).parse("mohamad salam ahmed salem").given
'mohamad salam'
>>> d = Lexicon.default()
>>> off = d.remove(bound_given_names=set(d.bound_given_names))
>>> Parser(lexicon=off).parse("abdul salam ahmed salem").given
'abdul'
Behavior: Policy¶
When your data source or application needs different parsing behavior
— a different name order, stricter suffix rules, extra delimiters —
set it on Policy, a small, closed set of fields,
listed below.
Field |
Type |
Effect |
|---|---|---|
|
one of the three exported order constants |
Assigns positional (no-comma) input to given/middle/family in
this order. Use the exported |
|
|
Reorders patronymic-shaped names via opt-in detectors — East
Slavic formal order ( |
|
|
Folds |
|
|
Routes content enclosed by these delimiter pairs to
|
|
|
Routes content enclosed by these delimiter pairs to |
|
|
Adds separators that split suffix groups, e.g. |
|
|
Reads an initial-shaped suffix word after a comma as a suffix:
|
|
|
Excludes emoji from tokenization — they appear in no field or
rendered view, though |
|
|
Excludes bidirectional control characters the same way.
Defaults to |
Family-first name order¶
name_order is the one most likely to matter for non-Western data.
Positional input is assigned in the order you declare, so a
family-first name parses as written instead of needing to be
rearranged afterwards:
>>> from nameparser import Parser, Policy, FAMILY_FIRST, parse
>>> parse("Nguyen Van Minh").family # default GIVEN_FIRST
'Van Minh'
>>> family_first = Parser(policy=Policy(name_order=FAMILY_FIRST))
>>> name = family_first.parse("Nguyen Van Minh")
>>> name.family, name.given
('Nguyen', 'Van Minh')
An explicit comma still wins, on the reasoning that someone who wrote
one meant it — so the same parser reads "Thomas, John" as
family-then-given regardless of the configured order:
>>> family_first.parse("Thomas, John").family
'Thomas'
Nicknames, maiden names, and brackets¶
A delimiter pair routes to exactly one field, and maiden_delimiters
states the more specific intent — so listing a pair there drops it from
the effective nickname_delimiters set automatically, and the
one-liner is the whole recipe:
>>> policy = Policy(maiden_delimiters={("(", ")")})
>>> Parser(policy=policy).parse("Jane (Jones) Smith").maiden
'Jones'
To add a delimiter pair rather than reroute one, build on the
exported default — assigning a bare set replaces the built-in pairs
instead of extending them, the same trap as capitalization_exceptions:
>>> from nameparser import DEFAULT_NICKNAME_DELIMITERS
>>> parse("Benjamin {Ben} Franklin").middle # not a pair by default
'{Ben}'
>>> policy = Policy(
... nickname_delimiters=DEFAULT_NICKNAME_DELIMITERS | {("{", "}")})
>>> Parser(policy=policy).parse("Benjamin {Ben} Franklin").nickname
'Ben'
Suffixes not separated by commas¶
extra_suffix_delimiters handles sources that separate post-nominals
with something other than a comma. The default reading of such a name
is bad enough to be the reason you’d go looking:
>>> name = parse("Jane Smith, RN - CRNA")
>>> name.given, name.family, name.suffix
('RN', 'Jane Smith', 'CRNA')
>>> policy = Policy(extra_suffix_delimiters={" - "})
>>> name = Parser(policy=policy).parse("Jane Smith, RN - CRNA")
>>> name.given, name.family, name.suffix
('Jane', 'Smith', 'RN, CRNA')
Keeping emoji and control characters¶
The strip flags keep characters the parser removes by default. Note what happens to an emoji you keep — it becomes a token like any other, and lands in the middle name:
>>> str(parse("Sam 😊 Smith")) # stripped by default
'Sam Smith'
>>> kept = Parser(policy=Policy(strip_emoji=False)).parse("Sam 😊 Smith")
>>> str(kept), kept.middle
('Sam 😊 Smith', '😊')
strip_bidi=False does the same for invisible bidirectional control
characters, which is occasionally what you want when round-tripping
right-to-left text verbatim.
Presentation: rendering arguments¶
Once a name is parsed, how it’s displayed is a separate decision made
at the point of output, not baked into the parse. Three methods on
ParsedName cover it — see API reference for full
signatures:
render()fills a format spec from the seven role fields.initials()is the same idea narrowed to first letters, with its owndelimiter/separatorarguments.capitalized()returns a new, case-fixedParsedNameinstead of a string. It only touches input that’s already single-case (all lower, all upper) unless you passforce=True— mixed case is left alone by default on the assumption that someone already capitalized it on purpose.
>>> from nameparser import parse
>>> name = parse("Dr. Juan Q. Xavier de la Vega III")
>>> name.render("{family}, {given} {middle}")
'de la Vega, Juan Q. Xavier'
>>> name.initials(spec="{given}{middle}{family}", delimiter="", separator="")
'JQXV'
>>> str(parse("DR. JUAN DE LA VEGA").capitalized())
'Dr. Juan de la Vega'
>>> str(parse("JuAn DE LA vEGA").capitalized())
'JuAn DE LA vEGA'
>>> str(parse("JuAn DE LA vEGA").capitalized(force=True))
'Juan de la Vega'
Looking for v1’s string_format? It’s the render(spec) argument
now — pass your own format string per call instead of setting it once
on a shared config object.
A spec chooses what the output is for. The default is written for
display and does not survive a reparse — it parenthesizes the maiden
name, which reads back as a nickname. When the rendered string will be
parsed again, spell the marker out (née {maiden}) so the field
round-trips; see the round-trip note in the tour.