手工打造文本数据清洗工具
手工打造文本数据清洗工具作者 白宁超 2019年4月30日09:43:59
1 新闻语料的准备语料可以理解为语言材料,包括口语材料和书面材料。语料的定义较为广泛,其来源可能是教材、报纸、综合刊物、新闻材料、图书等,语料所涉及的学科门类也较为复杂。 本章所介绍的新闻语料,狭义上来讲,就是为实验或工程应用所准备的相对规范的数据集。其目的是通过有监督学习方法,训练算法模型以达到工程应用。本书新闻语料来源于复旦大学新闻语料摘选,原始语料达到近千万条,为了更加适应教学,作者选择平衡语料30余万条,具体语料信息如下: 为什么我们不是自己构建语料,而采用开源数据集?前面我们介绍的数据采集和爬虫技术,可以完成对某特定领域数据的爬取和整理。然后结合第四章数据预处理技术,最终整理成相对规范的文本信息,理论上讲完全是可行的。但是,实际情况而言,语料库构建需要遵循以下几个原则:
2 高效读取文件2.1 递归遍历读取新闻递归在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归的作用可以完全取代循环,因此有很多在函数编程语言中用递归来取代循环的例子。计算机科学家尼克劳斯·维尔特如此描述递归:递归的强大之处在于它允许用户用有限的语句描述无限的对象。因此,在计算机科学中,递归可以被用来描述无限步的运算,尽管描述运算的程序是有限的。 事实上,递归算法核心思想就是分而治之。在一些适用情形下可以让人惊讶不已,但也不是任何场合都能发挥的作用的,诸如遍历读取大量文件的时候。往往科学的实验揭露事实的真相,我们开启这样一个实验即递归算法遍历读取CSCMNews文件夹下的30余万新闻语料,每读取5000条信息再屏幕上打印一条读取完成的信息。代码实现如下: # 遍历CSCMNews目录文件 def TraversalDir(rootDir): # 返回指定目录包含的文件或文件夹的名字的列表 for i,lists in enumerate(os.listdir(rootDir)): # 待处理文件夹名字集合 path = os.path.join(rootDir,lists) # 核心算法,对文件具体操作 if os.path.isfile(path): if i%5000 == 0: print(‘{t} *** {i} t docs has been read‘.format(i=i,t=time.strftime(‘%Y-%m-%d %H:%M:%S‘,time.localtime()))) # 递归遍历文件目录 if os.path.isdir(path): TraversalDir(path) 我们运行main主函数: if __name__ == ‘__main__‘: t1=time.time() # 根目录文件路径 rootDir = r"../Corpus/CSCMNews" TraversalDir(rootDir) ? t2=time.time() print(‘totally cost %.2f‘ % (t2-t1)+‘ s‘) 经过实验证明,完成约30万新闻文本读取花费65.28秒(这里还没有对文件执行任何操作),随着语料数量增加,执行速度也将会越来越慢。递归遍历读取新闻语料结果如图所示: 2.2 高效遍历读取新闻前面介绍的yield生成器大大提升了执行效率,对于读取文件操作,我们只需要构建一个类文件就可以,其中loadFiles类负责加载目录文件,而loadFolders类负责加载文件夹下的子文件,实现如下: ? # 加载目录文件 class loadFiles(object): def __init__(self,par_path): self.par_path = par_path def __iter__(self): folders = loadFolders(self.par_path) # level directory for folder in folders: catg = folder.split(os.sep)[-1] #secondary directory for file in os.listdir(folder): yield catg,file ? # 加载目录下的子文件 class loadFolders(object): # 迭代器 def __init__(self,par_path): self.par_path = par_path def __iter__(self): for file in os.listdir(self.par_path): file_abspath = os.path.join(self.par_path,file) # if file is a folder if os.path.isdir(file_abspath): yield file_abspath 上述代码最大的变化就是return关键字改为yield,如此便成了生成器函数了,我们通过yield生成器函数遍历30余万新闻文件,每完成5000个文件读取变打印一条信息,调用main函数如下: if __name__==‘__main__‘: start = time.time() filepath = os.path.abspath(r‘../Corpus/CSCMNews‘) files = loadFiles(filepath) for i,msg in enumerate(files): if i%5000 == 0: print(‘{t} *** {i} t docs has been Read‘.format(i=i,time.localtime()))) end = time.time() print(‘total spent times:%.2f‘ % (end-start)+ ‘ s‘) 执行以上函数,运行结果如下图所示: 递归遍历读取30万新闻数据耗时65.28秒,而yield生成器仅仅耗时0.71秒。前者是后者的87倍多,随着对文件操作和数据量的增加,这种区别可以达到指数级。本节封装的yield生成器类文件,读者可以保留下来,复用到其他文件操作之中。 3 正则表达式提取文本信息正则表达式(代码中常简写regex、regexp或RE),是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。许多程序设计语言都支持利用正则表达式进行字符串操作。正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。regular.py包含以下正则案例实现。 ?
line = ‘this is a dome about this scrapy2.0‘ regex_str=‘^t.*0$‘ match_obj = re.match(regex_str,line) if match_obj: print(match_obj.group(1))
line = ‘this is a dome about this scrapy2.0‘ regex_str=".*?(t.*?t).*" # 提取tt之间的子串 match_obj = re.match(regex_str,line) if match_obj: print(match_obj.group(0))
line = ‘这是Scrapy学习课程,这次课程很好‘ regex_str=".*?([u4E00-u9FA5]+课程)" match_obj = re.match(regex_str,line) if match_obj: print(match_obj.group(0))
line = ‘xxx出生于1989年‘ regex_str=".*?(d+)年" match_obj = re.match(regex_str,line) if match_obj: print(match_obj.group(0))
line = ‘张三出生于1990年10月1日‘ line = ‘李四出生于1990-10-1‘ line = ‘王五出生于1990-10-01‘ line = ‘孙六出生于1990/10/1‘ line = ‘张七出生于1990-10‘ ? regex_str=‘.*出生于(d{4}[年/-]d{1,2}([月/-]d{1,2}|[月/-]$|$))‘ match_obj = re.match(regex_str,line) if match_obj: print(match_obj.group(1)) 4 正则清洗文本数据?正则处理文本数据,可以剔除脏数据和指定条件的数据筛选。我们这些选用体育新闻中的一篇文本信息,读取文本信息如下: # 读取文本信息 def readFile(path): str_doc = "" with open(path,‘r‘,encoding=‘utf-8‘) as f: str_doc = f.read() return str_doc ? # 1 读取文本 path= r‘../Corpus/CSCMNews/体育/0.txt‘ str_doc = readFile(path) print(str_doc 原始新闻文本节选如下:
我们假设需要清除文本中的特殊符号、标点、英文、数字等,仅仅只保留汉字信息,甚至于去除换行符,还有多个空格转变成一个空格。当然,以上这几点也是数据清洗中常见的情况,其代码实现如下: def textParse(str_doc): # 正则过滤掉特殊符号、标点、英文、数字等。 r1 = ‘[a-zA-Z0-9’!"#$%&‘()*+,-./::;;|<=>[email?protected],—。?★、…【】《》?“”‘’![]^_`{|}~]+‘ # 去除换行符 str_doc=re.sub(r1,‘ ‘,str_doc) # 多个空格成1个 str_doc=re.sub(r2,str_doc) return str_doc ? # 正则清洗字符串 word_list=textParse(str_doc) print(word_list) 执行上述代码,文本信息清洗后结果如下: 以上实验,只是简单的使用正则方法处理文本信息,具体正则使用情况视情形而定。有时候我们面对的不一定是纯文本信息,也有可能是网页数据,或者是微博数据。我们如何去清洗这些半结构化数据呢? 5 正则HTML网页数据设想我们现在有这样一个需求,任务是做信息抽取,然后构建足球球员技能数据库。你首先想到的是一些足球网站,然后编写爬虫代码去爬取足球相关的新闻,并对这些网页信息本地化存储如下图所示: 乍一看非常头疼,我们如何抽取文本信息?一篇篇手工处理显然不现实,采用上面正则方法会出现各种形式干扰数据。这里,我们介绍一种网页数据通用的正则处理方法。实现代码如下: ? # 清洗HTML标签文本 # @param htmlstr HTML字符串. def filter_tags(htmlstr): # 过滤DOCTYPE htmlstr = ‘ ‘.join(htmlstr.split()) # 去掉多余的空格 re_doctype = re.compile(r‘<!DOCTYPE .*?> ‘,re.S) s = re_doctype.sub(‘‘,htmlstr) # 过滤CDATA re_cdata = re.compile(‘//<!CDATA[[ >]? //] > ‘,re.I) s = re_cdata.sub(‘‘,s) # Script re_script = re.compile(‘<s*script[^>]*>[^<]*<s*/s*scripts*>‘,re.I) s = re_script.sub(‘‘,s) # 去掉SCRIPT # style re_style = re.compile(‘<s*style[^>]*>[^<]*<s*/s*styles*>‘,re.I) s = re_style.sub(‘‘,s) # 去掉style # 处理换行 re_br = re.compile(‘<brs*?/?>‘) s = re_br.sub(‘‘,s) # 将br转换为换行 # HTML标签 re_h = re.compile(‘</?w+[^>]*>‘) s = re_h.sub(‘‘,s) # 去掉HTML 标签 # HTML注释 re_comment = re.compile(‘<!--[^>]*-->‘) s = re_comment.sub(‘‘,s) # 多余的空行 blank_line = re.compile(‘n+‘) s = blank_line.sub(‘‘,s) # 剔除超链接 http_link = re.compile(r‘(http://.+.html)‘) s = http_link.sub(‘‘,s) return s ? # 正则处理html网页数据 s=filter_tags(str_doc) print(s) 执行上述代码,得到以下结果: 6 实战案例:批量新闻文本数据清洗我们详细的介绍了迭代遍历与yield生成器遍历的两个小实验,通过实验对比,高效文件读取方式效果显著。上节我们只是读取文件名,那么对文件内容如何修改?这是本节侧重的知识点。其中loadFolders方法保持不变,主要对loadFiles方法进行修改。实现批量新闻文本数据清洗。 class loadFiles(object): def __init__(self,par_path): self.par_path = par_path def __iter__(self): folders = loadFolders(self.par_path) for folder in folders: # level directory catg = folder.split(os.sep)[-1] for file in os.listdir(folder): # secondary directory file_path = os.path.join(folder,file) if os.path.isfile(file_path): this_file = open(file_path,‘rb‘) #rb读取方式更快 content = this_file.read().decode(‘utf8‘) yield catg,content this_file.close() 在统计学中,抽样是一种推论统计方法,它是指从目标总体中抽取一部分个体作为样本,通过观察样本的某一或某些属性,依据所获得的数据对总体的数量特征得出具有一定可靠性的估计判断,从而达到对总体的认识。抽样方法诸多,常见的包括以下几个方法:
接下来,我们实现新闻文本的抽样读取,我们假设抽样率为5即每隔5条信息处理一篇文章,然后每处理5000篇文章在屏幕打印一条信息,其执行代码如下: if __name__==‘__main__‘: start = time.time() filepath = os.path.abspath(r‘../Corpus/CSCMNews‘) files = loadFiles(filepath) n = 5 # n 表示抽样率 for i,msg in enumerate(files): if i % n == 0: if int(i/n) % 1000 == 0: print(‘{t} *** {i} t docs has been dealed‘.format(i=i,time.localtime()))) end = time.time() print(‘total spent times:%.2f‘ % (end-start)+ ‘ s‘) 我们使用简单抽样的方法,信息处理结果如下图所示,总耗时为1120.44秒。 ? 我们最终目的是通过批量操作去清洗文本信息,到目前为止,我们只是实现了文件的遍历和文本提取。并且我们知道如何使用简单抽样,距离最终目的只是一步之遥。这里我们还有提前前文正则处理文本的textParse方法,本节直接导入即可,接下来,我们就是提取文章类别、文章内容、正则清洗。其实现代码如下 if __name__==‘__main__‘: start = time.time() filepath = os.path.abspath(r‘../Corpus/CSCMNews‘) files = loadFiles(filepath) n = 5 # n 表示抽样率, n抽1 for i,msg in enumerate(files): if i % n == 0: catg = msg[0] # 文章类别 content = msg[1] # 文章内容 content = textParse(content) # 正则清洗 if int(i/n) % 1000 == 0: print(‘{t} *** {i} t docs has been dealed‘ .format(i=i,time.localtime())),‘n‘,catg,‘:t‘,content[:20]) end = time.time() print(‘total spent times:%.2f‘ % (end-start)+ ‘ s‘) 运行main函数实现最终结果: (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |