使用机器学习或NLP处理格式错误的文本数据
我正在尝试从一些包含个人信息的大文本文件中提取数据。不过,问题是我无法控制数据的格式。
这些数据通常是这样的格式:
姓氏, 名字 中间名(可能还有个昵称)这段文字为什么在这里2012年1月25日
名字 姓氏 2001 一些我不关心的文字
姓氏, 名字 blah blah ... 2012年1月25日 ...
目前,我使用了一个超级复杂的正则表达式,它可以拆分所有kindaCamelcase的单词,所有带有月份名称的单词,以及很多特殊的名字情况。然后我又用更多的正则表达式来提取名字和日期的各种组合。
这样做似乎不是最优的选择。
有没有适合Python的机器学习库,可以处理这种有点结构但格式不太规范的数据呢?
我试过NLTK,但它无法处理我这些脏数据。我现在在尝试Orange,我喜欢它的面向对象编程风格,但我不确定这样做是否浪费时间。
理想情况下,我想做一些这样的事情来训练一个解析器(使用多个输入/输出对):
training_data = (
'LASTNAME, Firstname Middlename (Maybe a Nickname)FooBarJanuary 25, 2012',
['LASTNAME', 'Firstname', 'Middlename', 'Maybe a Nickname', 'January 25, 2012']
)
像这样做是否可能,还是我对机器学习的期望太高了?任何建议都非常感谢,因为我想更多地了解这个话题。
相关问题:
5 个回答
我遇到过类似的问题,主要是因为从Microsoft Office 2010导出数据时,结果是两个相邻的单词在某种规律下连在了一起。这个领域涉及到形态学操作,就像拼写检查器一样。你可以选择使用机器学习的方法,或者像我一样创建一个启发式的解决方案。
一个简单的解决办法是认为新形成的单词是一些专有名词的组合(首字母大写)。
第二个额外的解决方案是准备一个有效单词的字典,然后尝试一些分割位置,看看能否生成两个(或者至少一个)有效的单词。另一个问题可能是其中一个单词是专有名词,而根据定义,它在之前的字典中是不存在的。也许我们可以使用单词长度的统计数据来判断一个单词是错误形成的,还是实际上是一个合法的单词。
在我的情况下,这是一部分对大量文本进行手动校正的工作(需要人工验证),但唯一可以自动化的就是选择可能错误的单词以及给出它的修正建议。
你遇到的问题的一部分是:“所有以月份名称结尾的单词。”
如果你的字符串末尾有一个日期,格式是 月份名称 1或2位数字的日期, yyyy,那么你应该先用正则表达式把这个日期去掉。这样,剩下的字符串就会简单多了。
注意:否则你可能会遇到一些问题,比如有些名字也恰好是月份的名字,比如四月、五月、六月和八月。此外,三月也是一个姓,可以用作“中间名”,比如 SMITH, John March。
你使用的“最后/第一个/中间”这种说法挺“有趣”的。如果你的数据中包含一些非英语国家的名字,可能会出现问题,比如:
Mao Zedong 也叫 Mao Ze Dong 或 Mao Tse Tung
Sima Qian 也叫 Ssu-ma Ch'ien
Saddam Hussein Abd al-Majid al-Tikriti
Noda Yoshihiko
Kossuth Lajos
José Luis Rodríguez Zapatero
Pedro Manuel Mamede Passos Coelho
Sukarno
我最后实现了一系列有点复杂的正则表达式,这些表达式涵盖了所有可能的使用情况。通过文本形式的“过滤器”,在解析器加载时用合适的正则表达式替换了这些过滤器。
如果有人对代码感兴趣,我会把它编辑到这个回答里。
基本上,我使用了这些东西。为了从我的“语言”构建正则表达式,我需要创建一些替换类:
class Replacer(object):
def __call__(self, match):
group = match.group(0)
if group[1:].lower().endswith('_nm'):
return '(?:' + Matcher(group).regex[1:]
else:
return '(?P<' + group[1:] + '>' + Matcher(group).regex[1:]
然后,我创建了一个通用的 Matcher 类,这个类根据模式名称构建特定模式的正则表达式:
class Matcher(object):
name_component = r"([A-Z][A-Za-z|'|\-]+|[A-Z][a-z]{2,})"
name_component_upper = r"([A-Z][A-Z|'|\-]+|[A-Z]{2,})"
year = r'(1[89][0-9]{2}|20[0-9]{2})'
year_upper = year
age = r'([1-9][0-9]|1[01][0-9])'
age_upper = age
ordinal = r'([1-9][0-9]|1[01][0-9])\s*(?:th|rd|nd|st|TH|RD|ND|ST)'
ordinal_upper = ordinal
date = r'((?:{0})\.? [0-9]{{1,2}}(?:th|rd|nd|st|TH|RD|ND|ST)?,? \d{{2,4}}|[0-9]{{1,2}} (?:{0}),? \d{{2,4}}|[0-9]{{1,2}}[\-/\.][0-9]{{1,2}}[\-/\.][0-9]{{2,4}})'.format('|'.join(months + months_short) + '|' + '|'.join(months + months_short).upper())
date_upper = date
matchers = [
'name_component',
'year',
'age',
'ordinal',
'date',
]
def __init__(self, match=''):
capitalized = '_upper' if match.isupper() else ''
match = match.lower()[1:]
if match.endswith('_instant'):
match = match[:-8]
if match in self.matchers:
self.regex = getattr(self, match + capitalized)
elif len(match) == 1:
elif 'year' in match:
self.regex = getattr(self, 'year')
else:
self.regex = getattr(self, 'name_component' + capitalized)
最后,还有一个通用的 Pattern 对象:
class Pattern(object):
def __init__(self, text='', escape=None):
self.text = text
self.matchers = []
escape = not self.text.startswith('!') if escape is None else False
if escape:
self.regex = re.sub(r'([\[\].?+\-()\^\\])', r'\\\1', self.text)
else:
self.regex = self.text[1:]
self.size = len(re.findall(r'(\$[A-Za-z0-9\-_]+)', self.regex))
self.regex = re.sub(r'(\$[A-Za-z0-9\-_]+)', Replacer(), self.regex)
self.regex = re.sub(r'\s+', r'\\s+', self.regex)
def search(self, text):
return re.search(self.regex, text)
def findall(self, text, max_depth=1.0):
results = []
length = float(len(text))
for result in re.finditer(self.regex, text):
if result.start() / length < max_depth:
results.extend(result.groups())
return results
def match(self, text):
result = map(lambda x: (x.groupdict(), x.start()), re.finditer(self.regex, text))
if result:
return result
else:
return []
虽然过程变得相当复杂,但它确实有效。我不会发布所有的源代码,但这些应该能帮助某些人入门。最终,它将像这样的文件:
$LASTNAME, $FirstName $I. said on $date
转换成一个带有命名捕获组的编译正则表达式。