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 키워드를 붙여서 호출해야 한다.
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을 가져오는 프로그램

블로그 인기글
Windows 10 탐색기 느려지는 증상과 해결하는 방법
잘 작동하던 Windows 10 탐색기가 갑자기 느려지는 증상이 발생했을 때 어떻게 조치를 하는지 구글에서 찾아보니 많은 해결책들이 있었으나 어떤 것이 정확한 해결책인지는 알 수가 없었다. 그래서 해결방법이라고 제시한 것들을 정리해 보았다. 윈도우 탐색기가 느려지는 증상 해결 방법 어느 순간부터 응용프로그램(VS Code 등)에서 폴더 열기나 파일 불러오기를 했을 때 검색 팝업창이 안 뜨거나 열리는 시간이 엄청 느려지는 증상과, 더불어서 탐색기도 실행이 많이 느려지는 증상이 있었다. 기존에 사용하던 VS Code에 openpyxl 설치 후 실행이 느려지는 증상이 발생하더니 윈도우10 탐색기도 느려져서 사용할 수가 없었다. 노트북에 OS(Windows10)를 설치한지 1년이 다 되어가긴 했지만, 1개월 전..
goodthings4me.tistory.com
[엑셀] 근무연수 및 근무 개월수 계산하는 함수
직장을 다니다 보면 몇 년 몇 개월 또는 전체 며칠을 다니고 있는지 궁금할 때가 있다. 아니면, 총무나 인사 일을 할 때 직원들의 근속연수 또는 근속개월수 등을 계산하고 싶을 때도 있다. 이런 경우 엑셀 함수를 활용하면 어떨까!! 근무연수 및 근무 개월수 계산 함수 알아보기 엑셀에서 근무연수 또는 근무 개월수 계산하는 것은 datedif() 함수를 사용하면 간단하게 해결할 수 있다. 아래 이미지를 보면서 설명하면, 셀 E1에 기준일자를 입력하고, 근무연수를 구할 때는 =datedif(B3,$E$1,"Y")&"년" 을 입력한다. 근무개월수는 =datedif(B3,$E$1,"M")&"개월" 처럼 입력한다. 일수까지 파악할 때문 별로 없지만, 심심풀이로 구해보고 싶을 때 =datedif(B3,$E$1,"D")..
goodthings4me.tistory.com
[국세청] 현금영수증가맹점으로 가입바랍니다. 메시지 해결방법(개인사업자)
▶ 현금영수증 가맹점 가입 메시지를 받고... 온라인 쇼핑몰 사업을 시작하려고 사업자등록증을 발급받고 난 후 얼마 안 있어서 국세청으로부터 어느 시점까지 '현금영수증 가맹점'으로 가입하라는 문자메시지가 받았었다. 그 메시지 기한이 오늘 도래했는데, 인터넷에서 찾아보니 홈택스에서 현금영수증 발급 사업자 신청을 할 수가 있었다. [관련내용] 홈>국세정책/제도>전자(세금)계산서/현금영수증/신용카드>현금영수증∙신용카드>가맹점가입 ▶ 홈택스 사이트에서 신청하는 절차는 다음과 같다. 우선, 홈택스에 로그인을 해야 합니다. 세상이 좋아져서 공인인증서 없이도 손쉽게 간편인증 로그인이 가능하다. 여러 인증방법 중 카카오톡 인증이 가장 편리한 거 같다. 간편인증 로그인 후 상단 '조회/발급' 탭 클릭 후 '현금영수증>현금..
goodthings4me.tistory.com
'코딩 연습 > 코딩배우기' 카테고리의 다른 글
동적(JavaScript) 웹 페이지의 json 데이터 형식 이미지 다운로드 (with 파이썬) (0) | 2022.01.11 |
---|---|
11번가 실시간 쇼핑 검색어 추출해서 저장하기(python tkinter) (0) | 2022.01.10 |
selenium으로 네이버 쿠키를 얻고 세션을 유지하는 방법 (2) | 2022.01.03 |
웹 브라우저 페이지를 자동으로 스크롤 해보기 (with 파이썬) (0) | 2021.12.28 |
블로그 글 내용 저장 중 이모지 '\U0001f970' 에러 발생 (파이썬) (0) | 2021.12.26 |
댓글