为了账号安全,请及时绑定邮箱和手机立即绑定

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:\svnProject\TestSpider> scrapy genspider dongfeng www.dongfe.com

启动一个爬虫

E:\svnProject\TestSpider> scrapy crawl dongfeng

SHELL模式

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

pycharm调试启动文件

E:\svnProject\TestSpider> 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:\svnProject\TestSpider\TestSpider> 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("[\s\S]*?(\d{2,4}[/-]\d{1,2}[/-]\d{1,2})[\s\S]*", 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) + "\n"  # 关闭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("[\s\S]*?(\d{2,4}[/-]\d{1,2}[/-]\d{1,2})[\s\S]*", 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)
        ...
点击查看更多内容
14人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消