Python爬虫之Scrapy框架的使用(二)

一:创建爬虫

scrapy startproject ArticleSpider
cd ArticleSpider
scrapy genspider cnblogs cnblogs.com

二:settings配置文件

ROBOTSTXT_OBEY = False
DOWNLOAD_DELAY = 1
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11'
}
LOG_LEVEL = "WARNING"		# 增加此行配置

三:创建启动文件

from scrapy.cmdline import execute
import sys, os

sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(['scrapy', 'crawl', 'cnblogs'])

四:爬虫文件内容

import scrapy
from urllib import parse
import json
from ArticleSpider.items import ArticlespiderItem
from ArticleSpider.utils.common import get_url_md5

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs_bak'
    allowed_domains = ['cnblogs.com']
    start_urls = ['https://news.cnblogs.com/']

    def parse(self, response):
        # news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
        news_list = response.css('div#news_list div.news_block')
        for div in news_list:
            detail_url = div.xpath('.//h2/a/@href').get()
            thumbnail_url_data = div.xpath('.//img[@class="topic_img"]/@src').get()
            if not thumbnail_url_data:
                thumbnail_url = []
            elif thumbnail_url_data.startswith('/'):
                thumbnail_url = ['https:' + thumbnail_url_data]
            else:
                thumbnail_url = [thumbnail_url_data]
            yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
        # 提取下一页
        next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
        yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

    def parse_detail(self, response):
        thumbnail_url = response.meta.get('thumbnail_url', '')
        title = response.xpath('//div[@id="news_title"]/a/text()').get()
        author = response.xpath('//div[@id="news_info"]/span[@class="news_poster"]/a/text()').get()
        pubdate = response.xpath('//div[@id="news_info"]/span[@class="time"]/text()').get()
        pubdate = pubdate.replace('发布于', '').lstrip()
        content = ''.join(response.xpath('//div[@id="news_body"]//*').getall())
        tag_list = response.xpath('//div[@class="news_tags"]//a/text()').getall()
        tags = ','.join(tag_list)
        origin_url = response.url
        url_hash_id = get_url_md5(origin_url)
        content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
        item = ArticlespiderItem(title=title, author=author, pubdate=pubdate, content=content, tags=tags,thumbnail_url=thumbnail_url, origin_url=origin_url, url_hash_id=url_hash_id)
        # 不建议将再有的url请求放在该方法中,建议将url请求 yield 出去
        yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)),callback=self.parse_json_data, meta={'item': item})

    def parse_json_data(self, response):
        item = response.meta.get('item')
        json_data = json.loads(response.text)
        item['dig_count'] = json_data.get('DiggCount')
        item['comment_count'] = json_data.get('CommentCount')
        item['view_count'] = json_data.get('TotalView')
        yield item

五:item文件

class ArticlespiderItem(scrapy.Item):
    title = scrapy.Field()
    author = scrapy.Field()
    pubdate = scrapy.Field()
    content = scrapy.Field()
    tags = scrapy.Field()
    dig_count = scrapy.Field()
    comment_count = scrapy.Field()
    view_count = scrapy.Field()
    thumbnail_url = scrapy.Field()
    thumbnail_path = scrapy.Field()
    origin_url = scrapy.Field()
    url_hash_id = scrapy.Field()

六:pipelines文件

import codecs
import json
import pymysql
from scrapy.exporters import JsonItemExporter
from scrapy.pipelines.images import ImagesPipeline
from twisted.enterprise import adbapi

class ArticlespiderPipeline:
    def process_item(self, item, spider):
        return item

class ArticleImagesPipeline(ImagesPipeline):
	# 图片下载的pipeline
    def item_completed(self, results, item, info):
        if 'thumbnail_url' in item:
            thumbnail_path = ''
            for ok, value in results:
                thumbnail_path = value['path'] or ''
            item['thumbnail_path'] = thumbnail_path
            # thumbnail_path保存格式为:full/xxxx.jpg
        return item

class JsonWithEncodingPipeline(object):
    '''自定义导出为json文件pipeline,一行一条数据'''
    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) + '\n'
        self.file.write(lines)
        return item
    def spider_close(self, spider):
        self.file.close()

class JsonExporterPipeline:
    '''
    自定义导出为json文件pipeline,保存后的数据为一个列表
    该类中默认会执行三个方法:open_spider, close_spider,process_item。
    '''
    def __init__(self):
        self.file = open('article_export.json', 'wb')
        self.exporter = JsonItemExporter(self.file, ensure_ascii=False, encoding='utf-8')
        self.exporter.start_exporting()
    def open_spider(self, spider):
        print('start...')
    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()
        print('finish...')

class MysqlPipeline:
    '''保存数据到mysql数据库pipeline'''
    def __init__(self):
        self.conn = pymysql.connect(host='172.17.2.36', port=3306, user='root', password='xxxxxx',database='spider', charset='utf8')
        self.cursor = self.conn.cursor()
    def open_spider(self, spider):
        print('start...')
    def process_item(self, item, spider):
        insert_sql = 'insert into cnblogs values(%s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
        self.cursor.execute(insert_sql, (
            item.get('title', ''),
            item.get('author', ''),
            item.get('pubdate', '1970-07-01'),
            item.get('content', ''),
            item.get('origin_url', ''),
            item.get('url_hash_id', ''),
            item.get('tags', ''),
            item.get('dig_count', 0),
            item.get('comment_count', 0),
            item.get('view_count', 0),
            ''.join(item.get('thumbnail_url', [])),
            item.get('thumbnail_path', '')
        ))
        self.conn.commit()
        return item
    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()
        print('finish...')

class MysqlTwistedPipeline:
    # 自定义异步保存数据到mysql数据库类
    def __init__(self, dbpool):
        self.dbpool = dbpool
    @classmethod
    def from_settings(cls, settings):
        # 从settings配置文件读取参数
        db_params = dict(
            host=settings['MYSQL_HOST'],
            port=settings['MYSQL_PORT'],
            database=settings['MYSQL_DATABASE'],
            user=settings['MYSQL_USER'],
            password=settings['MYSQL_PASSWORD'],
            charset='utf8',
            cursorclass=pymysql.cursors.DictCursor
        )
        db_pool = adbapi.ConnectionPool('pymysql', **db_params)
        return cls(db_pool)
    def process_item(self, item, spider):
        query = self.dbpool.runInteraction(self.insert_item, item)
        query.addErrback(self.handle_error, item, spider)
        return item
    def insert_item(self, cursor, item):
        insert_sql = 'insert into cnblogs values(%s, %s, %s,%s,%s,%s,%s,%s,%s,%s,%s,%s) on duplicate key update view_count=values(view_count)'
        cursor.execute(insert_sql, (
            item.get('title', ''),
            item.get('author', ''),
            item.get('pubdate', '1970-07-01'),
            item.get('content', ''),
            item.get('origin_url', ''),
            item.get('url_hash_id', ''),
            item.get('tags', ''),
            item.get('dig_count', 0),
            item.get('comment_count', 0),
            item.get('view_count', 0),
            ''.join(item.get('thumbnail_url', [])),
            item.get('thumbnail_path', '')
        ))
    def handle_error(self, error, item, spider):
        print('=' * 10 + 'error' + '=' * 10)
        print(error)

settings文件配置:

ITEM_PIPELINES = {
    'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
    # 'scrapy.pipelines.images.ImagesPipeline': 1,
    'ArticleSpider.pipelines.ArticleImagesPipeline': 1,
    # 'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
    # 'ArticleSpider.pipelines.JsonExporterPipeline': 3,
    # 'ArticleSpider.pipelines.MysqlPipeline': 4,
    'ArticleSpider.pipelines.MysqlTwistedPipeline': 5,
}
# 图片下载pipeline相关配置
IMAGES_URLS_FIELD = 'thumbnail_url'
IMAGES_STORE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'images')
# mysql数据库信息配置
MYSQL_HOST = '172.17.2.36'
MYSQL_PORT = 3306
MYSQL_DATABASE = 'spider'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'xxxxxx'

到此,运行爬虫启动文件即可爬取数据。

七:使用scrapy中的ItemLoader提取数据

当项目很大,提取的字段数以百计,那么各种提取规则会越来越多,按照这种方式来做,维护的工作将会是一场噩梦!所以scrapy就提供了ItemLoader这样一个容器,在这个容器里面可以配置item中各个字段的提取规则。可以通过函数分析原始数据,并对Item字段进行赋值,非常的便捷。
可以这么来看 Item 和 Itemloader:Item提供保存抓取到数据的容器,而 Itemloader提供的是填充容器的机制。
Itemloader提供的是一种灵活,高效的机制,可以更方便的被spider或source format (HTML, XML, etc)扩展并重写,更易于维护,尤其是分析规则特别复杂繁多的时候。

7.1 爬虫文件内容

import scrapy
from urllib import parse
import json
from scrapy.loader import ItemLoader
from ArticleSpider.items import ArticlespiderItem
from ArticleSpider.utils.common import get_url_md5

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    allowed_domains = ['cnblogs.com']
    start_urls = ['https://news.cnblogs.com/']
    
    def parse(self, response):
        # news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
        news_list = response.css('div#news_list div.news_block')
        for div in news_list:
            detail_url = div.xpath('.//h2/a/@href').get()
            # 因为ImagesPipeline要求图片url是一个列表,所以上面的案例对该字段进行了处理,但使用itemload,这里传递一个空字符串,itemload不会报错会转换成一个空列表
            thumbnail_url = div.xpath('.//img[@class="topic_img"]/@src').get()
            yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
        # 提取下一页
        # next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
        # yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

    def parse_detail(self, response):
        # 使用item_loader提取数据。下面使用css选择器提取数据,当然也可以使用
        item_loader = ItemLoader(item = ArticlespiderItem(), response = response)
        # 使用item_loader提取的数据都是list类型
        item_loader.add_css('title', '#news_title a::text')
        item_loader.add_css('author', '.news_poster a::text')
        item_loader.add_css('content', '#news_content')
        item_loader.add_css('tags', '.news_tags a::text')
        item_loader.add_css('pubdate', '#news_info .time::text')
        item_loader.add_value('origin_url', response.url)
        item_loader.add_value('thumbnail_url', response.meta.get('thumbnail_url', ''))
        item_loader.add_value('url_hash_id', get_url_md5(response.url))
        # 当所有数据被收集起来之后,还需要调用 ItemLoader.load_item() 方法, 实际上填充并且返回了之前通过调用 add_xpath(),add_css(),and add_value() 所提取和收集到的数据。
        item = item_loader.load_item()
        content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
        # 不建议将url请求放在该方法中,建议将url请求 yield 出去
        yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)),callback=self.parse_json_data, meta={'item': item})

    def parse_json_data(self, response):
        item = response.meta.get('item')
        json_data = json.loads(response.text)
        item['dig_count'] = json_data.get('DiggCount')
        item['comment_count'] = json_data.get('CommentCount')
        item['view_count'] = json_data.get('TotalView')
        yield item

7.2 input_processor和output_processor

使用输入处理器input_processor和输出处理器output_processor对提取到的数据进行加工处理。
从上面的示例中,可以看到,存在两个问题:
第一,提取的数据,填充进去的对象都是List类型。而我们大部分的需求是要取第一个数值,取List中的第一个非空元素,那么如何实现取第一个呢?
第二,在做item字段解析时,经常需要再进一步解析,过滤出我们想要的数值,例如用正则表达式将 $10 price中的数字10提取出来。那么又如何对字段加一些处理函数呢?

input_processor and output_processor在item文件定义字段时对数据进行处理,下面举例说明用法:

from scrapy.loader.processors import MapCompose, TakeFirst
def date_convert(value):
    # 这里的value传递过来的就是列表中的元素
    if value:
        value = re.sub('[\u4e00-\u9fa5]', '', value).strip()  # 去除字符串中的中文字符
        try:
            create_date = datetime.datetime.strptime(value, "%Y/%m/%d").date()
        except Exception as e:
            create_date = datetime.datetime.now().date()
        return create_date
class ArticlespiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    author = scrapy.Field()
    pubdate = scrapy.Field(input_processor=MapCompose(date_convert), output_processor=TakeFirst())
    # 可以定义多个函数,按顺序写到MapCompose中
    ......................

TakeFirst:返回第一个非空(non-null/ non-empty)值,常用于单值字段的输出处理器,无参数。
MapCompose:与Compose处理器类似,区别在于各个函数结果在内部传递的方式(会涉及到list对象解包的步骤):

  1. 输入值是被迭代的处理的,List对象中的每一个元素被单独传入,第一个函数进行处理,然后处理的结果被连接起来形成一个新的迭代器,并被传入第二个函数,以此类推,直到最后一个函数。最后一个函数的输出被连接起来形成处理器的输出。
  2. 每个函数能返回一个值或者一个值列表,也能返回None(会被下一个函数所忽略)
  3. 这个处理器提供了很方便的方式来组合多个处理单值的函数。因此它常用于输入处理器,因为传递过来的是一个List对象。

Compose:用给定的多个函数的组合,来构造的处理器。list对象(注意不是指list中的元素),依次被传递到第一个函数,然后输出,再传递到第二个函数,一个接着一个,直到最后一个函数返回整个处理器的输出。默认情况下,当遇到None值(list中有None值)的时候停止处理。可以通过传递参数stop_on_none = False改变这种行为。
Identity:最简单的处理器,不进行任何处理,直接返回原来的数据。无参数。
Join:返回用分隔符连接后的值。分隔符默认为空格。不接受Loader contexts。当使用默认分隔符的时候,这个处理器等同于:u’ '.join

7.3 重用和扩展ItemLoaders

从上面的信息来看,ItemLoaders是非常灵活的,但是假设有个需求,所有的字段,我们都要去取第一个,那么如果有300个字段,我们就要添加300次,每个都要写,就会觉得很麻烦。那么有没有办法统一设置呢,答案是有的,如下:
如果想要实现每个字段都只取第一个,那么可以定义一个自己的ItemLoader类:ArticleItemLoader(继承自ItemLoader类)
我们首先可以看一下原始的 ItemLoader 的定义:

class ItemLoader:
	# 可以看到是有默认的输入/输出处理器的,而且默认是什么都不做
    default_item_class = Item
    default_input_processor = Identity()
    default_output_processor = Identity()
    default_selector_class = Selector

在item文件中自定义一个ItemLoader类:

def date_convert(value):
    # 这里的value传递过来的就是列表中的元素
    if value:
        value = re.sub('[\u4e00-\u9fa5]', '', value).strip()  # 去除字符串中的中文字符
        try:
            pubdate = datetime.datetime.strptime(value, "%Y/%m/%d").date()
        except Exception as e:
            pubdate = datetime.datetime.now().date()
        return pubdate
class ArticleItemLoad(ItemLoader):
    default_output_processor = TakeFirst()
class ArticlespiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    author = scrapy.Field()
    pubdate = scrapy.Field(input_processor=MapCompose(date_convert))
    content = scrapy.Field()
    tags = scrapy.Field(output_processor=Join(separator=','))
    dig_count = scrapy.Field()
    comment_count = scrapy.Field()
    view_count = scrapy.Field()
    thumbnail_url = scrapy.Field(output_processor=Identity())
    thumbnail_path = scrapy.Field()
    origin_url = scrapy.Field()
    url_hash_id = scrapy.Field()

下面把爬虫文件修改全部使用自定义的ItemLoader类:

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    allowed_domains = ['cnblogs.com']
    start_urls = ['https://news.cnblogs.com/']

    def parse(self, response):
        # news_list = response.xpath('//div[@id="news_list"]//div[@class="news_block"]')
        news_list = response.css('div#news_list div.news_block')
        for div in news_list:
            detail_url = div.xpath('.//h2/a/@href').get()
            thumbnail_url_data = div.xpath('.//img[@class="topic_img"]/@src').get()
            if not thumbnail_url_data:
                thumbnail_url = []
            elif thumbnail_url_data.startswith('/'):
                thumbnail_url = ['https:' + thumbnail_url_data]
            else:
                thumbnail_url = [thumbnail_url_data]
            yield scrapy.Request(url=parse.urljoin(response.url, detail_url), callback=self.parse_detail,meta={'thumbnail_url': thumbnail_url})
        # 提取下一页
        # next_url = response.xpath('//div[@class="pager"]/a[contains(text(), "Next")]/@href').get()
        # yield scrapy.Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

    def parse_detail(self, response):
        # 使用item_loader提取数据
        item_loader = ArticleItemLoad(item=ArticlespiderItem(), response=response)
        item_loader.add_css('title', '#news_title a::text')
        item_loader.add_css('author', '.news_poster a::text')
        item_loader.add_css('content', '#news_content')
        item_loader.add_css('tags', '.news_tags a::text')
        item_loader.add_css('pubdate', '#news_info .time::text')
        item_loader.add_value('origin_url', response.url)
        item_loader.add_value('thumbnail_url', response.meta.get('thumbnail_url', ''))
        item_loader.add_value('url_hash_id', get_url_md5(response.url))
        # 下面的parse_json_data方法可以不使用ItemLoader
        # 如果需要使用ItemLoader那么延缓使用item_loader.load_item()方法
        # item = item_loader.load_item()
        content_id = response.xpath('.//input[@id="lbContentID"]/@value').get()
        # 不建议将url请求放在该方法中,建议将url请求 yield 出去
        yield scrapy.Request(parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(content_id)), callback=self.parse_json_data, meta={'item_loader': item_loader})

    def parse_json_data(self, response):
        item_loader = response.meta.get('item_loader')
        json_data = json.loads(response.text)
        item_loader.add_value('dig_count', json_data.get('DiggCount'))
        item_loader.add_value('comment_count', json_data.get('CommentCount'))
        item_loader.add_value('view_count', json_data.get('TotalView'))
        item = item_loader.load_item()
        yield item
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值