zoukankan      html  css  js  c++  java
  • Scrapy 学习记录

    首先,安装virtualenv虚拟环境

    启动虚拟环境,进入希望保存项目的目录

    使用下面的命令新建一个scrapy的项目(由于pycharm中没有内置scrapy的项目,只能手动创建)

    scrapy startproject ArticleSpider(项目名称)

    系统返回表示成功

    New Scrapy project 'ArticleSpider', using template directory '/Users/qiuyang/virtualenv/scrapy/lib/python3.6/site-packages/scrapy/templates/project', created in:
        /Users/qiuyang/Documents/python/ArticleSpider
    
    You can start your first spider with:
        cd ArticleSpider
        scrapy genspider example example.com
    系统返回内容

    在pycharm中打开项目文件,设置解释器为虚拟环境,然后就可以进行代码的编写工作了

    在命令行,在项目目录中创建爬虫文件(指定爬虫文件名称,爬取起始地址):

    $ scrapy genspider jobbole blog.jobbole.com

    这时在spides目录中就新建了一个jobbole.py的文件

    由于pycharm没有默认的scrapy的debug模式,需要自行创建

    在项目的根目录下创建main.py文件

    from scrapy.cmdline import execute
    
    import sys,os
    # 获取项目路径,将项目路径添加到系统path路径中
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    # print(os.path.dirname(os.path.abspath(__file__)))
    
    # 执行命令,在命令行中是$ scrapy crawl jobbole
    execute(['scrapy','crawl','jobbole'])
    main.py

    执行前需要修改settings文件

    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False     
    #需要修改为False,这个配置默认读取每个网站上的robots协议,爬取时须关闭

    使用命令行在项目目录中执行命令:

    $ scrapy crawl jobbole

    可以看到程序正常执行

    此时可以使用pycharm的debug模式,在jobbole.py文件中设置断点

    点击debug运行程序后,在设置断点的位置将停止程序

    选择request时,会浮动一个框,可以点击+号查看详情

    这时可以看到返回的类型、url、body等内容,在最右边的view还可以看到详情

    开始爬取

    首先我们先选择一个页面:http://blog.jobbole.com/114329/

    打开页面之后,检查,这时我们就能看到网页上的代码

    注意:此时检查的代码与查看源文件的代码有可能不一致,是由于有些html代码是通过js渲染出来的,此时我们就需要针对源文件的代码进行查找,否则有可能出现找不到的情况。

    我们在jobbole.py文件中进行编写xpath查找代码:

        def parse(self, response):
            # 通过XPATH查找这个‘h1’标签
            re_selector = response.xpath('//*[@class="entry-header"]/h1')
            # 可以通过text()的方法获取内部文本信息
            re_selector_data = response.xpath('//*[@id="post-114329"]/div[1]/h1/text()')
            # 获取title的标签的文本信息
            title = response.xpath('//*[@class="entry-header"]/h1/text()')
    View Code

    由于如果经常这样调试,会反复进行网站请求,scrapy提供了一个shell的功能,可以在终端通过命令进行调试:

    # 在终端中,可以通过这个命令,对网址进行请求
    scrapy shell http://blog.jobbole.com/114329/
    
    #然后在终端中会显示如下:
    [s] Available Scrapy objects:
    [s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
    [s]   crawler    <scrapy.crawler.Crawler object at 0x10e40a6d8>
    [s]   item       {}
    [s]   request    <GET http://blog.jobbole.com/114329/>
    [s]   response   <200 http://blog.jobbole.com/114329/>
    [s]   settings   <scrapy.settings.Settings object at 0x10f1c76d8>
    [s]   spider     <DefaultSpider 'default' at 0x10f487898>
    [s] Useful shortcuts:
    [s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
    [s]   fetch(req)                  Fetch a scrapy.Request and update local objects 
    [s]   shelp()           Shell help (print this help)
    [s]   view(response)    View response in a browser
    
    # 这其中,response对象就是我们获取的html返回
    View Code

    我们可以通过命令进行本地调试:

    >>> title = response.xpath('//*[@class="entry-header"]/h1')
    >>> title
    [<Selector xpath='//*[@class="entry-header"]/h1' data='<h1>如何在 Linux Shell 编程中定义和使用函数</h1>'>]
    >>> title = response.xpath('//*[@class="entry-header"]/h1/text()')
    
    # 我们可以用extract()命令将selector对象转换为列表对象
    >>> title.extract()
    ['如何在 Linux Shell 编程中定义和使用函数']
    
    # 从列表对象中取出的第一个值,就是我们希望得到的字符串
    >>> title.extract()[0]
    '如何在 Linux Shell 编程中定义和使用函数'
    
    # 获取p标签的内容
    >>> create_date = response.xpath('//*[@id="post-114329"]/div[2]/p/text()')
    # 此时我们看到,标签内容中有很多的换行符,我们可以通过strip()命令进行过滤
    >>> create_date.extract()
    ['
    
     2018/08/29 · ', '
     
     
    
     
     · ', '
     
    ']
    # 过滤后的内容中还有一个‘.‘存在
    >>> create_date.extract()[0].strip()
    '2018/08/29 ·'
    # 我们可以将这个.进行替换操作
    >>> create_date.extract()[0].strip().replace('·','')
    '2018/08/29 '
    # 随后再进行一个strip()操作,即可得到一个理想的值
    >>> create_date.extract()[0].strip().replace('·','').strip()
    '2018/08/29'
    View Code

     随后我们希望获取文章的点赞数量,我们查看了HTML代码,发现点赞数量的html代码如下:

    我们使用如下代码,发现获取的列表为空,是因为xpath的class中,除了我们搜索的内容外,还有其他的内容

    # 这个代码无法获取内容
    response.xpath('//span[@class="vote-post-up"]')

    我们用contains语法进行获取,含义是:获取class中包含vote-post-up的标签

    response.xpath('//span[contains(@class="vote-post-up")]')

    随后我们再获取这个标签下面的h10标签的内容,进行extract,并获取列表的第一个值,此时获取到的是一个str类型的,由于希望获得的是数字类型的,我们再转换为数字类型的

    vote =  int(response.xpath('//span[contains(@class,"vote-post-up")]/h10/text()').extract()[0])

    获取点赞数量等

    # 首先我们获取点赞的标签文本,文本内容如:【1 点赞】        
    faver_num = response.xpath('//span[contains(@class,"bookmark-btn")]/text()').extract()[0]
    
    #我们需要将数字信息提取出来,用正则表达式进行提取
    match_fav_re = re.match(".*(d+).*",faver_num)
    
    #如果没人点赞的情况,我们默认设为0,如果有则取出,并转换为数字
            if match_fav_re:
                faver_num = int(match_fav_re.group(1))
            else:
                faver_num = 0

    最后获取一下标签信息

    tag_list = response.css('.entry-meta-hide-on-mobile a::text').extract()
    
    # 有可能获取到的列表中包含了不需要的项目如:【it技术,3评论,微世界】        
    # 列表去重,去掉以“评论”字符为结尾的所有项  
    tag_list = [element for element in tag_list if not element.strip().endswith("评论")]

    除了通过xpath方式,还可以通过css方式进行标签获取

    此时我们就针对每一个页面的内容获取完毕了,我们定义一个获取详情的函数

    def parse_detail(self,response):
        # 提取文章的具体字段
        # re_selector_data = response.xpath('//*[@id="post-114329"]/div[1]/h1/text()')
        title = response.xpath('//*[@class="entry-header"]/h1/text()').extract()[0]
    
        create_date = response.css('.entry-meta .entry-meta-hide-on-mobile::text').extract()[0].strip().replace('·','').strip()
        vote_num = int(response.xpath('//span[contains(@class,"vote-post-up")]/h10/text()').extract()[0])
        faver_num = response.xpath('//span[contains(@class,"bookmark-btn")]/text()').extract()[0]
        match_fav_re = re.match(".*(d+).*",faver_num)
        if match_fav_re:
            faver_num = int(match_fav_re.group(1))
        else:
            faver_num = 0
        comment_num = response.xpath('//a[@href="#article-comment"]/span/text()').extract()[0]
        match_com_re = re.match(".*(d+).*",comment_num)
        if match_com_re:
            comment_num = int(match_com_re.group(1))
        else:
            comment_num = 0
        cotent = response.xpath('//div[@class="entry"]').extract()[0]
        tag_list = response.css('.entry-meta-hide-on-mobile a::text').extract()
        # 列表去重,去掉以评论字符为结尾的所有项
        tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
        pass
    parse_detail

    在学习了itemload以后,可以对这里进行大幅修改,将获取的方式留下,处理方式则放在item中处理

    1、首先在item.py文件中新建一个类继承ItemLoader类,就可以修改其中的配置

    from scrapy.loader.processors import TakeFirst
    from scrapy.loader import ItemLoader
    
    # 自定义一个类,继承ItemLoader类,就可以修改其中的配置
    class ArticleItemLoader(ItemLoader):
        # 自定义itemloader,修改默认取第一个值,全部变为str
        default_output_processor = TakeFirst()
    View Code

    2、在jobbole.py文件中的类中新建

    class JobboleSpider(scrapy.Spider):
        name = 'jobbole'
        allowed_domains = ['blog.jobbole.com']
        start_urls = ['http://blog.jobbole.com/all-posts/']
    def parse_detail(self, response):
        # 提取文章的具体字段
        # 将原来代码中的meta传来的url获取一下
        front_image_url = response.meta.get('front_image_url', '')
    
        # 通过item loader加载item,通过自定义的ArticleItemLoader类进行实例对象,传入两个参数,一个是item对象,一个是获取的response
        item_loader = ArticleItemLoader(item=JobboleArticleItem(), response=response)
        # item_loader.add_xpath()
        item_loader.add_css('title', '.entry-header h1::text')
        item_loader.add_css('create_date', '.entry-meta .entry-meta-hide-on-mobile::text')
        item_loader.add_xpath('vote_num', '//span[contains(@class,"vote-post-up")]/h10/text()')
        item_loader.add_xpath('faver_num', '//span[contains(@class,"bookmark-btn")]/text()')
        item_loader.add_xpath('comment_num', '//a[@href="#article-comment"]/span/text()')
        item_loader.add_css('tag_list', '.entry-meta-hide-on-mobile a::text')
        item_loader.add_xpath('cotent', '//div[@class="entry"]')
        item_loader.add_value('front_image_url', [front_image_url])
        item_loader.add_value('url', response.url)
        item_loader.add_value('url_object_id', get_md5(response.url))
        # 调用默认的item方法以后,会将所有的值全部变为list形式
        article_item = item_loader.load_item()
    
        # yield之后,会传递到pipelines.py中,需要在settings中将pipelines生效,将系统已经生成好的ITEM_PIPELINES打开注释
        yield article_item
    parse_detail

    3、设置itme.py,新建了很多的方法,将方法传入item中

    # 测试函数,可以将值后面增加字符串
    def add_jobbole(value):
        return value+'jobbole'
    
    
    # 将日期格式字符串格式化为日期格式
    def date_convert(value):
        try:
            create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
        except Exception as e:
            create_date = datetime.datetime.now().date()
        return create_date
    
    
    # 获取文字中的数字的函数
    def get_num(value):
        match_re = re.match(".*(d+).*", value)
        if match_re:
            num = int(match_re.group(1))
        else:
            num = 0
        return num
    
    
    # 专门去掉标签中的'评论字样'
    def remove_comment_tags(value):
        if '评论' in value:
            return ''
        else:
            return value
    
    
    # 空函数,用于将值变为列表格式
    def return_value(value):
        return value
    
    
    # 自定义一个类,继承ItemLoader类,就可以修改其中的配置
    class ArticleItemLoader(ItemLoader):
        # 自定义itemloader,修改默认取第一个值,全部变为str
        default_output_processor = TakeFirst()
    
    
    class JobboleArticleItem(scrapy.Item):
        # scrapy只有一个Field类型
        # title = scrapy.Field()              # 文章名称
        # create_date = scrapy.Field()        # 创建日期
        # url = scrapy.Field()                # 文章url
        # url_object_id = scrapy.Field()      # url->md5
        # front_image_url = scrapy.Field()    # 封面图片
        # front_image_path = scrapy.Field()   # 封面图片存放路径
        # vote_num = scrapy.Field()           # 点赞数
        # faver_num = scrapy.Field()          # 收藏数
        # comment_num = scrapy.Field()        # 评论数
        # cotent = scrapy.Field()             # 文章正文html
        # tag_list = scrapy.Field()           # 文章标签
    
        title = scrapy.Field(
            # 给title全部增加一个jobbole的结尾,甚至可以调用多个函数
            #input_processor=MapCompose(add_jobbole, lambda x: x+'-Trunkslisa')
        )              # 文章名称
        create_date = scrapy.Field(
            # 给时间格式进行转换
            input_processor=MapCompose(date_convert),
            # 将获取的值取第一个
            # output_processor=TakeFirst()
        )        # 创建日期
        url = scrapy.Field()                # 文章url
        url_object_id = scrapy.Field()      # url->md5
        front_image_url = scrapy.Field(
            # 下载文件时,系统需要数组形式的数据,调用一个空函数,重新赋值
            output_processor=MapCompose(return_value),
        )    # 封面图片
        front_image_path = scrapy.Field()   # 封面图片存放路径
        vote_num = scrapy.Field(
            input_processor=MapCompose(get_num)
        )           # 点赞数
    
        faver_num = scrapy.Field(
            input_processor=MapCompose(get_num)
        )          # 收藏数
        comment_num = scrapy.Field(
            input_processor=MapCompose(get_num)
        )        # 评论数
        cotent = scrapy.Field()             # 文章正文html
        tag_list = scrapy.Field(
            # 由于获取的内容本身就是list,这时候使用默认的TakeFirst()就不是太合适了
            # 这时使用scrapy提供的Join类
            output_processor=Join(","),
        )           # 文章标签
    items.py

    获取全部文章

    1、获取文章列表页中的文章url 并scrapy下载后交给解析函数进行具体字段的获取
    2、获取下一页的url 并交给scrapy进行下载,下载完成后再交给parse函数

    定义parse函数

    from scrapy.http import Request
    
    def parse(self, response):
        """
        1、获取文章列表页中的文章url 并scrapy下载后交给解析函数进行具体字段的获取
        2、获取下一页的url 并交给scrapy进行下载,下载完成后再交给parse函数
        """
        post_list = response.css("#archive .floated-thumb .post-thumb a::attr(href)").extract()
        for plist in post_list:
            # 如果页面的url没有全部,只能提取到url的部分,需要针对url做拼接操作
            # Request(url=parse.urljoin(response.url, plist), callback=self.parse_detail)
            yield Request(url=plist, callback=self.parse_detail)
            # print(post_list)
    
        # 提取下一页的url,并交给scrapy进行下载
        # 同一个节点2个class标签,可以通过没有空格的方式指定
        next_urls = response.css(".next.page-numbers::attr(href)").extract()[0]
        if next_urls:
            yield Request(url=next_urls, callback=self.parse)
    View Code

    再看一下列表页,我们还有可能希望获取每条文章前面的图片,并增加到request中一起发给detail中进行保存

    图中可以看到图片的url与文章的url分别处于两个不同的div之中,scrapy可以进行再次筛选,并将image_url通过meta参数,按照字典的方式传入

    post_node = response.css("#archive .floated-thumb")
    for plist in post_node:
    
        image_url = plist.css('.post-thumb a img::attr(src)').extract()[0]
        post_url = plist.css('.post-thumb a::attr(href)').extract()[0]
        # 可以将image_url通过meta参数,按照字典的方式传入 
        # 如果页面的url没有全部,只能提取到url的部分,scrapy可以将response.url直接拼接进入;若已经有域名,则不生效
        yield Request(url=post_url, callback=self.parse_detail, meta={'front_image_url': image_url})

     通过debug方式可以看到,response中的meta里面,已经有front_image_url的值,现在我们在detail中按照字典方式将其取出

    # 通过字典方式将meta中的front_image_url取出,通过get方式防止异常,并赋一个空值
    front_image_url = response.meta.get('front_image_url', '')

    items.py

    定义一个数据类

    class JobboleArticleItem(scrapy.Item):
        # scrapy只有一个Field类型
        title = scrapy.Field()              # 文章名称
        create_date = scrapy.Field()        # 创建日期
        url = scrapy.Field()                # 文章url
        url_object_id = scrapy.Field()      # url->md5
        front_image_url = scrapy.Field()    # 封面图片
        front_image_path = scrapy.Field()   # 封面图片存放路径
        vote_num = scrapy.Field()           # 点赞数
        faver_num = scrapy.Field()          # 收藏数
        comment_num = scrapy.Field()        # 评论数
        cotent = scrapy.Field()             # 文章正文html
        tag_list = scrapy.Field()           # 文章标签
    View Code

    然后再jobbole.py中引入

    from ArticleSpider.ArticleSpider.items import JobboleArticleItem

     在parse_detail中实例化item

    # 实例化Item类
    article_item = JobboleArticleItem()
    
    # 给item设置的字段赋值
    article_item['title'] = title
    article_item['create_date'] = create_date
    article_item['url'] = response.url
    article_item['front_image_url'] = [front_image_url]
    article_item['vote_num'] = vote_num
    article_item['faver_num'] = faver_num
    article_item['comment_num'] = comment_num
    article_item['cotent'] = cotent
    article_item['tag_list'] = tag_list
    
    # 这是将获取到的url进行了md5的编码
    article_item['url_object_id'] = get_md5(response.url)
    
    # yield之后,会传递到pipelines.py中,需要在settings中将pipelines生效,将系统已经生成好的ITEM_PIPELINES打开注释
    yield article_item
    View Code

    建立一个文件处理md5编码的情况

    import hashlib
    
    
    def get_md5(url):
        # 判断传入的url是否是str格式的(str格式的默认都是unicode编码),并用utf8进行编码,用来确保可以进行md5运算
        if isinstance(url,str):
            url = url.encode('utf8')
        m = hashlib.md5()
        m.update(url)
        return m.hexdigest()
    View Code

    pipeline想要生效还需要在setting文件中进行设置

    # 默认路径是注释掉的,打开注释
    ITEM_PIPELINES = {
        'ArticleSpider.pipelines.ArticleSpiderPipeline': 300,
        # 如果希望使用系统的功能对图片进行下载,需要加入这个pipelines,并设置一下pipylines的执行顺序,数字越小,越早执行 
        'scrapy.pipelines.images.ImagesPipeline': 1,
    }
    
    # 这里设置一下是哪个字段为下载图片的字段
    IMAGES_URLS_FIELD = "front_image_url"
    # 获取工程路径
    project_dir = os.path.abspath(os.path.dirname(__file__))
    # 加入图片保存路径
    IMAGES_STORE = os.path.join(project_dir, 'image')
    
    # 配置下载100*100以上的图片
    IMAGES_MIN_HEIGHT = 100
    IMAGES_MIN_WIDTH = 100
    View Code

    还可以对下载图片进行配置,这里我们主要是希望能获得图片文件的保存路径,并赋值给item中我们设定的字段

    # 引入类
    from scrapy.pipelines.images import ImagesPipeline
    
    # 新建一个类,并继承系统的ImagesPipeline类
    class ArticleImagePipeline(ImagesPipeline):
        """
        自定义编写这个函数,可以通过results获取文件保存的路径
        通过debug可以看到results是一个元组形式的,元组第一个值是True,第二个值是一个字典。
        字典中有一个path的key,value是保存的地址
        通过一个for循环,将value取出,这个value是一个列表形式的
        """
        def item_completed(self, results, item, info):
            for ok, value in results:
                img_file_path = value['path']
            # 此时我们就可以将文件保存的路径保存在front_image_path中
            item['front_image_path'] = img_file_path
            # pipeline类必须要返回item
            return item    
    View Code

    自定义将结果保存为json文件

    import codecs   # codecs与open类似,但是减少了很多的编码工作
    import json
    
    # 自定义保存json的pipeline
    class JsonWithEncodingPipeline(object):
        def __init__(self):
            self.file = codecs.open('artcle.json','w',encoding='utf-8')
    
        # 处理item的写入文件的主要函数
        def process_item(self,item,spider):
            lines = json.dumps(dict(item), ensure_ascii=False) + "
    "
            self.file.write(lines)
            return item
    
        # 定义一个关闭文件的函数
        def spider_closed(self,spider):
            self.file.close()
    View Code

    使用scrapy自带的json文件保存模块

    # 调用scrapy提供的JsonExporter导出json文件
    class JsonExporterPipeline(object):
        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 close_spider(self,spider):
            self.exporter.finish_exporting()
            self.file.close()
    
        def process_item(self,item,spider):
            self.exporter.export_item(item)
            return item
    View Code

    将结果保存在mysql中,首先需要在mysql中新建一个表,并设置好字段

    # 需要在虚拟环境安装 mysqlclient
    # pip3 install mysqlclient
    
    import MySQLdb
    
    
    class MysqlPipeline(object):
        def __init__(self):
            # self.conn = MySQLdb.connect('host','user','password','dbname',charset="utf8",use_unicode=True)
            self.conn = MySQLdb.connect('127.0.0.1','root','123','article_spider',charset="utf8",use_unicode=True)
            self.cursor = self.conn.cursor()
    
        def process_item(self,item,spider):
            insert_sql = "insert into jobbole(title,create_date,url,url_object_id,front_image_url,front_image_path,vote_num,faver_num,comment_num,cotent) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            sql = self.cursor.execute(insert_sql, (
                item['title'],
                item['create_date'],
                item['url'],
                item['url_object_id'],
                item['front_image_url'],
                item['front_image_path'],
                item['vote_num'],
                item['faver_num'],
                item['comment_num'],
                item['cotent'],
                # 这里本来希望把标签也进行导入,但是实际操作过程中报错了,就先注释掉了
                # item['tag_list']
            ))
            self.conn.commit()    
    View Code
  • 相关阅读:
    大学那点破事
    我是计算机专业的学生
    acm 血泪教训
    汉诺塔问题(竟然还与Sierpiński三角形分形有关)
    证明:log(n!)与nlogn是等价无穷大
    priority_queue POJ 3253 Fence Repair
    插入排序之直接插入排序
    对Huffman编码的思考,熵
    Sudan Function
    给力小程序
  • 原文地址:https://www.cnblogs.com/trunkslisa/p/9555927.html
Copyright © 2011-2022 走看看