Scrapy 下载多层请求、多页图片 (重写get_media_requests、file_path方法)

本文介绍了使用Python Scrapy框架进行网页爬虫的实践,详细讲解了数据结构定义、爬虫编写、图片下载处理及数据保存到Excel的过程。在items.py中定义了图片字段,在spiders模块中编写了爬虫逻辑,包括逐页抓取和解析详情页URL。在pipelines.py中实现了数据保存到Excel以及自定义ImagesPipeline进行图片下载,通过file_path方法重命名图片文件。最后在settings.py中配置了下载延时和图片存储路径。整个流程展示了Scrapy爬虫处理图片数据的完整流程。

思路

  	1. 确定数据结构 item
  	2. 写爬虫程序 spider
  		① 每一页的每一个详情页 url
  		② 翻页
  		③ 详情页匹配目标数据
  	3. 管道处理数据 piplines
  		① 保存到 excel
  		② 下载图片
  	4. 配置设置 settings

① items.py

import scrapy

class BizhiItem(scrapy.Item):
    image_urls = scrapy.Field()  # 如果要调用 ImagesPipeline 的 get_media_requests并且不重写它的返回值(是一个Requests请求对象列表),必须要用 imag_urls 这个数据名(而且该数据必须传送来一个 list列表)!
    image = scrapy.Field()  # 暂时用不到 # list类型,用于保存下载的image对象,该字段无需赋值,由scrapy自动完成

  • 如果涉及下列方式下载图片

    • 1.用 ImagesPipeline 的默认方法下载或者不修改 ImagesPipeline 的 get_media_requests的返回值

    • 2.重写 ImagesPipeline 的 get_media_requests 方法,但不修改其返回值(即返回值还是一个 Request请求对象列表)
      那么item中存储图片路径字段必须是 list! 即:这里要下载的图片(即使只有一张) url 必须命名为 image_urls 放到 list 中传送给 item

除非重写 ImagesPipeline 的 get_media_requests 的方法的返回值(本是一个请求对象的列表,修改为返回单个Request请求对象即可)

② photo.py (spiders)

import scrapy
from ..items import BizhiItem

class PhotoSpider(scrapy.Spider):
    name = 'photo'
    allowed_domains = ['wallpaperscraft.com']
    # start_urls = ['http://wallpaperscraft.com']

    domain = 'https://wallpaperscraft.com'
    page_url = 'https://wallpaperscraft.com/all/page'
    page = 1

    def start_requests(self):
        main_url = 'https://wallpaperscraft.com/all/page1'
        yield scrapy.Request(url=main_url, callback=self.parse_page_html)

    def parse_page_html(self, response):
        detail_urls = response.xpath('/html/body/div[1]/div[2]/div[2]/div/div[2]/div[1]/ul/li/a/@href').extract()
        # print(detail_urls)
        for detail_url in detail_urls:
            detail_url = self.domain + detail_url
            print('正在请求详情页:' + detail_url + '...')
            yield scrapy.Request(url=detail_url, callback=self.parse_detail_html)

        if self.page < 21:
            self.page += 1
            page_url = self.page_url + str(self.page)
            yield scrapy.Request(url=page_url, callback=self.parse_page_html)

    def parse_detail_html(self, response):
        image_url = response.xpath('/html/body/div[1]/div[2]/div[2]/div/div[2]/div[1]/div[1]/img/@src').extract_first()
        print('请求下载: ' + image_url)
        yield BizhiItem(image_urls=[image_url])
        # 如果要用 ImagesPipeline 的默认方法下载或者不修改 ImagesPipeline 的 get_media_requests的返回值
        # 那么这里要下载的图片(即使只有一张) url 必须命名为 image_urls 放到 list 中传送给 item
        # 除非重写 ImagesPipeline 的 get_media_requests 的方法的返回值(是一个请求对象的列表)

③ piplines.py

import os.path
import xlwt

# 保存到 excel
class BizhiPipeline:
    def open_spider(self, spider):
        self.workbook = xlwt.Workbook()
        self.worksheet = self.workbook.add_sheet('sheet1')
        self.line_cnt = 0
        self.col_name = ['image_urls']
        # 写入表头
        self.worksheet.write(self.line_cnt, 0, self.col_name[0])
        self.line_cnt += 1

    def process_item(self, item, spider):
        try:
            # 写入数据
            for i in range(1):
                self.worksheet.write(self.line_cnt, i, item[self.col_name[i]][0])
            self.line_cnt += 1
            self.workbook.save('wall_paper.xls')

            return item  # 必须加,否则其他管道就无法获得item了!
        except Exception as e:
            print('写入失败!有残缺数据!已自动跳过!')


from scrapy.pipelines.images import ImagesPipeline
from bizhi import settings  # 记得把根目录标记为: 源/根 后导入(此处没用到)
class DownloadImagePipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        # 直接推送 request请求(重写了返回值)
        # yield scrapy.Request(url=item['img_url'], meta={'folder': item['img_url'].split('/')[-1].split('_')[0],
        #                                                 'name': item['img_url'].split('/')[-1]})
        
        request_list = super().get_media_requests(item, info)  # 调用父类的方法获得请求对象列表
        for request in request_list:
            request.item = item  # 给 request请求对象 增加一个属性 item,用于在其他函数中可以用 item
            # request.item  = item 直接给每一个 Request对象 添加一个 item属性;
            # request.meta = {"item": item} 这里不行!meta为只读属性!想法是给Requests的meta属性添加值,效果一样,都是为了传参用,但只能是在构造函数中 scrapy.Request(meta=) 才能用,其他地方不允许修改值

        return request_list
        
        # 或直接
        # urls = ItemAdapter(item).get(self.images_urls_field, [])
        # return [Request(u,meta = {'item' : item}) for u in urls]  # 加个meta

    # 重命名,若不重写这函数,图片名为哈希,就是一串乱七八糟的名字
    def file_path(self, request, response=None, info=None):
        # category = request.item['image_urls'][0].split('/')[-1].split('_')[0]
        # dir_category = os.path.join(settings.IMAGES_STORE,category)  # 没必要
        # name = request.item['image_urls'][0].split('/')[-1]
        # print(name)
        # file_path = os.path.join(dir_category, name)
        # return file_path
        
		# 在 settings.py 中设置了 IMAGES_STORE = 'D:\img6'后,储存路径就会定位到此处,再加入类别目录即可
        category = request.item['image_urls'][0].split('/')[-1].split('_')[0]
        name = request.item['image_urls'][0].split('/')[-1]
        file_path = os.path.join(category, name)
        return file_path

  • meta 为只读属性
# request.meta = {"item": item} 这里不行!meta为只读属性!
想法是给Requests的meta属性添加值,效果一样,都是为了传参用,
但只能是在构造函数中 scrapy.Request(meta=) 才能用,其他地方不允许修改值
  • 导入 settings 失败
 from bizhi import settings  # 记得把根目录标记为: 源/根 后导入

原因: 这个报错的意思是:试图在顶级包(top-level package)之外进行相对导入。也就是说相对导入只适用于顶级包之内的模块

由于在"顶层模块"之外引用包,这里用到"顶层模块"的概念,“顶层模块” 是这执行文件同级的文件

from . import XXX
  或者 
from .. import XXX

  时会遇到这样两个错误:
SystemError: Parent module '' not loaded, cannot perform relative impor
  和   
ValueError: attempted relative import beyond top-level package

其实这两个错误的原因归根结底是一样的:在涉及到相对导入时,package所对应的文件夹必须正确的被python解释器视作package,而不是普通文件夹。否则由于不被视作package,无法利用package之间的嵌套关系实现python中包的相对导入。

文件夹被python解释器视作package需要满足两个条件:

  • 1、文件夹中必须有__init__.py文件,该文件可以为空,但必须存在该文件

  • 2、不能作为顶层模块来执行该文件夹中的py文件(即不能作为主函数的入口 模块的__name__ 不能等于__main__)

很多时候就是导入和当前执行的py文件同级的package中的模块时报错: attempted relative import beyond top-level package

此时,该包作为顶层模块(和执行文件同级),已经不被视为一个package了,需要将他们的父级目录标记为源/根

补充:在"from YY import XX"这样的代码中,无论是XX还是YY,只要被python解释器视作package,就会首先调用该package的__init__.py文件。如果都是package,则调用顺序是YY,XX。

也就是说 你不能在一个x.py 文件中 执行 from .模块名 import * 同时运行 python x.py

另外,练习中“from . import XXX”和“from … import XXX”中的’.‘和’…‘,可以等同于linux里的shell中’.‘和’…'的作用,表示当前工作目录的package和上一级的package。

Pycharm中的解决方案:把根目录标记为: 源/根 后导入(右键相应文件夹,选择 Mark Directory as Sources 即可)

④ settings.py

BOT_NAME = 'bizhi'

SPIDER_MODULES = ['bizhi.spiders']
NEWSPIDER_MODULE = 'bizhi.spiders'


ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
   'bizhi.pipelines.BizhiPipeline': 300,
   # 'scrapy.pipelines.images.ImagesPipeline':1,           #自动保存时使用,引入ImagesPipeline,优先级设为最高
   'bizhi.pipelines.DownloadImagePipeline':1,
}

DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True

IMAGES_STORE = 'D:\img6'

⑤ start.py (启动文件)

from scrapy import cmdline
cmdline.execute('scrapy crawl photo'.split(' '))

Scrapy 基础链接: Python爬虫|Scrapy 基础用法

<think>好的,我现在需要帮助用户解决如何在Python函数`def get_media_id(self, file_path: str):`中正确使用或传递`file_path`参数的问题。首先,我需要理解用户的具体需求。用户提到“在函数定义之后添加file_path参数的值或使用方法”,这可能意味着他们想知道如何在调用这个函数时传递有效的文件路径,或者如何在函数内部处理这个参数。 首先,我要回顾用户提供的上下文。用户之前讨论了路径类型注解的不同方法,包括使用`pathlib.Path`、混合类型注解等。此外,用户引用了几个代码片段,涉及文件路径的拼接、Scrapy爬虫的设置以及Django模型的外键。这可能意味着用户正在处理一个涉及文件操作的项目,比如爬虫或数据处理,需要正确管理文件路径。 接下来,用户的问题集中在`get_media_id`函数中的`file_path`参数。我需要考虑这个函数可能的用途。例如,这个函数可能是用来获取媒体文件的ID,需要读取文件或处理文件路径。因此,正确传递和处理`file_path`参数是关键。 我需要考虑以下几个步骤: 1. **参数类型注解的优化**:用户之前提到了使用`pathlib.Path`作为类型注解的优势,比如IDE自动补全和类型检查。虽然当前函数使用`str`作为类型,但可能建议用户改用`Path`以提高代码健壮性。 2. **路径验证和处理**:在函数内部,需要确保传入的路径是有效的。例如,检查文件是否存在,是否是文件而不是目录,或者处理相对路径转换为绝对路径。 3. **兼容性处理**:用户之前的回答提到了跨操作系统路径处理,可能需要考虑不同操作系统下的路径分隔符问题,使用`pathlib`来处理可以自动适应不同系统。 4. **错误处理**:添加异常处理,当路径无效时抛出明确的错误信息,方便调试。 5. **示例用法**:提供如何调用该函数的示例,包括使用字符串路径和`Path`对象,以及如何处理可能出现的异常。 同时,我需要参考用户提供的引用内容。例如,引用[1]中的`os.path`操作,引用[3]中的路径设置,以及引用[4]中的类型注解和鸭子类型。这些引用可能提示用户在处理文件路径时需要注意模块的选择和类型注解的最佳实践。 可能的疑问点包括:用户是否希望保持`file_path`为字符串类型,还是更倾向于使用`pathlib.Path`?是否需要处理相对路径或绝对路径?函数是否需要返回媒体ID的具体逻辑是什么?但由于用户没有提供函数的具体实现,我需要专注于参数的传递和处理。 另外,用户之前提到的运行时类型验证装饰器(方案五)可能适用,可以在调用函数前验证路径是否存在,但这可能增加额外开销,需要权衡。 总结,我需要给出几种方法,包括类型注解优化、路径处理、验证和示例,同时结合用户之前的回答,确保建议符合最佳实践,并提升代码的健壮性和可维护性。</think>根据Python类型系统和文件操作的最佳实践,以下是`get_media_id`函数中`file_path`参数的使用方案,结合您提供的代码上下文和路径操作规范: --- ### 一、基础参数传递模式 ```python class MediaProcessor: def get_media_id(self, file_path: str) -> int: """核心逻辑:通过文件路径获取媒体ID""" # 示例实现 with open(file_path, 'r') as f: return hash(f.read()) # 替换为实际业务逻辑 # 调用示例 processor = MediaProcessor() id = processor.get_media_id("/data/media/video.mp4") # 直接传递字符串路径 ``` **特点**: - 符合传统字符串路径使用习惯 - 需要手动处理路径验证和转换[^1] --- ### 二、增强类型注解方案(推荐) ```python from pathlib import Path class MediaProcessor: def get_media_id(self, file_path: Path) -> int: """使用pathlib增强路径安全""" if not file_path.is_file(): raise ValueError(f"无效媒体文件: {file_path}") return file_path.stat().st_ino # 使用文件inode作为示例ID # 调用时自动转换路径类型 processor.get_media_id(Path("relative/path.mp3").resolve()) # 确保绝对路径 ``` **优势**: - 获得路径方法自动补全(`.exists()`, `.name`等) - 内置路径规范化处理[^3] --- ### 三、混合参数类型支持 ```python from typing import Union from pathlib import Path FilePath = Union[str, Path, bytes] # 兼容多种路径类型 class MediaProcessor: def get_media_id(self, file_path: FilePath) -> int: """兼容新旧系统的实现""" path = Path(file_path).expanduser().resolve() # 处理~符号和相对路径 if not path.exists(): raise FileNotFoundError(f"路径不存在: {path}") return id_generator(path) # 自定义ID生成逻辑 ``` **特性**: - 符合PEP 519路径协议标准 - 自动转换输入路径类型[^4] --- ### 四、防御性编程增强 ```python from functools import wraps def validate_media_path(func): @wraps(func) def wrapper(self, file_path, *args, **kwargs): path = Path(file_path).resolve() if path.suffix not in {'.mp4', '.mov', '.avi'}: raise ValueError(f"不支持的媒体格式: {path.suffix}") return func(self, path, *args, **kwargs) return wrapper class MediaProcessor: @validate_media_path def get_media_id(self, file_path: Path) -> int: """带装饰器的安全访问""" return generate_id(file_path) ``` **安全机制**: 1. 路径解析和标准化 2. 文件扩展名白名单验证 3. 自动路径类型转换 --- ### 五、跨平台路径处理 ```python import platform class MediaProcessor: def get_media_id(self, file_path: str) -> int: """统一处理路径分隔符""" # 转换Windows路径为POSIX格式 if platform.system() == 'Windows': clean_path = file_path.replace('\\', '/') else: clean_path = file_path # 示例:提取文件名哈希 file_name = Path(clean_path).name return hash(file_name) ``` **兼容性处理**: - 自动适配不同操作系统路径格式 - 使用`pathlib`进行核心操作[^1] --- ### 六、异步文件处理扩展 ```python import aiofiles from pathlib import Path class AsyncMediaProcessor: async def get_media_id(self, file_path: Path) -> int: """异步文件读取方案""" async with aiofiles.open(file_path, 'r') as f: content = await f.read() return compute_media_id(content) # 自定义异步计算逻辑 ``` **应用场景**: - 高并发媒体处理系统 - 需要非阻塞IO操作时 --- ### 参数传递模式对比 | 方法 | 类型安全 | 自动补全 | 执行效率 | 兼容旧代码 | |--------------------|----------|----------|----------|------------| | 纯字符串 | 低 | 无 | 高 | 完全兼容 | | pathlib.Path | 高 | 有 | 中 | 需改造 | | 混合类型注解 | 中 | 部分 | 中高 | 较好兼容 | --- ### 迁移策略建议 1. **渐进式改造**: ```python # 过渡阶段支持两种类型 def get_media_id(self, file_path: str | Path) -> int: path = Path(file_path).resolve() # 核心逻辑保持不变 ``` 2. **类型提示强化**: ```python from typing import TypeAlias MediaPath: TypeAlias = Path # Python 3.10+类型别名 def get_media_id(self, file_path: MediaPath) -> int: """强类型约束方案""" ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值