70585


## 質問内容

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

## コード

[code]

    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)
    
[/code]

## 実行結果（タイムアウト時）

[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]


CC BY-SA 4.0
2020-09-21T07:24:21.650
70582
2020-09-22T03:58:39.873
22565
question
python python3 非同期 python-requests
非同期処理を行なっているrequestsがタイムエラーを起こした時、再帰処理を行いたい。


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

[code]

    do_task(next_links, name, color, depth - 1)
    
[/code]

を実行する前に

[code]

    next_links = [s for s in next_links if s]
    
[/code]

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

[code]

    if next_links:
        return do_task(next_links, name, color, depth - 1)
    
[/code]

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

[code]

    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)
    
[/code]

そちらの環境に合わせて

[code]

    self.__rt = 0.01   # for debug
    
[/code]

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

[code]

    canceled = False
    ans_url = ""
    gtask = []
    
[/code]

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

[code]

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

とすべきです。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 のメソッド名の由来が理解できていません。適切でなければ変更した方が良いと思います。

[code]

    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)
    
[/code]

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


CC BY-SA 4.0
2020-09-21T09:37:47.523
70585
2020-09-22T03:58:39.873
2020-09-22T03:58:39.873
41756
41756
70582
answer