1. 煮粥之前先淘米——预处理
其实我们应该先寻找算法,有了方向再对数据进行相应的预处理,不过刚好最近在学习正则表达式,以及文件读写,就顺便练手,对数据进行“粗加工”。
也就是简单地:
去除坏值,比如编码有问题的。我觉得这种认为打了label的数据应该很可靠不应该有太多的问题,但还是有30多个编码有问题的。
舍弃冗余内容。比如:
id = {D10-1003} author = {Cheung, Jackie Chi Kit; Penn, Gerald} title = {Utilizing Extra-Sentential Context for Parsing} venue = {EMNLP} year = {2010}
year这个信息再id中有表现( - 前的数字),title的作用就是这个作品的名字,里面的内容我们并不关心,所以只需要用id这一个信息来表示就可以。我们就需要舍弃year和title这两个冗余信息。
从信息中提取出有效信息。比如:
author = {Cheung, Jackie Chi Kit; Penn, Gerald}
其中有效信息是 ’Cheung, Jackie Chi Kit‘ 和 ‘Penn, Gerald’ 这两个字符串。其他的等号,分号,大括号都要去掉。
整合信息。
比如:需要知道作者之间的关系,但其实作者是无序的,可以用集合来存放作者;id和author,venue关系紧密,我们可以用字典来表示id和他们之间的关系,便于查询。
需要注意的一点是,一些外国作者名字中会出现西语,法语,或者德语字符。比如:Nria 显示成 N ú ;ria;Iaki显示成Iñ ;aki; Germn显示成Germ á ; n。对于这种情况,我们在此不做处理,因为作者的名字不会变,此处编码成如此,在别处也会编码成如此。
2. 求求你了别报错——编码
之前从来没有接触过文件编码的问题,遇到之后一直报错,根本停不下来,这就让人觉得有点头大。
代码如下:
with open('acl-metadata.txt','r') as data: print(data.readlines(),'\n')
报错:
UnicodeDecodeError: 'gbk' codec can't decode byte 0x94 in position 394157: illegal multibyte sequence
因为这个项目提供的文本文件不是 UTF8 编码的(例如,可能是 GBK 编码的),而系统默认采用 UTF8 解码。在网上搜索之后找到了解决方法,应该将文件读取进来的时候改为对应的解码方式。
with open('acl-metadata.txt','rb') as data:
这种方式可以解决文件编码的问题,因为 ‘rb’ 关键字表示read as binary data,故读进来之后,储存每一行信息的多维list里面的内容就是二进制格式,不会将读取的字节转换成字符。
但如果他是二进制信息,我们之后想用正则表达式,和split方法处理字符串,就只能成为美梦了。所以我们需要找到另一种处理文件的方法!
total_aclmetadata.append(str(line, encoding = "utf-8") )
先强制转换编码格式,再将获取到的内容转为字符串格式就OK了。
3. 删除坏值像狙击——定位,出击
删除坏值的原则有两个:
这条数据(id,venue,title等)不能为我所用。
如果这条数据要删,就要把相对应的五条数据(id,author,venue,year,title)全部删掉。
保留删去的数据组对应的id。
我们现在是拿着aclmetadata.txt这个数据集说事,我们如果要删去这个文件里的某些数据(或者说数据组),就要把相对应的ppraletionship.txt里面的文章之间的索引信息也给删掉。
有一点需要强调的是,也是我绕的一点弯路:坏值处理要先于获取有效信息!
我最初的想法是先处理有效信息:
先用正则表达式,和split获得有效信息(前提是再找到坏值之后在对应位置填上提示信息“abnormal data = {'坏值待处理'}”,这样就可以用同样的正则规则来处理坏值的信息),将id作为key,author和venue作为value合成为一个字典。
那怎么把对应的内容都删了呢:
先查看坏值之前或之后一行信息,split获取是id,author,year之类的。
再找到对应的id。
找到id之后,再把对应字典中的内容删掉!
凭借badvalue找到对应的id,然后用它做key去author里ramove掉对应元素。
问题是这个abnormal data你根本不知道他是什么信息,如果是author,venue肯定会造成字典中的元素不对应,更不要提坏值是id的情况了——怎么去找对应的id?
也尝试了一些其他想法,比如设置一个标志为,如果这组数据里坏值出现的标志位abdata被置1了,则这组数据不添加到字典中。但都太不优雅了,会使程序的可读性变得十分糟糕。
那我们现在来老老实实地进行坏值删除,两步走:
1> 定位:
定位这一步在读取数据的时候就要进行了,记录下来坏值出现的位置,非常简单故不再赘述。遇到一个坏值,就将其索引存放到badvalue里。
2> 进攻策略
我一开始写了一个函数,自我感觉非常良好:
def data_erasing(badvalue, dataset): for i in badvalue: for j in range(6): dataset.pop(i-(i%6)+j)
一运行就发现了问题,pop这个方法每pop出一个内容,该位置之后的所有元素的索引都减1!!!,所以我的这个:for j in range(6):循环,其实都是删一个元素,然而元素整体上已经向前移动一位了,索引再加一,那就完全乱了套。
我心想:在删除了数据之后,数据整体向前移动,那我后面坏值的索引是不固定的。那如果我不想让剩下的坏值的索引变化,该怎么办呢?
有了!咱倒着删不就成了吗,从最后一个开始处理,剩余的坏值都在它前面,那不就不用考虑坏值索引的问题了嘛!当然,对于每一个坏值对应的数据组,我们也要按照索引从后往前删,所以就有了下面的代码:
def data_erasing(badvalue, dataset): for i in reversed(badvalue): for j in range(5,-1,-1): dataset.pop(i-(i%6)+j)
可问题层出不穷,你经历过的东西越少,你犯的错误就越多,错误也越白痴。只想着删除一个数据组,忘了怎么样才算一个数据组了。坏值在year这条信息的位置上,按照上面的代码,就删掉了year及其后面五行(个)元素。这当然是不合理的,我们最终想要删除的是这个坏值所在的这组数据。
所以我们需要第三步,实际上是我们第一步就该做好的。
3> 再次定位
这次定位不是定“坏值在全局中的索引”了,而是“坏值在对应数据组中的位置”。因为这些数据在给的时候已经处理过了,都是按照id,author,title,venue,year的顺序排列的,我们只需要找出坏值前后的信息各是什么,就可以对坏值所对应的数据组做出推测,我们找到了该数据组的id对应的索引,在用上面的方式删除就好了。
区别就在于:之前删除坏值的基础位置是badvalue(坏值本身索引),这种方法是基于坏值所在数据组的id所在的位置。
4. 字符串雌雄双煞——RE & split方法
处理字符串有两个神器:split方法,和正则表达式(regular expression,缩写为regexp)
split方法,用于拆分字符串。通过指定分隔符对字符串进行切片,并返回分割后的字符串列表。比如,A.split(' = '),将A按“空格+等号+空格”切分成n个字符串,返回一个列表。B.split(';'),将B按“分号+空格”切分成n个字符串,返回一个列表。
正则表达式就更有趣了,我举几个例子:
匹配中文字符的正则表达式: [\u4e00-\u9fa5] 匹配Email地址的正则表达式: \w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* 匹配信用卡卡号校验,检查16位四组格式的,以空个或-分开的卡号,或者连在一起的: ^(\d{4}[- ]){3}\d{4}|\d{16}$ 匹配任何大写或小写字母组成,以逗号或空格分开的文本,空格的数量不限。 ^\s*[a-zA-Z,\s]+\s*$
有了这两个神器,我们从字符串中获取有效信息就游刃有余了。
5. label团队扣鸡腿——获取信息
之前的想法(version 1)是:观察到信息包括id, author, title, venue, year一共五项,外加一个空格。那么我可以按照六个一循环来读取数据。
for i in range(len(dataset)): temp = [] if i%6 == 0: ids.append((re.match(pattern_allinfor, dataset[i])).groups()[0]) elif i%6 == 1: temp.append((re.match(pattern_allinfor, dataset[i])).groups()[0]) splitedname = temp[-1].split('; ') authors.append(splitedname) elif i%6 == 3: venues.append((re.match(pattern_allinfor, dataset[i])).groups()[0]) infors[ids[j]] = np.array([authors[j], venues[j]]) j += 1 else: continue
注意到上式代码,按索引对6取模来分类,模为0的都是六个一循环中的第一位,即id;模为1的都是六个一循环中的第二位,即author;模为3的都是六个一循环中的第四位,即venue。而且我们只需要这几个信息。
但在运行之后问题出现了!2012年一整年的信息的year的格式都有问题:最右侧的 } 被分到了下一行。不仅我们的正则表达式要进行改进,而且整个数据集就不能再用“六个一循环”这个策略读取了。我想把这个锅给打label的小组,午餐扣一个鸡腿!
for 在datasset的长度范围内,从第一行遍历到最后一行: 用split(' = ')获取信息的类型: if 类型是id: id.append() elif 类型是author: author.append() elif 类型是venue: venue.append() 在这个if块里将id,author,venue的最后一个元素合成一个字典元素对。 else: continue
这样就不存在要求数据集必须六个一循环了,是不是更简洁优雅了?呵呵。