70318


requestsを非同期にしたのですが、タイムアウト時のエラーハンドリングしたいです。

**プログラムで実行したい事**

[前回](https://ja.stackoverflow.com/questions/70112/python%E3%81%AErequests%E5%87%A6%E7%90%86%E3%82%92%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AB%E3%81%97%E3%81%9F%E3%81%84)欲しい商品を検索するプログラムを作成しました。  
商品全体のページからurlsを取得して非同期でアクセスを出して欲しい商品とマッチする商品urlを取得するものです。

安定してプログラムが動作するように  
サイトが重くなり読み込みが遅い場合にフリーズを避けるためにrequestsにタイムアウトの時間を設定しました。  
もしタイムアウトでエラーが発生した場合は、一旦そこでタスク完了にして、エラーが出たurlをリストに格納して貯めます。  
場合によって、検索に引っかかった：urlとアクセスできなかった：urlsが出来る。

１回目のイベントループによる仕事が終了した際、エラーが起きても、欲しい商品urlは見つかれば処理を終了します。  
もし欲しいurlが見つからない場合は先ほど探せなかったurlsから再び探すためにイベントループに仕事を与えます。  
これを再帰的に行うプログラムを書きたくて挑戦したのですが再帰呼びすぎとエラーが出て上手く動作しないです。

[code]

    import asyncio
    import time
    import requests
    from bs4 import BeautifulSoup
    from requests.exceptions import Timeout
    
    class Test():
        # initとあるがこれもあくまで最初のメソッドに過ぎないためJSのコンストラクタのように使用出来ない。
        def __init__(self):
            self.links = []
    
        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
            try:
                item_page = requests.get(url, timeout=(3.0, 7.5))
            except Timeout:
                print('商品ページがひらけない')
                return url
            soup = BeautifulSoup(item_page.content, 'lxml')
            item_name = soup.select('h1[itemprop="name"]')[0].string
            print(item_name)
            item_color = soup.select('#details > p.style')[0].string
            print(item_color)
            if name in item_name and color in item_color:
                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))]
                return await asyncio.gather(*tasks)
            links = self.get_item_urls(category)
            def do_task(links, name, color):
                loop = asyncio.get_event_loop()
                data = loop.run_until_complete(want_item_url(loop, links, name, color))
                next_links = []
                url = ''
                for s in data:
                    if s in links:
                        next_links += [s]
                    else:
                        url = s
                if url:
                    loop.close()
                    return url
                else:
                    do_task(next_links, name, color)
            return do_task(links, name, color)
    
    
    global start
    start = time.time()
    Test = Test()
    want_url = Test.non_req_url('accessories', 'Crew Socks', 'White')
    print(want_url)
    time_of_script = time.time() - start
    print('実行時間：{}'.format(time_of_script))
    
[/code]

# 追記9/11

> この条件で timeout したものを続けて再帰処理を行うことに意味があるのか疑問に感じます。

限定商品の販売日とかはサイトが重くなるので、その中でもなるべく早い処理を行えないかと思い考えました。

実際にこの処理を追加すると普段の時でも倍の時間がかかるようになってしまいました。これはif文、try文やfor文の内包表記を辞めてしまった事によるのでしょうか？それとも何か再帰にする事で通常の状態で無駄な処理を行なっているのでしょうか？

depthの制限を追加したら速度が改善しました。  
普通にサイトアクセス出来る時でも再帰的な処理が多重に行われているのでしょうか？通常の状態でも多重に再帰処理が行われているのは少し無駄に感じてしまいます。

> とするのであれば data にはヒットした url とヒットできなかった場合の link を混在して使う前提になっているのではありませんか？  
>  （あまり気持ち良い設計ではないですね）

素人なので気持ちの良い設計については分からないですが、`loop.run.until()`の中にある処理を触るのはまだ知識不足で難しそうと判断したため返ってきた値を仕分けすれば良いと考えてしまいました。

個人的には`loop.run.until()`より下のコードで`if`文を多用しているのでもう少しスッキリ出来ないかなとか考えてしまいます。

** 9/21 結果報告

[code]

    再帰の回数確認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
    
[/code]

いくつか取得出来ないページあるにも関わらず、再帰処理で取得する様子が見られませんでした。イメージだと再帰回数の確認9と8の間に取得出来なかったページを再び巡回して取得出来た、出来ないが表示されても良さそうなのですが、やはり再帰処理は上手く行われていないのでしょうか？


CC BY-SA 4.0
2020-09-11T06:20:37.840
70315
2020-09-21T09:42:42.883
2020-09-21T01:39:05.173
22565
22565
question
python python3 非同期 python-requests
非同期にしたrequestsの処理でタイムアウト時のエラーハンドリングをしたい。


timeout=(3.0, 7.5)  
となると connect timeout が 3秒 read timeout が 7.5 秒ですが、この条件で timeout
したものを続けて再帰処理を行うことに意味があるのか疑問に感じます。

[code]

    for s in data:
        if s in links:
        next_links += [s]
    else:
        url = s
    
[/code]

とするのであれば data にはヒットした url とヒットできなかった場合の link を混在して使う前提になっているのではありませんか？  
（あまり気持ち良い設計ではないですね）

最も大きな問題は

[code]

    else:
        url = s
    
[/code]

としているため data の最後が None の場合は url が None となって永遠に再帰処理を呼び出しますね。

で期待するコードと思われる内容を提示します。

[code]

        def __init__(self):
            #self.links = []   # 使用していない
            pass
    
        # ...
        def search_item(self, link, name, color):
            # ...
            except Timeout:
                print('商品ページがひらけない')
                #return url
                return link  # 返したいのは link
    
        def search_item(self, link, name, color):
            # 再帰のリミット回数を設ける
            def do_task(links, name, color, depth):   # depth 追加
                if depth <= 0:                        # 追加
                    return                            # 追加
                loop = asyncio.get_event_loop()
                data = loop.run_until_complete(want_item_url(loop, links, name, color))
                next_links = []
                url = ''
                for s in data:
                    if s in links:
                        next_links += [s]
                    #else:                            # 削除
                    elif s:                           # 追加
                        url = s
                        break
                if url:
                    loop.close()
                    return url
                else:
                    # 2020/09/21 do_task の前に return 追記
                    return do_task(next_links, name, color, depth - 1)  # depth - 1 追加
            return do_task(links, name, color, 10)    # 10 depth 指定
    
[/code]

## 追記9/11 の回答

>
> 実際にこの処理を追加すると普段の時でも倍の時間がかかるようになってしまいました。これはif文、try文やfor文の内包表記を辞めてしまった事によるのでしょうか？それとも何か再帰にする事で通常の状態で無駄な処理を行なっているのでしょうか？

性能については事実を把握すべきです。  
time.time() を使用して処理毎の経過時間を調べて元のコードの時間と再帰版の時間を比較しましょう。

> depthの制限を追加したら速度が改善しました。  
>
> 普通にサイトアクセス出来る時でも再帰的な処理が多重に行われているのでしょうか？通常の状態でも多重に再帰処理が行われているのは少し無駄に感じてしまいます。

url が取得できれば再帰処理は動かないコードにしています。  
適宜 ptint('depth: ', depth) を入れて再帰が動作しているか確認しましょう。


CC BY-SA 4.0
2020-09-11T10:52:58.780
70318
2020-09-21T09:42:42.883
2020-09-21T09:42:42.883
41756
41756
70315
answer