一、架构原理及运行流程
1.1 架构图解
1.2 模块分析
- 爬虫调度器:爬虫调度器只要负责统筹其他四个模块的协调工作。
- URL 管理器:负责管理 URL 链接,维护已经爬取的 URL 集合和未爬取的 URL 集合,提供获取新 URL 链接接口。
- HTML 下载器:用于从 URL 管理器中获取未爬取的 URL 链接并下载 HTML 网页。
- HTML 解析器:用于从 HTML 下载器中获取已经下载的 HTML 网页,并从中解析出新的 URL 交给 URL 管理器,解析出有效数据交给数据存储器。
- 数据存储器:用于将 HTML 解析器解析出来的数据通过文件或者数据库形式存储起来。
1.3 运行流程
二、URL 管理器
2.1 实现原理
URL 管理器主要包括两个变量,一个是已爬取 URL 的集合,另一个是未爬取 URL 的集合。采用 Python 中的 set 类型,主要是使用 set 的去重复功能, 防止链接重复爬取,因为爬取链接重复时容易造成死循环。链接去重复在 Python 爬虫开发中是必备的功能,解决方案主要有三种:1)内存去重 2)关系数据库去重 3)缓存数据库去重。大型成熟的爬虫基本上采用缓存数据库去重的方案,尽可能避免内存大小的限制,又比关系型数据库去重性能高很多。由于基础爬虫的爬取数量较小,因此我们使用 Python 中 set 这个内存去重方式。
URL 管理器除了具有两个 URL 集合,还需要提供以下接口,由于配合其他模块使用,接口如下:
- 判断是否有待取的 URL, 方法定义为 has_new_url()。
- 添加新的 URL 到未爬取集合中, 方法定义为 add_new_url(url),add_new_urls(urls)。
- 获取一个未爬取的 URL,方法定义为 get_new_url()。
- 获取未爬取 URL 集合的大小,方法定义为 new_url_size()。
- 获取已经爬取的 URL 集合的大小,方法定义为 old_url_size()。
2.2 代码如下
1 class UrlManager:
2 def __init__(self):
3 self.new_urls = set() # 未爬取 url 集合
4 self.old_urls = set() # 已爬取 url 集合
5
6 def has_new_url(self):
7 """
8 判断是否有未爬取的 url
9 :return: bool
10 """
11 return self.new_urls_size() != 0
12
13 def get_new_url(self):
14 """
15 返回一个未爬取的 url
16 :return: str
17 """
18 new_url = self.new_urls.pop() 19 self.old_urls.add(new_url) 20 return new_url 21 22 def add_new_url(self, url): 23 """ 24 添加一个新的 url 25 :param url: 单个 url 26 :return: None 27 """ 28 if url is None: 29 return None 30 if (url not in self.new_urls) and (url not in self.old_urls): 31 self.new_urls.add(url) 32 33 def add_new_urls(self, urls): 34 """ 35 添加多个新的url 36 :param urls: 多个 url 37 :return: None 38 """ 39 if urls is None: 40 return None 41 for url in urls: 42 self.add_new_url(url) 43 44 def new_urls_size(self): 45 """ 46 返回未爬过的 url 集合的大小 47 :return: int 48 """ 49 return len(self.new_urls) 50 51 def old_urls_size(self): 52 """ 53 返回已爬过的 url 集合的大小 54 :return: int 55 """ 56 return len(self.old_urls)
三、HTML 下载器
3.1 实现原理
HTML 下载器用来下载网页,这时候需要注意网页的编码,以保证下载的网页没有乱码。下载器需要用到 Requests 模块,里面只需要实现一个接口即可:download(url)。
3.2 代码如下
1 import requests
2
3
4 class HtmlDownloader:
5 def download(self, url):
6 """
7 下载 html 页面源码
8 :param url: url
9 :return: str / None
10 """
11 if not url:
12 return None
13
14 headers = { 15 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0', 16 } 17 r = requests.get(url, headers=headers) 18 if r.status_code == 200: 19 r.encoding = 'utf-8' 20 return r.text 21 else: 22 return None
四、HTML 解析器
4.1 实现原理
HTML 解析器使用 Xpath 规则进行 HTML 解析,需要解析的部分主要有书名、评分和评分人数。
4.2 代码如下
1 from lxml.html import etree
2 import re
3
4 class HtmlParser:
5 def parser(self, page_url, html_text):
6 """
7 解析页面新的 url 链接和数据
8 :param page_url: url
9 :param html_text: 页面内容
10 :return: tuple / None
11 """
12 if not page_url and not html_text:
13 return None 14 new_urls = self._get_new_urls(page_url, html_text) 15 new_data = self._get_new_data(html_text) 16 17 return new_urls, new_data 18 19 def _get_new_urls(self, page_url, html_text): 20 """ 21 返回解析后的 url 集合 22 :param page_url: url 23 :param html_text: 页面内容 24 :return: set 25 """ 26 new_urls = set() 27 links = re.compile(r'\?start=\d+').findall(html_text) 28 for link in links: 29 new_urls.add(page_url.split('?')[0] + link) 30 return new_urls 31 32 def _get_new_data(self, html_text): 33 """ 34 返回解析后的数据列表 35 :param html_text: 页面内容 36 :return: list 37 """ 38 datas = [] 39 for html in etree.HTML(html_text).xpath('//ol[@class="grid_view"]/li'): 40 name = html.xpath('./div/div[@class="info"]/div[@class="hd"]/a/span[1]/text()')[0] 41 score = html.xpath('./div/div[@class="info"]/div[@class="bd"]/div[@class="star"]/span[2]/text()')[0] 42 person_num = html.xpath('./div/div[@class="info"]/div[@class="bd"]/div[@class="star"]/span[4]/text()')[0].strip('人评价') 43 datas.append([name, score, person_num]) 44 return datas
五、数据存储器
5.1 实现原理
数据存储器主要包括两个方法:store_data(data)用于将解析出来的数据存储到内存中,output_csv()用于将存储的数据输出为指定的文件格式,我们使用的是将数据输出为 csv 格式。
5.2 代码如下
1 import csv
2
3 class DataOutput:
4 def __init__(self):
5 self.file = open('数据.csv', 'w')
6 self.csv_file = csv.writer(self.file)
7 self.csv_file.writerow(['书名', '评分', '评分人数']) 8 9 def output_csv(self, data): 10 """ 11 将数据写入 csv 文件 12 :param data: 数据 13 :return: None 14 """ 15 self.csv_file.writerow(data) 16 17 def close_file(self): 18 """ 19 关闭文件链接 20 :return: None 21 """ 22 self.file.close()
六、爬虫调度器
6.1 实现原理
爬虫调度器首先要做的是初始化各个模块,然后通过 crawl(start_url) 方法传入入口 URL,方法内部实现按照运行流程控制各个模块的工作。
6.2 代码如下
1 from UrlManager import UrlManager
2 from HtmlDownloader import HtmlDownloader
3 from HtmlParser import HtmlParser
4 from DataOutput import DataOutput
5
6
7 class SpiderManager:
8 def __init__(self): 9 self.manager = UrlManager() 10 self.downloader = HtmlDownloader() 11 self.parser = HtmlParser() 12 self.output = DataOutput() 13 14 def crawl(self, start_url): 15 """ 16 负责调度其他爬虫模块 17 :param start_url: 起始 url 18 :return: None 19 """ 20 self.manager.add_new_url(start_url) 21 while self.manager.has_new_url(): 22 try: 23 new_url = self.manager.get_new_url() 24 html = self.downloader.download(new_url) 25 new_urls, new_datas = self.parser.parser(start_url, html) 26 self.manager.add_new_urls(new_urls) 27 for data in new_datas: 28 self.output.output_csv(data) 29 except Exception: 30 print('爬取失败') 31 self.output.close_file() 32 33 34 if __name__ == '__main__': 35 sm = SpiderManager() 36 sm.crawl('https://movie.douban.com/top250?start=0')