python异步爬虫:(视频过大导致的)TimeoutError

爬取目标:原神官网新闻页面上的封面图片及其中的大图与视频
遇到的问题:原神官网视频太大,由于有时限导致任务被取消并报错TimeoutError,结果导致视频字节数据不完整即无法打开

问题出现的部分代码:


    async def request_IV(self, href, special, page_str):
        for src in href:
            src_href = src.group('href')

            self.src_href = src_href

            if self.check('http'):  # 检查得到的内容开头字符串是否是http
                src_href = src_href.replace('\\u002F', '/')
            else:
                special = True
                break

            async with aiohttp.ClientSession() as img_session:
                async with img_session.get(src_href) as picture:
                    # 请求网址,并写入字节数据
                    name = src_href.split('/')[-1]
                    async with aiofiles.open(f"img&video/{name}", 'wb') as f:
                        # 原神官网视频太大,由于有时限导致任务被取消,视频字节数据不完整即无法打开,遇到视频时保证字节写入得以正常进行
                        await f.write(picture.content.read())
                        print(f'下载地址:{src_href} 下载完成!{name}')
        if special:
            await special_href(page_str)

程序整个代码:

import asyncio
import aiohttp
import aiofiles
import re


# 从方法脱离出的函数:
async def cover(value, title):
    if value:
        cover_picture_url = value[0]['url']
        # 请求封面图片网址,获取字节数据并写入:
        # cover_picture = requests.get(cover_picture_url)
        async with aiohttp.ClientSession() as main_session:
            async with main_session.get(cover_picture_url) as cover_picture:
                content = await cover_picture.content.read()
                async with aiofiles.open(f'cover_img/{title}.jpg', 'wb') as cpf:
                    await cpf.write(content)
                    print(f'下载完成!地址:{cover_picture_url}标题名:{title}')


async def special_href(page_str):
    text = await page_str.text()
    special_obj = re.compile(r'window.+?height=.+?src=\\"(?P<href>.+?)\\" width=.+?;</script>')
    if not special_obj.findall(text):
        special_obj = re.compile(r'window.+?height=.+?src=\\"(?P<href>.+?)\\" style=\\"max-width.+?;</script>')
    href_special_obj = special_obj.finditer(text)  # text原:await page_str.text()
    for src in href_special_obj:
        src_href = src.group('href')
        src_href = src_href.replace('\\u002F', '/')
        # special = requests.get(href)
        async with aiohttp.ClientSession() as special_session:
            async with special_session.get(src_href) as special:
                # 请求视频或图片网址,并写入字节数据
                name = src_href.split('/')[-1]
                async with aiofiles.open(f"img&video/{name}", 'wb') as pf:
                    await pf.write(await special.content.read())
                    print(f'下载地址:{src_href} 下载完成!{name}')


class GenshinImgVideo:
    def __init__(self, url: str):
        self.url = url
        # 在后期运行时发现存在的问题:
        self.problem = None
        # 保存匹配到的内容,以检查是否正确:
        self.src_href = None

    # 问题处理
    def check(self, goal_str: str):
        goal = self.src_href[0:4]
        # 检查所获取的网址内容前四位是否是目标字符
        if goal_str == goal:
            return True
        else:
            return False

    async def get_data_list(self):
        tasks = []
        url = self.url
        # resp = requests.get(url)
        async with aiohttp.ClientSession() as data_session:
            async with data_session.get(url) as resp:
                all_dic = await resp.json()
                dicA = all_dic['data']['list']
                # 开始查获相关数据
                for dic in dicA:
                    contentId = dic['contentId']
                    title = dic['title']
                    value = dic['ext'][1]['value']
                    task = asyncio.create_task(self.main(contentId, title, value))
                    tasks.append(task)
                await asyncio.wait(tasks)

    async def request_IV(self, href, special, page_str):
        for src in href:
            src_href = src.group('href')

            self.src_href = src_href

            if self.check('http'):
                src_href = src_href.replace('\\u002F', '/')
            else:
                special = True
                break

            async with aiohttp.ClientSession() as img_session:
                async with img_session.get(src_href) as picture:
                    # 请求网址,并写入字节数据
                    name = src_href.split('/')[-1]
                    async with aiofiles.open(f"img&video/{name}", 'wb') as f:
                        # 原神官网视频太大,由于有时限导致任务被取消,视频字节数据不完整即无法打开,遇到视频时保证字节写入得以正常进行
                        await f.write(picture.content.read())
                        print(f'下载地址:{src_href} 下载完成!{name}')
        if special:
            await special_href(page_str)

    async def enter_to_find(self, contentId):
        # 开始进入子页面
        child_page_url = f'https://ys.mihoyo.com/main/news/detail/{contentId}'

        self.problem = child_page_url  # 应对后期发现的某个问题

        # page_str = requests.get(child_page_url)
        async with aiohttp.ClientSession() as child_session:
            async with child_session.get(child_page_url) as page_str:
                # 寻找文件名并合成视图请求网址
                special = False
                obj_img = re.compile(r'window.+?src=\\"(?P<href>.+?)\\" width=.+?;</script>')
                obj_video = re.compile(r'window.+?src=\\"(?P<href>.+?)\\" style=\\"max-width.+?;</script>')
                obj_video_new = re.compile(r'window.+?poster=.+?src=\\"(?P<href>.+?)\\".+?&hellip;&hellip;', re.S)
                text = await page_str.text()
                href_obj_img = obj_img.finditer(text)
                href_obj_video = obj_video.finditer(text)
                href_obj_video_new = obj_video_new.finditer(text)
                if obj_img.findall(text):
                    await self.request_IV(href_obj_img, special, page_str)
                elif obj_video.findall(text):
                    # 原神官网视频太大,由于有时限导致任务被取消,视频字节数据不完整即无法打开
                    await asyncio.shield(self.request_IV(href_obj_video, special, page_str))
                    """但是这个地方不是问题源头"""
                elif obj_video_new.findall(text):
                    # 原神官网视频太大,由于有时限导致任务被取消,视频字节数据不完整即无法打开
                    await asyncio.shield(self.request_IV(href_obj_video_new, special, page_str))
                    """但是这个地方不是问题源头"""

    async def main(self, contentId, title, value):
        tasks = [
            asyncio.create_task(cover(value, title)),
            asyncio.create_task(self.enter_to_find(contentId))
        ]
        await asyncio.wait(tasks)

    async def run(self):
        try:
            await self.get_data_list()
        except TimeoutError:
            self.write_error(None)
        except aiohttp.ClientPayloadError as eC:
            self.write_error(eC)
        else:
            pass

    def write_error(self, em):
        if em:
            print(em)
            with open('Error log.txt', 'a', encoding='utf-8') as fE:
                fE.write(str(em))
        print(self.problem)
        with open(f'log.txt', 'a', encoding='utf-8') as file:
            file.write(self.problem)
    """
    # 对于后期发现的问题,打印以显示,并保存到日志文件中
    def puts_problem(self, num_time: int):
        print(self.problem)
        with open(f'log{num_time}.txt', 'w', encoding='utf-8') as file:
            file.write(self.problem)"""


async def main(number: int):
    tasks = []
    for i in range(1, number+1):
        url = f'https://content-static.mihoyo.com/content/ysCn/getContentList?pageSize=5&pageNum={i}&channelId=10'
        exe = GenshinImgVideo(url)
        task = asyncio.create_task(exe.run())
        tasks.append(task)
    await asyncio.wait(tasks)


if __name__ == '__main__':
    times = input('请输入所翻页数:')
    try:
        num = int(times)
    except ValueError:
        print('请输入数字!')
    else:
        asyncio.run(main(num))

有人说说用asyncio.shield()进行封装,保证任务不会被取消,但我不太会用,封装picture.content.read()结果一样,封装f.write(picture.content.read())又报TypeError错误,所以谢谢各位帮我解决这个问题了

我根据你的逻辑没有找到原神视频url,不知道这视频传输是什么,只能建议你这用多线程试试看,另外我发现你这所有的地方异步,但是实际上只需要异步下载图片和视频那里就行了,需要运用一个生产者-多个消费者模型。
我一般就用线程池爬取,只要能够获取到准确的数据就行了,效率高低不太重要,快了被封或者把别人服务器搞崩,那是要喝茶的

换个配置高一点的电脑,估计是系统速度太慢,导致的!

TimeoutError通常是由于网络请求超时导致的。在异步爬虫中,可以通过以下方法来解决TimeoutError:

  1. 增加请求超时时间:在发起网络请求时,可以增加请求超时时间,例如使用aiohttp库时,可以在ClientSession中设置timeout参数,例如: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60)) as session: 。这里的timeout参数表示整个请求的超时时间,total参数表示连接、读取和写入的超时时间。
  2. 减少同时发起的请求数量:在异步爬虫中,同时发起的请求数量较多,会增加网络请求的负担,导致请求超时。可以通过限制同时发起的请求数量来减轻网络负担,例如使用asyncio.Semaphore()来限制并发请求数量。
  3. 增加网络连接数:如果同时发起的请求数量较少,但是网络连接数较少,也会导致请求超时。可以通过增加网络连接数来解决这个问题,例如使用aiohttp库时,可以在ClientSession中设置connector参数,例如: async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=100)) as session: 。这里的limit参数表示最大连接数。
  4. 检查网络状况:如果网络状况较差,也会导致请求超时。可以通过ping命令或者其他网络监测工具来检查网络状况,如果网络状况较差,可以尝试更换网络环境或者联系网络运营商解决问题。

你在处理原神官网视频时遇到的超时问题可以尝试使用asyncio.shield()进行封装,以确保任务不会被取消。下面是修改后的代码示例:


async def request_IV(self, href, special, page_str):
    for src in href:
        src_href = src.group('href')

        self.src_href = src_href

        if self.check('http'):
            src_href = src_href.replace('\\u002F', '/')
        else:
            special = True
            break

        async with aiohttp.ClientSession() as img_session:
            async with img_session.get(src_href) as picture:
                # 请求网址,并写入字节数据
                name = src_href.split('/')[-1]
                async with aiofiles.open(f"img&video/{name}", 'wb') as f:
                    # 使用 asyncio.shield() 封装,确保任务不会被取消
                    content = await picture.content.read()
                    await asyncio.shield(f.write(content))
                    print(f'下载地址:{src_href} 下载完成!{name}')
    if special:
        await special_href(page_str)

这样修改后,封装的部分f.write(content)不会被取消,并且可以正常写入字节数据。希望这能解决你遇到的问题。如果还有其他疑问,请随时向我提问。

运行结果

img

有没有想过分片下载呢


 from retrying import retry
 源码部分:
    def __init__(self,
                 stop=None, wait=None,
                 stop_max_attempt_number=None,
                 stop_max_delay=None,
                 wait_fixed=None,
                 wait_random_min=None, wait_random_max=None,
                 wait_incrementing_start=None, wait_incrementing_increment=None,
                 wait_exponential_multiplier=None, wait_exponential_max=None,
                 retry_on_exception=None,
                 retry_on_result=None,
                 wrap_exception=False,
                 stop_func=None,
                 wait_func=None,
                 wait_jitter_max=None):


stop_max_attempt_number:用来设定最大的尝试次数,超过该次数就停止重试
stop_max_delay:比如设置成10000,那么从被装饰的函数开始执行的时间点开始,到函数成功运行结束 或者失败报错中止的时间点,只要这段时间超过10秒,函数就不会再执行了
wait_fixed:设置在两次retrying之间的停留时间
wait_random_min和wait_random_max:用随机的方式产生两次retrying之间的停留时间
wait_exponential_multiplier和wait_exponential_max:以指数的形式产生两次retrying之间的停留时间,产 生的值为2^previous_attempt_number * wait_exponential_multiplier,previous_attempt_number是前面已经retry的次数,如果产生的这个值超过了wait_exponential_max的大小,那么之后两个retrying之间的停留值都为wait_exponential_max



import asyncio
import aiohttp
import aiofiles
import re

async def request_IV(self, href, special, page_str):
    for src in href:
        src_href = src.group('href')
        self.src_href = src_href

        if self.check('http'):
            src_href = src_href.replace('\\u002F', '/')
        else:
            special = True
            break

        async with aiohttp.ClientSession() as img_session:
            async with img_session.get(src_href) as picture:
                # 使用asyncio.shield()封装picture.content.read(),确保字节数据完整写入
                content = await asyncio.shield(picture.content.read())
                name = src_href.split('/')[-1]
                async with aiofiles.open(f"img&video/{name}", 'wb') as f:
                    await f.write(content)
                    print(f'下载地址:{src_href} 下载完成!{name}')
    if special:
        await special_href(page_str)

你可以使用asyncio.shield()函数来保护任务不被取消。这样,在遇到视频字节数据不完整导致任务被取消的情况下,你可以封装picture.content.read()方法,确保字节数据的完整写入。

从上面答友可以看出,排除软硬件因素,硬件的话更新更高的配置以适应你爬取的超大视频文件,软件方面的尝试更改新的爬取方式:
在爬取的时候禁用图片加载功能,仅获取图片的URL地址,这样可以避免因为图片加载导致页面卡顿或者超时。
又或者是在爬取过程中,可以考虑优化爬取策略,如采用分布式爬取、限制请求频率、随机化请求头等,以避免被目标网站识别和封禁。

增加时限或者设置特定超时时限,或者用线程处理

有没有可能是原神服务器那边的反爬机制
你尝试更换 ua和cookie 之类的试试,或者加代理池?

增加连接超时时限

可以把视频链接弄出来后,用迅雷下载

我能想到的是这两个方法
1增加重试连接次数

#设置重连次数
requests.adapters.DEFAULT_RETRIES = 15
# 设置连接活跃状态为False
s = requests.session()
s.keep_alive = False  # 在连接时关闭多余连接


2限制爬取速度

DOWNLOAD_DELAY = 0.5  
RANDOMIZE_DOWNLOAD_DELAY = True  # 随机的等待时间
CONCURRENT_REQUESTS = 1  # Scrapy框架同时请求的最大数量