初始scrapy

小满三岁啦發表於2024-04-04

楔子

Scrapy是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。可以應用在包括資料探勘,資訊處理或儲存歷史資料等一系列的程式中。

image-20240403194441462

安裝

方式1

pip install scrapy

方式2

如果方式1安裝不了,在看此方式,如果方式1能正常安裝,那就不需要檢視此方式了。

# 1. 安裝wheel檔案
pip3 install wheel

# 2. 安裝 lxml 解析器
pip3 install lxml

# 3. 安裝 pyopenssl
pip3 install pyopenssl

# 4. 下載並安裝pywin32
下載並安裝 pywin32 可以從其官網下載最適合您的版本:https://sourceforge.net/projects/pywin32/files/pywin32/
請根據您的作業系統和Python版本選擇正確的安裝檔案進行下載。
下載完成後,按照安裝嚮導進行安裝。

# 5.下載twisted的wheel檔案
要安裝 Scrapy,需要先下載 twisted 的 wheel 檔案。
可以從官方網站下載 twisted 的 wheel 檔案:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
請注意選擇與您的Python環境相匹配的 wheel 檔案進行下載。

# 6. 安裝twisted
下載完成 twisted 的 wheel 檔案後,可以使用以下命令安裝 twisted:
pip3 install 下載目錄\Twisted-17.9.0-cp36-cp36m-win_amd**.whl
將 下載目錄 替換為您實際下載 twisted wheel 檔案所在的目錄
並根據您的 Python 環境選擇正確的檔名進行替換。

報錯解決方案

image-20240403195125680

scrapy的基本使用

檢視幫助 scrapy - h

  • Project-only必須切到專案資料夾下才能執行
  • Global的命令則不需要
Global commands:
startproject #建立專案
genspider    #建立爬蟲程式
settings     #如果是在專案目錄下,則得到的是該專案的配置
runspider    #執行一個獨立的python檔案,不必建立專案
shell        #scrapy shell url地址  在互動式除錯,如選擇器規則正確與否
fetch        #獨立於程單純地爬取一個頁面,可以拿到請求頭
view         #下載完畢後直接彈出瀏覽器,以此可以分辨出哪些資料是ajax請求
version      #scrapy version 檢視scrapy的版本,scrapy version -v檢視scrapy依賴庫的版本

Project-only commands:
crawl        #執行爬蟲,必須建立專案才行,確保配置檔案中ROBOTSTXT_OBEY = False
check        #檢測專案中有無語法錯誤
list         #列出專案中所包含的爬蟲名
parse        #scrapy parse url地址 --callback 回撥函式  #以此可以驗證我們的回撥函式是否正確
bench        #scrapy bentch壓力測試

建立專案

須知:

  1. 路徑不要包含中文
  2. 專案名稱不要以數字開頭
  3. 專案名稱不建議包含漢字
# cd 到某一個目錄下,然後執行  scrapy startproject 專案名
scrapy startproject spider

進入到專案裡面

切換到爬蟲專案錄

cd spider

建立spider專案

建立自定義的爬蟲程式指令碼

# scrapy genspider 自定義爬蟲名稱 目標網址
# 這裡的域名不需要新增協議,因為建立後scrapy會自動幫你加上協議
scrapy genspider QAQ www.QAQ.com
├── NewsPro                    # 專案名
│   ├── __init__.py
│   ├── items.py               # 定義資料結構的地方,爬蟲的資料包含哪一些 通俗的說 你要下載的資料都有哪一些
│   ├── middlewares.py         # 中介軟體,用於處理請求和響應 也可以設定代理
│   ├── pipelines.py           # 管道,用於資料持久化等操作 一般用來處理下載的資料 
│   ├── settings.py            # 專案配置檔案,包含各種設定選項 比如robots協議 ua等
│   └── spiders                # 存放自定義的爬蟲,類似於應用程式
│       ├── __init__.py
│       ├── huanqiu.py         # 自定義的爬蟲檔案 核心功能檔案 *************
└── scrapy.cfg                 # Scrapy 專案配置檔案
  • items.py:定義了專案中要抓取的資料模型,類似於 Django 中的 models.py 檔案。您可以在這裡定義專案需要爬取的資料結構。
  • middlewares.py:中介軟體,用於在請求傳送給伺服器之前或從伺服器返回給爬蟲之後進行處理。您可以在這裡新增各種中介軟體,例如代理、使用者代理等。
  • pipelines.py:管道,用於資料的後處理,比如資料的儲存、清洗等操作。每個管道元件都是獨立的,可以對資料進行不同的處理。
  • settings.py:專案配置檔案,包含了專案的各種配置選項,比如爬蟲的設定、中介軟體的設定、管道的設定等。您可以在這裡配置專案的各種引數。
  • spiders 目錄:存放自定義的爬蟲檔案,用於實際的資料抓取工作。每個爬蟲檔案定義了一個獨立的爬蟲,用於從特定網站抓取資料。
  • scrapy.cfg:Scrapy 專案配置檔案,包含了專案的基本配置資訊,比如專案的名稱、版本號、部署設定等。

君子協議 robots.txt

擴充閱讀

# settings.py
# 預設為True,如果拿不到資料 改為False或者註釋掉即可 
ROBOTSTXT_OBEY = True

簡單案例

# spider\mc.py
import scrapy


class McSpider(scrapy.Spider):
    # 爬蟲的名字,用於執行爬蟲的時候 使用的值
    name = "mc"
    # 允許訪問的域名,也就是除了這個域名以及子域名之外,其他的域名都不允許訪問
    allowed_domains = ["www.xx.com"]
    # start_urls 是在allowed_domains前面加一個協議http/https, 後面加一個/【可選】
    # 如果字尾是html的時候,後面是不允許加/的
    start_urls = ["https://www.xx.com/"]

    # parse是執行了start_urls之後執行的方法
    # 方法中的response就是返回的那個物件
    # 相當於 response = urllib.requests.urlopen()
    #       response = requests.get()
    def parse(self, response):
        print('小滿最棒啦!')

如何執行

# cd到剛剛的爬蟲專案的spider資料夾下,執行 scrapy crawl spider_name
scrapy crawl mc  # 會列印日誌
scrapy crawl mc --nolog # 不列印日誌

scrapy的settings配置

基礎配置

#1 專案名字,整個爬蟲名字
BOT_NAME = "firstscrapy"  

#2 爬蟲存放位置的設定
SPIDER_MODULES = ["firstscrapy.spiders"]
NEWSPIDER_MODULE = "firstscrapy.spiders"

#3  是否遵循爬蟲協議,一般都設為False
ROBOTSTXT_OBEY = False

# 4 User-Agent設定
USER_AGENT = "firstscrapy (+http://www.yourdomain.com)"

#5 日誌級別設定
LOG_LEVEL='ERROR'

#6  DEFAULT_REQUEST_HEADERS 預設請求頭
DEFAULT_REQUEST_HEADERS = {
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
   'Accept-Language': 'en',
}

#7 SPIDER_MIDDLEWARES 爬蟲中介軟體
SPIDER_MIDDLEWARES = {
    'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
#8 DOWNLOADER_MIDDLEWARES  下載中介軟體
DOWNLOADER_MIDDLEWARES = {
    'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}

#9 ITEM_PIPELINES 持久化配置
ITEM_PIPELINES = {
    'cnblogs.pipelines.CnblogsPipeline': 300,
}

高階配置

#1 增加併發數,預設為16,可以根據需求進行調整
# 預設scrapy開啟的併發執行緒為32個,可以適當進行增加。
# 值為100,併發設定成了為100。
# 在settings配置檔案中修改
CONCURRENT_REQUESTS = 100

#2 降低日誌級別,可設定為INFO或ERROR,減少日誌輸出,提高效能
# 在執行scrapy時,會有大量日誌資訊的輸出,為了減少CPU的使用率。
# 可以設定log輸出資訊為INFO或者ERROR即可。
在配置檔案中編寫:
LOG_LEVEL = 'INFO'


# 3 禁止使用Cookie,預設為True,如果不需要使用Cookie可以設定為False
# 如果不是真的需要cookie,則在scrapy爬取資料時可以禁止cookie從而減少CPU的使用率,提升爬取效率。
COOKIES_ENABLED = False

# 4 禁止重試,預設為True,如果不需要進行重試請求可以設定為False
# 對失敗的HTTP進行重新請求(重試)會減慢爬取速度,因此可以禁止重試。
RETRY_ENABLED = False

# 5 設定下載超時時間,預設180秒,可以根據需求進行調整
# 如果對一個非常慢的連結進行爬取,減少下載超時可以能讓卡住的連結快速被放棄,從而提升效率。
# 超時時間為10s
DOWNLOAD_TIMEOUT = 10

持久化儲存

parse的返回值

每個爬蟲中的parse方法以及其他後續解析方法通常需要返回一個可迭代物件(如列表),其中包含的是經過處理後的資料結構,通常是符合Item定義的字典。

def parse(self, response):
    items = []
    for element in response.css('some_selector'):
        item = {
            'field1': element.xpath('...').get(),
            'field2': element.xpath('...').get(),
        }
        items.append(item)
    return items

命令列匯出json和csv

# settings.py
FEED_FORMAT = 'json'  # 選擇要匯出的格式,可以選擇 'json' 或 'csv'
FEED_URI = 'output_file.json'  # 指定要儲存的檔案路徑,可以是 'output_file.json' 或 'output_file.csv'
# 匯出json檔案
scrapy crawl your_spider_name -o output.json

# 匯出csv
scrapy crawl your_spider_name -o output.csv -t csv

response的屬性和方法

response.text # 結果原始碼,同response.text 一樣 是字串
respnse.body  # 解碼原始碼,同response.content 一樣 結果是二進位制
response.xpath() # 可以直接使用xpath語法來解析response中的內容 和正常的xpath一樣,所有的selector都可以再次進行xpath
response.extract() # 提取selector物件的data屬性值
response.extract_first() # 提取的是select列表的第一個資料

# 沒有寫extract()
[<Selector query='//div[@class="slist"]//li[1]' data='<li><a href="/tupian/34023.html" titl...'>]
# 寫了沒有寫extract()
['<li><a href="/tupian/34023.html" title="報紙牆女孩粉色頭髮4K動漫桌布" target="_blank"><span><img src="/uploads/allimg/240403/002821-1712075301366c.jpg" alt="報紙牆女孩粉色頭髮4K動漫桌布"></span><b>報紙牆女孩粉色頭髮4K動漫桌布</b></a></li>']

attributes  # 請求的屬性,包括方法、URL、頭部資訊等
body  # 響應的正文內容
cb_kwargs  # 在回撥函式中傳遞額外的關鍵字引數
certificate  # 響應中的 SSL 證書資訊
copy  # 複製當前的 Response 物件
css  # 使用 CSS 選擇器從響應中提取資料
encoding  # 響應的編碼格式
flags  # 請求的標誌,例如是否跟蹤重定向等
follow  # 根據響應的狀態碼決定是否跟蹤重定向
follow_all  # 跟蹤所有重定向
headers  # 響應的頭部資訊
ip_address  # 請求的 IP 地址
jmespath  # 使用 JMESPath 查詢從響應中提取資料
json  # 解析響應的 JSON 內容
meta  # 請求和響應的後設資料資訊
protocol  # 請求的協議,例如 HTTP 或 HTTPS
replace  # 替換當前的 Request 物件
request  # 當前的 Request 物件
selector  # 使用 Selector 物件從響應中提取資料
status  # 響應的狀態碼
text  # 響應的文字內容
url  # 請求的 URL
urljoin  # 將相對 URL 轉換為絕對 URL
xpath  # 使用 XPath 從響應中提取資料

CSS選擇器 response.css

# 獲取屬性 response.css("選擇器 ::('屬性值')")
response.css(".slist img ::attr('alt')") # 提取圖片的alt屬性
response.css(".slist b::text") # 提取文字的直接內容

extract()和extract_first()

extract()extract_first() 方法都是用於從 Scrapy 的 Selector 物件中提取文字內容的方法。

  • extract() 方法會提取所有匹配的內容,並將其以列表的形式返回。如果有多個匹配項,將返回多個文字內容。
  • extract_first() 方法會提取第一個匹配的內容,並將其作為字串返回。如果沒有匹配項,則返回 None

這兩個方法通常與 XPath 或 CSS 選擇器一起使用,用於從網頁中提取所需的資料。例如:

# 使用 XPath 提取所有匹配的文字內容
texts = response.xpath('//div[@class="content"]/p/text()').extract()

# 使用 CSS 選擇器提取第一個匹配的文字內容
first_text = response.css('h1::text').extract_first()

在這個例子中,extract()extract_first() 方法用於提取網頁中特定元素的文字內容,以供後續處理或儲存。

scrapy的工作原理

如果是資料,就交給管道下載,如果還是一個要請求的url,就在走一遍。

image-20240403212606286

scrapy shell

  1. 什麼是scrapy shell?
    1. scrapy終端,是一個互動終端,供您在未啟動scrapy的情況下嘗試及除錯您的爬蟲程式碼。其本意是用來測試提取資料的程式碼,不過您可以將其作為正常的python終端,在上面測試任何的python程式碼。
    2. 該終端是用來測試XPath或CSS表示式,檢視他們的工作方式及從爬取的網頁中提取的資料,在編寫您的spider時,該終端提供了互動性測試您的表達程式碼的功能,免去了每次修改後執行spider的麻煩。
    3. 一旦熟悉了scrapy終端後,您會發現其在開發和除錯spider時發揮的巨大作用。
  2. 安裝ipython
    1. 安裝:pip install ipython
    2. 簡介:如果您安裝了ipython,scrapy的終端將使用ipython(替代標準python終端), ipython終端與其他相比更為強大,提供了智慧的自動補全,高亮輸出,及其他特性。

如何進入終端

# 直接在cmd視窗中 輸入 scrapy shell 域名
scrapy shell www.xx.com

yield

  1. 帶有yield的函式不再是一個普通函式,而是一個生成器generator,可用於迭代
  2. yield是一個類似return的關鍵字,迭代一次遇到yield時,就返回yield後面(右邊)的值,重點是:下一次迭代時,從上一次迭代遇到的yield後面的程式碼(下一行)開始執行
  3. 簡要理解:yield就是return返回的一個值,並且記住這個值的返回位置,下次迭代就從這個位置後(下一行)開始。

梳理流程

# spider\book.py
import scrapy
from ..items import DangdangItem


class BookSpider(scrapy.Spider):
    name = "book"
    allowed_domains = ["xxx"]
    start_urls = ["xxx"]

    def parse(self, response):
        # src = '//ul[@id="component_59"]//li/a/img/@src'
        # title = '//ul[@id="component_59"]//li/a/img/@alt'
        # price = '//ul[@id="component_59"]//li/p[@class="price"]/span[1]/text()'

        li_list = response.xpath('//ul[@id="component_59"]//li')
        for li in li_list:
            src = li.xpath('./a/img/@data-original').extract_first()
            name = li.xpath('./a/img/@alt').extract_first()
            price = li.xpath('./p[@class="price"]/span[1]/text()').extract_first()

            # print(src, name, price)

            # 這個物件要交給管道pipelines去下載
            book = DangdangItem(src=src, name=name, price=price)

            # 獲取一個book 就將book交給pipelines
            yield book

# items.py
# 就相當於django裡面的模型層
class DangdangItem(scrapy.Item):
    # 名字
    name = scrapy.Field()
    # 圖片
    src = scrapy.Field()
    # 價格
    price = scrapy.Field()
# pipelines.py
# 如果想要使用管道的話,那麼就必須在settings中開啟管道
class DangdangPipeline:

    # item 就是book後面的yield物件, 也就是你的spider下面的爬蟲返回的資料的
    def process_item(self, item, spider):
        # 以下這種方法不推薦 因為操作IO太過頻繁

        # 1. write寫入必須是一個字串,而不能是其他的物件
        # 2. w模式會覆蓋寫
        with open('book.json', 'at', encoding='utf-8') as file:
            file.write(str(item) + "\n")

        # 預設的item型別是<class 'dangdang.items.DangdangItem'> 這裡可以使用字典強轉,然後操作就可以了
        # data = dict(item)
        return item

item

# 可以透過get取值,但是並不是字典的型別,參考上面
src = item.get('src')
name = item.get('name')

# 這個鍵就是你在item裡面定義的

open_spider和close_spider

# pipelines.py
class DangdangPipeline:
    # 在爬蟲檔案開始之前就執行的一個方法
    # 名字不能寫錯 要指定引數
    def open_spider(self, spider):
        print('+++++++++++++++++++++++++++++++++++++++++++++++')

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

    # 在爬蟲執行完畢之後,執行的方法
    def close_spider(self, spider):
        print('-----------------------------------------------------')
        
"""
+++++++++++++++++++++++++++++++++++++++++++++++
下面全是spider裡面的程式碼
//img3m3.ddimg.cn/71/16/29687003-1_b_1711521731.jpg  驕陽似我(下)(親籤版) ¥32.00
//img3m2.ddimg.cn/46/8/29124262-1_b_5.jpg  那個不為人知的故事 ¥20.10
//img3m8.ddimg.cn/72/12/23945598-1_b_1706687985.jpg  大哥(套裝共2冊) ¥32.00
//img3m4.ddimg.cn/72/17/29687004-1_b_1711521722.jpg  驕陽似我(上下 兩冊套裝) ¥49.00
......
-----------------------------------------------------
"""

# 最佳化後的程式碼
import json
from itemadapter import ItemAdapter

# 如果想要使用管道的話,那麼就必須在settings中開啟管道
class DangdangPipeline:
    # 在爬蟲開始之前執行的方法
    def open_spider(self, spider):
        # 初始化一個空列表,用來儲存爬取的資料
        self.data_list = []

    # 處理每個爬取到的資料項
    def process_item(self, item, spider):
        # 將每個資料項轉換成字典格式
        data = dict(item)
        
        # 將資料項新增到列表中
        self.data_list.append(data)

        # 返回item,這樣其他的pipeline元件(如果有的話)也能處理該item
        return item

    # 在爬蟲執行完畢後執行的方法
    def close_spider(self, spider):
        # 將爬取到的資料列表寫入到JSON檔案中
        with open('book.json', 'wt', encoding='utf-8') as file:
            json.dump(self.data_list, file, ensure_ascii=False)
# settings.py
# 管道預設沒有開啟
# 開啟管道,管道是有優先順序的,優先順序的返回是1-1000 數字越低 優先順序越高 預設是300
ITEM_PIPELINES = {
   "dangdang.pipelines.DangdangPipeline": 300,
}

開啟多條管道 (模仿就行)

不同管道可以執行不同的任務,比如你儲存資料,我下載圖片到本地。

# 1. 在pipelines.py中 模仿預設的類去寫,類名隨意,裡面定義一樣的方法就行 process_item(self, item, spider).
# 例如
class ImageDownload:
    def process_item(self, item, spider):
        # 寫邏輯
        return item
    
# 2. 在settings中,一樣模仿預設的去寫
ITEM_PIPELINES = {
   "dangdang.pipelines.DangdangPipeline": 300,  # 這個是預設的,DangdangPipeline是建立scrapy就建立好的類名
   "dangdang.pipelines.ImageDownload": 301  # 這個是自己新增的,ImageDownload 就是你自己定義的類名
}

# 完畢
# 引入必要的庫
from pathlib import Path
import json
from urllib.request import urlretrieve
from itemadapter import ItemAdapter

# 設定儲存圖片的資料夾路徑
BASE_DIR = Path(__file__).parent
path = BASE_DIR / 'images'
path.mkdir(parents=True, exist_ok=True)

# 定義第一個管道類,用於處理資料儲存到 JSON 檔案中
class DangdangPipeline:
    # 在爬蟲開始之前執行的方法
    def open_spider(self, spider):
        # 初始化一個空列表,用於儲存爬取的資料
        self.data_list = []

    # 處理每個爬取到的資料項
    def process_item(self, item, spider):
        # 將每個資料項轉換成字典格式
        data = dict(item)

        # 列印圖片連結
        print(item.get('src'))

        # 將資料項新增到列表中
        self.data_list.append(data)

        # 返回 item,這樣其他的 pipeline 元件(如果有的話)也能處理該 item
        return item

    # 在爬蟲執行完畢後執行的方法
    def close_spider(self, spider):
        # 將爬取到的資料列表寫入到 JSON 檔案中
        with open('book.json', 'wt', encoding='utf-8') as file:
            json.dump(self.data_list, file, ensure_ascii=False)

# 定義第二個管道類,用於下載圖片
# "dangdang.pipelines.ImageDownload": 301
class ImageDownload:
    def process_item(self, item, spider):
        # 獲取書籍名稱和圖片連結
        name = item.get('name')
        src = f"http:{item.get('src')}"

        # 使用 urllib 庫下載圖片,並儲存到指定路徑下
        urlretrieve(src, filename=f'{path}/{name}.png')

        # 返回 item,以便其他 pipeline 元件(如果有的話)繼續處理該 item
        return item

使用非同步下載

import aiohttp
import aiofiles
import asyncio
import os
from itemadapter import ItemAdapter

class ImageDownload:
    def __init__(self):
        self.session = None

    async def download_image(self, url, filename):
        # 使用 aiohttp 庫傳送非同步請求下載圖片
        async with self.session.get(url) as response:
            # 如果響應狀態為 200,則表示請求成功
            if response.status == 200:
                # 使用 aiofiles 庫非同步寫入檔案
                async with aiofiles.open(filename, 'wb') as f:
                    await f.write(await response.read())

    async def process_item(self, item, spider):
        # 獲取書籍名稱和圖片連結
        name = item.get('name')
        src = f"http:{item.get('src')}"
        # 拼接圖片檔案路徑
        filename = os.path.join(path, f"{name}.png")

        # 建立 aiohttp 的非同步會話
        async with aiohttp.ClientSession() as session:
            self.session = session
            # 呼叫非同步下載圖片的方法
            await self.download_image(src, filename)

        return item

多頁下載

清楚一件事情:每一頁的業務邏輯都是完全一樣的,只需要開啟多頁就可以了,怎麼開啟?

  • 我們只需要將執行頁的那個請求,再次呼叫parse方法就可以了
# spider\your_spider.py

import scrapy
from ..items import DangdangItem


class BookSpider(scrapy.Spider):
    name = "book"
    # 如果是多頁下載,那麼必須要調整allowed_domains的範圍,一般只寫域名
    allowed_domains = ["xx"]
    start_urls = ["xx"]
    base_url = "xx"
    page = 1

    def parse(self, response):
        # src = '//ul[@id="component_59"]//li/a/img/@src'
        # title = '//ul[@id="component_59"]//li/a/img/@alt'
        # price = '//ul[@id="component_59"]//li/p[@class="price"]/span[1]/text()'

        li_list = response.xpath('//ul[@id="component_59"]//li')
        for li in li_list:
            src = li.xpath('./a/img/@data-original').extract_first()
            name = li.xpath('./a/img/@alt').extract_first()
            price = li.xpath('./p[@class="price"]/span[1]/text()').extract_first()

            # print(src, name, price)

            # 這個物件要交給管道pipelines去下載
            book = DangdangItem(src=src, name=name, price=price)

            # 獲取一個book 就將book交給pipelines
            yield book

        if self.page < 100:
            self.page += 1
            url = f"{self.base_url}{self.page}-cp01.01.02.00.00.00.html"

            # 怎麼去呼叫parse方法
            # yield Request 這個就是scrapy的get請求
            # url就是請求的地址 callback就是要執行的函式,放記憶體地址就行,不需要加括號
            yield scrapy.Request(url, callback=self.parse)

meta的使用

在 Scrapy 中,yield scrapy.Request 方法用於傳送HTTP請求,並且可以傳遞額外的後設資料(metadata)資訊給回撥函式。這些額外的後設資料可以在後續處理中使用。

具體來說,meta 引數是一個字典,可以包含任意鍵值對的資訊。當你使用 yield scrapy.Request(url, callback=self.parse_detail, meta={'key': 'value'}) 傳送請求時,你可以在 parse_detail 回撥函式中透過 response.meta['key'] 來獲取傳遞的值。

例如,假設你有一個爬蟲,爬取網頁中的標題和連結,並且想要將標題和連結一起儲存到資料庫中。你可以使用 meta 引數將標題傳遞給下一個回撥函式。示例程式碼如下:

import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'
    
    def start_requests(self):
        urls = ['https://example.com/page1', 'https://example.com/page2']
        for url in urls:
            yield scrapy.Request(url, callback=self.parse_title)
    
    def parse_title(self, response):
        title = response.css('h1::text').get()
        yield scrapy.Request(response.url, callback=self.parse_detail, meta={'title': title})
    
    def parse_detail(self, response):
        title = response.meta['title']
        link = response.url
        # Save title and link to database or do other processing

在上面的示例中,parse_title 方法提取頁面的標題,並將標題作為後設資料傳遞給 parse_detail 方法。在 parse_detail 方法中,可以透過 response.meta['title'] 獲取到傳遞過來的標題資訊。

這樣,透過使用 meta 引數,可以方便地在 Scrapy 中傳遞和共享資料資訊。

連線提取器 CrawlSpider

1. 繼承自scrapy.Spider
2. 獨門秘籍
    - crawlSpider可以自定義規則,在解析HTML內容的時候,可以根據連線規則提取出指定的連線,然後再向這些連線傳送請求。
    - 比如,不知道一共有多少頁,就可以使用crawlspider
    - 所以,如果有需要跟進連線的需求,意思就是爬取了網頁之後,需要提取連線再次爬取,使用crawlSpider是非常合適的。
3. 提取連結
	連結提取器,在這裡就可以寫規則提取指定連線
	scrapy.linkextractors.LinkExtractor(
    allow=(),  # 正規表示式,提取符合正則規則的連結  
    deny=(),   # (不用)正規表示式,不提取符合正則規則的連結
    allow_domains=(),  # (不用)允許的域名
    deny_domains=(),   # (不用)不允許的域名
    restrict_xpaths=(),  # XPath,提取符合XPath規則的連結
    restrict_css=()  # CSS選擇器,提取符合CSS選擇器規則的連結
	)
4. 模擬使用
		from scrapy.linkextractors import LinkExtractor # 匯入必要的模組
		正則用法:links1 = linkExtractor(allow='list_23_\d+\.html')
		xpath用法:links2 = linkExtractor(restrict_xpaths=r'//div[@class="x"]') # 這裡記得是xpaths不是xpath
		css用法:links3 = LinkExtractor(restrict_css=".x")
5. 提取連線
		link.extract_links(response) # link1.extract_links(response) ...
6. 注意事項
		【注意1】callback只能寫函式名字串,callback="parse_item"
		【注意2】在基本的spider中,如果重新傳送請求,那裡的callback寫的是 callback=self.parse_item
				fllow=true  # 是否更進,就是按照提取連線規則進行提取

fllow=True

CrawlSpider中的follow=True參數列示是否在爬取頁面時遵循定義的規則(Rule)。如果follow=True,則爬蟲會根據規則繼續跟進頁面,解析頁面中的連結,並且對這些連結再次進行爬取。換句話說,它會讓爬蟲繼續向下深入爬取新的頁面。

通常情況下,當follow=True時,爬蟲會繼續爬取滿足規則條件的連結,直到滿足某種條件(比如達到指定的頁數、深度等)或者沒有新的連結可爬取為止。因此,這個引數可以用來控制爬蟲的深度和範圍。

總之,follow=True會使爬蟲按照定義的規則繼續爬取新的頁面,直到滿足指定條件為止。

image-20240404162626906

如何使用crawlspider

1. cd到某個目錄,然後正常建立爬蟲專案 
	- scrapy startproject 專案的名字
2. 跳轉到spiders資料夾的目錄下 
	- cd專案名字\專案名字\spiders
3. 建立爬蟲檔案 (區別於一般的建立 -->  scrapy genspider 爬蟲的名字 爬取的域名)
	- 要使用下面的建立語法
	- scrapy genspider -t crawl 爬蟲檔案的名字 爬取的域名   
# 使用crawlspider建立後的spider
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class ReadSpider(CrawlSpider):
    name = "read"
    allowed_domains = ["www.xx.com"]
    start_urls = ["https://www.xx.com/book/1188_1.html"]

    rules = (Rule(LinkExtractor(allow=r"Items/"), callback="parse_item", follow=True),)

    def parse_item(self, response):
        item = {}
        # 下面幾個註釋的是建立後就自動生成的,我們不適用這種,刪除就行。
        #item["domain_id"] = response.xpath('//input[@id="sid"]/@value').get()
        #item["name"] = response.xpath('//div[@id="name"]').get()
        #item["description"] = response.xpath('//div[@id="description"]').get()
        return item

寫入json檔案

# spider\read.py
import scrapy
from ..items import ReadbookItem
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class ReadSpider(CrawlSpider):
    name = "read"
    allowed_domains = ["www.xx.com"]
    start_urls = ["https://www.xx.com/book/1188_1.html"]

    rules = (Rule(LinkExtractor(
                    allow=r"/book/1188_\d+\.html"),
                    callback="parse_item",  # callback只能寫函式名字串,callback="parse_item"
                    follow=True),)

    def parse_item(self, response):
        data_list = response.xpath("//div[@class='book-info']/div/a/img")
        for line in data_list:
            name = line.xpath("./@alt").extract_first()
            src = line.xpath("./@data-original").extract_first()
            print(name, src)

            book = ReadbookItem(name=name, src=src)

            yield book
# items.py
import scrapy

class ReadbookItem(scrapy.Item):
    name = scrapy.Field()
    src = scrapy.Field()
# pipelines.py
import json
from itemadapter import ItemAdapter

class ReadbookPipeline:
    def open_spider(self, spider):
        # 在爬蟲啟動時初始化一個空列表用來儲存資料
        self.data_list = []

    def process_item(self, item, spider):
        # 將Item物件轉換為字典格式並新增到資料列表中
        data = dict(item)
        self.data_list.append(data)
        return item  # 返回Item物件

    def close_spider(self, spider):
        # 當爬蟲關閉時,將資料列表寫入到JSON檔案中
        with open('book.json', 'wt', encoding='utf-8') as file:
            json.dump(self.data_list, file, ensure_ascii=False)

寫入資料庫 sqlalchemy

注意!commit一定要拿到一個資料就提交一次!!

資料庫設定

# settings.py 中任意位置新增

DB_HOST = "localhost"  # 資料庫主機地址 localhost同107.0.0.1
DB_PORT = 3306  # 資料庫埠號
DB_USER = "root"  # 資料庫使用者名稱
DB_PASSWORD = "123456"  # 資料庫密碼
DB_NAME = "spider1"  # 資料庫名稱
DB_CHARSET = 'utf8'  # 資料庫字符集設定  這裡不能加橫槓 -

載入settings檔案

如果用得到,因為我使用的是sqlalchemy,所以壓根就不需要設定settings

from scrapy.utils.project import get_project_settings

核心程式碼

# items.py
import scrapy

class ReadbookItem(scrapy.Item):
    # 定義您的資料項欄位在這裡,例如:
    name = scrapy.Field()  # 書名
    src = scrapy.Field()   # 連結地址
# spiders\read.py
import scrapy
from ..items import ReadbookItem  # 匯入自定義的資料項類 ReadbookItem
from scrapy.linkextractors import LinkExtractor  # 匯入 LinkExtractor 類
from scrapy.spiders import CrawlSpider, Rule  # 匯入 CrawlSpider 類和 Rule 類

class ReadSpider(CrawlSpider):
    name = "read"  # 爬蟲的名稱
    allowed_domains = ["www.xx.com"]  # 允許爬取的域名
    start_urls = ["https://www.xx.com/book/1188_1.html"]  # 爬蟲啟動的起始連結

    rules = (Rule(LinkExtractor(
                    allow=r"/book/1188_\d+\.html"),  # 定義規則,允許匹配的連結格式
                    callback="parse_item",  # 指定回撥函式為 parse_item  這裡只能給字串
                    follow=False),)  # 不跟蹤提取到的連結 ,如果要跟蹤爬取設定為True即可,一般設定為True

    def parse_item(self, response):
        data_list = response.xpath("//div[@class='book-info']/div/a/img")  # 使用 XPath 選擇器提取書籍資訊
        for line in data_list:
            name = line.xpath("./@alt").extract_first()  # 提取書名
            src = line.xpath("./@data-original").extract_first()  # 提取連結地址
            print(name, src)  # 列印提取到的書名和連結地址

            book = ReadbookItem(name=name, src=src)  # 建立一個 ReadbookItem 例項

            yield book  # 返回提取的資料項
import scrapy
from ..items import ReadbookItem
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class ReadSpider(CrawlSpider):
    name = "read"
    allowed_domains = ["www.dushu.com"]
    start_urls = ["https://www.dushu.com/book/1188_1.html"]

    rules = (Rule(LinkExtractor(
                    allow=r"/book/1188_\d+\.html"),
                    callback="parse_item",  # callback只能寫函式名字串,callback="parse_item"
                    follow=False),)

    def parse_item(self, response):
        data_list = response.xpath("//div[@class='book-info']/div/a/img")
        for line in data_list:
            name = line.xpath("./@alt").extract_first()
            src = line.xpath("./@data-original").extract_first()
            print(name, src)

            book = ReadbookItem(name=name, src=src)

            yield book #
# pipelines.py
import json
import sqlalchemy
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from itemadapter import ItemAdapter

# 建立資料庫引擎
engine = sqlalchemy.create_engine("mysql://root:243204@localhost/spider1?charset=utf8")

# 建立基礎模型類
Base = declarative_base()

# 定義書籍類
class ReadBook(Base):
    __tablename__ = "readbook"
    
    id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
    name = sqlalchemy.Column(sqlalchemy.String(128), nullable=True)
    src = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)

# 建立所有的表結構
Base.metadata.create_all(engine)

# 建立資料庫會話
Session = sessionmaker(bind=engine)

class ReadbookPipeline:
    def open_spider(self, spider):
        self.data_list = []

    def process_item(self, item, spider):
        data = dict(item)
        self.data_list.append(data)
        return item

    def close_spider(self, spider):
        # 將資料寫入 JSON 檔案
        with open('book.json', 'wt', encoding='utf-8') as file:
            json.dump(self.data_list, file, ensure_ascii=False)

class MysqlPipeline:
    def open_spider(self, spider):
        # 在爬蟲啟動時建立資料庫會話
        self.session = Session()

    def process_item(self, item, spider):
        # 處理每個條目並將其新增到資料庫會話中
        book = ReadBook(
            name=item['name'],
            src=item['src']
        )
        self.session.add(book)
        # 每次處理完一個條目就提交一次
        self.session.commit()

    def close_spider(self, spider):
        # 在爬蟲關閉時提交事務並關閉資料庫會話
        self.session.commit()
        self.session.close()  # 關閉資料庫會話

日誌資訊和等級

1. 日誌級別
	- CRITICAL: 嚴重錯誤
	- ERROR: 一般錯誤
	- WARNING: 警告
	- INFO: 一般資訊
	- DEBUG: 除錯資訊
	
	預設的日誌等級是DEBUG
	只要出現了DEBUG或者DEBUG以上等級的日誌
	那麼這些日誌就會列印

2. settings.py檔案設定
	預設的級別為DEBUG,會顯示上面所有的資訊
	在配置檔案中 settings.py
	LOG_FILE:將螢幕顯示的資訊全部記錄到檔案中,螢幕不再顯示,注意檔案後續一定是.log
	LOG_LEVEL: 設定日誌顯示的等級,就是顯示哪些,不顯示哪些
	
	eg:
		- LOG_FILE = "logdemo.log"
		- LOG_LEVEL = "LEVEL" # 一般不會設定等級, 而是寫入到檔案裡面,方便查錯,定義此項後,後續的日誌記錄就會追加到這個日誌裡面
		- 執行語句後面新增 --nolog
			- scrapy crawl xx --nolog
		
	【*】 推薦使用LOG_FILE 的方式

scrapy的post請求

# 1. 重寫start_requests方法
	def start_requests(self)
# 2. start_requests的返回值
	scrapy.FromFequest(url=url, headers=headers, callback=self.parse_item, formdata=data)
    	url: 要傳送的post地址
        headers: 可以定製頭資訊
        callback: 回撥函式
        formdata: post所攜帶的資料,這是一個字典
import json
import scrapy


class TestpostSpider(scrapy.Spider):
    name = "testpost"
    allowed_domains = ["fanyi.baidu.com"]
    # post請求 如果不寫任何引數,那麼寫過請求將會沒有任何的意義
    # 所以 這個start_urls也沒有用了
    # 由於parse依賴於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": "cancer"}
        yield scrapy.FormRequest(url=url, formdata=data, callback=self.parse_second)
        
    
    def parse_second(self, response):
        content = response.body
        data = json.loads(content)
        print(data)

爬蟲中介軟體和下載中介軟體

爬蟲中介軟體

# 爬蟲中介軟體
class Day06StartSpiderMiddleware:
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the spider middleware does not modify the
    # passed objects.

    @classmethod
    def from_crawler(cls, crawler):
        # 該方法是Scrapy用於建立爬蟲例項的方法。
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_spider_input(self, response, spider):
        # 當響應從爬蟲中介軟體進入爬蟲時,呼叫該方法進行處理。
        # 應返回None或引發異常。
        # Called for each response that goes through the spider
        # middleware and into the spider.
        # Should return None or raise an exception.
        return None

    def process_spider_output(self, response, result, spider):
        # 當爬蟲處理完響應後,呼叫該方法對處理結果進行處理。
        # 必須返回一個可迭代的Request物件或item物件。
        # Called with the results returned from the Spider, after
        # it has processed the response.

        # Must return an iterable of Request, or item objects.
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # 當爬蟲中丟擲異常時,呼叫該方法進行處理。
        # 應返回None或者一個可迭代的Request物件或item物件。
        # Called when a spider or process_spider_input() method
        # (from other spider middleware) raises an exception.

        # Should return either None or an iterable of Request or item objects.
        pass

    def process_start_requests(self, start_requests, spider):
        # 在爬蟲啟動時,對初始請求進行處理。
        # Called with the start requests of the spider, and works
        # similarly to the process_spider_output() method, except
        # that it doesn’t have a response associated.

        # Must return only requests (not items).
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

這段程式碼是一個爬蟲中介軟體,用於在Scrapy框架中對爬蟲進行處理。下面是對每個方法的功能和用途的詳細解釋:

class Day06StartSpiderMiddleware:
 # Not all methods need to be defined. If a method is not defined,
 # scrapy acts as if the spider middleware does not modify the
 # passed objects.

 @classmethod
 def from_crawler(cls, crawler):
     # 該方法是Scrapy用於建立爬蟲例項的方法。

     s = cls()
     crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
     return s

from_crawler 方法是一個類方法,它會在建立爬蟲例項時被Scrapy呼叫,用於初始化爬蟲中介軟體的例項。在該方法中,首先建立了一箇中介軟體例項 s,然後透過 crawler.signals.connect 方法連線了 spider_opened 訊號和對應的處理方法。

 def process_spider_input(self, response, spider):
     # 當響應從爬蟲中介軟體進入爬蟲時,呼叫該方法進行處理。

     # 應返回None或引發異常。
     return None

process_spider_input 方法會在響應從爬蟲中介軟體傳遞到爬蟲之前呼叫。它接收兩個引數:response 是響應物件,spider 是當前爬蟲例項。這個方法可以用來對響應進行預處理或檢查。應該返回 None 或引發異常。

 def process_spider_output(self, response, result, spider):
     # 當爬蟲處理完響應後,呼叫該方法對處理結果進行處理。

     # 必須返回一個可迭代的Request物件或item物件。
     for i in result:
         yield i

process_spider_output 方法在爬蟲處理完響應後會被呼叫。它接收三個引數:response 是爬蟲處理後的響應物件,result 是爬蟲的處理結果,spider 是當前爬蟲例項。這個方法主要用於對爬蟲處理結果進行進一步處理或過濾,並將處理結果返回。必須返回一個可迭代的Request物件或item物件。

 def process_spider_exception(self, response, exception, spider):
     # 當爬蟲中丟擲異常時,呼叫該方法進行處理。

     # 應返回None或者一個可迭代的Request物件或item物件。
     pass

process_spider_exception 方法在爬蟲或 process_spider_input() 方法中丟擲異常時會被呼叫。它接收三個引數:response 是發生異常的響應物件,exception 是丟擲的異常物件,spider 是當前爬蟲例項。這個方法可以用來對爬蟲處理過程中的異常進行處理,可以返回 None 或一個可迭代的Request物件或item物件。

 def process_start_requests(self, start_requests, spider):
     # 在爬蟲啟動時,對初始請求進行處理。

     # Must return only requests (not items).
     for r in start_requests:
         yield r

process_start_requests 方法在爬蟲啟動時被呼叫,用於對初始請求進行處理。它接收兩個引數:start_requests 是初始請求的列表,spider 是當前爬蟲例項。這個方法必須返回一個可迭代的Request物件,而不能返回item物件。

 def spider_opened(self, spider):
     spider.logger.info("Spider opened: %s" % spider.name)

spider_opened 方法在爬蟲開啟時被呼叫。它接收一個引數 spider,表示當前爬蟲例項。在這個方法中,透過日誌記錄器(logger)輸出 "Spider opened: 爬蟲名稱" 的資訊。

下載中介軟體

# 下載中介軟體
class Day06StartDownloaderMiddleware:
    # 不是所有的方法都需要定義。如果某個方法沒有被定義,
    # Scrapy會認為這個下載中介軟體不會修改傳遞的物件。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用該方法建立您的爬蟲。
        s = cls()
        # 透過signals連線spider_opened訊號和spider_opened方法
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s
	
    # 攔截處理所有的請求物件
    # 引數:request就是攔截到的請求物件,spider爬蟲檔案中爬蟲類例項化的物件
    # spider引數的作用可以實現爬蟲類和中間類的資料互動
    def process_request(self, request, spider):
        # 返回None:繼續處理本次請求,執行下一個中介軟體的process_request方法
        # 返回一個Response物件:執行當前中介軟體的process_response方法,重新回到引擎,被排程
        # 返回一個Request物件:直接返回給引擎,被排程。進入排程器等待下次被呼叫
        # 丟擲IgnoreRequest異常:呼叫已安裝的下載中介軟體的process_exception方法
        return None
	
    # 攔截處理所有的響應物件
    # 引數:response就是攔截到的響應物件,request就是被攔截到響應物件對應的唯一的一個請求物件
    def process_response(self, request, response, spider):
        # - 返回一個Response物件:繼續執行,進入引擎,被排程到爬蟲進行解析
        # - 返回一個Request物件:進入引擎,返回到排程器被重新呼叫
        # - 或者丟擲IgnoreRequest異常:丟擲異常
        return response
	
    # 攔截和處理發生異常的請求物件
    # 引數:reqeust就是攔截到的發生異常的請求物件
    # 方法存在的意義:將發生異常的請求攔截到,然後對其進行修正
    def process_exception(self, request, exception, spider):
        # 當下載處理程式或process_request()方法(來自其他下載中介軟體)引發異常時呼叫。

        # 必須返回以下之一:
        # - 返回None:繼續處理該異常
        # - 返回一個Response物件:停止process_exception()鏈
        # - 返回一個Request物件:停止process_exception()鏈
        pass
	
    # 控制日誌資料的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)
  • from_crawler(cls, crawler)方法:該方法是Scrapy用來建立爬蟲的入口點。它返回一箇中介軟體物件,並透過crawler.signals連線到spider_opened訊號,以便在爬蟲開啟時執行相應的操作。
  • process_request(self, request, spider)方法:該方法在傳送請求之前被呼叫。您可以在此方法中對請求進行處理和修改。返回值決定了後續處理的行為,可以是None繼續處理當前請求,返回一個Response物件以便執行process_response方法,返回一個Request物件以便重新排程,或者丟擲IgnoreRequest異常以呼叫其他下載中介軟體的process_exception方法。
  • process_response(self, request, response, spider)方法:該方法在收到響應後被呼叫。您可以在此方法中對響應進行處理和修改。返回值決定了後續處理的行為,可以是返回Response物件以便進一步處理和解析,返回Request物件以便重新排程,或者丟擲IgnoreRequest異常。
  • process_exception(self, request, exception, spider)方法:當下載處理程式或其他下載中介軟體的process_request方法引發異常時呼叫該方法。您可以在此處處理異常,並根據需要返回值。
  • spider_opened(self, spider)方法:在爬蟲開啟時被呼叫。在這個示例中,它會輸出一個日誌資訊。

配置檔案

# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
   "day06Start.middlewares.Day06StartSpiderMiddleware": 543,
}

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
   "day06Start.middlewares.Day06StartDownloaderMiddleware": 543,

請求頭相關

修改請求頭

在下載中介軟體的def process_request(self, request, spider):寫程式碼

加代理

  • 在下載中介軟體寫process_request方法
def get_proxy(self):
    import requests
    res = requests.get('http://127.0.0.1:5010/get/').json()
    if res.get('https'):
        return 'https://' + res.get('proxy')
    else:
        return 'http://' + res.get('proxy')


def process_request(self, request, spider):
    request.meta['proxy'] = self.get_proxy()
    return None
  • 代理可能不能用,會觸發process_exception,在裡面寫
def process_exception(self, request, exception, spider):
    # 第二步:代理可能不能用,會觸發process_exception,在裡面寫

    def process_exception(self, request, exception, spider):
        print('-----', request.url)  # 這個地址沒有爬
        return request

加cookies

def process_request(self, request, spider):
    # 新增cookie
    request.cookies['cookies'] = 'cookies'
    print(request.url+':請求物件攔截成功!')
    return None

修改請求頭

def process_request(self, request, spider):
    request.headers['referer'] = 'http://www.lagou.com'
    return None

隨機UA

# 動態生成User-agent使用
def process_request(self, request, spider):
    # fake_useragent模組
    from fake_useragent import UserAgent
    request.headers['User-Agent']=str(UserAgent().random)
    pprint(request.url+':請求物件攔截成功!')

    return None

小結

def process_request(self, request, spider):
    # 返回None:繼續處理本次請求,執行下一個中介軟體的process_request方法
    # 返回一個Response物件:執行當前中介軟體的process_response方法,重新回到引擎,被排程
    # 返回一個Request物件:直接返回給引擎,被排程。進入排程器等待下次被呼叫
    # 丟擲IgnoreRequest異常:呼叫已安裝的下載中介軟體的process_exception方法

    # 構建代理池
    # 第一步:構建代理池
    def get_proxy(self):
        import requests
        res = requests.get('http://127.0.0.1:5010/get/').json()
        if res.get('https'):
            return 'https://' + res.get('proxy')
        else:
            return 'http://' + res.get('proxy')

    def process_request(self, request, spider):
        # 在 meta 中加入代理
        request.meta['proxy'] = self.get_proxy()
        # 代理可能不能用,會觸發process_exception,在裡面寫
        return None

    # 新增cookie
    request.cookies['cookies'] = 'cookies'

    # 修改請求頭
    request.headers['referer'] = 'http://www.lagou.com'

    # 動態生成User-agent使用
    request.headers['User-Agent'] = str(UserAgent().random)

    return None

def process_exception(self, request, exception, spider):
    # 第二步:代理可能不能用,會觸發process_exception,在裡面寫

    def process_exception(self, request, exception, spider):
        print('-----', request.url)  # 這個地址沒有爬
        return request

相關文章