Python爬虫之Scrapy框架基础入门

时间:2024-12-17 14:08:44

Scrapy 是一个用于Python的开源网络爬虫框架,它为编写网络爬虫来抓取网站数据并提取结构化信息提供了一种高效的方法。Scrapy可以用于各种目的的数据抓取,如数据挖掘、监控和自动化测试等。

【1】安装

pip install scrapy

安装成功如下所示:
在这里插入图片描述
如果安装过程出错,可以参考下面步骤解决:

# (1) pip install scrapy
# (2) 报错1: building 'twisted.test.raiser' extension
#              error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
#              Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
#     解决1
#       http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
#       Twisted‑20.3.0‑cp37‑cp37m‑win_amd64.whl
#       cp是你的python版本
#       amd是你的操作系统的版本
#       下载完成之后 使用pip install twisted的路径  安装
#       切记安装完twisted 再次安装scrapy

# (3) 报错2  提示python -m pip install --upgrade pip
#      解决2   运行python -m pip install --upgrade pip

# (4) 报错3   win32的错误
#      解决3   pip install pypiwin32

# (5) anaconda

【2】基础入门

scrapy项目的结构

项目名字
    项目名字
        spiders文件夹 (存储的是爬虫文件)
            init
            自定义的爬虫文件    核心功能文件  ****************
        init
        items        定义数据结构的地方 爬取的数据都包含哪些
        middleware   中间件    代理
        pipelines    管道   用来处理下载的数据
        settings     配置文件    robots协议  ua定义等

在这里插入图片描述

创建Scrapy项目

创建一个新的Scrapy项目,你可以在命令行中输入以下命令:

scrapy startproject myproject

这将在当前目录下创建一个名为myproject的新目录,其中包含了Scrapy项目的结构。

scrapy startproject jane

在这里插入图片描述

定义Item

在Scrapy中,Item是被用来保存抓取到的数据的容器。你可以定义自己的Item类,类似于Python字典,但是提供了额外保护机制和便利方法。Item通常定义在items.py文件中。

class ScrapyDangdangItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 通俗的说就是你要下载的数据都有什么
    # 图片
    src = scrapy.Field()
    # 名字
    name = scrapy.Field()
    # 价格
    price = scrapy.Field()
    # 详情URL
    detail_url = scrapy.Field()

编写Spider

创建爬虫文件, 要在spiders文件夹中去创建爬虫文件: cd 项目的名字\项目的名字\spiders

scrapy genspider 爬虫文件的名字  要爬取网页

# 示例如下
scrapy genspider baidu  http://www.baidu.com

一般情况下不需要添加http协议 因为start_urls的值是根据allowed_domains修改的 所以添加了http的话 那么start_urls就需要我们手动去修改了

Spiders是定义如何抓取某个(或某些)网站的类。每个Spider负责处理一个特定的网站,或者一组相关的网页。Spiders通常位于spiders目录下,并且以.py文件的形式存在。

class BaiduSpider(scrapy.Spider):
    name = "baidu"
    allowed_domains = ["www.baidu.com"]
    start_urls = ["http://www.baidu.com"]

# 是执行了start_urls之后 执行的方法   方法中的response 就是返回的那个对象
    # 相当于 response = urllib.request.urlopen()
    #       response  = requests.get()
    def parse(self, response):
    	# 可以直接使用xpath或BS4
     	span = response.xpath('//div[@id="filter"]/div[@class="tabs"]/a/span')[0]
        print('=======================')
        print(span.extract())

response的属性和方法

response.text   获取的是响应的字符串
response.body   获取的是二进制数据
response.xpath  可以直接是xpath方法来解析response中的内容
response.extract()   提取seletor对象的data属性值
response.extract_first() 提取的seletor列表的第一个数据

管道 (Pipeline)

管道是用来处理由Spider抓取并返回的Items的地方。你可以在pipelines.py中定义如何处理这些Items,比如清洗、验证数据或者将它们存储到数据库中。

设置 (Settings)

Scrapy的行为可以通过修改settings.py文件来定制,例如设置下载延迟、启用/禁用中间件、更改用户代理等。

运行 Spider

最后,你可以通过命令行运行你的Spider:

scrapy crawl baidu

【3】管道的使用

pipelines管道就是用来处理数据的,如数据清洗、处理、校验、修正以及存储。

如果想使用管道的话 那么就必须在settings中开启管道,可以定义多个管道通过优先级数值来决定执行次序。

from itemadapter import ItemAdapter
class ScrapyDangdangPipeline:
    # 在爬虫文件开始的之前就执行的一个方法
    def open_spider(self,spider):
        self.fp = open('book.json','w',encoding='utf-8')

    # item就是yield后面的book对象
    def process_item(self, item, spider):
        # 以下这种模式不推荐  因为每传递过来一个对象 那么就打开一次文件  对文件的操作过于频繁

        # # (1) write方法必须要写一个字符串 而不能是其他的对象
        # # (2) w模式 会每一个对象都打开一次文件 覆盖之前的内容
        # with open('book.json','a',encoding='utf-8')as fp:
        #     fp.write(str(item))

        self.fp.write(str(item))


        return item

    # 在爬虫文件执行完之后  执行的方法
    def close_spider(self,spider):
        self.fp.close()

import urllib.request

# 多条管道开启
#    (1) 定义管道类
#   (2) 在settings中开启管道
# 'scrapy_dangdang_.pipelines.DangDangDownloadPipeline':301
class DangDangDownloadPipeline:
    def process_item(self, item, spider):

        url = 'http:' + item.get('src')
        filename = './books/' + item.get('name') + '.jpg'

        urllib.request.urlretrieve(url = url, filename= filename)

        return item

settings.py中开启管道:

ITEM_PIPELINES = {
   #  管道可以有很多个  那么管道是有优先级的  优先级的范围是1到1000   值越小优先级越高
   'scrapy_dangdang_.pipelines.ScrapyDangdangPipeline': 300,

#    DangDangDownloadPipeline
   'scrapy_dangdang_.pipelines.DangDangDownloadPipeline':301
}

【4】Scrapy中yield的使用

在 Scrapy 中,yield 有着特别重要的作用,尤其是在 Spider 类中。Scrapy 使用 yield 来返回请求(Request)和项目(Item),而不需要将它们全部加载到内存中。这使得 Scrapy 可以高效地处理大量的页面和数据。

在 Scrapy 中,yield 主要用于以下两个场景:

1. 返回 Request 对象

当您需要从一个页面抓取多个链接并发送请求时,可以使用 yield 来逐个返回 Request 对象。Scrapy 会自动处理这些请求,并将响应传递给指定的回调函数。

class DangSpider(scrapy.Spider):
    name = 'dang'
    start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']

    def parse(self, response):
        li_list = response.xpath('//ul[@id="component_59"]/li')

        for li in li_list:
            detail_url = 'http:' + li.xpath('./a/@href').get()
            if detail_url:
                yield scrapy.Request(detail_url, callback=self.parse_detail)

    def parse_detail(self, response):
        review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
        if review_count is None:
            review_count = "评论数未找到"
        else:
            review_count = review_count.strip()

        self.logger.info(f"当前图书评论数为: {review_count}")

在这个例子中,parse 方法会遍历列表页中的每个书籍链接,并使用 yield 逐个返回 Request 对象。Scrapy 会依次处理这些请求,并将详情页的响应传递给 parse_detail 方法。

2. 返回 Item 对象

当您从页面中提取数据并构建 Item 时,可以使用 yieldItem 返回给 Scrapy 的管道进行进一步处理(如保存到数据库、导出为文件等)。

class DangSpider(scrapy.Spider):
    name = 'dang'
    start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']

    def parse(self, response):
        li_list = response.xpath('//ul[@id="component_59"]/li')

        for li in li_list:
            src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
            name = li.xpath('.//img/@alt').get()
            price = li.xpath('.//p[@class="price"]/span[1]/text()').get()
            detail_url = 'http:' + li.xpath('./a/@href').get()

            if not src or not name or not price or not detail_url:
                self.logger.warning("缺少关键信息,跳过此条目")
                continue

            book_info = {
                'src': src,
                'name': name,
                'price': price,
                'detail_url': detail_url
            }

            yield scrapy.Request(detail_url, callback=self.parse_detail, cb_kwargs={'book_info': book_info})

    def parse_detail(self, response, book_info):
        review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
        if review_count is None:
            review_count = "评论数未找到"
        else:
            review_count = review_count.strip()

        book_info['review_count'] = review_count
        book_item = ScrapyDangdangItem(**book_info)
        self.logger.info(f"抓取到完整图书信息: {book_item}")

        yield book_item

在这个例子中,parse_detail 方法会从详情页提取评论数,并将其添加到 book_info 字典中。然后,它会创建一个 ScrapyDangdangItem 实例,并使用 yield 将其返回给 Scrapy 的管道。

【5】parse方法之间如何传参?

比如,需要先获取分类列表,然后获取每一个详情,最后整合获取得到book信息。

在 Scrapy 中,通常情况下,您会希望将从详情页获取的数据(如评论数)与列表页获取的数据(如书名、价格等)结合起来,形成一个完整的 Item。为了实现这一点,您可以使用 Requestmeta 参数来传递数据,或者使用 cb_kwargs 参数(Scrapy 1.7+)来传递关键字参数。

方法 1: 使用 meta 参数

meta 参数允许您在请求之间传递数据。您可以在 parse 方法中将书籍的基本信息(如书名、价格、图片链接等)通过 meta 传递给 parse_detail 方法,然后在 parse_detail 中提取评论数并返回一个完整的 Item。

修改后的代码:

import scrapy
from ..items import ScrapyDangdangItem  # 确保这里正确导入了Item

class DangSpider(scrapy.Spider):
    name = 'dang'
    allowed_domains = ['dangdang.com']  # 放宽域名限制
    start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']

    base_url = 'http://category.dangdang.com/pg'
    page = 1

    def parse(self, response):
        li_list = response.xpath('//ul[@id="component_59"]/li')

        for li in li_list:
            src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
            name = li.xpath('.//img/@alt').get()
            price = li.xpath('.//p[@class="price"]/span[1]/text()').get()

            detail_url = 'http:' + li.xpath('./a/@href').get()

            if not src or not name or not price or not detail_url:
                self.logger.warning("缺少关键信息,跳过此条目")
                continue

            # 构建基础的图书信息
            book_info = {
                'src': src,
                'name': name,
                'price': price,
                'detail_url': detail_url
            }

            # 发送详情页请求,并通过 meta 传递图书信息
            yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'book_info': book_info})

    def parse_detail(self, response):
        # 从 meta 中获取图书信息
        book_info = response.meta['book_info']

        # 提取评论数
        review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
        if review_count is None:
            review_count = "评论数未找到"
        else:
            review_count = review_count.strip()

        # 将评论数添加到图书信息中
        book_info['review_count'] = review_count

        # 创建并返回完整的 Item
        book_item = ScrapyDangdangItem(**book_info)
        self.logger.info(f"抓取到完整图书信息: {book_item}")

        yield book_item

方法 2: 使用 cb_kwargs 参数 (Scrapy 1.7+)

cb_kwargs 是 Scrapy 1.7 版本引入的一个新特性,它允许您直接在 Request 中传递关键字参数,而不需要通过 meta。这种方式更加直观和简洁。

修改后的代码:

import scrapy
from ..items import ScrapyDangdangItem  # 确保这里正确导入了Item

class DangSpider(scrapy.Spider):
    name = 'dang'
    allowed_domains = ['dangdang.com']  # 放宽域名限制
    start_urls = ['http://category.dangdang.com/cp01.01.02.00.00.00.html']

    base_url = 'http://category.dangdang.com/pg'
    page = 1

    def parse(self, response):
        li_list = response.xpath('//ul[@id="component_59"]/li')

        for li in li_list:
            src = li.xpath('.//img/@data-original').get() or li.xpath('.//img/@src').get()
            name = li.xpath('.//img/@alt').get()
            price = li.xpath('.//p[@class="price"]/span[1]/text()').get()

            detail_url = 'http:' + li.xpath('./a/@href').get()

            if not src or not name or not price or not detail_url:
                self.logger.warning("缺少关键信息,跳过此条目")
                continue

            # 构建基础的图书信息
            book_info = {
                'src': src,
                'name': name,
                'price': price,
                'detail_url': detail_url
            }

            # 发送详情页请求,并通过 cb_kwargs 传递图书信息
            yield scrapy.Request(detail_url, callback=self.parse_detail, cb_kwargs={'book_info': book_info})

    def parse_detail(self, response, book_info):
        # 提取评论数
        review_count = response.xpath('//a[@id="comm_num_down"]/@dd_name').get()
        if review_count is None:
            review_count = "评论数未找到"
        else:
            review_count = review_count.strip()

        # 将评论数添加到图书信息中
        book_info['review_count'] = review_count

        # 创建并返回完整的 Item
        book_item = ScrapyDangdangItem(**book_info)
        self.logger.info(f"抓取到完整图书信息: {book_item}")

        yield book_item

关键点解释

  1. meta 参数

    • parse 方法中,我们构建了一个包含图书基本信息的字典 book_info
    • 使用 meta 参数将 book_info 传递给 parse_detail 方法。
    • parse_detail 中,通过 response.meta['book_info'] 获取传递过来的图书信息。
  2. cb_kwargs 参数

    • parse 方法中,我们同样构建了一个包含图书基本信息的字典 book_info
    • 使用 cb_kwargs 参数将 book_info 作为关键字参数传递给 parse_detail 方法。
    • parse_detail 中,直接通过函数参数 book_info 获取传递过来的图书信息。
  3. 合并数据

    • parse_detail 中,我们提取了评论数,并将其添加到 book_info 字典中。
    • 最后,我们创建了一个 ScrapyDangdangItem 实例,并将其返回给 Scrapy 的管道进行处理。

【6】管道中使用pymysql存储数据

from itemadapter import ItemAdapter


class ScrapyReadbookPipeline:

    def open_spider(self,spider):
        self.fp = open('book.json','w',encoding='utf-8')


    def process_item(self, item, spider):
        self.fp.write(str(item))
        return item

    def close_spider(self,spider):
        self.fp.close()

# 加载settings文件
from scrapy.utils.project import get_project_settings
import pymysql


class MysqlPipeline:

    def open_spider(self,spider):
        settings = get_project_settings()
        self.host = settings['DB_HOST']
        self.port =settings['DB_PORT']
        self.user =settings['DB_USER']
        self.password =settings['DB_PASSWROD']
        self.name =settings['DB_NAME']
        self.charset =settings['DB_CHARSET']

        self.connect()

    def connect(self):
        self.conn = pymysql.connect(
                            host=self.host,
                            port=self.port,
                            user=self.user,
                            password=self.password,
                            db=self.name,
                            charset=self.charset
        )

        self.cursor = self.conn.cursor()


    def process_item(self, item, spider):

        sql = 'insert into book(name,src) values("{}","{}")'.format(item['name'],item['src'])
        # 执行sql语句
        self.cursor.execute(sql)
        # 提交
        self.conn.commit()


        return item


    def close_spider(self,spider):
        self.cursor.close()
        self.conn.close()

数据库配置信息在settings.py中:

在这里插入图片描述

【7】日志级别与存储路径

settings.py中配置日志级别与路径即可:

# 指定日志的级别
# LOG_LEVEL='WARNING'

LOG_FILE = 'logdemo.log'

【8】POST请求

如下所示是GET请求:

yield scrapy.Request(url=detail_url, callback=self.parse_detail)

重写start_requests方法使用FormRequest发送POST请求

class TestpostSpider(scrapy.Spider):
    name = 'testpost'
    allowed_domains = ['https://fanyi.baidu.com/sug']
    # post请求 如果没有参数 那么这个请求将没有任何意义
    # 所以start_urls 也没有用了
    # parse方法也没有用了
    # start_urls = ['https://fanyi.baidu.com/sug/']
    #
    # def parse(self, response):
    #     pass

    def start_requests(self):
        url = 'https://fanyi.baidu.com/sug'

        data = {
            'kw': 'final'
        }

        yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse_second)

    def parse_second(self,response):

        content = response.text
        obj = json.loads(content,encoding='utf-8')

        print(obj)

【9】与xpath结合使用

如下图所示,想要提取week内容
在这里插入图片描述

dl_weather = response.xpath('//dl[@class="weather_info"]')
dl_weather_xpath = dl_weather.xpath('//dd[@class="week"]')
# 这里获取的是Selector 数组,data为目标html 元素
print("dl_weather.xpath('//dd[@class='week\"]')", dl_weather_xpath) 

# 这里获取的是Selector 数组,data为目标html 元素的文本内容
weather_xpath = dl_weather.xpath('//dd[@class="week"]/text()')
print("dl_weather.xpath('//dd[@class='week\"]/text()')", weather_xpath)

# 这里获取的是文本数组
week__extract = weather_xpath.extract()

# extract_first表示获取第一个文本节点
print("weather_xpath.extract_first() :",weather_xpath.extract_first())

print("weather_xpath.extract() :",week__extract)

结果如下所示:

dl_weather.xpath('//dd[@class='week"]') [<Selector query='//dd[@class="week"]' data='<dd class="week">2024年12月16日\u3000星期一\u3000甲辰年冬...'>]

dl_weather.xpath('//dd[@class='week"]/text()') [<Selector query='//dd[@class="week"]/text()' data='2024年12月16日\u3000星期一\u3000甲辰年冬月十六 '>]
weather_xpath.extract_first() : 2024年12月16日 星期一 甲辰年冬月十六 
weather_xpath.extract() : ['2024年12月16日\u3000星期一\u3000甲辰年冬月十六 ']
{'date': ['2024年12月16日\u3000星期一\u3000甲辰年冬月十六 ']}