• Python爬虫框架Scrapy学习笔记原创


    scrapy

    [TOC]

    开始

    scrapy安装

    1. 首先手动安装windows版本的Twisted

      https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

      pip install Twisted-18.4.0-cp36-cp36m-win_amd64.whl

    2. 安装scrapy

      pip install -i https://pypi.douban.com/simple/ scrapy

    3. windows系统额外需要安装pypiwin32

      pip install -i https://pypi.douban.com/simple pypiwin32

    新建项目

    开始一个项目

    E:svnProject> scrapy startproject TestSpider

    生成一个新的爬虫(generate)

    E:svnProject> cd TestSpider
    E:svnProjectTestSpider> scrapy genspider dongfeng www.dongfe.com

    启动一个爬虫

    E:svnProjectTestSpider> scrapy crawl dongfeng

    SHELL模式

    > scrapy shell http://www.dongfe.com/  # 命令行调试该网页

    pycharm调试启动文件

    E:svnProjectTestSpider> vim main.py
    
    import sys
    import os
    from scrapy.cmdline import execute
    
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(BASE_DIR)
    
    # scrapy crawl dongfeng
    execute(["scrapy", "crawl", "dongfeng"])

    项目基本配置

    E:svnProjectTestSpiderTestSpider> vim settings.py
    
    ROBOTSTXT_OBEY = False  # 不要遵循网站robots文件

    XPATH

    表达式说明
    /body 选出当前选择器的根元素body
    /body/div 选取当前选择器文档的根元素body的所有div子元素
    /body/div[1] 选取body根元素下面第一个div子元素
    /body/div[last()] 选取body根元素下面最后一个div子元素
    /body/div[last()-1] 选取body根元素下面倒数第二个div子元素
    //div 选取所有div子元素(不论出现在文档任何地方)
    body//div 选取所有属于body元素的后代的div元素(不论出现在body下的任何地方)
    /body/@id 选取当前选择器文档的根元素body的id属性
    //@class 选取所有元素的class属性
    //div[@class] 选取所有拥有class属性的div元素
    //div[@class='bold'] 选取所有class属性等于bold的div元素
    //div[contains(@class,'bold')] 选取所有class属性包含bold的div元素
    /div/* 选取当前文档根元素div的所有子元素
    //* 选取文档所有节点
    //div[@*] 获取所有带属性的div元素
    //div/a | //div/p 选取所有div元素下面的子元素a和子元素p(并集)
    //p[@id='content']/text() 选取id为content的p标签的内容(子元素的标签和内容都不会获取到)

    > 注意: XPATH在选择时,参考的是HTML源码,而不是JS加载后的HTML代码

    操作例子

    title_selector = response.xpath("//div[@class='entry-header']/h1/text()")  
    title_str = title_selector.extract()[0]

    CSS选择器

    表达式说明
    * 选择所有节点
    #container 选择Id为container的节点
    .container 选取所有包含container类的节点
    li a 选取所有li下的所有后代a元素(子和孙等所有的都会选中)
    ul + p 选取ul后面的第一个相邻兄弟p元素
    div#container > ul 选取id为container的div的所有ul子元素
    ul ~ p 选取与ul元素后面的所有兄弟p元素
    a[title] 选取所有有title属性的a元素
    a[href='http://taobao.com'] 选取所有href属性等于http://taobao.com的a元素
    a[href*='taobao'] 选取所有href属性包含taobao的a元素
    a[href^='http'] 选取所有href属性开头为http的a元素
    a[href$='.com'] 选取所有href属性结尾为.com的a元素
    input[type=radio]:checked 选取选中的radio的input元素
    div:not(#container) 选取所有id非container的div元素
    li:nth-child(3) 选取第三个li元素
    tr:nth-child(2n) 选取偶数位的tr元素
    a::attr(href) 获取所有a元素的href属性值

    操作例子

    h1_selector = response.css(".container h1::text")  # 选取h1标题的内容
    h1_str = h1_selector.extract_first()  # 取出数组第一个,如果没有为空

    爬虫

    爬取某网站文章列表例子

    >>> vim ArticleSpider/spiders/jobbole.py
    
    import scrapy
    from scrapy.http import Request
    from urllib import parse
    import re
    from ArticleSpider.items import ArticleItem
    from ArticleSpider.utils.common import get_md5  # url转md5
    
    class JobboleSpider(scrapy.Spider):
        name = 'jobbole'
        allowed_domains = ['blog.jobbole.com']
        start_urls = ['http://blog.jobbole.com/all-posts/']
    
        def parse(self, response):
            """
            文章列表页的文章链接解析
            :param response:
            :return:
            """
            css = "#archive > .post > .post-thumb > a"
            article_urls_selector = response.css(css)  # 获取当前列表页所有文章的链接
            for article_url_selector in article_urls_selector:
                head_img_url = article_url_selector.css("img::attr(src)").extract_first()  # 封面URL
                head_img_full_url = parse.urljoin(response.url, head_img_url)  # 封面图片完整URL
                article_url = article_url_selector.css("a::attr(href)").extract_first("")  # 文章URL
                article_full_url = parse.urljoin(response.url, article_url)  # 智能的拼接URL,相对地址直接对接;绝对地址只取出域名对接;完全地址不对接,直接获取。
                yield Request(url=article_full_url, callback=self.article_parse, meta={"head_img_full_url": head_img_full_url})  # 请求文章详情页并设置回调函数解析内容和meta传参
            next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
            if next_url:
                yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)  # 下一页文章列表使用递归
    
        def article_parse(self, response):
            """
            文章详情页的内容解析
            :param response:
            :return:
            """
            title = response.css(".grid-8 .entry-header > h1::text").extract_first("")  # 标题内容
            add_time = response.css(".grid-8 .entry-meta p::text").extract_first("")
            add_time_match = re.match("[sS]*?(d{2,4}[/-]d{1,2}[/-]d{1,2})[sS]*", add_time)
            if add_time_match:
                add_time = add_time_match.group(1)
            else:
                add_time = add_time.strip()
            content = response.css(".grid-8 .entry").extract_first("")  # 文章内容
            star = response.css("h10::text").extract_first("")  # 点赞数
            head_img_url = response.meta.get("head_img_full_url")  # 封面URL,通过上一个解释器在回调时传参得到的数据
    
            # 把数据整理到item
            article_item = ArticleItem()  # 实例化一个item
            article_item["title"] = title
            article_item["content"] = content
    
            # 把时间字符串转为可保存mysql的日期对象
            try:
                add_time = datetime.datetime.strptime(add_time, "%Y/%m/%d").date()
            except Exception as e:
                add_time = datetime.datetime.now().date()
            article_item["add_time"] = add_time
            article_item["star"] = star
            article_item["head_img_url"] = [head_img_url]  # 传递URL图片保存列表供ImagesPipeline使用
            article_item["url"] = response.url
            article_item["url_object_id"] = get_md5(response.url)  # 获取url的md5值
    
            yield article_item  # 传递到pipeline。请看settings.py中ITEM_PIPELINES字典
    # Item设计,类似于django的表单类
    >>> vim ArticleSpider/items.py
    import scrapy
    
    class ArticleItem(scrapy.Item):
        title = scrapy.Field()  # 标题
        content = scrapy.Field()  # 内容
        add_time = scrapy.Field()  # 文章添加时间
        url = scrapy.Field()  # 文章URL
        url_object_id = scrapy.Field()  # URL的MD5值
        head_img_url = scrapy.Field()  # 封面图URL
        head_img_path = scrapy.Field()  # 封面图本地路径
        star = scrapy.Field()  # 点赞数
    >>> vim ArticleSpider/spiders/settings.py
    # 修改配置文件,去掉这个地方的注释,当爬虫解析函数返回Item对象时,需要经过这个管道
    # Item管道
    ITEM_PIPELINES = {  # item的pipeline处理类;类似于item中间件
        'ArticleSpider.pipelines.ArticlespiderPipeline': 300,  # 处理顺序是按数字顺序,1代表第一个处理
    }
    # URL转md5函数
    >>> create ArticleSpider/utils/__init__.py  # 公共工具包
    >>> vim common.py
    import hashlib
    def get_md5(url):  # 获取URL的MD5值
        if isinstance(url, str):  # 如果是Unicode字符串
            url = url.encode("utf-8")
        m = hashlib.md5()
        m.update(url)  # 只接受UTF-8字节码
        return m.hexdigest()

    图片自动下载

    >>> vim ArticleSpider/settings.py
    
    PROJECT_DIR = os.path.join(BASE_DIR, "ArticleSpider")
    # 修改配置文件,去掉这个地方的注释,当爬虫解析函数返回Item对象时,需要经过这个管道
    # Item管道
    ITEM_PIPELINES = {  # item的pipeline处理类;类似于item中间件
        'ArticleSpider.pipelines.ArticlespiderPipeline': 300,  # 处理顺序是按数字顺序,1代表第一个处理
        'scrapy.pipelines.images.ImagesPipeline': 1,
    }
    
    IMAGES_URLS_FIELD = "head_img_url"  # 图片URL的字段名
    IMAGES_STORE = os.path.join(PROJECT_DIR, "images")  # 图片本地保存地址
    # IMAGES_MIN_HEIGHT = 100  # 接收图片的最小高度
    # IMAGES_MIN_WIDTH = 100  # 接收图片的最小宽度

    图片自动下载自定义类

    >>> vim ArticleSpider/settings.py
    
    ITEM_PIPELINES = {
        ...
        #'scrapy.pipelines.images.ImagesPipeline': 1,
        'ArticleSpider.pipelines.ArticleImagePipeline': 1,
    }
    >>> vim ArticleSpider/pipelines.py
    class ArticleImagePipeline(ImagesPipeline):
        def item_completed(self, results, item, info):
            if "head_img_url" in item:  # 只处理有数据的URL
                for ok, value in results:  # 默认是多个图片URL,其实只传递了一个,所以results内只有一个
                    image_file_path = value["path"]  # 获取图片保存的本地路径
    
            item["head_img_path"] = image_file_path
    
            return item  # 返回item,下一个pipeline接收处理

    数据保存

    把item数据导出到json文件中

    vim ArticleSpider/pipelines.py
    import codecs  # 文件操作模块
    import json
    # 把item保存到json文件
    class JsonWithEncodingPipeline(object):
        def __init__(self):
            self.file = codecs.open("article.json", 'w', encoding="utf-8")
    
        def process_item(self, item, spider):
            lines = json.dumps(dict(item), ensure_ascii=False) + "
    "  # 关闭ascii保存,因为有中文
            self.file.write(lines)
            return item
    
        def spider_closed(self, spider):
            # 当爬虫关闭时
            self.file.close()

    注册到item管道配置中

    
    vim ArticleSpider/settings.py
    
    ITEM_PIPELINES = { 
        '...',
        'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
    }

    使用自带的模块导出json文件

    >>> vim ArticleSpider/pipelines.py
    from scrapy.exporters import JsonItemExporter
    class JsonExporterPipeline(object):
        # 调用scrapy提供的json export导出json文件
        def __init__(self):
            self.file = open("articleexport.json", "wb")
            self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
            self.exporter.start_exporting()
    
        def process_item(self, item, spider):
            self.exporter.export_item(item)
            return item
    
        def close_spider(self, spider):
            self.exporter.finish_exporting()
            self.file.close()

    注册到item管道配置中

    vim ArticleSpider/settings.py
    
    ITEM_PIPELINES = { 
        '...',
        'ArticleSpider.pipelines.JsonExporterPipeline': 2,
    }

    使用mysql保存

    # 安装mysql驱动
    >>> pip install mysqlclient
    # centos需要另外安装驱动
    >>> sudo yum install python-devel mysql-devel

    使用同步的机制写入mysql

    import MySQLdb
    class MysqlPipeline(object):
        def __init__(self):
            self.conn = MySQLdb.connect('dongfe.com', 'root', 'Xiong123!@#', 'article_spider', charset="utf8", use_unicode=True)
            self.cursor = self.conn.cursor()
    
        def process_item(self, item, spider):
            insert_sql = """
                insert into article(title, url, add_time, star)
                values (%s, %s, %s, %s)
            """
            self.cursor.execute(insert_sql, (item['title'], item['url'], item['add_time'], item['star']))
            self.conn.commit()

    使用异步的机制写入mysql

    import MySQLdb
    import MySQLdb.cursors
    from twisted.enterprise import adbapi
    class MysqlTwistedPipline(object):
    
        def __init__(self, dbpool):
            self.dbpool = dbpool
    
        @classmethod
        def from_settings(cls, settings):
            pass  # 这个方法会把settings文件传进来
            dbparms = dict(
                host="dongfe.com",
                db="article_spider",
                user="root",
                passwd="Xiong123!@#",
                charset="utf8",
                cursorclass=MySQLdb.cursors.DictCursor,
                use_unicode=True,
            )
            dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
    
            return cls(dbpool)
    
        def process_item(self, item, spider):
            # 使用twisted将mysql插入变成异步执行
            query = self.dbpool.runInteraction(self.do_insert, item)
            query.addErrback(self.handle_error)  # 处理异常
    
        def do_insert(self, cursor, item):
            # 执行具体的插入
            insert_sql = """
                insert into article(title, url, add_time, star)
                values (%s, %s, %s, %s)
            """
            cursor.execute(insert_sql, (item['title'], item['url'], item['add_time'], item['star']))
    
        def handle_error(self, failure, item, spider):
            # 处理异步插入的异常
            print(failure)

    item loader

    直接将ItemCSS选择器绑定到一起,直接把选择出来的数据放入Item中。

    item loader的一般使用

    >>> vim ArticleSpider/spiders/jobbole.py
    
    class JobboleSpider(scrapy.Spider):
        name = 'jobbole'
        allowed_domains = ['blog.jobbole.com']
        start_urls = ['http://blog.jobbole.com/all-posts/']
    
        ...
    
        # 文章详情页的内容解析
        def article_parse(self, response):
    
            # 通过item loader加载item
            item_loader = ItemLoader(item=ArticleItem(), response=response)
    
            item_loader.add_css("title", ".grid-8 .entry-header > h1::text")
            item_loader.add_css("content", ".grid-8 .entry")
            item_loader.add_css("add_time", ".grid-8 .entry-meta p::text")
            item_loader.add_value("url", response.url)
            item_loader.add_value("url_object_id", get_md5(response.url))
            item_loader.add_value("head_img_url", [head_img_url])
            item_loader.add_css("star", "h10::text")
    
            article_item = item_loader.load_item()
            yield article_item  # 传递到pipeline。请看settings.py中ITEM_PIPELINES字典

    > 使用Item Loader的两个问题:
    >
    > 1. 原始数据需要处理
    > 1. 解决办法:在Item内使用字段的处理器
    > 2. 不管数据有几个,获取的是一个数组
    > 1. 解决办法:在Item内字段处理器中使用TakeFirst()方法

    配合item 的processor处理器的使用

    >>> vim ArticleSpider/items.py
    # MapCompose:可以调用多个函数依次运行
    # TakeFirst: 与extract_first()函数一样,只选择数组第一个数据
    # Join: 把数组用符号连接成字符串,比如Join(",")
    from scrapy.loader.processors import MapCompose, TakeFirst, Join  
    
    # add_time键处理函数
    def date_convert1(value):
        add_time_match = re.match("[sS]*?(d{2,4}[/-]d{1,2}[/-]d{1,2})[sS]*", value)
        if add_time_match:
            add_time = add_time_match.group(1)
        else:
            add_time = value.strip()
        return add_time
    
    def date_convert2(value):
        try:
            add_time = datetime.datetime.strptime(value, "%Y/%m/%d").date()
        except Exception as e:
            add_time = datetime.datetime.now().date()
        return add_time
    
    class ArticleItem(scrapy.Item):
        ...
        add_time = scrapy.Field(
            input_processor=MapCompose(date_convert1, date_convert2),  # 处理原始数据
            output_processor=TakeFirst()  # 只取数组中第一个数据
        )  # 文章添加时间
        ...

    自定义item loader

    > 可以设置默认的default_output_processor = TakeFirst()

    >>> vim ArticleSpider/items.py
    
    from scrapy.loader import ItemLoader
    class ArticleItemLoader(ItemLoader):
        # 自定义item loader
        default_output_processor = TakeFirst()
    
    # 默认item处理器,什么都不做
    def  default_processor(value):
        return value
    
    class ArticleItem(scrapy.Item):
        ...
        head_img_url = scrapy.Field(
            # 图像URL需要一个数组类型,不取第一个数据,定义一个默认处理器覆盖掉
            # 另外注意在使用sql保存时,需要取出数组第一个
            output_processor=default_processor  
        )  # 封面图URL
        ...
    ##########################################################################################
    >>> vim ArticleSpider/spiders/jobbole.py
    
    from ArticleSpider.items import ArticleItemLoader
    
    class JobboleSpider(scrapy.Spider):
        name = 'jobbole'
        allowed_domains = ['blog.jobbole.com']
        start_urls = ['http://blog.jobbole.com/all-posts/']
    
        ...
    
        # 文章详情页的内容解析厦工叉车
        def article_parse(self, response):
            ...
            # 通过item loader加载item
            item_loader = ArticleItemLoader(item=ArticleItem(), response=response)
            ...
  • 相关阅读:
    【Java开发记录】Dnslog访问记录模块实现(六)
    Java 内部类使用
    SQL 万能分页存储过程
    oom排查方法
    vite项目中报错require使用问题
    使用taro和原生开发小程序零散总结
    ECMscript与javaScript在不同平台下的关系
    微信createOffscreenCanvas将两张图合成一张图
    小程序使用taro将两张图合在一起,并保存到相册
    报Error: cannot find module 'XX/package.json'
  • 原文地址:https://www.cnblogs.com/xyou/p/9315959.html
Copyright © 2020-2023  润新知