非同期処理を行なっているrequestsがタイムエラーを起こした時、再帰処理を行いたい。

投稿者: Anonymous

質問内容

Pythonのモジュールrequestsを非同期処理で実装してタイムエラーが起きた際はHTMLを取得出来なかったURLを集めて再び同じ処理を行うようにプログラムを組みました。
しかし、最初の処理で取得出来ないURLがあっても再帰処理が行われていないように見えます。
ご教授お願いします。

コード

import asyncio
import time
import requests
from urllib.parse import quote
from config import settings 
from bs4 import BeautifulSoup
from requests.exceptions import Timeout


class Purchase():
    canceled = False
    ans_url = ""
    gtask = []
    # initとあるがこれもあくまで最初のメソッドに過ぎないためJSのコンストラクタのように使用出来ない。
    def __init__(self):
        pass

    def get_item_urls(self, category):
        url = 'https://www.supremenewyork.com/shop/all/' + category
        for i in range(3):
            try:
                category_page = requests.get(url, timeout=(3.0, 7.5))
            except Timeout:
                print('カテゴリページ読み込めなかった。')
            else:
                break
        
        soup = BeautifulSoup(category_page.content, 'lxml')
        items_div = soup.select('article > .inner-article > a')
        links = [url.get('href') for url in items_div]
        return links
    
    def search_item(self, link, name, color):
        
        url = 'https://www.supremenewyork.com' + link

        if Purchase.canceled: return

        try:
            item_page = requests.get(url, timeout=(3.0, 7.5))
        except Timeout:
            print('商品ページがひらけない')
            return link

        if Purchase.canceled: return

        soup = BeautifulSoup(item_page.content, 'lxml')
        try:
            item_name = soup.select('h1[itemprop="name"]')[0].string
            print(item_name)
            item_color = soup.select('#details > p.style')[0].string
            print(item_color)
        except IndexError as e:
            print('商品名が取得出来ない')
            return link
        if name in item_name and color in item_color:
            if not Purchase.canceled:
                Purchase.canceled = True
                Purchase.ans_url = url
                print('*** set ans_url ***')
                Purchase.gtask.cancel()
                #return url

    def non_req_url(self, category, name, color):
        async def want_item_url(loop, links, name, color):
            sem = asyncio.Semaphore(20)
            async def async_ex(i):
                async with sem:
                    return await loop.run_in_executor(None, self.search_item, links[i], name, color)
            tasks = [async_ex(i) for i in range(len(links))]
            Purchase.gtask = asyncio.gather(*tasks)
            return await Purchase.gtask
        links = self.get_item_urls(category)
        def do_task(links, name, color, depth):
            print('再帰の回数確認depth: ', depth)
            next_links = []
            if depth <= 0:
                return
            loop = asyncio.get_event_loop()
            try:
                next_links = loop.run_until_complete(want_item_url(loop, links, name, color))
            except asyncio.exceptions.CancelledError as e:
                print("*** CancelledError ***", e)
            finally:
                if Purchase.ans_url:
                    loop.close()
                    return Purchase.ans_url
                else:
                    do_task(next_links, name, color, depth - 1)
        return do_task(links, name, color, 10)


test = Purchase()
item_url = test.non_req_url('accessories', 'Crew Socks', 'White')
print(item_url)

実行結果(タイムアウト時)

再帰の回数確認depth:  10
Stripe Appliqué S/S Top
Navy
Stripe Appliqué S/S Top
Slate
商品ページがひらけない
商品ページがひらけない
S/S Pocket Tee
Heather Coral
Textured Small Box Sweater
Black
S/S Pocket Tee
Black
商品ページがひらけない
商品ページがひらけない
商品ページがひらけない
Small Box Tee
Digi Floral
Small Box Tee
Fluorescent Yellow
Small Box Tee
Heather Grey
商品ページがひらけない
Small Box Tee
Rust

再帰の回数確認depth:  9
再帰の回数確認depth:  8
再帰の回数確認depth:  7
再帰の回数確認depth:  6
再帰の回数確認depth:  5
再帰の回数確認depth:  4
再帰の回数確認depth:  3
再帰の回数確認depth:  2
再帰の回数確認depth:  1
再帰の回数確認depth:  0

解決

商品ページを開いて ans_url を設定しない場合は None が返っていますので

do_task(next_links, name, color, depth - 1)

を実行する前に

next_links = [s for s in next_links if s]

と None を削除して next_links に有効な link がある場合に

if next_links:
    return do_task(next_links, name, color, depth - 1)

と do_task() しなければなりませんね。
以前の質問の回答でも None の link は削除していますが do_task() には return を付け忘れていました。
再帰動作をデバッグしたいのであれば、次のように timeout を変化させながら実行してはいかがでしょうか?

import asyncio
import time
import requests
#from urllib.parse import quote
#from config import settings 
from bs4 import BeautifulSoup
from requests.exceptions import Timeout

class Purchase():
    #canceled = False
    #ans_url = ""
    #gtask = []
    # initとあるがこれもあくまで最初のメソッドに過ぎないためJSのコンストラクタのように使用出来ない。
    def __init__(self, max_recur=10):
        self.__ans_url = ""
        self.__gtask = []
        self.__canceled = False
        self.__max_recur = max_recur
        self.__rt = 0.01   # for debug

    def get_item_urls(self, category):
        url = 'https://www.supremenewyork.com/shop/all/' + category
        for i in range(3):
            try:
                category_page = requests.get(url, timeout=(3.0, 7.5))
            except Timeout:
                print('カテゴリページ読み込めなかった。')
            else:
                break
        
        soup = BeautifulSoup(category_page.content, 'lxml')
        items_div = soup.select('article > .inner-article > a')
        links = [url.get('href') for url in items_div]
        return links
    
    def search_item(self, link, name, color):
        url = 'https://www.supremenewyork.com' + link
        #if Purchase.canceled: return
        if self.__canceled: return
        try:
            #item_page = requests.get(url, timeout=(3.0, 7.5))
            item_page = requests.get(url, timeout=(self.__rt, self.__rt))   # for debug
        except Timeout:
            print('商品ページがひらけない')
            return link
        #if Purchase.canceled: return
        if self.__canceled: return
        soup = BeautifulSoup(item_page.content, 'lxml')
        try:
            item_name = soup.select('h1[itemprop="name"]')[0].string
            print(item_name)
            item_color = soup.select('#details > p.style')[0].string
            print(item_color)
        except IndexError as e:
            print('商品名が取得出来ない')
            return link
        if name in item_name and color in item_color:
            #if not Purchase.canceled:
            if not self.__canceled:
                #Purchase.canceled = True
                #Purchase.ans_url = url
                self.__canceled = True
                self.__ans_url = url
                print('*** set ans_url ***', url)
                #Purchase.gtask.cancel()
                self.__gtask.cancel()
                #return url

    def non_req_url(self, category, name, color):
        async def want_item_url(loop, links, name, color):
            sem = asyncio.Semaphore(20)
            async def async_ex(i):
                async with sem:
                    return await loop.run_in_executor(None, self.search_item, links[i], name, color)
            tasks = [async_ex(i) for i in range(len(links))]
            #Purchase.gtask = asyncio.gather(*tasks)
            #return await Purchase.gtask
            self.__gtask = asyncio.gather(*tasks)
            return await self.__gtask
        links = self.get_item_urls(category)
        def do_task(links, name, color, depth):
            print('再帰の回数確認depth: ', self.__max_recur - depth)
            print("links", links)
            self.__rt += 0.01               # for debug
            print("timeout: ", self.__rt)   # for debug
            next_links = []
            if depth <= 0:
                return
            loop = asyncio.get_event_loop()
            try:
                next_links = loop.run_until_complete(want_item_url(loop, links, name, color))
            except asyncio.exceptions.CancelledError as e:
                print("*** CancelledError ***", e)
            finally:
                #if Purchase.ans_url:
                if self.__ans_url:
                    loop.close()
                    #return Purchase.ans_url
                    return self.__ans_url
                else:
                    #do_task(next_links, name, color, depth - 1)
                    next_links = [s for s in next_links if s]
                    if next_links:
                        return do_task(next_links, name, color, depth - 1)
        return do_task(links, name, color, self.__max_recur)

test = Purchase()
item_url = test.non_req_url('accessories', 'Crew Socks', 'White')
print("item_url: ", item_url)

そちらの環境に合わせて

self.__rt = 0.01   # for debug

の 0.01 sec は変更してください。
なお、

canceled = False
ans_url = ""
gtask = []

はクラス変数です。内容的にはインスタンス変数ですので

def __init__(self, max_recur=10):
    self.__ans_url = ""
    self.__gtask = []
    self.__canceled = False

とすべきです。Purchase のインスタンスを複数動作させた場合には意図しない結果になります。

9/22 追記

ご提示された Purchase クラスには次の点を含めた改善したい内容があります。
do_task() 内で loop = asyncio.get_event_loop() を再帰処理のたびに実行するのは非効率
non_req_url() メソッド実行時に loop.close() を実行する場合としない場合がある
Purchase クラスの non_req_url メソッドを 呼び出し do_task() 内で loop.close() を実行た場合以降このスレッド内でイベントループは asyncio.new_event_loop() 等の処置なしでは使えなくなってしまう。
もし、イベントループをクローズするのであれば
if name == ‘main‘:
を設けてこの if 文内の最後で行うのが無難と思います。

Purchase クラスの改善案
クラス利用する方から url 情報、最大再帰深さ、タイムアウトを設定できるようにしています。
non_req_url のメソッド名の由来が理解できていません。適切でなければ変更した方が良いと思います。

import asyncio
import time
import requests
from bs4 import BeautifulSoup
from requests.exceptions import Timeout

class Purchase:
    def __init__(self, url, sub_url, max_recur=10, timeout=(3.0, 7.5)):
        self.__target_url = url
        self.__sub_url = sub_url
        self.__timeout = timeout
        self.__ans_url = ""
        self.__gtask = []
        self.__canceled = False
        self.__max_recur = max_recur
    
    def get_item_urls(self, category):
        url = self.__target_url + self.__sub_url + category
        for i in range(3):
            try:
                category_page = requests.get(url, timeout=self.__timeout)
            except Timeout:
                print('カテゴリページ読み込めなかった。')
            else:
                break
        soup = BeautifulSoup(category_page.content, 'lxml')
        items_div = soup.select('article > .inner-article > a')
        links = [url.get('href') for url in items_div]
        return links
    
    def search_item(self, link, name, color):
        url = self.__target_url + link
        if self.__canceled: return
        try:
            item_page = requests.get(url, timeout=self.__timeout)
        except Timeout:
            print('商品ページがひらけない')
            return link
        if self.__canceled: return
        soup = BeautifulSoup(item_page.content, 'lxml')
        try:
            item_name = soup.select('h1[itemprop="name"]')[0].string
            print(item_name)
            item_color = soup.select('#details > p.style')[0].string
            print(item_color)
        except IndexError as e:
            print('商品名が取得出来ない')
            return link
        if name in item_name and color in item_color:
            if not self.__canceled:
                self.__canceled = True
                self.__ans_url = url
                print('*** set ans_url ***', url)
                self.__gtask.cancel()
    
    def non_req_url(self, category, name, color):
        async def want_item_url(loop, links, name, color):
            sem = asyncio.Semaphore(20)
            async def async_ex(i):
                async with sem:
                    return await loop.run_in_executor(None, self.search_item, links[i], name, color)
            tasks = [async_ex(i) for i in range(len(links))]
            self.__gtask = asyncio.gather(*tasks)
            return await self.__gtask
        def do_task(links, name, color, depth):
            print('再帰 depth:', self.__max_recur - depth)
            links = [s for s in links if s]
            if depth <= 0 or not links:
                return self.__ans_url
            next_links = []
            try:
                next_links = loop.run_until_complete(want_item_url(loop, links, name, color))
            except asyncio.exceptions.CancelledError as e:
                print("*** CancelledError ***", e)
            finally:
                if self.__ans_url:
                    return self.__ans_url
                else:
                    return do_task(next_links, name, color, depth - 1)
        loop = asyncio.get_event_loop()
        links = self.get_item_urls(category)
        return do_task(links, name, color, self.__max_recur)


test = Purchase('https://www.supremenewyork.com', '/shop/all/')
item_url = test.non_req_url('accessories', 'Crew Socks', 'White')
print("item_url: ", item_url)

ヒットしなかった場合の non_req_url() の戻り値は None でなく "" にしました。

回答者: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *