16--Scrapy02:管道

Edmond辉仔發表於2024-04-17

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"

相關文章