Scrapy爬取伯乐在线所有文章

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_1290259791/article/details/82318130

Scrapy爬取伯乐在线所有文章

爬取网站伯乐在线

1、目标分析

爬取内容:所有的文章及文章标题、发布时间、文章内容、文章url、文章图片url及下载、评论数目等…

处理内容:

  1. 将url进行md5处理后保存到mysql。
  2. 异步插入数据到mysql。
  3. 对文章内容进行简单处理。
  4. 自定义图片下载的管道。

使用方法:

  1. 在spider文件中使用ArticleItem类,来规范Item里的内容。
  2. 在spider文件中进行url的md5加密。
  3. 在items使用模块scrapy.loader.processors中的MapCompose、TakeFirst、Join类。
  4. 在items使用模块scrapy.loader中的ItemLoader类,来定义一个Item实例。
  5. 在main.py中定义启动方法。
  6. 在pipelines中使用ImagesPipeline图片下载管道、JsonItemExporterJSON文件的保存、adbapi来将数据异步保存在Mysql。

使用模块

Scrapy--1.5.1   
PyMySQL--0.9.2
Pillow--5.2.0

文件结构

├── bole
│   ├── __init__.py
│   ├── images      # 图片存取
│   │   └── full
│   ├── items.py    # 结构体定义
│   ├── main.py     # 启动文件
│   ├── middlewares.py      # 中间件
│   ├── pipelines.py    # 管道
│   ├── settings.py     # 配置文件
│   ├── spiders     # 爬虫文件编写
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   └── boleblog.py
│   └── utils   # 自定义的方法
│       ├── __init__.py
│       ├── __pycache__
│       └── common.py
└── scrapy.cfg

2、Spiders的编写

2.1、网站结构分析

我们爬取的是所有文章的url,在最新文章中我们发现这里是所有的文章,所以获取当前页面的文章的url,然后继续爬取下一页。

2.2、获取当页文章URL

我们使用Xpath来获取页面的所有URL。

2.3、获取文章的信息

首先获取文章的所有内容。

然后继续获取标题、文章内容等信息。
这是获取标题的xpath,获取其他信息类似。

2.4、文章列表下一页

继续编写Xpath获取下一页。

2.4、编写spiders.py

这里编写的是获取当前页面文章的url,和下一页的url,以及重复调用。

# -*- coding: utf-8 -*-
import scrapy
from scrapy import Request
from urllib import parse
from bole.items import ArticleItem, ArticleItemLoader
from bole.utils.common import get_md5


class BoleblogSpider(scrapy.Spider):
    name = 'boleblog'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/all-posts/']

    def parse(self, response):
        """
        1、获取文章列表中的url,并交给解析函数进行解析
        2、获取下一页的url并交给scrapy进行下载
        :param response:
        :return:
        """
        # 分析当前页面的文章url
        post_nodes = response.xpath('//*[@id="archive"]//div[@class="post-thumb"]')     # 首先获取文章的所有url
        for post_node in post_nodes:
            post_url = post_node.xpath('.//a/@href').extract_first()    # 文章的地址
            image_url = post_node.xpath('.//img/@src').extract_first()      # 文章图片的地址
            image_url = parse.urljoin(response.url, image_url)  #以前的文章图片是在本域名下,所以拼接一下。
            yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse_detail,
                          meta={'image_url': image_url})    # 用回调函数分析文章页面的元素
        # 提取下一页的url
        next_url = response.xpath('//*[@id="archive"]//a[contains(@class,"next")]/@href').extract_first()
        if next_url:
            yield Request(url=next_url, callback=self.parse)

先说获取文章数据的结构再来,继续爬取页面。

3、Item爬取数据结构的定义

  • 在提取的字段数很多时,各种提取规则会越来越多,维护也会变困难。
  • scrapy就提供了ItemLoader这样一个容器,在这个容器里面可以配置item中各个字段的提取规则。可以通过函数分析原始数据,并对Item字段进行赋值。

**Item和Itemloader区别:**Item提供保存抓取到数据的容器,而 Itemloader提供的是填充容器的机制。

3.1、Spiders编写

    def parse_detail(self, response):
        # 通过Itemloader加载Item
        image_url = response.meta.get('image_url')  # 传递图片的url
        item_loader = ArticleItemLoader(item=ArticleItem(), response=response)  # 将类设置为自定义的Itemloader类
        item_loader.add_xpath('title', '//*[@class="entry-header"]/h1/text()')  # 通过xpath来提取数据
        item_loader.add_value('url', response.url)  # 直接添加值
        item_loader.add_value('url_object_id', get_md5(response.url))
        item_loader.add_xpath('create_date', '//p[@class="entry-meta-hide-on-mobile"]/text()[1]')
        item_loader.add_value('image_url', [image_url])
        item_loader.add_xpath('praise_nums', "//span[contains(@class,'vote-post-up')]/h10/text()")
        item_loader.add_xpath('fav_nums', "//span[contains(@class,'bookmark-btn')]/text()")
        item_loader.add_xpath('comment_nums', "//a[@href='#article-comment']/span/text()")
        item_loader.add_xpath('content', '//*[@class="entry"]/p | //*[@class="entry"]/h3 | //*[@class="entry"]/ul')

        article_item = item_loader.load_item()  # 将规则进行解析,返回的是list
        yield article_item
  1. ArticleItemLoader是自定义的一个继承ItemLoader的类,其中的参数。
    • 第一个参数:Item对象是自己定义的Item传递的是一个实例。
    • 第二个参数:response就是提取的数据源。
  2. 生成的item_loader用了两个对象
    • add_xpath:第一个参数字段名,第二个参数提取的xpath规则。
    • add_value:第一个参数字段名,第二个参数直接添加值。
    • 这些字段默认情况下填充的都是list类型。
  3. 注意的是最后要调用load_item()方法,返回刚刚提取的数据。
  4. 我们还要对获取的字段做进一步处理。

3.2、Item自定义的类

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import datetime
import re

import scrapy
from scrapy import Field
from scrapy.loader.processors import MapCompose, TakeFirst, Join
from scrapy.loader import ItemLoader


class BoleItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass


def add_name(value):  # 接受的值就是title所有值,这值是列表的遍历
    return value + 'ok'


def date_convert(value):
    # 对时间进行处理格式
    value = value.strip().replace('·', '').strip()
    try:
        create_data = datetime.datetime.strptime(value, '%Y/%m/%d').date()
    except Exception as e:
        create_data = datetime.datetime.now().date()
    return create_data


def get_nums(value):
    # 处理评论和点赞的数
    math_re = re.search(r'(\d+)', value)
    if math_re:
        value = math_re.group(1)
    else:
        value = 0
    return value


class ArticleItemLoader(ItemLoader):
    # 自定义Itemloader
    default_output_processor = TakeFirst()  # 自定义ouput_processor


class ArticleItem(scrapy.Item):
    # 标题
    title = Field(
        input_processor=MapCompose(add_name, lambda x: x + 'no')  # 可以传递任意多的函数进行从左到右处理。
    )
    # 时间
    create_date = Field(
        input_processor=MapCompose(date_convert),
    )
    # 文章url
    url = Field()
    # 对url进行md5
    url_object_id = Field()
    # 文章图片
    image_url = Field(  # 因为要求返回list,不能用str
        output_processor=MapCompose(lambda x: x)    # 变为一个list,但是在插入mysql的时候要求是str。
    )
    image_path = Field()
    # 点赞数
    praise_nums = Field()
    # 评论数
    comment_nums = Field(
        input_processor=MapCompose(get_nums)
    )
    # 点赞数
    fav_nums = Field(
        input_processor=MapCompose(get_nums)
    )
    # 内容
    content = Field(
        output_processor=Join('\n')  # 不选择第一个使用Join来进行链接
    )
  1. 定义的字段结构体ArticleItem,其中Field字段有两个参数。
    • 输入处理器:input_process当传入字段值时,在传进来的时候对字段进行处理。
    • 输出处理器:output_process当字段处理完后,输出前的处理。
  2. TakeFirst是Scrapy提供的内置处理器,提取字段List中的第一个非空元素,用于输出。
  3. MapCompose能把多个函数执行的结果按顺序组合起来,产生最终的输出,通常用于输入处理器。
  4. Identity不进行任何处理,直接返回原来的数据,因为所有字段默认去取第一个非空元素,当我们要保留前面的空元素,就是用这个。
  5. Join返回用分隔符连接后的值,分隔符默认为空格。
  6. Compose用于多个函数的组合。List对象,依次被传递到第一个参数(就是处理函数),然后再穿入到第二个参数,默认情况遇到Node(List中有None值)就停止处理,参数stop_on_none = False取消停止。
  7. MapCompose用于多个函数的组合。List对象中的元素,List中的每一个元素依次传到第一个参数,然后第二个参数。返回None(会被下一个函数所忽略)。
  8. 因为返回的是列表每次都要输出第一个,所以我们定义一个ItemLoader类继承ItemLoader,重写default_output_processor,当我们重写output_processor时继承的就会失效。

4、启动函数

from scrapy.cmdline import execute
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute('scrapy crawl boleblog'.split())

5、保存文件到Mysql

主要说异步插入数据,暂时只支持关系数据库。

import pymysql
from twisted.enterprise import adbapi
class MysqlTwistedPipeline(object):
    """
    异步插入数据,暂时支持关系数据库。
    """

    def __init__(self, dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls, settings):  # 该方法就是获取setting中的配置信息
        dbparms = dict(
            host=settings['MYSQL_HOST'],
            db=settings['MYSQL_DBNAME'],
            user=settings['MYSQL_USER'],
            cursorclass=pymysql.cursors.DictCursor,
            charset='utf8',
            use_unicode=True,
        )
        dbpool = adbapi.ConnectionPool('pymysql', **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)  # 处理异常
        return item

    def handle_error(self, failure):
        # 处理异步插入的异常
        print('出现异常')

    def do_insert(self, cursor, item):
        # 执行具体的插入
        insert_sql = """
            insert into article(title,url,url_object_id,create_date,fav_nums) values(%s,%s,%s,%s,%s) 
        """
        cursor.execute(insert_sql,
                       (item['title'], item['url'], item['url_object_id'], item['create_date'], item['fav_nums']))
  1. 调用from twisted.enterprise import adbapi支持异步的adbapi。
  2. from_settings获取setting的配置信息。
  3. dbpool = adbapi.ConnectionPool(‘pymysql’, **dbparms)调用连接线程池使用pymysql将连接信息进行解包传入。
  4. self.dbpool.runInteraction(self.do_insert, item)调用启动的方法插入数据库。

总结

  1. 在使用Xpath选择元素的时候,在不确定的情况下最好不要用数组选择//span[4]因为你不确定其他以前的历史界面,最好还是用属性定位获取的内容。
  2. 使用ItemLoader来定义数据结构,以及TakeFirst、MapCompose、Join和input_process()\output_process()方法。
  3. 以及异步插入数据到Mysql。

猜你喜欢

转载自blog.csdn.net/qq_1290259791/article/details/82318130
今日推荐