Ea5ter's Bolg

python爬虫学习(三)

字数统计: 1.7k阅读时长: 7 min
2018/07/31 Share

0.00 引言

从这章开始到了书上的第二部分——高级数据采集。两章一总结,内容是数据清洗和自然语言处理,其中还有六度分隔的完结篇。

1.00 数据清洗

这里先说了一个概念叫n-gram,简单说就是n个连续的单词为一组,用这个来找对自然语言分析。产生n-gram的代码如下:

1
2
3
4
5
6
def ngrams(input, n):
input = input.split(' ')
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output

这段代码只是简单的以空格来区分每个单词显然是不够的。为了完全清洗还要考虑:把换行符变成空格,连续多个空格变成一个空格。
然后对于要处理的页面也得增加些规则,举书上的例子,处理维基百科还得增加以下规则:

  • 剔除单字符的,“单词”,除非这个字符是 “i” 或 “a” ;
  • 剔除维基百科的引用标记(如 [1] )
  • 剔除标点符号

最后单独写出一个函数 cleanInput :

1
2
3
4
5
6
7
8
9
10
11
12
def cleanInput(input):
input = re.sub('\n+', " ", input)
input = re.sub('\[[0-9]*\]', "", input)
input = re.sub(' +', " ", input)
input = input.decode("ascii", "ignore")
input = input.split(' ')
cleanInput = []
for item in input:
item = item.strip(string.punctuation) # strip() 方法用于移除字符串头尾指定的字符(默认为空格)。string.punctuation为标点符号
if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
cleanInput.append(item.lower())
return cleanInput

2.00 概括数据

利用上面的2-gram我们可以分析一篇文章的词频。以美国总统哈里森的就职演说为数据源来进行词频分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# _*_ coding: utf-8 _*_
import requests
from bs4 import BeautifulSoup
import re
import string

def cleanInput(input):
input = re.sub('\n+', " ", input)
input = re.sub('\[[0-9]*\]', "", input)
input = re.sub(' +', " ", input)
input = input.split(' ')
cleanInput = []
for item in input:
item = item.strip(string.punctuation) # strip() 方法用于移除字符串头尾指定的字符(默认为空格)。string.punctuation为标点符号
if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
cleanInput.append(item.lower())
return cleanInput

def ngrams(input, n):
input = cleanInput(input)
output = {}
for i in range(len(input)-n+1):
ngramTemp = ' '.join(input[i:i+n])
if ngramTemp not in output:
output[ngramTemp] = 0
output[ngramTemp] += 1
return output

html = requests.get("http://www.pythonscraping.com/files/inaugurationSpeech.txt")
html.encoding="utf-8"
soup = BeautifulSoup(html.text, "html.parser")
content = soup.get_text()
ngrams = ngrams(content, 2)
ngrams = sorted(ngrams.items(), key=lambda t: t[1], reverse=True)
print(ngrams)
print("2-ngrams count is" + str(len(ngrams)))

这里的 ngrams 函数输出的是一个:以一组 2-gram 为键名,这组 2-gram 出现的次数为键值的字典。然后用 sorted() 以词频排序。
这个 sorted 函数理解起来还有点杂。在这我就这段代码中的 sorted 进行简单的分析:

  • 首先是第一个参数,这个参数要求是传一个可迭代对象,可迭代对象简单来说就是可以放到 for…in…: 里面循环的,而字典并不是可迭代对象,所以后面要加个 .items() 将其转换。
  • 第二个参数 key 主要是用来进行比较的元素,t 这个名字是自己取的,t[1] 说明比较的元素就是词频。
  • reverse 排序规则,reverse = True 降序 , reverse = False 升序(默认)。

关于可迭代对象和 sorted() 可参考两篇文章:python中的可迭代对象Python中sorted函数的用法
最后这是运行结果的部分:(u’of the’, 213), (u’in the’, 65), (u’to the’, 61), (u’by the’, 41), (u’the constitution’, 34), (u’of our’, 29),
看这些 “of the” “in the” 我们也看不出这篇文章重点是什么,因为这些都是“没有意义”的单词。有人已经总结了最常用的5000个单词,我们可以用其中的一点进行过滤,这样得到的结果就会好很多。

2.10 马尔可夫模型

马尔可夫模型其实就像在看完视频后会出现的猜你喜欢版块。这里用马尔可夫模型来分析上面那位总统就演讲,计算出每个单词后面一个单词出现的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def buildWordDict(text):
text = text.replace("\n", " ")
text = text.replace("\"", "")
punctuation = [',', '.', ';', ':']
for symbol in punctuation:
text = text.replace(symbol, " "+symbol+" ")

words = text.split(" ")
words = [word for word in words if word != ""]
wordDist = {}
for i in range(1, len(words)):
if words[i - 1] not in wordDist:
wordDist[words[i - 1]] = {}
if words[i] not in wordDist[words[i - 1]]:
wordDist[words[i - 1]][words[i]] = 0
wordDist[words[i - 1]][words[i]] += 1
return wordDist

先创建了字典 wordDist ,再以各个单词为键名创建字典 wordDist[words[i - 1]] ,接着以各个单词的后一个单词为键名创建字典 wordDist[words[i - 1]][words[i]] ,最后将各个单词的后一个单词的出现次数作为这个字典的键值。

3.00 维基百科六度分割:终结篇

最后解决的方法就是BFS,虽然书上这么说但给的代码用的却是DFS。虽然有点失望还以为会用什么更为巧妙的算法,但这个代码对结果的处理却十分巧妙。
为了使结果回显,得到跳转的路径,代码通过字典的层层包含来实现了这一目的。
先是主函数:

1
2
3
4
5
try:
serchDepth("/wiki/Batman", "/wiki/Kobe_Bryant", {}, 4)
print("No solution found")
except SolutionFound as e:
print e.message

可以看到代码实现的核心函数就是 serchDepth , 通过这里的处理错误代码可以推测:函数如果得到了答案就会报错。接着是参数,前两个就是起始和结束的词条名,然后是一个空字典和递归的深度。
接着进入 serchDepth :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def serchDepth(targetPageId, currentPageId, linkTree, depth):
if depth == 0:
return linkTree
if not linkTree:
linkTree = constructDict(currentPageId)
if not linkTree:
return {}
if targetPageId in linkTree.keys():
print("Target" + str(targetPageId) + "Found!")
raise SolutionFound("PAGE: " + str(currentPageId))
for branchKey, branchValue in linkTree.items():
try:
linkTree[branchKey] = serchDepth(targetPageId, branchKey, branchValue, depth-1)
except SolutionFound as e:
print(e.message)
raise SolutionFound("PAGE: " + str(currentPageId))
return linkTree

重点关注字典 linkTree 这段代码结果的体现也在这个上面,它的初始化是在第5行,又在最后的循环中递归。那么 linkTree 的键位和值分别是什么?这里可以类比13行的代码, branchKey = currentPageId 、branchValue = {}。所以键位 branchKey 就是词条名,对应的键值 branchValue 就是一个空字典。 通过判断键位来报错,最后就会得到一个层层包含的字典,通过键位名也就可以回溯链接的路径了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SolutionFound(RuntimeError):
def __init__(self, message):
self.message = message

def getLink(fromPageId):
html = get("http://en.wikipedia.org" + fromPageId)
soup = BeautifulSoup(html.text, "html.parser")
return soup.findAll("a", {"href":re.compile('^(/wiki/)')})

def constructDict(currentPageId):
links = getLink(currentPageId)
urls = []
for link in links:
urls.append(link["href"])
if links:
return dict(zip(urls, [{}]*len(urls)))
return {}

这是剩下的两个函数,生成字典的代码还是可以学习一下的。

4.00 结语

这块的知识感觉针对性比较强,每个点都可以玩很深。我也就作为一个了解来学,所以总结的就浅了点,以后遇到具体情况再来思考下。

CATALOG
  1. 1. 0.00 引言
  2. 2. 1.00 数据清洗
  3. 3. 2.00 概括数据
    1. 3.1. 2.10 马尔可夫模型
  4. 4. 3.00 维基百科六度分割:终结篇
  5. 5. 4.00 结语