第一个 Python 爬虫

这是一个爬取百度百科内容的爬虫,主要由以下几个部分组成:

url 管理器

它的作用是管理需要爬取的 url ,它自身维护了两个 set() 集合, new_urlsold_urls ,用 set() 集合当然是因为它可以自动去重:

1
2
3
def __init__(self):
self.new_urls = set()
self.old_urls = set()

该类主要有四个方法:

添加一个或者多个 urlset() 集合中,在 new_urls 或者 old_urls 中出现的 url 当然是不做操作了:

1
2
3
4
5
6
7
8
9
10
11
def add_new_url(self, url):
if url is None:
return
if url not in self.new_urls and url not in self.old_urls:
self.new_urls.add(url)

def add_new_urls(self, urls):
if urls is None or len(urls) == 0:
return
for url in urls:
self.add_new_url(url)

然后提供获取 url 和判断集合是否已经遍历完成的方法:

1
2
3
4
5
6
7
8
def has_new_url(self):
return len(self.new_urls) != 0


def get_new_url(self):
new_url = self.new_urls.pop()
self.old_urls.add(new_url)
return new_url

执行过获取 url 方法之后的 url 就添加到 old_urls 集合中。

下载器

使用 urllib2 模块的功能可以轻松完成:

1
2
3
4
5
6
7
def download(self, url):
if url is None:
return None
response = urllib2.urlopen(url)
if response.getcode() != 200:
return None
return response.read()

解析器

该部分也是爬虫最为核心的部分,它决定了爬虫应该爬取哪些内容,

使用了 BeautifulSoup 模块来完成核心的解析部分。

关于 BeautifulSoup 的使用可以先看一个小 Demo :

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
37
38
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup
import re

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

soup = BeautifulSoup(html_doc,'html.parser',from_encoding='utf-8')

print "获取所有链接"
links = soup.find_all('a')
for link in links:
print link.name,link['href'],link.get_text()

print '获取lacie的链接'
link_node = soup.find('a',href='http://example.com/lacie')
print link.name,link_node['href'],link_node.get_text()

print '正则匹配'
re_node = soup.find('a',href=re.compile(r'ill'))
print re_node.name,re_node['href'],link_node.get_text()

print '获取p段落文字'
p_node = soup.find('p',class_='title')
print p_node.name,p_node.get_text()

结果为:

1
2
3
4
5
6
7
8
9
10
获取所有链接
a http://example.com/elsie Elsie
a http://example.com/lacie Lacie
a http://example.com/tillie Tillie
获取lacie的链接
a http://example.com/lacie Lacie
正则匹配
a http://example.com/tillie Lacie
获取p段落文字
p The Dormouse's story

它是将整个 url 中的内容解析成 Dom 树,按节点来查找相应的内容,更高效。

回到我们的解析器,它主要由两个功能组成,一个是获取 url 中的数据,这个数据所在的节点需要我们自己决定。

我们所要爬取的就是标题和内容。

根据审查,百度百科中我们所要获取的内容的信息分别为:

屏幕快照 2017-05-27 上午9.29.08

以及:

屏幕快照 2017-05-27 上午9.28.49

然后就可以编写对应的数据获取代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
def get_new_data(self, page_url, soup):
res_data = {'url': page_url}

# url

res_data['url'] = page_url
# <dd class="lemmaWgt-lemmaTitle-title"><h1>Python</h1>
title_node = soup.find('dd', class_= "lemmaWgt-lemmaTitle-title").find("h1")
res_data['title'] = title_node.get_text()

summary_node = soup.find('div', class_="lemma-summary")
res_data['summary'] = summary_node.get_text()
return res_data

解析器的另外一个功能是从内容中获取新的需要爬取的 url 地址:

1
2
3
4
5
6
7
8
def _get_new_urls(self, page_url, soup):
new_urls = set()
links = soup.find_all('a', href=re.compile(r"/item/(.*)"))
for link in links:
new_url = link['href']
new_full_url = urlparse.urljoin(page_url, new_url)
new_urls.add(new_full_url)
return new_urls

这里我们使用正则来匹配 href 中的内容是否符合我们要爬取的 url

1
links = soup.find_all('a', href=re.compile(r"/item/(.*)"))

将两个功能整合,我们的解析器就完成了:

1
2
3
4
5
6
7
8
def parse(self,page_url,html_cont):
if page_url is None or html_cont is None:
return

soup = BeautifulSoup(html_cont,'html.parser',from_encoding='utf-8')
new_urls = self._get_new_urls(page_url,soup)
new_data = self.get_new_data(page_url,soup)
return new_urls,new_data

内容输出

应该是要将其导出到数据库中的,暂时我们将它写入到一个文档中:

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

class HtmlOutputer(object):

def __init__(self):
self.datas = []

def collect_data(self,data):
if data is None:
return
self.datas.append(data)

def output_html(self):
fout = open('output.html','w')
fout.write("<html>")
fout.write("<body>")
fout.write("<table>")

for data in self.datas:
fout.write('<tr>')
fout.write("<td>%s</td>" % data['url'])
fout.write("<td>%s</td>" % data['title'].encode('utf-8'))
fout.write("<td>%s</td>" % data['summary'].encode('utf-8'))
fout.write("</tr>")
fout.write("</table>")
fout.write("</body>")
fout.write("</html>")
fout.close()

该部分的逻辑还是比较简单的。

主程序

关于主程序的编写,首先逻辑一定要清晰,主要按照如下的流程进行:

屏幕快照 2017-05-27 上午9.42.43

其中我们是直接导出到一个文档,而不是应用中的。

首先我们导包:

1
import url_manager,html_parser,html_downloader,html_outputer

初始化程序:

1
2
3
4
5
def __init__(self):
self.urls = url_manager.UrlManager()
self.downloader = html_downloader.HtmlDownloader()
self.parser = html_parser.HtmlParser()
self.outputer = html_outputer.HtmlOutputer()

然后按照上图的逻辑,定义爬取方法,一步步执行对应操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def craw(self, root_url):
count = 1
self.urls.add_new_url(root_url)
while self.urls.has_new_url():
try :
new_url = self.urls.get_new_url()
print 'craw %d : %s' % (count, new_url)
html_cont = self.downloader.download(new_url)
new_urls, new_data = self.parser.parse(new_url, html_cont)
self.urls.add_new_urls(new_urls)
self.outputer.collect_data(new_data)


if count == 50:
break
count = count + 1
except:
print 'craw failed'

self.outputer.output_html()

由于抓取的途中会出现一些 url 内容为空或者是不符合规范,所以我们使用 try...catch... 语句包裹,这里我们先抓取 50 条数据测试一下。

启动爬虫程序:

1
2
3
4
if __name__ == '__main__':
root_url = 'http://baike.baidu.com/item/Python'
obj_spider = SpiderMain()
obj_spider.craw(root_url)

运行了 28 秒左右 Exited with code=0 in 28.146 seconds.

控制台:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
craw 1 : http://baike.baidu.com/item/Python
craw 2 : http://baike.baidu.com/item/GPL
craw failed
craw 2 : http://baike.baidu.com/item/Zope
craw 3 : http://baike.baidu.com/item/%E7%BC%96%E8%AF%91%E5%99%A8
craw 4 : http://baike.baidu.com/item/%E7%9B%AE%E6%A0%87%E4%BB%A3%E7%A0%81
craw 5 : http://baike.baidu.com/item/FORTRAN%E8%AF%AD%E8%A8%80
craw 6 : http://baike.baidu.com/item/%E6%95%B0%E6%8D%AE
craw 7 : http://baike.baidu.com/item/%E6%95%B0%E6%8D%AE?force=1
craw failed
craw 7 : http://baike.baidu.com/item/%E8%A1%A8%E8%BE%BE%E5%BC%8F
craw 8 : http://baike.baidu.com/item/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86
craw 9 : http://baike.baidu.com/item/%E5%8F%AF%E7%A7%BB%E6%A4%8D%E6%80%A7
craw 10 : http://baike.baidu.com/item/%E5%BC%80%E6%94%BE%E6%BA%90%E7%A0%81
craw 11 : http://baike.baidu.com/item/%E8%BD%AF%E4%BB%B6%E7%B3%BB%E7%BB%9F
craw 12 : http://baike.baidu.com/item/%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95
craw 13 : http://baike.baidu.com/item/%E5%90%88%E5%BC%8F
craw 14 : http://baike.baidu.com/item/MySQLdb
craw 15 : http://baike.baidu.com/item/%E4%BF%9D%E5%AF%86
craw 16 : http://baike.baidu.com/item/%E5%8E%9F%E5%A7%8B%E6%95%B0%E6%8D%AE
craw 17 : http://baike.baidu.com/item/%E8%BD%AF%E4%BB%B6%E7%8E%AF%E5%A2%83
craw 18 : http://baike.baidu.com/item/%E5%B1%9E%E6%80%A7
craw 19 : http://baike.baidu.com/item/%E4%B8%AD%E5%8D%8E%E4%BA%BA%E6%B0%91%E5%85%B1%E5%92%8C%E5%9B%BD%E4%BF%9D%E5%AE%88%E5%9B%BD%E5%AE%B6%E7%A7%98%E5%AF%86%E6%B3%95
craw 20 : http://baike.baidu.com/item/%E5%88%98%E6%96%8C
craw 21 : http://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98
craw 22 : http://baike.baidu.com/item/%E6%B1%87%E7%BC%96%E7%A8%8B%E5%BA%8F
craw 23 : http://baike.baidu.com/item/%E5%A4%A7%E5%AE%85%E9%97%A8/6775752
craw 24 : http://baike.baidu.com/item/%E7%AD%89%E5%80%BC
craw 25 : http://baike.baidu.com/item/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A1%AC%E4%BB%B6
craw 26 : http://baike.baidu.com/item/%E8%B5%B5%E5%AE%9D%E5%88%9A/1460717
craw 27 : http://baike.baidu.com/item/CD-RW
craw 28 : http://baike.baidu.com/item/%E4%BD%BF%E7%94%A8%E7%8E%87
craw 29 : http://baike.baidu.com/item/%E5%90%95%E4%B8%BD%E8%90%8D
craw 30 : http://baike.baidu.com/item/%E6%9D%A8%E7%8E%84
craw 31 : http://baike.baidu.com/item/%E5%86%AF%E5%B0%8F%E5%88%9A
craw 32 : http://baike.baidu.com/item/%E5%8A%A8%E7%89%A9/216062
craw 33 : http://baike.baidu.com/item/%E6%B9%84%E5%85%AC%E6%B2%B3%E5%B7%A8%E5%9E%8B%E9%B2%B6%E9%B1%BC
craw 34 : http://baike.baidu.com/item/%E5%AD%99%E7%BA%A2%E9%9B%B7
craw 35 : http://baike.baidu.com/item/%E5%BC%B9%E6%B6%82%E9%B1%BC
craw 36 : http://baike.baidu.com/item/%E6%96%B0%E4%B8%8D%E4%BA%86%E6%83%85
craw 37 : http://baike.baidu.com/item/%E7%BA%A2%E7%86%8A%E7%8C%AB
craw 38 : http://baike.baidu.com/item/%E5%A4%96%E9%83%A8%E8%AE%BE%E5%A4%87
craw 39 : http://baike.baidu.com/item/1985
craw 40 : http://baike.baidu.com/item/1984
craw failed
craw 40 : http://baike.baidu.com/item/1987
craw 41 : http://baike.baidu.com/item/1986
craw 42 : http://baike.baidu.com/item/%E8%A1%A8%E6%BC%94%E7%B3%BB
craw 43 : http://baike.baidu.com/item/%E5%86%AF%E5%B7%A9
craw 44 : http://baike.baidu.com/item/%E5%AE%B6%EF%BC%8CN%E6%AC%A1%E6%96%B9/9465220
craw 45 : http://baike.baidu.com/item/%E7%AC%AC7%E5%B1%8A%E4%B8%AD%E5%9B%BD%E7%94%B5%E5%BD%B1%E5%AF%BC%E6%BC%94%E5%8D%8F%E4%BC%9A
craw 46 : http://baike.baidu.com/item/%E8%BF%90%E7%AE%97%E9%80%9F%E5%BA%A6
craw 47 : http://baike.baidu.com/item/%E7%8E%8B%E5%B0%91%E4%BC%9F
craw 48 : http://baike.baidu.com/item/%E6%BD%98%E6%85%A7%E5%A6%82
craw 49 : http://baike.baidu.com/item/%E6%B2%B3%E6%AD%A3%E5%AE%87
craw 50 : http://baike.baidu.com/item/%E8%B0%A2%E7%A5%96%E6%AD%A6

output.html 文件也获取到了想要爬取的内容:

屏幕快照 2017-05-27 上午9.55.01

至此,人生第一个 Python 爬虫完成。


该文主要是根据慕课网的视频 Python 开发简单爬虫 整理而成。