多线程爬虫python迷你定向网页抓取器mini_spider


背景:
【迷你定向网页抓取器】
在调研过程中,经常需要对一些网站进行定向抓取。由于python包含各种强大的库,使用python做定向抓取比较简单。请使用python开发一个迷你定向抓取器mini_spider.py,实现对种子链接的广度优先抓取,并把URL长相符合特定pattern的网页内容(图片或者html等)保存到磁盘上。

程序运行:python mini_spider.py -c conf/spider.conf
配置文件spider.conf:

[spider] 
url_list_file: ./urls ; 种子文件路径 
output_directory: ./output ; 抓取结果存储目录 
max_depth: 1 ; 最大抓取深度(种子为0级) 
crawl_interval: 1 ; 抓取间隔. 单位:秒 
crawl_timeout: 1 ; 抓取超时. 单位:秒 
target_url: .*\.(html|gif|png|jpg|bmp)$ ; 需要存储的目标网页URL pattern(正则表达式) ,需要考虑兼容抓取html等情况,不止是抓取图片
thread_count: 8 ; 抓取线程数 

种子文件每行一条链接,例如:
http://cup.baidu.com/spider/
http://www.baidu.com
http://www.sina.com.cn

一、整体架构

1.1 流程图

在这里插入图片描述

1.2 代码结构

project

mini_spider.py: 主程序入口  
lib目录:存放不同模块的代码  
    config_load.py: 读取配置文件  
    seedfile_load.py: 读取种子文件  
    url_table.py: 构建url管理队列  
    res_table.py: 结果保存队列  
    crawl_thread.py: 实现抓取线程  
    webpage_download.py: 下载网页  
    webpage_parse.py: 对抓取网页的解析  
    webpage_save.py: 将网页保存到磁盘   
logs目录: 存放日志文件  
tests目录: 存放单测文件  
    mini_spider_test.py: 单元测试  
conf目录: 存放配置文件  
    spider.conf: 配置文件  
README.md: 说明文档  
urls: 种子文件 

1.3 代码主逻辑

"""
Desc:    迷你定向网页抓取器
实现对种子链接的广度优先抓取,并把URL长相符合特定pattern的网页内容(图片或者html等)保存到磁盘上

Authors: zhoujie
Date:    2020/10/14
"""

import argparse
import time

import lib.log as log
import lib.config_load as config_load
import lib.seedfile_load as seedfile_load
import lib.url_table as url_table
import lib.res_table as res_table
import lib.crawl_thread as crawl_thread
import lib.webpage_save as webpage_save


def main(conf_path):
    """
    主程序入口
    :param conf_path: 配置文件路径
    :return:
    """
    # 日志初始化, 设置日志写入文件和控制台
    log.init_log('./logs/mini_spider_%s' % time.strftime('%Y-%m-%d', time.localtime(time.time())))

    # 读取配置文件
    spider_config = config_load.main(conf_path)
    # 读取种子文件
    seedfile_list = seedfile_load.main(spider_config.url_list_file)

    # 构建url管理队列
    url_queue = url_table.main()
    # 构建res保存队列
    res_queue = res_table.main()

    # 构建抓取线程列表
    crawl_thread_list = crawl_thread.main(url_queue, res_queue, spider_config.thread_count, spider_config.target_url,
                                          spider_config.crawl_interval, spider_config.crawl_timeout)
    # 构建收集结果线程
    save_thread = webpage_save.main(res_queue, spider_config.output_directory)

    # 对抓取进行控制
    crawl_thread.controller(seedfile_list, url_queue, res_queue, spider_config.max_depth, crawl_thread_list, save_thread)


if __name__ == '__main__':
    # 命令行参数处理
    desc = """迷你定向网页抓取器"""
    argparse = argparse.ArgumentParser(prog="mini_spider", description=desc)
    argparse.add_argument("-v",
                          "--version",
                          action="version",
                          version="%(prog)s 1.0",
                          help="显示版本信息")
    argparse.add_argument("-c",
                          "--config",
                          required=True,
                          help="必填选项,输入爬虫配置文件路径")
    args = argparse.parse_args()
    main(args.config)

二、知识学习总结

2.1 多线程 Threading

2.1.1 抓取线程

class CrawlThread(threading.Thread):
    """
    抓取线程
    """
    def __init__(self, url_queue, res_queue, crawl_interval, crawl_timeout, target_re,
                 thread_name="crawl_thread"):
        """
        抓取线程初始化
        :param url_queue: url管理队列
        :param res_queue: 结果保存队列
        :param crawl_interval: 抓取间隔. 单位:秒
        :param crawl_timeout: 抓取超时. 单位:秒
        :param target_re: 正则对象
        """
        threading.Thread.__init__(self)
        self.thread_name = thread_name
        self.url_queue = url_queue
        self.res_queue = res_queue
        self.crawl_interval = crawl_interval
        self.crawl_timeout = crawl_timeout
        self.target_re = target_re
        self.stop_event = threading.Event()
        self.url_list_cur = list()  # 待抓取url列表
        self.res_list_cur = list()  # 待保存res列表

    def run(self):
        """
        执行
        """
        try:
            logging.info("%s 初始化完成,开始执行" % self.thread_name)

            while not self.stop_event.is_set():
                crawl_url = self.url_queue.get()
                if crawl_url:
                    logging.info("%s 开始执行抓取url:%s" % (self.thread_name, crawl_url))
                    webpage_res = webpage_download.main(self.thread_name, crawl_url, self.crawl_timeout)
                    url_list, res_list = webpage_parse.main(crawl_url, webpage_res, self.target_re)
                    self.url_list_cur.extend(url_list)
                    self.res_list_cur.extend(res_list)
                    self.url_queue.task_done()
                time.sleep(self.crawl_interval)
        except Exception as e:
            logging.error("保存抓取文件异常, msg is [%s]" % traceback.format_exc())

    def get_url_list_cur(self):
        """
        获取待抓取url列表
        """
        return self.url_list_cur

    def get_res_list_cur(self):
        """
        获取待保存res列表
        """
        return self.res_list_cur

    def clear(self):
        """
        清理待抓取url列表与保存res列表
        """
        self.url_list_cur.clear()
        self.res_list_cur.clear()

    def stop(self):
        """
        结束线程
        """
        self.stop_event.set()

2.1.2 保存线程

class SaveThread(threading.Thread):
    """
    保存线程
    """
    def __init__(self, res_queue, output_directory, thread_name="save_thread"):
        """
        初始化保存线程
        :param res_queue: 结果保存队列
        :param output_directory: 抓取结果存储目录
        """
        threading.Thread.__init__(self)
        self.thread_name = thread_name
        self.res_queue = res_queue
        self.output_directory = output_directory
        self.stop_event = threading.Event()

    def run(self):
        """
        启动线程
        """
        try:
            logging.info("%s 初始化完成,开始执行" % self.thread_name)

            with open(self.output_directory, 'w') as outfile:
                while not self.stop_event.is_set():
                    res = self.res_queue.get()
                    if res:
                        logging.info("保存 %s" % res)
                        outfile.write(res + os.linesep)
                        self.res_queue.task_done()
        except Exception as e:
            logging.error("保存抓取文件异常, msg is [%s]" % traceback.format_exc())

    def stop(self):
        """
        结束线程
        """
        self.stop_event.set()

2.2 线程间的同步 Queue

2.2.1 UrlQueue

class UrlQueue(object):
    """
    url管理队列
    """
    def __init__(self):
        """
        初始化url队列
        """
        self.url_queue = queue.Queue()
        self.url_set = set()  # 去重
        self.timeout = 5  # 获取队列,timeout等待时间

    def put_list(self, url_list):
        """
        新增url列表
        """
        if isinstance(url_list, list):
            for url in url_list:
                if url is not None and url not in self.url_set:
                    self.url_queue.put(url)
                    self.url_set.add(url)

    def get(self):
        """
        取url
        """
        try:
            return self.url_queue.get(timeout=self.timeout)
        except Exception as e:
            logging.info("队列中暂无url")

    def task_done(self):
        """
        已完成
        """
        self.url_queue.task_done()

    def join(self):
        """
        阻塞
        """
        self.url_queue.join()

2.2.2 ResQueue

class ResQueue(object):
    """
    res结果保存队列
    """

    def __init__(self):
        """
        初始化res队列
        """
        self.res_queue = queue.Queue()
        self.timeout = 5  # 获取队列,timeout等待时间

    def put_list(self, res_list):
        """
        新增res列表
        """
        if isinstance(res_list, list):
            for res in res_list:
                self.res_queue.put(res)

    def get(self):
        """
        取结果
        """
        try:
            return self.res_queue.get(timeout=self.timeout)
        except Exception as e:
            logging.info("队列中暂无res")

    def task_done(self):
        """
        已完成
        """
        self.res_queue.task_done()

    def join(self):
        """
        阻塞
        """
        self.res_queue.join()

2.3 核心控制器部分 controller

def controller(seedfile_list, url_queue, res_queue, max_depth, crawl_thread_list, save_thread):
    """
    构建启动抓取线程
    :param seedfile_list: 种子列表
    :param url_queue: url管理队列
    :param res_queue: res保存队列
    :param max_depth: 最大抓取深度
    :param crawl_thread_list: 抓取线程列表
    :param save_thread: 收集结果线程
    :return:
    """
    # 加入urls中初始链接
    url_queue.put_list(seedfile_list)
    # 启动抓取线程
    for crawl_thread in crawl_thread_list:
        crawl_thread.start()
    # 启动保存线程
    save_thread.start()

    cur_depth = 1
    while True:
        logging.info("***************************** 当前抓取深度:%d,最大抓取深度:%d *****************************"
                     % (cur_depth, max_depth))
        url_queue.join()
        # 深度 cur_depth 抓取结束,更新 url_queue、res_queue
        if cur_depth < max_depth:
            for crawl_thread in crawl_thread_list:
                url_queue.put_list(crawl_thread.get_url_list_cur())
                res_queue.put_list(crawl_thread.get_res_list_cur())
                crawl_thread.clear()
        elif cur_depth == max_depth:
            for crawl_thread in crawl_thread_list:
                res_queue.put_list(crawl_thread.get_res_list_cur())
                crawl_thread.clear()
                crawl_thread.stop()
            break
        cur_depth += 1

    res_queue.join()
    save_thread.stop()

    for crawl_thread in crawl_thread_list:
        crawl_thread.join()

2.4 页面内容处理 bs4

2.4.1 页面下载

def is_url(url):
    """
    判断url是否合法
    """
    pattern = r"^https?:/{2}\w.+$"
    res = re.match(pattern, url)
    if res:
        return True
    else:
        return False


def main(thread_name, crawl_url, crawl_timeout):
    """
    下载url
    :param thread_name: 线程名
    :param crawl_url: 下载url
    :param crawl_timeout: 抓取超时时间
    :return: 返回url对应的网页内容
    """
    crawl_res = None
    try:
        if is_url(crawl_url):
            crawl_res_req = requests.get(crawl_url, timeout=crawl_timeout)
            if crawl_res_req and crawl_res_req.status_code == requests.codes.ok:
                crawl_res = crawl_res_req.text
                logging.info('%s 从 url:%s 抓取页面信息,status_code: %s'
                             % (thread_name, crawl_url, crawl_res_req.status_code))
        else:
            logging.error("url:%s 是无效地址" % crawl_url)
    except Exception as e:
        # 打印异常,只打印第一和最后一行
        formatted_lines = traceback.format_exc().splitlines()
        logging.error("下载 url:%s 失败, %s, %s" % (crawl_url, formatted_lines[0], formatted_lines[-1]))
    finally:
        return crawl_res

2.4.2 页面解析

def get_url_list_or_res_list(crawl_url, urls, target_re):
    """
    获取 url_list 、res_list
    :param crawl_url: 当前抓取url
    :param urls: 页面元素
    :param target_re: 正则对象
    :return 返回的url_list 或 res_list
    """
    fanal_list = []
    for url in urls:
        href_url = url.get("href")
        if not href_url:
            continue
        elif href_url.startswith("http"):
            final_url = href_url
        elif "javascript:location.href" in href_url:
            # javascript:location.href="/url"
            final_url = url_parse.urljoin(crawl_url, href_url.split("=")[-1].split("\"")[-2])
        else:
            final_url = url_parse.urljoin(crawl_url, href_url)
        # 匹配校验
        if target_re.match(final_url):
            fanal_list.append(final_url)
    return fanal_list


def main(crawl_url, webpage_res, target_re):
    """
    下载url
    :param crawl_url: 当前抓取url
    :param webpage_res: 下载的网页内容
    :param target_re: 正则对象
    :return url_list: 子网页的url
    :return res_list: 网页抓取内容
    """
    url_list = []
    res_list = []
    if webpage_res:
        soup = bs4.BeautifulSoup(webpage_res, "html.parser")
        # 获取所有的<a><link><img>
        tag_a_link_res = soup.find_all(["a", "link"])
        tag_a_link_img_res = soup.find_all(["a", "link", "img"])

        url_list = get_url_list_or_res_list(crawl_url, tag_a_link_res, target_re)
        res_list = get_url_list_or_res_list(crawl_url, tag_a_link_img_res, target_re)
        logging.info("%s 页面含新url个数:%s,页面获取到符合正则限定的数据个数:%s" % (crawl_url, len(url_list), len(res_list)))
    return url_list, res_list
  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
在进行调研过程中,经常需要对一些网站进行定向抓取。而使用Python作为开发语言是一个不错的选择,因为Python拥有各种强大的库和框架。 首先,Python中最常用的库之一是Requests库。该库提供了简洁的API,使得与网站进行HTTP请求变得非常容易。通过使用Requests库,我们可以发送GET或POST请求,并能够轻松地获取网站的内容。使用该库,我们可以方便地获取网页的源代码,并从中提取我们感兴趣的数据。 其次,Python还有另一个强大的库叫做BeautifulSoup。BeautifulSoup是用来解析HTML和XML文档的库,它可以将网页源代码转换为可读性强的树形结构。通过使用这个库,我们可以方便地从网页中提取所需信息,并进行进一步的处理。BeautifulSoup还提供了一些高级特性,如CSS选择和正则表达式,使得定向抓取变得更加灵活和强大。 另外,对于一些JavaScript动态加载的网页内容,Python中也有相应的解决方案。Selenium是一个流行的Python库,它可用于自动化浏览操作。通过Selenium,我们可以模拟用户在网页上的操作,例如点击按钮、填写表单等。这样一来,我们就能够获取到网页中 JavaScript 动态加载的内容,并进行进一步的处理。 此外,Python还有其他一些用于数据抓取的库,如Scrapy,它是一个功能强大的网络爬虫框架,它可以用于高效的数据抓取和处理。Scrapy提供了丰富的功能,如自动化网页跟踪、XPath和正则表达式匹配等,使得网站定向抓取变得更加高效和可靠。 总之,Python拥有许多强大的库和框架,使其成为调研中进行网站定向抓取的理想选择。无论是发送HTTP请求、解析网页、处理 JavaScript 动态加载的内容,还是进行高效的数据抓取Python都能提供丰富而灵活的工具和功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值