본문 바로가기
코딩 연습/코딩배우기

단축 URL 스크래핑에 파이썬 비동기 처리 개념 적용해보기

by good4me 2022. 1. 7.

goodthings4me.tistory.com


파이썬에서 쓰레드와 비동기는 어떻게 사용되는지 궁금하여 알아본 결과, 하나의 쓰레드로 동시 처리를 하는 비동기 프로그래밍이 당연히 좋다고 해서, 기초적인 지식이지만, 이를 응용하여 단축 URL 생성하는 스크래핑을 비동기로 구현해보기로 했다.

URL주소 단축URL 생성 시 파이썬 비동기 처리로 스크래핑해보기

원래 파이썬은 기본적으로 동기 방식으로 설계되었고, 내장 모듈(라이브러리) 대부분도 동기 방식으로 동작한다고 한다. 그러다가 3.4 버전부터 asyncio 모듈이 추가되었고, 이후 async와 await가 채택되면서 비동기 프로그래밍이 가능해졌다.

  • 비동기 함수는 def 대신 'async def' 키워드 사용
  • await는 작업 완료 통보가 올 때까지 다음 작업을 지연시키고 이벤트 루프에 작업이 있으면 해당 작업을 처리하면서 기다리게 하는 키워드임
  • 비동기(async)인 함수(메소드)인 경우 await 키워드 붙임

※ 맨 마지막에 단축 URL 생성 웹 페이지 스크래핑을 비동기로 처리하는 소스 있음

 

 

파이썬의 동기 함수(일반 함수) 수행 시간 테스트

import time

start = time.time()
def sync_func():
    for i in range(5):
        time.sleep(1)
        print(i, end=',')

sync_func()
end = time.time()

print(f'\n수행시간: {end - start}\n')

######################################

[결과]
0,1,2,3,4,
수행시간: 5.044050216674805

 

 

함수를 3개 실행 시 수행 시간

import time

start = time.time()
def sync_func():
    for i in range(5):
        time.sleep(1)
        print(i, end=',')

sync_func()
sync_func()
sync_func()
end = time.time()

print(f'\n수행시간: {end - start}\n')

######################################

[결과]
0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,
수행시간: 15.148053646087646

 

- 일반 함수(다른 말로, 동기 함수)는 순차적으로 함수를 호출하기 때문에 작업을 완료 후 다음 작업(또는 값을 리턴하고 다음 작업)을 진행 할 수 있다.

 

 

 

하나의 비동기 함수로 처리 시 수행 시간

import time
import asyncio

start = time.time()
async def async_func():  # 비동기 처리 함수 선언 async 붙임
    for i in range(5):
        # time.sleep(1)
        await asyncio.sleep(1)
        print(i, end=',')

loop = asyncio.get_event_loop()
loop.run_until_complete(async_func())
loop.close()
end = time.time()

print(f'\n수행시간: {end - start}\n')
print(type(async_func()))  # <class 'coroutine'>

#####################################

[결과]
0,1,2,3,4,
수행시간: 5.037583112716675

 

※ 파이썬 비동기(async) 실행 절차

- 현재의 쓰레드에 이벤트 루프 객체 생성
  * 이벤트 루프는 작업(Task)들을 루프(반복)를 돌면서 하나씩 실행시키는 역할
- 해당 이벤트 루프에 인자로 넘어오는 코루틴 객체를 태스크로 예약하여 실행
- 실행 완료 후 이벤트 루프 닫기

loop = asyncio.get_event_loop()  # 현재의 이벤트 루프를 반환하는 함수
loop.run_until_complete(비동기 함수인 코루틴 객체)
loop.close()

파이썬 버전 3.7 이상부터는 위 명령 부분이 asyncio.run(비동기 함수명)으로 
간단하게 비동기 함수를 호출하고 실행하고 닫을 수 있다.

 

- 비동기 함수 선언을 위해 asyncio 모듈 import

- 파이썬에서 기존 함수(다른 말로, 동기 함수)는 def 키워드를 붙이고, 비동기 함수는 def 앞에 async를 더 붙인다.

- 이런 비동기 함수를 호출하면 코루틴 객체(<class 'coroutine'>가 반환된다.

- 하나의 비동기 함수(async_func) 수행 결과, 하나의 동기 함수(sync_func) 수행 시간과 별 차이가 없다.

 

여기서 코루틴이란,
특정한 시점의 실행 상태를 잠시 중단하고 저장한 뒤 다른 일을 하다가 어떤 이벤트(응답 완료 등)가 있을 경우 중단 상태를 복원하여 실행을 재개하는 서브 루틴(함수)을 의미히며, 파이썬에서는 async를 키워드를 통해서 생성된 비동기 함수 객체를 말한다.
 그리고, 비동기 함수(또는 async 붙인 함수)에서 다른 비동기 함수를 호출할 때는 그 함수명 앞에 await 키워드를 붙여서 호출해야 한다.


good4me.co.kr


await는,
비동기 함수의 즉시 반환으로 인한 문제(완료되지 않은 상태(결과)에 접근하는 것)을 방지하는 역할을 하는 것으로, 완료 통보가 있을 때까지 기다리면서 이벤트 루프에 있는 다른 작업(Task)을 처리하는 시간을 부여한다. (즉, await 뒤에 코루틴 객체를 두어 실행 오류 방지를 수행하는 키워드이다.)

 

time.sleep() 함수는 
지정 시간동안 CPU의 해당 프로그램 작업을 중지(Block)시키는데, 이벤트 루프도 sleep 시켜서 지정 시간 전에 동시성 처리가 끝났어도 완료 통보가 안되는 반면,

asyncio.sleep() 함수는 
지정 시간동안 완료 통보 지연은 하되 이벤트 루프를 계속 돌게 한다. (즉, CPU가 다른 작업 처리를 할 수 있도록 해준다)
다만, time.sleep(1)처럼 asyncio.sleep(1)도 지정시간만큼 지연시키기 때문에 하나의 동기 함수나 비동기 함수가 수행시간은 유사하다.

여기서 주의할 점은 asyncio.sleep 자체도 비동기 함수이기 때문에 호출할 때 반드시 await 키워드를 붙여야 한다는 것이다.

 

 

비동기 함수 3개 처리 시간

import time
import asyncio

start = time.time()
async def async_func():  # 비동기 처리 함수 선언 async 붙임
    for i in range(5):
        # time.sleep(1)
        await asyncio.sleep(1)
        print(i, end=',')

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(async_func(), async_func(), async_func()))  # def gather(*coros_or_futures, loop=None, return_exceptions=False):
loop.close()
end = time.time()

print(f'\n수행시간: {end - start}\n')

#####################################

[결과]
0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,
수행시간: 5.0364837646484375

- asyncio 모듈에서 비동기 함수들을 한 번에 등록하게하는 gather() 함수를 제공함

- 여러 함수를 실행할 때 비동기 함수의 위력이 나온다

- 동기 함수의 1개 실행기간과 비슷한 결과가 나온 것을 확인할 수 있다.

 

 

비동기 함수의 퓨처(Future) 방식으로 리턴값 받기

## Furute 방식
import time
import asyncio

start = time.time()
async def async_func():  # 비동기 처리 함수 선언 async 붙임
    for i in range(5):
        # time.sleep(1)
        await asyncio.sleep(1)
        print(i, end=',')
    return 'Finish..'

loop = asyncio.get_event_loop()
af = asyncio.ensure_future(async_func())  # return 값 받게 함
loop.run_until_complete(asyncio.gather(
    af, asyncio.ensure_future(async_func()), asyncio.ensure_future(async_func()) ))
loop.close()
end = time.time()
print(f'\n수행시간: {end - start}\n')
print(af.result())  # 결과 값은 result()

#########################################

[결과]
0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,
수행시간: 5.042426586151123   

Finish..

 

※ 참고 : https://www.youtube.com/watch?v=yOtXl8cW-ag&t=1435s

 


다른 예)

import time

def add_sum():
    time.sleep(1)  # 작업 진행 1초 지연(처리시간 비교 위해 사용)
    sum = 0
    for i in range(10):
        sum += 1
    print(f'sum:{sum}')

def main():
    add_sum()
    add_sum()
    add_sum()

start = time.time()
main()
end = time.time()
print(f'Sync#2 수행 시간: {end - start}\n')

###########################################

[결과]
sum:10
sum:10
sum:10
Sync#2 수행 시간: 3.035911798477173

 

import time
import asyncio

async def add_sum():
    # time.sleep(1)  # 작업 진행 1초 지연
    await asyncio.sleep(1)
    sum = 0
    for i in range(10):
        sum += 1
    print(f'sum:{sum}')

async def main():
    # def gather(*coros_or_futures, loop=None, return_exceptions=False):
    await asyncio.gather(  
        add_sum(),
        add_sum(),
        add_sum(),
    )
start = time.time()
asyncio.run(main())  # 비동기 함수를 호출하고 실행
end = time.time()
print(f'수행 시간: {end - start}\n')

############################################

[결과]
sum:10
sum:10
sum:10
수행 시간: 1.0162804126739502

 

 

동기 함수 requests.get()을 비동기로 동작하도록 동시성 작업 진행

 

▶ 동기 함수

import time
import requests

def download_html(url):
    response = requests.get(url)
    print(f'페이지 용량: {len(response.text)}')

def html_main():
    download_html('https://goodthings4me.tistory.com/563')
    download_html('https://goodthings4me.tistory.com/560')
    download_html('https://goodthings4me.tistory.com/547')


start = time.time()
html_main()
end = time.time()
print(f'Sync#1 수행 시간: {end - start}\n')

############################################

[결과]
페이지 용량: 45583
페이지 용량: 43769
페이지 용량: 48096
Sync#1 수행 시간: 0.9664969444274902

 

 

▶ 비동기 동작 처리

동기 함수를 비동기로 동작하도록 하기 위해서는 이벤트 루프에서 제공하는 run_in_executor() 함수가 필요하고, 
run_in_executor() 함수 사용을 위해서는 asyncio.get_event_loop()를 통해 이벤트 루프 객체를 얻어야 한다.

import time
import requests
import asyncio

async def download_html(url):  # 비동기 함수 정의
    loop = asyncio.get_event_loop()  # 이벤트 루프 객체 얻기
    response = await loop.run_in_executor(None, requests.get, url)  # 동기 함수를 비동기로 호출
    # def run_in_executor(self, executor, func, *args):
    # 지정 executor에서 func 호출하여 등록 (executor=None이면, 기본 executor 사용)

    print(f'페이지 용량: {len(response.text)}')

async def html_main():
    await asyncio.gather(
        download_html('https://goodthings4me.tistory.com/563'),
        download_html('https://goodthings4me.tistory.com/560'),
        download_html('https://goodthings4me.tistory.com/547')
    )

start = time.time()
asyncio.run(html_main())
end = time.time()
print(f'Async#1 수행 시간: {end - start}\n')

#################################################

[결과]
페이지 용량: 45583
페이지 용량: 48096
페이지 용량: 43769
Async#1 수행 시간: 0.3538508415222168

 

 

단축 URL을 생성하는 웹 페이지 스크래핑을 비동기로 처리해보기

import requests
from bs4 import BeautifulSoup
import json
import asyncio


headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36',
}


def han_gl(long_url):
    try:
        url = 'https://han.gl/shorten'
        response = requests.post(url, headers=headers, data={'url': long_url})
        r2 = json.loads(response.text)
        # print(r2['data']['shorturl'])
        shorten_url_result = r2['data']['shorturl']
    except Exception as e:
        print('Error:', e)
        return None

    return shorten_url_result    


def c11_kr(long_url):
    try:
        url = 'https://c11.kr/createurl.php'
        r1 = requests.get('https://c11.kr')
        soup = BeautifulSoup(r1.text, 'html.parser')
        hp = soup.find('input', id='hp')['value']
        response = requests.post(url, headers=headers, data={'urlr': long_url, 'hp': hp,})
        soup = BeautifulSoup(response.text, 'html.parser')
        shtn_url = soup.find('div', id='copyme')['data-clipboard-text']
        # print(shtn_url)
        shorten_url_result = shtn_url
    except Exception as e:
        print('Error:', e)
        return None
    return shorten_url_result    


def shorturl_at(long_url):
    try:
        url = 'https://www.shorturl.at/shortener.php'
        response = requests.post(url, headers=headers, data={'u': long_url})
        soup = BeautifulSoup(response.text, 'html.parser')
        shtnUrl = soup.find('input', id='shortenurl')['value']
        shtn_url = 'https://' + shtnUrl
        # print(shtn_url)
        shorten_url_result = shtn_url
    except Exception as e:
        print('Error:', e)
        return None
    return shorten_url_result    


def t2m_kr(long_url):
    try:
        url = 'http://t2m.kr/shorten'
        response = requests.post(url, headers=headers, data={'url': long_url})
        r2 = json.loads(response.text)
        # print(r2['short'])
        shorten_url_result = r2['short']
    except Exception as e:
        print('Error:', e)
        return None
    return shorten_url_result


def vo_la(long_url):
    try:
        url = 'https://vo.la/shorten'
        response = requests.post(url, headers=headers, data={'url': long_url})
        r2 = json.loads(response.text)
        # print(r2['short'])
        shorten_url_result = r2['short']
    except Exception as e:
        print('Error:', e)
        return None
    return shorten_url_result


async def make_shortenUrl(func, url):
    loop = asyncio.get_event_loop()  # 현재의 이벤트 루프 반환
    res = await loop.run_in_executor(None, func, url)  # 동기 함수를 코루틴으로 실행하는 메소드
    print(f'\n단축URL : {res}')


async def main(long_url) :
    await asyncio.gather(
        make_shortenUrl(han_gl, long_url),
        make_shortenUrl(c11_kr, long_url),
        make_shortenUrl(shorturl_at, long_url),
        make_shortenUrl(t2m_kr, long_url),
        make_shortenUrl(vo_la, long_url),
    )

long_url = 'https://blog.naver.com/k-esco/222564012422'

asyncio.run(main(long_url))

- 각 단축 URL 생성 사이트를 각각 순차적으로 실행하는 것보다 빠르게 처리됨

 

[실행 결과]

단축URL : https://c11.kr/vfkl

단축URL : http://t2m.kr/DfgHg

단축URL : https://vo.la/o0Gyi

단축URL : https://shorturl.at/knNR8

단축URL : https://han.gl/GtfxH

 

 

 

※ 비동기 모드는 아니지만, 단축URL 서비스 사이트 선택 시 생성된 단축URL을 가져오는 프로그램

 

 프로그램 다운로드

단축url 생성

 

댓글