分布式爬虫笔记(一)- 非框架实现的Crawlspider

不久前写过一篇使用Scrapy框架写的Crawlspider爬虫笔记(五)- 关于Scrapy 全站遍历Crawlspider,本次我再次沿用上次的网站实现全站爬虫,希望目标网址的小伙伴原谅我~~~
目标站点:www.cuiqingcai.com
代码已经上存到github下载

代码已经有详细的注释,这里附上流程图和部分代码解析~~

主要用到的库和技术

import urllib2 
from collections import deque  # deque是为了高效实现插入和删除操作的双向列表
import httplib
from lxml import etree
from pybloom import BloomFilter
  • urllib2:伪造请求
  • deque:双向列表,存储待下载的URL
  • httplib:生成md5值
  • BloomFilter:用于URL过滤
  • etree:获取页面,结合xpath过滤页面中的URL

伪流程图

伪流程图

首先是紫色部分,就是主要流程:通过主流程来控制整个爬取的过程。
然后,主要流程里面有三个伪小流程【初始化流程__ini__】、【获取URL流程getqueneURL】和【爬取流程getPageContent】。

重点代码说明

【初始化流程__ini__】

  • 断点续传逻辑:通过将下载过的md5和url记录到文件中的方式,在每次执行脚本前分析已记录的md5值的方式来实现断点续传
self.md5_file = open(self.md5_file_name, 'r')  # 只读方式打开md5的文件
self.md5_lists = self.md5_file.readlines()  # 将文件的内容以列表的方式读取出来
self.md5_file.close()  # 关闭文件
for md5_item in self.md5_lists:  # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
    self.download_bf.add(md5_item[:-2])  # 将去掉回车换行符的md5写入BloomFilter对象当中

【获取URL流程getqueneURL】

  • deque双向列表:通过popleft()高效读取URL爬取。(这里有个GIL锁,想了解可以自己深入了解下~~~)
# 用于记录爬取URL的队列(先进先出)
now_queue = deque()  # 爬取队列
bak_queue = deque()  # 备用队列(爬取队列为空的时候置换)
···
url = self.now_queue.popleft()  # 从左边进行获取队列内容
  • try except:队列为空的时候,增加深度&置换队列
try:
    url = self.now_queue.popleft()  # 从左边进行获取队列内容
    return url
except IndexError:
    self.now_level += 1  # 深度加一
    if self.now_level == self.max_level:  # 如果深度与设定的最大深度相等,停止爬虫返回None
        return None
    if len(self.bak_queue) == 0:  # 如果备用队列长度为0,停止爬虫返回None
        return None
    self.now_queue = self.bak_queue  # 将备用队列传递给爬取队列
    self.bak_queue = deque()  # 重置备用队列
    return self.getQueneURL()  # 继续执行dequeuUrl方法,直到获取到URL或者None

【爬取流程getPageContent】

  • md5:__init__流程中的断点续传和过滤URL
# 计算md5的值并将md5和写入到文件中
dumd5 = hashlib.md5(now_url).hexdigest()  # 生成md5值
self.md5_lists.append(dumd5)  # 将md5加入到md5的列表中
self.md5_file.write(dumd5 + '\r\n')  # 将md5写入文件
self.url_file.write(now_url + '\r\n')  # 将url写入文件
self.download_bf.add(dumd5)  # 将md5加入到BloomFilter对象当中
num_downloaded_pages += 1  # 用于统计当前下载页面的总数
  • xpath:获取当前文件中所有的URL
html = etree.HTML(html_page.lower().decode('utf-8'))
hrefs = html.xpath(u"//a")
for href in hrefs:
    # 用于处理xpath抓取到的href,获取有用的
    try:
        if 'href' in href.attrib:
            val = href.attrib['href']
            if val.find('javascript:') != -1:  # 过滤掉类似"javascript:void(0)"
                continue
            if val.startswith('http://') is False:  # 给"/mdd/calendar/"做拼接
                if val.startswith('/'):
                    val = 'http://cuiqingcai.com/{0}'.format(val)
                else:
                    continue
            if val[-1] == '/':  # 过滤掉末尾的/
                val = val[0:-1]
  • BloomFilter:过滤URL后入列
if hashlib.md5(val).hexdigest() not in self.download_bf:
    self.bak_queue.append(val)

结果展示

(占位---待继续更新)

代码不够200行,这里附上所有代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
# @Time    : 2017/6/9 19:23
# @Author  : Spareribs
# @File    : cuiqingcai_crawl.py
# @Notice  : 这是使用宽度优先算法BSF实现的全站爬取的爬虫


详解1:我们已经将md5和URL记录到md5.txt和url.txt中,但是我们暂时不用url.txt,我们只需要将md5的值读取到用于做判断逻辑的BloomFilter对象当中即可
"""

import os
import time
import urllib2
from collections import deque  # deque是为了高效实现插入和删除操作的双向列表
import httplib
import hashlib
from lxml import etree
from pybloom import BloomFilter

num_downloaded_pages = 0


class CuiQingCaiBSF():
    """
    这是使用宽度优先算法BSF实现的全站爬取的爬虫类,通过max_level来自定义抓取的深度
    """
    # 定义请求的头部(目标网站没有做太多的安全措施,所以原谅我)
    request_headers = {
        'user-agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36",
    }

    # BSF宽度优先算法的深度标记
    now_level = 0  # 初始深度
    max_level = 2  # 爬取深度

    # 记录文件(URL和计算得到的md5)
    dir_name = 'cuiqingcai/'
    if not os.path.exists(dir_name):  # 检查用于存储网页文件夹是否存在,不存在则创建
        os.makedirs(dir_name)
    md5_file_name = dir_name + "md5.txt"  # 记录已经下载的md5的值
    url_file_name = dir_name + "url.txt"  # 记录已经下载的URL

    # 用于记录爬取URL的队列(先进先出)
    now_queue = deque()  # 爬取队列
    bak_queue = deque()  # 备用队列(爬取队列为空的时候置换)

    # 定义一个BloomFilter对象,用于做URL去重使用
    download_bf = BloomFilter(1024 * 1024 * 16, 0.01)

    # 定义一个存放md5值的列表
    md5_lists = []

    def __init__(self, begin_url):
        """
        初始化处理,主要是断点续传的逻辑
        """
        self.root_url = begin_url  # 将初始的URL传入
        self.now_queue.append(begin_url)  # 将首个URL加入爬取队列now_queue
        self.url_file = open(self.url_file_name, 'a+')  # 将首个url写入url记录文件

        # 用于处理断点续传逻辑(详细请看-->详解一)
        try:
            self.md5_file = open(self.md5_file_name, 'r')  # 只读方式打开md5的文件
            self.md5_lists = self.md5_file.readlines()  # 将文件的内容以列表的方式读取出来
            self.md5_file.close()  # 关闭文件
            for md5_item in self.md5_lists:  # md5_item 的格式是"7e9229e7650b1f5b58c90773433ae2bc\r\n"
                self.download_bf.add(md5_item[:-2])  # 将去掉回车换行符的md5写入BloomFilter对象当中
        except IOError:
            print "【Error】{0} - File not found".format(self.md5_file_name)
        finally:
            self.md5_file = open(self.md5_file_name, 'a+')  # 增加编辑方式打开md5的文件

    # def enqueueUrl(self, url):
    #     self.bak_queue.append(url) # 将获取到的url加入到备用队列当中

    def getQueneURL(self):
        """
        爬取队列为空的时候,将备用队列置换到爬取队列
        """
        try:
            url = self.now_queue.popleft()  # 从左边进行获取队列内容
            return url
        except IndexError:
            self.now_level += 1  # 深度加一
            if self.now_level == self.max_level:  # 如果深度与设定的最大深度相等,停止爬虫返回None
                return None
            if len(self.bak_queue) == 0:  # 如果备用队列长度为0,停止爬虫返回None
                return None
            self.now_queue = self.bak_queue  # 将备用队列传递给爬取队列
            self.bak_queue = deque()  # 重置备用队列
            return self.getQueneURL()  # 继续执行dequeuUrl方法,直到获取到URL或者None

    def getPageContent(self, now_url):
        """
        下载当前爬取页面,
        """
        global filename, num_downloaded_pages
        print "【Download】正在下载网址 {0} 当前深度为{1}".format(now_url, self.now_level)
        try:
            # 使用urllib库请求now_url地址,将页面通过read方法读取下来
            req = urllib2.Request(now_url, headers=self.request_headers)
            response = urllib2.urlopen(req)
            html_page = response.read()

            filename = now_url[7:].replace('/', '_')  # 处理URL信息,去掉"http://",将/替换成_

            # 将获取到的页面写入到文件中
            fo = open("{0}{1}.html".format(self.dir_name, filename), 'wb+')
            fo.write(html_page)
            fo.close()



        # 处理各种异常情况
        except urllib2.HTTPError, Arguments:
            print "【Error】:{0}\n".format(Arguments)
            return
        except httplib.BadStatusLine:
            print "【Error】:{0}\n".format('BadStatusLine')
            return
        except IOError:
            print "【Error】:IOError {0}\n".format(filename)
            return
        except Exception, Arguments:
            print "【Error】:{0}\n".format(Arguments)
            return

        # 计算md5的值并将md5和写入到文件中
        dumd5 = hashlib.md5(now_url).hexdigest()  # 生成md5值
        self.md5_lists.append(dumd5)  # 将md5加入到md5的列表中
        self.md5_file.write(dumd5 + '\r\n')  # 将md5写入文件
        self.url_file.write(now_url + '\r\n')  # 将url写入文件
        self.download_bf.add(dumd5)  # 将md5加入到BloomFilter对象当中
        num_downloaded_pages += 1  # 用于统计当前下载页面的总数

        # 解析页面,获取当前页面中所有的URL
        try:
            html = etree.HTML(html_page.lower().decode('utf-8'))
            hrefs = html.xpath(u"//a")
            for href in hrefs:
                # 用于处理xpath抓取到的href,获取有用的
                try:
                    if 'href' in href.attrib:
                        val = href.attrib['href']
                        if val.find('javascript:') != -1:  # 过滤掉类似"javascript:void(0)"
                            continue
                        if val.startswith('http://') is False:  # 给"/mdd/calendar/"做拼接
                            if val.startswith('/'):
                                val = 'http://cuiqingcai.com/{0}'.format(val)
                            else:
                                continue
                        if val[-1] == '/':  # 过滤掉末尾的/
                            val = val[0:-1]
                        # 判断如果这个URL没有在BloomFilter中就加入BloomFilter的队列
                        if hashlib.md5(val).hexdigest() not in self.download_bf:
                            self.bak_queue.append(val)
                        else:
                            print '【Skip】已经爬取 {0} 跳过'.format(val)
                except ValueError:
                    continue
        except UnicodeDecodeError:  # 处理utf-8编码无法解析的异常情况
            pass

    def start_crawl(self):
        """
        启动脚本的主程序
        """

        while True:
            # time.sleep(10)
            url = self.getQueneURL()
            if url is None:
                break
            self.getPageContent(url)
            print "爬取队列剩余URL数量为:{0},备用队列剩余URL数量为:{1}".format(len(self.now_queue), len(self.bak_queue))

        # 最后关闭打开的md5和rul文件
        self.md5_file.close()
        self.url_file.close()


if __name__ == "__main__":
    print '【Begin】---------------------------------------------------------------'
    start_time = time.time()
    CuiQingCaiBSF("http://cuiqingcai.com/").start_crawl()
    print '【End】下载了 {0} 个页面,花费时间 {1:.2f} 秒'.format(num_downloaded_pages, time.time() - start_time)

以上都是我的个人观点,如果有不对,或者有更好的方法,欢迎留言指正~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 下载 下载管理器 SDWebImageDownLoader作为一个单例来管理图片的下载操作。图片的下载是放在一个N...
    wind_dy阅读 1,445评论 0 1
  • 原文链接http://www.cnblogs.com/kenshincui/p/4186022.html 音频在i...
    Hyman0819阅读 21,661评论 4 74
  • “自律给我自由”,这次话是来自于KEEP运动软件里。 近期经常会想去这句话,从坚持写作到看书,到现在又重新开始起运...
    郭颜阅读 422评论 0 0
  • 在敢写下这些文字时我就不怕被别人知道我曾经做过传销,是,我是做过传销,可那又怎么了?我,有酒有故事,我的态度就是...
    沐沐与木木阅读 588评论 0 1