16--Scrapy02:管道

Scrapy02--管道

0. 关于管道

上一节内容,我们已经可以从spider中提取到数据. 然后通过引擎将数据传递给pipeline

那么在pipeline中如何对数据进行保存呢? 主要针对四种数据存储,展开讲解

前三个案例以:https://match.lottery.sina.com.cn/lotto/pc_zst/index?lottoType=ssq&actionType=chzs

最后一个案例以:https://desk.zol.com.cn/dongman/

1. 写入csv文件

写入文件是一个非常简单的事情. 直接在pipeline中开启文件即可

但这里要说明的是,如果只在process_item中进行处理文件是不够优雅的. 总不能有一条数据就open一次吧

class CaipiaoFilePipeline:
    
    def process_item(self, item, spider):
        with open("caipiao.txt", mode="a", encoding='utf-8') as f:
            # 以追加的模式,写入文件
            f.write(f"{item['qihao']},{'_'.join(item['red_ball'])},{'_'.join(item['blue_ball'])}\n")
        return item

我们希望的是,能不能打开一个文件,然后就用这一个文件句柄来完成数据的保存?

答案是可以的,可以在pipeline中创建两个方法,一个是open_spider(),另一个是close_spider()

open_spider() 在爬虫开始时,执行一次
close_spider() 在爬虫结束时,执行一次

class CaipiaoFilePipeline:
    def open_spider(self, spider):
        # 同一个类中,其他方法要使用该变量,可放在对象中
        self.f = open("caipiao.txt", mode="a", encoding='utf-8')

    def close_spider(self, spider):
        if self.f:
            self.f.close()

    def process_item(self, item, spider):
        # 写入文件
        self.f.write(f"{item['qihao']},{'_'.join(item['red_ball'])},{'_'.join(item['blue_ball'])}\n")
        return item
    
    
# 设置settings
ITEM_PIPELINES = {
   'caipiao.pipelines.CaipiaoFilePipeline': 300,
}

2. 写入mysql

有了上面的示例,写入数据库其实也就很顺其自然了

首先,在open_spider中创建好数据库连接,在close_spider中关闭链接. 在proccess_item中对数据,进行保存工作

先把mysql相关设置丢到settings里

# MYSQL配置信息
MYSQL_CONFIG = {
   "host": "localhost",
   "port": 3306,
   "user": "root",
   "password": "test123456",
   "database": "spider",
}
from caipiao.settings import MYSQL_CONFIG as mysql

import pymysql

class CaipiaoMySQLPipeline:

    def open_spider(self,spider):
        self.conn = pymysql.connect(
            host=mysql["host"],
            port=mysql["port"],
            user=mysql["user"],
            password=mysql["password"],
            database=mysql["database"]
        )

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

    def process_item(self,item,spider):
        # 写入文件
        try:
            cursor = self.conn.cursor()
            sql = "insert into caipiao(qihao,red,blue) values(%s,%s,%s)"
            red = ",".join(item['red_ball'])
            blue = ",".join(item['blue_ball'])
            cursor.execute(sql,(item['qihao'],red,blue))
            self.conn.commit()
            spider.logger.info(f"保存数据{item}")
        except Exception as e:
            self.conn.rollback()
            spider.logger.error(f"保存数据库失败!",e,f"数据是: {item}")  # 记录错误日志
        return item

    
# 设置settings  开启管道
ITEM_PIPELINES = {
   'caipiao.pipelines.CaipiaoMySQLPipeline': 301,
}

3. 写入mongodb

mongodb数据库和mysql如出一辙

# settings.py  

MONGO_CONFIG = {
   "host": "localhost",
   "port": 27017,
   #'has_user': True,
   #'user': "python_admin",
   #"password": "123456",
   "db": "python"
}


ITEM_PIPELINES = {
    # 三个管道可以共存~
   'caipiao.pipelines.CaipiaoFilePipeline': 300,
   'caipiao.pipelines.CaipiaoMySQLPipeline': 301,
   'caipiao.pipelines.CaipiaoMongoDBPipeline': 302,
}
from caipiao.settings import MONGO_CONFIG as mongo

import pymongo

class CaipiaoMongoDBPipeline:
    def open_spider(self,spider):
        client = pymongo.MongoClient(host=mongo['host'], port=mongo['port'])
        db = client[mongo['db']]
        
        # if mongo['has_user']:
        #    db.authenticate(mongo['user'], mongo['password'])
        self.client = client
        self.collection = db['caipiao']

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

    def process_item(self,item,spider):
        self.collection.insert_one({"qihao": item['qihao'],'red': item["red_ball"],'blue': item['blue_ball']})
        return item

4. 文件保存

尝试使用Scrapy 来下载一些图片

图片网址: https://desk.zol.com.cn/dongman/

首先,创建好项目,完善spider,注意看 yield scrapy.Request()

import scrapy

from urllib.parse import urljoin


class ZolSpider(scrapy.Spider):
    name = 'zol'
    allowed_domains = ['zol.com.cn']
    start_urls = ['https://desk.zol.com.cn/dongman/']

    def parse(self,response,**kwargs):  # scrapy自动执行这个parse -> 解析数据
        # print(resp.text)
        # 1. 拿到详情页的url
        a_list = response.xpath("//*[@class='pic-list2  clearfix']/li/a")
        for a in a_list:
            href = a.xpath("./@href").extract_first()
            if href.endswith(".exe"):
                continue
                
            # print(response.url)   # response.url  从响应对象中,获取当前请求的url
             # print(href)  # '/bizhi/9109_111583_2.html'

            # href = urljoin(response.url, href)  # 这个拼接才是没问题的.
            # 仅限于scrapy
            href = response.urljoin(href)  # response.url 和你要拼接的东西
            # print(href)
            # 2. 请求到详情页. 拿到图片的下载地址

            # 发送一个新的请求
            # 返回一个新的请求对象
            # 我们需要在请求对象中,给出至少以下内容(spider中)
            # url  -> 请求的url
            # method -> 请求方式
            # callback -> 请求成功后.得到了响应之后. 如何解析(parse),把解析函数名字放进去
            yield scrapy.Request(
                url=href,
                method="get",
                # 当前url返回之后.自动执行的那个解析函数
                callback=self.suibianqimignzi,
            )

    def suibianqimignzi(self,response,**kwargs):
        # 在这里得到的响应就是url=href返回的响应
        img_src = response.xpath("//*[@id='bigImg']/@src").extract_first()
        # print(img_src)
        yield {"img_src": img_src}

4.1 URL拼接

# url 拼接的逻辑:   核心就是资源文件路径是相对路径,还是绝对路径 

### 总体原则:
当前请求的url (eg: https://desk.zol.com.cn/dongman/aaa) 和 子url,进行拼接

1.若子url是'/bizhi/sss.html'     # 绝对路径  以'/' 开头,表示资源路径的根目录  
  应当和 请求url的域名,进行拼接
    
 eg: 'https://desk.zol.com.cn/'  + '/bizhi/sss.html' = 'https://desk.zol.com.cn/bizhi/sss.html'


2.若子url是'bizhi/xxx.html'      # 相对路径  是拼接到 当前请求的url中 最后一层目录 的 同级目录中
  应当和 将当前请求的url中 最后一层目录 删除后 ,再进行拼接
    
 eg: 'https://desk.zol.com.cn/dongman/aaa'  + 'bizhi/sss.html' = 'https://desk.zol.com.cn/dongman/bizhi/sss.html'

    
    
### 处理办法:  简单   不用自己判断处理
# 1.通用方案
from urllib.parse import urljoin

urljoin(当前请求的url, 子url)


# 2.scrapy中  响应对象提供url拼接   源码本质就是上面通用方案
response.urljoin(href)   # ==> response.url + 要拼接的东西
                         # response.url: 从响应对象中,获取当前请求的ur

4.2 Request请求对象

### 关于Request()的参数:
url        请求地址
method     请求方式
callback   回调函数
errback    报错回调
dont_filter  默认False  # 表示"不过滤",该请求会重新进行发送
headers    请求头
cookies    cookie信息

meta       元数据       # 用来存储,其他地方能从 该Request对象中 获取到的数据

4.3 图片下载管道

其次,就是下载,如何在pipeline中下载一张图片呢?

在Scrapy中有一个ImagesPipeline,可以实现自动图片下载功能.

# 先安装 图片处理模块    ImagesPipeline依赖这个图片模块
pip install pillow     
import scrapy
from itemadapter import ItemAdapter

# ImagesPipeline 图片专用的管道
from scrapy.pipelines.images import ImagesPipeline

# FilesPipeline 文件下载管道     两者实质和用法 差不多
from scrapy.pipelines.files import FilesPipeline


class TuPipeline:
    def process_item(self, item, spider):
        print(item['img_src'])
        # 一个存储方案:自己发请求 + open 二进制文件
        # import requests
        # resp = requests.get(item['img_src'])
        # with open(f'{(item['img_src'].split('/')[-1]}', 'wb') as f:
            # f.write(resp.content)
        return item

    
    
### scrapy方案: scrapy提供的图片管道
class MyTuPipeline(ImagesPipeline):    # 重写下面三个方法
    # 1. 发送请求(下载图片,文件,视频,xxx)
    def get_media_requests(self, item, info):
        url = item['img_src']
        yield scrapy.Request(url=url, meta={"sss": url})  # 直接返回一个请求对象即可

        
    # 2. 图片的存储路径   # 在这个过程中. 文件夹自动创建
    # return 字符串  返回图片的存储路径    
    # 完整的路径: settings中的IMAGES_STORE + file_path()的返回值
    def file_path(self, request, response=None, info=None, *, item=None):
        # 准备文件夹
        img_path = "dongman/imgs/kunmo/libaojun/liyijia"
        
        # 准备文件名     根据url来切片 文件名
        # 方法1:用响应对象拿url
        # file_name = response.url.split("/")[-1]
        # 坑: response.url 没办法正常使用     该函数位置,从返回对象获取不到url,默认为None到嘛

        # 方法2:用item拿url   可以 但item 在详情页时,一般存放是多个图片的url列表  不是特别精准
        # file_name = item['img_src'].split("/")[-1]
        # print("item:", file_name)
        
         # 方法3:通过请求对象中参数meta,存放该请求的url       最优方案
        file_name = request.meta['sss'].split("/")[-1]
        print("meta:", file_name)

        real_path = img_path + "/" + file_name  # 文件夹路径拼接
        return real_path  # 返回文件存储路径即可

    
    # 3. item数据处理完(图片下完)后的操作   一般用于对item进行更新 和 下载完文件的信息打印记录
    def item_completed(self, results, item, info):
        # results:多个请求完(图片下载完)的结果 列表  
        # eg: [(响应状态:True 或者 False, 一堆数据的对象), (True, 对象), (True, 对象)]
        
        for ok, info in results:
            if ok:
                print(info['path'])   # 图片下载存放的路径
            
        return item  # 一定要return item 把数据传递给下一个管道

最后,在settings中设置

LOG_LEVEL = "WARNING"

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/101.0.4951.54 Safari/537.36'

ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
   'tu.pipelines.TuPipeline': 300,
   'tu.pipelines.MyTuPipeline': 301,
}


# 在下载文件(图片)时,可能出现302重定向的问题
MEDIA_ALLOW_REDIRECTS = True   # 媒体_允许_重定向


# 图片存放的根目录(总路径) 配置
IMAGES_STORE = "./qiaofu"
posted @ 2024-04-17 00:49  Edmond辉仔  阅读(2)  评论(0编辑  收藏  举报