第二部分:高级抓取(第七章、清理脏数据)
你已经奠定了一些网页抓取的基础:现在到了有趣的部分。在现在之前,我们的网络爬虫一直都比较愚蠢
。他们无法检索信息,除非服务器会立即呈现给他们一个很好的格式。他们收集一切信以为真的信息并且
没有任何分析的简单的存储。他们因为格式、网站的互动甚至JavaScript导致程序出错。总之,他们没有
很好的检索信息,除非该信息真的想被检索。
书的这一部分将帮助你分析原始数据来获取数据下的故事——这个故事是经常被网站隐藏在JavaScript层
,登陆表单和反爬虫措施的下面。
你将学习如何使用网页爬虫来测试你的网站,使该流程自动化并且大规模的访问互联网。在本节结束时,
你应该有工具来收集和操作任何部分的互联网上的任何类型的数据。
第七章 清理脏数据
到目前为止,在这本书中我们忽略了格式错误的数据,而使用通常格式正确的数据源,会删除完全偏离我
们期待的数据。但是网页抓取通常不能从获取数据的地方太挑剔。
由于错误的标点符号,大小写不一致,换行符和拼写错误,脏数据是网络上的一个大问题。在这一章,我
将介绍一些工具和技巧帮助你改变你的编写代码的方式,可以帮助你从源头上解决这些问题和一旦它们出
现在数据库就清理它们。
在代码中清理
正如你编写代码来处理明显的异常,你应该练习防御性的代码来处理意外。
在自然语言学,n-gram是在文本或者语音处理使用的n个字的序列。当做自然语言分析时,它往往能很方
便的从一段文字中分开寻找常用的n-gram或者重复的单词集,两者经常同时使用。
在本节中,我们将专注于获取正确格式的n-gram,而不是使用它们做任何分析。随后在第八章,你可以看
到使用2-grams,3-grams做文字总结和分析。
下面将返回维基百科中Python程序语言文章的2-grams列表:
from urllib.request import urlopen
from bs4 import BeautifulSoup
def ngrams(imput, n):
input = input.split(' ')
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+1])
return output
html = urlopen("http://en.wikipedia.org/wiki/Python_(programming_language)")
bsObj = BeautifulSoup(html)
content = bsObj.fand("div", {"id":"mw-content-text"}).get_text()
ngrams = ngrams(content, 2)
print(ngrams)
print("2-grams count is: "+str(len(ngrams)))
ngrams函数把输入字符串分解成一个单词序列(假设所有单词由空格分隔),并且添加n-gram(此时为2
-gram)的每个单词到一个数组。
这从文本返回一些真正有趣的和有用的2-grams:
['of', 'free'], ['free', 'and'], ['and', 'open-source'], ['open-source', 'software']
但也有很多垃圾:
['software\nOutline\nSPDX\n\n\n\n\n\n\n\n\nOperating', 'system\nfamilies\n\n\n\n
AROS\nBSD\nDarwin\neCos\nFreeDOS\nGNU\nHaiku\nInferno\nLinux\nMach\nMINIX\nOpenS
olaris\nPlan'], ['system\nfamilies\n\n\n\nAROS\nBSD\nDarwin\neCos\nFreeDOS\nGNU\
nHaiku\nInferno\nLinux\nMach\nMINIX\nOpenSolaris\nPlan', '9\nReactOS\nTUD:OS\n\n
\n\n\n\n\n\n\nDevelopment\n\n\n\nBasic'], ['9\nReactOS\nTUD:OS\n\n\n\n\n\n\n\n\n
Development\n\n\n\nBasic', 'For']
此外,由于每遇到一个单词就创建一个2-gram(除了最后一个),在写这篇文章的时候还有7,411个2-
gram在文本中。不是一个非常易于管理的数据集。
使用正则表达式来删除转义字符(如\n)和过滤删除任何Unicode字符,我们可以清理输出:
def ngrams(input, n):
content = re.sub('\n+', ' ', content)
content = re.sub(' +', ' ', content)
content = bytes(content, 'UTF-8')
content = content.decode("ascii", "ignore")
print(content)
input = input.split(' ')
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output
第一个用空格替换所有的换行符(或多个换行符)实例,然后把一排中的空格实例用一个空格替换,确保
所有的单词之间都有一个空格。然后通过内容编码为UTF-8进行淘汰。
这些措施大大提高了函数的输出,但仍存在一些问题:
['Pythoneers.[43][44]', 'Syntax'], ['7', '/'], ['/', '3'], ['3', '=='], ['==', '2']
此时需要做一些决定以便数据处理的更加有趣。有一些更多的规则,我们可以使用更接近理想的数据。
①一个字符的“词语”应该被丢弃,除非该字符是'i'或者'a'。
②维基百科的应用标记(在括号中的数字),应该被丢弃
③标点符号应该被丢弃(注:此规则是一种简化,并且将在第九章更详细的讨论,但是在这个例子的目的
这样做是好的)
现在“清理任务”的代码越来越长,最好是把他们移出去然后放在一个单独的函数cleanInput中:
from urllib.request import urlopen
from bs4 import BeatifulSoup
import re
import string
def cleanInput(input):
input = re.sub("\n+", " ", input)
input = re.sub("\[0-9]*\", "", input)
input = re.sub(" +", " ", input)
input = bytes(input, 'UTF-8')
input = input.decode("ascii", ignore)
cleanInput = []
for item in input:
item = item.strip(string.punctuation)
if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
cleanInput.append(item)
return cleanInput
def ngrams(input, n):
input = cleanInput(input)
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output
注意在Python中使用import string使用string.punctuation来获取所有标点的列表。你可以从Python的
终端查看string.punctuation的输出:
>>> import string
>>> print(string.punctuation)
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
通过使用item.strip(string.punctuation)在循环内迭代所有单词的内容,单词两端的任何标点字符都将
被剥离,连字符(标点字符界定是在字母的两边)的单词将保持不变。
努力之后的更干净的2-gram输出结果:
['Linux', 'Foundation'], ['Foundation', 'Mozilla'], ['Mozilla', 'Foundation'], [
'Foundation', 'Open'], ['Open', 'Knowledge'], ['Knowledge', 'Foundation'], ['Fou
ndation', 'Open'], ['Open', 'Source']
数据标准化
每个人都遇到过设计不当的web表单:“请输入您的手机号码。你的电话号码形式必须是:'XXX-XXX-
XXXX'。”
作为一个优秀的程序员,你可能会想,“为什么他们不只是在我输入那里自动去掉非数字字符?”数据标
准化是确保字符串在语言或者是逻辑上彼此等同的过程。如电话号码“(555)123-4567”和
“555.123.4567”这样显示,但至少相比是等同的。
从上一节使用的n-gram代码,我们可以添加以下数据标准化特征:
这个代码一个明显的问题就是包含许多重复的2-grams。遇到的每个2-gram都添加到列表,没有记录词语
的频率。它不仅仅是为了记录这些2-gram的频率,而不是他们的存在,而是它也可以在图表中反映清理和
数据标准化算法变化的影响。如果数据标准化的非常成功,独特的n-grams总数将会减少,在整个发现的
的n-gram中数目(即,数量唯一或者非唯一的名目被确定为一个n-gram)不会减少。换句话说,相同数量
的n-grams将有更少的“桶”。
不幸的是这个练习的目的,Python字典是无序的。“字典排序”没有任何意义,除非是你复制字典中的值
在其他的内容类型去排序。一个简单的解决办法就是OrderedDict,来自Python的集合库:
from collections import OrderedDict
...
ngrams = ngrams(content, 2)
ngrams = OrderedDict(sorted(ngrams.items(), key=lambda t: t[1], reverse=True))
print(ngrams)
在这里,我利用Python的排序函数,把项目放入一个新的OrderedDict对象中,按照值排序。结果是:
("['Software', 'Foundation']", 40), ("['Python', 'Software']", 38), ("['of', 'th
e']", 35), ("['Foundation', 'Retrieved']", 34), ("['of', 'Python']", 28), ("['in
', 'the']", 21), ("['van', 'Rossum']", 18)
在撰写本文时,共有7696个2-grams和6005个唯一的2-grams,最流行的2-gram是“Software Foundation
”,其次是“Python Software”。不过,分析结果显示,“Python Software”实际出现了两次“Python
software”。同样,无论“Van Rossum”和“van Rossum”在列表中是单独出现。
添加这一行:
input = input.upper()
到cleanInput函数保持2-grams的总数稳定到7696,降低了唯一的2-grams数目到5882。
除此之外,通常很好的是停下来考虑你消耗的标准化数据需要多大的计算能力。有多种情况,单词不同的
拼写是等价的,为了解决这个等值,需要运行一个对单个字符的检查,看它是否符合你预先编程设定的等
值。
例如,“Python 1st”和“Python first”都出现在2-grams的列表中。然而,做一个公共的规则,“所
有的‘first’,‘second’,‘third’等将会被解析为1st, 2nd,3rd等(或者反之亦然)”将导致每个
单词由额外的10个左右的检查。
同样,使用不一致的连字符(“co-ordinated”与“coordinator”),拼写错误以及其他自然语言的不
协调都会影响n-grams的分组,如果不协调是很常见的,可能会输出结果混乱。
一个解决方案,再连字符单词的情况下,可能会删除连字符对待单词作为单个字符串,这只需要一个单一
操作。然而,这也意味着复姓短语(一个太常见的现象)会被视为单个单词。去其他途径来对待连字符视
为空格可能是一个更好的选择。只是准备偶尔的“co ordinated”和“ordinated attack”出差错。
在事后清理
在代码中只有这么多你可以或者想要做的。此外,你可能要处理你没有创建的数据集,有一个挑战是甚至
不限看到这个数据集不知道如何清洁它。
许多程序员下意识的反应是“写一个脚本”,是一个很好的解决方案。然而,也有第三方工具,比如
OpenRefine,其不仅能够快速,方便的清理数据,还允许你的数据可以轻易的被使用的非程序员看到。
OpenRefine
OpenRefine是一家名叫Metaweb公司在2009年开始的一个开源项目。谷歌在2010年收购Metaweb,把项目名
称从Freebase Girdworks改为Google Refine。在2012年,谷歌放弃支持Refine,并且再次改名为
OpenRefine,欢迎任何人贡献发展这个项目。
安装
OpenRefine是不寻常的,尽管它的界面是运行在一个浏览器,它在技术上是一个桌面应用,必须下载安装
。你可以从它的网站上下载Linux,Windows和Mac OS X版本的应用程序。
注意
如果你是一个MAC用户,再打开文件时碰上任何麻烦,进入系统设置——安全隐私——常规——并选中“
允许来自任何地方的应用程序”。不幸的是,在从谷歌项目过度到一个开源项目时,OpenRefine在苹果严
重似乎已经失去了它的合法性。
为了使用OpenRefine,你需要将数据保存为CSV(如果你需要复习怎么做,文件指回第五章“数据存储”)
。另外,如果你有存储在数据库中的数据,你也可以将其导出到一个CSV文件。
使用OpenRefine
在下面的例子中,我们将使用从维基百科“文本编辑器的比较”表抓取的数据,见图7-1。尽管该表有比
较好的格式化,它包含很长一段时间内编辑的人,所以他有一些轻微的格式不一致。此外,由于它的数据
是由人阅读而不是机器,一些编程选择(例如,使用“免费”而不是“$0.00”)是不适合编程输入。
图7-1:
关于OpenRefine需要注意的第一件事就是:每一列标签旁边有一个箭头。这个箭头提供可以用来在该列过
滤,排序,转化或者删除数据的工具。
过滤
数据过滤可以用两种方法来进行:过滤器和面。过滤器使用正则表达式进行良好数据过滤;例如,“只展
示给我,在编程语言列中包含四个或者更多逗号分隔的编程语言”,见图7-2:
过滤器可以组合,编辑,并通过在有右边栏简单的操纵块。它们还可以和面结合。
面是在包括或者基于列的全部内容排除数据。(例如,“显示说有2005年以后首次发型,且使用GPL或者
MIT许可的行”如图7-3)。
它们有内置过滤器。例如,过滤一个数字值,提供了一个滑杆来选择要包括的数值范围。
然而你筛选数据,OpenRefine支持在任何时候导出各种格式类型。这包括CSV,HTML(一个HTML表),
Excel和其他几个格式。
清理
数据过滤可以在数据清理开始时候做的很成功。例如,在上一节面的例子中,文本编辑器有一个发行日期
“01-01-2006”不会被选择在“首次发行”面,将会查看值“2006”,会忽略那些不是它选择的。
OpenRefine中的数据变换使用OpenRefine表达语言(OpenRefine Expression Language),称为GREL(“G
”来自OpenRefine以前的名字Google Refine)。这种语言用来创建短的lambda函数在单元中根据简单规则
转换值。例如:
if(value.length() != 4, "invalid", value)
当此功能被应用到“第一个稳定版本”栏,它保留其中日期是一个“YYYY”的值得单元,并且标记其他列
是“无效的”。
如图7-4:
任意的GREL语句可以通过单击列标签旁边向下的箭头然后选择要编辑的单元→变换。
然而,标记所有小于理想值的全部无效使得它们很容易被识别,我们没有做的多好。如果可能我们宁愿尝
试从格式错误值中搜寻信息。这个可以通过使用GREL的匹配函数完成:
value.match(".*([0-9]{4}).*").get(0)
这个用给出的正则表达式匹配字符串值。如果正则表达式匹配到字符串,返回值是数组。通过正则表达式
(在此由括号中的表达式划分,例如“[0-9]{4}”)匹配的“捕获组”的子字符串返回为数组值。
事实上,这个代码是发现所有行中的四位十进制实例,并返回第一个。这通常足够从文本或者错误格式日
期中提取年份。它也有不存在的日期返回“null”的优势。(GERL不会抛出一个空指针,除了对一个空变
量进行操作)
许多其他数据转换通过单元编辑和GERL是可能的。一个完整的语言指导可以在OpenRefine的Github页面查
看。
转载请注明出处http://blog.csdn.net/qq_15297487