레이블이 python인 게시물을 표시합니다. 모든 게시물 표시
레이블이 python인 게시물을 표시합니다. 모든 게시물 표시

2024년 9월 4일 수요일

bookscan image enhance(python으로 스캔한 책 전처리 )

 book scan(카메라로 찍은 사진) 한 파일의 품질을 올리는 방법을 고민을 하게 되었습니다.

ChatGPT를 이용해서 굉장히 많은 기법들을 소개(?) 받았고, 이것 저것 조합하면서 가장 무난한 방법을 찾아봤습니다.


sharpen -> bilateralFilter -> 배경 부분만 밝기 높이기 순으로 되어 있습니다.


아래 코드를 참고하시고, 모르는건 ChatGPT에 문의해보면 자세히 알려줍니다.

세상이 편해졌네요. 그래도 이것저것 해보고 제일 좋은지 어떤지는 인간이 좀 판단을 해야합니다. filename만 적절히 변경 하시면 됩니다.

filename=r'D:\temp\1_20240830\0066.jpg'

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 이미지 불러오기
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 1. 샤프닝 적용
kernel_sharpen = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
sharpened_image = cv2.filter2D(image_rgb, -1, kernel_sharpen)

# 2. bilateralFilter 적용
bilateral_filtered_image = cv2.bilateralFilter(sharpened_image, d=9, sigmaColor=75, sigmaSpace=75)

# 3. Grayscale 변환
gray = cv2.cvtColor(bilateral_filtered_image, cv2.COLOR_RGB2GRAY)

# Otsu's Thresholding을 사용하여 글씨와 배경 분리
# Otsu's Method는 최적의 threshold 값을 자동으로 선택함
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 선택된 Otsu Threshold 값 출력
otsu_threshold_value = _ # Otsu가 선택한 최적의 Threshold
print(f"Otsu's Threshold Value: {otsu_threshold_value}")

# 배경 마스크를 생성 (글씨가 있는 부분은 제외)
background_mask = cv2.bitwise_not(mask)

# 배경을 밝게 조정 (50 정도 밝기 증가)
upcolor = 50
background_brightened = cv2.add(bilateral_filtered_image, np.array([upcolor, upcolor, upcolor], dtype=np.float32), mask=background_mask)

# 글씨 부분은 원본에서 그대로 가져옵니다.
final_image = cv2.add(background_brightened, cv2.bitwise_and(bilateral_filtered_image, bilateral_filtered_image, mask=mask))

# 결과 출력
plt.figure(figsize=(18, 6))
plt.subplot(131), plt.imshow(image_rgb), plt.title('Original')
plt.subplot(132), plt.imshow(bilateral_filtered_image), plt.title('Sharpened + Bilateral Filtered')
plt.subplot(133), plt.imshow(final_image), plt.title('Background Brightened with Otsu Thresholding')
plt.axis('off')
plt.show()


소스는 아래 링크 참고

Create jpg_to_enhance3.py · donarts/sourcecode@54ee6af · GitHub


추가 개선 


threshold -> adaptiveThreshold 로 변경함

filename=r'D:\temp\0005.jpg'

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 이미지 불러오기
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 1. 샤프닝 적용
kernel_sharpen = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
sharpened_image = cv2.filter2D(image_rgb, -1, kernel_sharpen)

# 2. bilateralFilter 적용
bilateral_filtered_image = cv2.bilateralFilter(sharpened_image, d=9, sigmaColor=75, sigmaSpace=75)

# 3. Grayscale 변환
gray = cv2.cvtColor(bilateral_filtered_image, cv2.COLOR_RGB2GRAY)

# 4. GaussianBlur 적용 (Thresholding 전에)
gray = cv2.GaussianBlur(gray, (5, 5), 0)

# Otsu's Thresholding을 사용하여 글씨와 배경 분리
# Otsu's Method는 최적의 threshold 값을 자동으로 선택함
#_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# Adaptive Thresholding을 사용하여 글씨와 배경 분리
mask = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 41, 8)

# 선택된 Otsu Threshold 값 출력
#otsu_threshold_value = _ # Otsu가 선택한 최적의 Threshold
#print(f"Otsu's Threshold Value: {otsu_threshold_value}")

# 배경 마스크를 생성 (글씨가 있는 부분은 제외)
background_mask = cv2.bitwise_not(mask)

# 배경을 밝게 조정 (50 정도 밝기 증가)
upcolor = 50
background_brightened = cv2.add(bilateral_filtered_image, np.array([upcolor, upcolor, upcolor], dtype=np.float32), mask=background_mask)

# 글씨 부분은 원본에서 그대로 가져옵니다.
final_image = cv2.add(background_brightened, cv2.bitwise_and(bilateral_filtered_image, bilateral_filtered_image, mask=mask))

# 결과 출력
plt.figure(figsize=(18, 6))
plt.subplot(131), plt.imshow(image_rgb), plt.title('Original')
#plt.subplot(132), plt.imshow(bilateral_filtered_image), plt.title('Sharpened + Bilateral Filtered')
plt.subplot(132), plt.imshow(mask, cmap='gray'), plt.title('mask')
#plt.subplot(132), plt.imshow(gray, cmap='gray'), plt.title('gray')
plt.subplot(133), plt.imshow(final_image), plt.title('Background Brightened with Otsu Thresholding')
plt.axis('off')
plt.show()


sourcecode/python/example/_64_jpeg_to_pdf/jpg_to_enhance3.py at main · donarts/sourcecode · GitHub



2024년 9월 1일 일요일

remove background(배경 제거) in python, rembg 사용

AI로 이미지를 만들면 배경을 제거해야 하는 경우가 있는데 파일이 여러개면 대량으로 제거하기가 불편합니다. 그래서 python 스크립트로 만들어 봤습니다.

GPT로 생성해봤는데 생각보다 잘되지 않아서 작업한 코드를 공유합니다.

이미지 파일들에서 배경을 제거하고 투명 배경을 적용하여 다른 폴더에 저장하는 파이썬 코드입니다. 먼저 이 작업을 수행하기 위해서는 Pillow와 rembg 라이브러리를 사용 해야 합니다.

rembg는 이미지에서 배경을 제거하는데 사용되며, Pillow는 이미지를 처리하고 저장하는 데 사용됩니다.


아래 패키지들을 설치해줍니다.

pip install pillow rembg


rembg 설치하는데 시간이 오래 걸리니 참고 해주세요

전체 코드

import os
from PIL import Image
from rembg import remove, new_session

def remove_background_and_save(source_folder, destination_folder, model_name):
# 세션 객체 생성, 특정 모델 지정
session = new_session(model_name=model_name)

# 소스 폴더에서 모든 파일을 검색
for filename in os.listdir(source_folder):
if filename.endswith('.webp') or filename.endswith('.jpg') \
or filename.endswith('.png') or filename.endswith('.jpeg'):
file_path = os.path.join(source_folder, filename)
output_filename = os.path.splitext(filename)[0] + '.png' # 원래 파일 이름에서 확장자를 png로 변경
output_path = os.path.join(destination_folder, output_filename)
print(file_path)
if os.path.exists(output_path):
print(f"File {output_path} already exists, skipping...")
continue

# 이미지 파일을 열고 배경 제거
with Image.open(file_path) as img:
input_data = img.convert("RGBA") # rembg RGBA 포맷의 바이트 데이터가 필요

# alpha_matting_foreground_threshold: 알파 매팅에서 전경 임계값을 설정합니다. 기본값은 240입니다.
# alpha_matting_background_threshold: 알파 매팅에서 배경 임계값을 설정합니다. 기본값은 10입니다.
result_data = remove(input_data, session=session, alpha_matting=True,
alpha_matting_background_threshold=128) # 배경 제거

# 결과 이미지 생성 및 저장
result_data.save(output_path, format='PNG') # PNG 형식으로 저장


if __name__ == "__main__":
# 폴더 경로 설정 (사용자가 수정 가능)
source_folder = r'D:\dev\game_art\dalle\monster'
destination_folder = r'D:\dev\game_art\dalle\monster\rbg'

model_name = 'birefnet-general' # 'u2net', 'u2net_human_seg' 등 다른 모델도 사용 가능

'''
The available models are:

u2net (download, source): A pre-trained model for general use cases.
u2netp (download, source): A lightweight version of u2net model.
u2net_human_seg (download, source): A pre-trained model for human segmentation.
u2net_cloth_seg (download, source): A pre-trained model for Cloths Parsing from human portrait.
Here clothes are parsed into 3 category: Upper body, Lower body and Full body.
silueta (download, source): Same as u2net but the size is reduced to 43Mb.
isnet-general-use (download, source): A new pre-trained model for general use cases.
isnet-anime (download, source): A high-accuracy segmentation for anime character.
sam (download encoder, download decoder, source): A pre-trained model for any use cases.
birefnet-general (download, source): A pre-trained model for general use cases.
birefnet-general-lite (download, source): A light pre-trained model for general use cases.
birefnet-portrait (download, source): A pre-trained model for human portraits.
birefnet-dis (download, source): A pre-trained model for dichotomous image segmentation (DIS).
birefnet-hrsod (download, source): A pre-trained model for high-resolution salient object detection (HRSOD).
birefnet-cod (download, source): A pre-trained model for concealed object detection (COD).
birefnet-massive (download, source): A pre-trained model with massive dataset.
'''

# 경로가 없다면 생성
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)

remove_background_and_save(source_folder, destination_folder, model_name)


여러가지 모델을 사용해봤는데 제일 잘 나오는 형태가 birefnet-general 모델 이었습니다.
이것은 이미지에 따라 다르므로 여러가지 시도해보시기 바랍니다.

전체 코드는 아래링크에서 다운로드 하세요


2024년 3월 22일 금요일

python xml comment parsing

python xml comment parsing

python에서는 xml 파싱하는 방법은 여러가지 준비가 되어 있습니다.

그런데 간혹 comment 까지 파싱 해야 하는 경우가 있습니다. 

XML은 데이터를 저장하고 전송하는 데 사용되는 마크업 언어입니다. 다음은 XML의 간단한 예제입니다

python에서 아래와 같은 xml을 string에 담았습니다.

xml_str = \
"""<?xml version="1.0" encoding="UTF-8"?>
<!-- hello -->
<note>
<!-- dash1 -->
<to>kim</to><!-- dash2 -->
<from>lee</from>
<heading>alert</heading>
<body>i am a boy</body>
</note>
"""

여기에서 XML 파서는 lxml 을 사용합니다. 이 패키지는 comment까지 완벽하게 처리해냅니다.

기본 xml 처리는 아래와 같은 형태를 사용 가능합니다.

def print_xml(xml_str_):
# XML 파일을 불러옵니다.
root = etree.fromstring(xml_str.encode())

# 루트 엘리먼트의 태그와 속성을 출력합니다.
print(f'Root element: {root.tag}')
for name, value in root.attrib.items():
print(f'Attribute - {name}: {value}')

# 모든 자식 엘리먼트를 순회하며 출력합니다.
for child in root:
print(f'Child element: {child.tag}')
for name, value in child.attrib.items():
print(f'Attribute - {name}: {value}')

결과를 아래와 같이 실행시키면

print_xml(xml_str)

다음과 같은 결과를 얻습니다.

Root element: note
Child element: <cyfunction Comment at 0x00000234BF7132B0>
Child element: to
Child element: <cyfunction Comment at 0x00000234BF7132B0>
Child element: from
Child element: heading
Child element: body

cyfunction 이라고 나오는곳이 comment가 됩니다. 이 부분을 처리하려면 아래와 같은 코드가 필요합니다. tag가 etree.Comment 인지 비교해서 따로 처리하는 방법입니다.

if child.tag == etree.Comment:
print("comment:", child.text)

그러면 처음 예제와 합쳐서 구현해보도록 하겠습니다.

def print_xml_wc(xml_str_):
# XML 파일을 불러옵니다.
root = etree.fromstring(xml_str.encode())

# 루트 엘리먼트의 태그와 속성을 출력합니다.
print(f'Root element: {root.tag}')
for name, value in root.attrib.items():
print(f'Attribute - {name}: {value}')

# 모든 자식 엘리먼트를 순회하며 출력합니다.
for child in root:
print(f'Child element: {child.tag}')
if child.tag == etree.Comment:
print("comment:", child.text)
continue
for name, value in child.attrib.items():
print(f'Attribute - {name}: {value}')

이런 식으로 됩니다.

print_xml_wc(xml_str)

수행한 결과는 아래와 같습니다.

Root element: note
Child element: <cyfunction Comment at 0x00000234BF7132B0>
comment: dash1
Child element: to
Child element: <cyfunction Comment at 0x00000234BF7132B0>
comment: dash2
Child element: from
Child element: heading
Child element: body


xml 데이터의 root node보다 앞쪽에 있는 comment 처리 방법

xml을 자세히 보면 앞쪽 hello 라는 주석을 파싱하지 못하는 부분이 있습니다.

이건 어떻게 해야할까요?

처음에 문서도 보고 한참 헤맸는데요. 의외로 간단합니다.

element의 getprevious() 함수를 사용합니다.

변경된 코드는 아래와 같습니다.

def print_xml_wc_root(xml_str_):
# XML 파일을 불러옵니다.
root = etree.fromstring(xml_str.encode())

if root.getprevious() != None:
if root.getprevious().tag == etree.Comment:
print("comment:", root.getprevious().text)

# 루트 엘리먼트의 태그와 속성을 출력합니다.
print(f'Root element: {root.tag}')
for name, value in root.attrib.items():
print(f'Attribute - {name}: {value}')

# 모든 자식 엘리먼트를 순회하며 출력합니다.
for child in root:
print(f'Child element: {child.tag}')
if child.tag == etree.Comment:
print("comment:", child.text)
continue
for name, value in child.attrib.items():
print(f'Attribute - {name}: {value}')

root node의 앞쪽 tag 가 comment 라면 출력하는 코드를 넣었습니다.

실행한 결과는 아래와 같고

comment:  hello 
Root element: note
Child element: <cyfunction Comment at 0x0000026F045732B0>
comment: dash1
Child element: to
Child element: <cyfunction Comment at 0x0000026F045732B0>
comment: dash2
Child element: from
Child element: heading
Child element: body

전체 소스는 git hub에서 보시기 바랍니다.

sourcecode/python/example/_63_lxml/lxml_comment.py at main · donarts/sourcecode · GitHub



2024년 2월 20일 화요일

python requests 사용시 multipart 로 사용하는 방법

python requests 를 사용해서 post를 사용하다면, 기본적으로 application/x-www-form-urlencoded 을 기본적으로 사용하게 됩니다.

그런데 간혹 multipart/form-data 를 원하는 경우가 있습니다.

기본 예제를 살펴 보겠습니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}

r = requests.post(url, data=data)
print(r.request.headers)
print(r.request.body)

위와 같이 사용하게 되면 'Content-Type': 'application/x-www-form-urlencoded' 이 됩니다.

{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '23', 'Content-Type': 'application/x-www-form-urlencoded'}
key1=value1&key2=value2

그렇다면 multipart 가 되는 조건은 뭘까요?

파일을 첨부 할때입니다.

파일 첨부 예를 만들어 보겠습니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}

files1 = {'fl': open('test.txt', 'rb')}
r = requests.post(url, files=files1)
print(r.request.headers)
print(r.request.body)


{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '149', 'Content-Type': 'multipart/form-data; boundary=40aac4e5ef113fc32999aeb43e78702e'}
b'--40aac4e5ef113fc32999aeb43e78702e\r\nContent-Disposition: form-data; name="fl"; filename="test.txt"\r\n\r\ntest123\r\n--40aac4e5ef113fc32999aeb43e78702e--\r\n'

결과가 알아보기 어렵게 되긴했지만 'Content-Type': 'multipart/form-data; 가 된것을 알 수 있습니다. 뒤에 있는 boundary는 여러개를 구분하는 용도로 사용하게 됩니다.

처음으로 돌아와서 그렇다면 어떻게 하면 data를 multipart로 전달이 가능할까요?

다른 패키지를 사용하는 방법도 있지만, requests에서는 files와 같이 보내는 방법이 있습니다.

그런데 파일을 보내지 않고 싶은데 어떻게 하냐고요? dummy로 임의의 변수를 넣어 주면 됩니다.

최종 해결책입니다.

import requests

url = 'http://127.0.0.1:5000/funs'
data = {"key1": "value1", "key2": "value2"}


files_dummy = {'fl': None}
r = requests.post(url, data=data, files=files_dummy)
print(r.request.headers)
print(r.request.body)

리턴값입니다.

{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '220', 'Content-Type': 'multipart/form-data; boundary=21825c01c73525d71646d70babe7f0b5'}
b'--21825c01c73525d71646d70babe7f0b5\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n--21825c01c73525d71646d70babe7f0b5\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n--21825c01c73525d71646d70babe7f0b5--\r\n'

위에서 'fl' 이란 부분을 보냈는데 requests body부분을 살펴보면 관련 내용이 없음을 알 수 있습니다.


여기에서 사용된 server 소스 입니다.

from flask import Flask, json
from flask import request
api = Flask(__name__)

@api.route('/funs', methods=['POST'])
def post_funs():
    print(request.form)
    print(request.headers)
    return json.dumps({"success": True}), 201

if __name__ == '__main__':
    api.run(debug=True)

 


2024년 1월 8일 월요일

requests post 에서 streaming 사용방법

requests streaming 의 필요성

requests에서 streaming이란 조금씩 buffering 하는것을 의미합니다. 일반적으로 파일을 upload하게 된다면 requests 모듈안에서는 파일 통째로 메모리에 올려서 upload하게 됩니다.

만약 파일의 크기가 크다면 어떻게 될까요? 많은 메모리가 필요하게 됩니다. 심지어는 메모리 부족 사태까지 발생될 수 있습니다.


requests streaming 사용법

가이드 문서에서는 아래 링크를 참고 하면 됩니다.

https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads

Streaming Uploads

Requests supports streaming uploads, which allow you to send large streams or files without reading them into memory. To stream and upload, simply provide a file-like object for your body:

with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

Warning

It is strongly recommended that you open files in binary mode. This is because Requests may attempt to provide the Content-Length header for you, and if it does this value will be set to the number of bytes in the file. Errors may occur if you open the file in text mode.

위 용법을 보면 크게 기존 사용하는 방법과 큰 차이가 없어보입니다.

그렇다면 이미 streaming 하고 있는걸까요?

실제 테스트해보니 동작하고 있지 않았습니다.


코드 안쪽으로 디버깅 해보기

일반적으로 requests를 사용해서 파일을 업로드시 아래와 같은 방식으로 사용하게 됩니다.

https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file

POST a Multipart-Encoded File

Requests makes it simple to upload Multipart-encoded files:

>>> url = 'https://httpbin.org/post'
>>> files = {'file': open('report.xls', 'rb')}

>>> r = requests.post(url, files=files)
>>> r.text
{
  ...
  "files": {
    "file": "<censored...binary...data>"
  },
  ...
}

차이가 뭘까요? 

정답은 data=f , files=files 넘어가는 인자가 틀립니다. 

그렇습니다. 

!중요! requests에서 files로 넘어가는 인자에 대해서는 streaming 을 지원하지 않습니다.

엥 이게 무슨말이냐고요? 거짓말 아닌지 문의하실것 같은데요.

코드에서 로그를 넣어서 확인이 가능했습니다.

requets/models.py라는 코드를 살펴보다보면 아래와 같은 부분이 보입니다.

def prepare_body(self, data, files, json=None):
"""Prepares the given HTTP body data."""

# Check if file, fo, generator, iterator.
# If not, run through normal process.

# Nottin' on you.
body = None
content_type = None

if not data and json is not None:
# urllib3 requires a bytes-like body. Python 2's json.dumps
# provides this natively, but Python 3 gives a Unicode string.
content_type = "application/json"

try:
body = complexjson.dumps(json, allow_nan=False)
except ValueError as ve:
raise InvalidJSONError(ve, request=self)

if not isinstance(body, bytes):
body = body.encode("utf-8")

is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

if is_stream:
try:
length = super_len(data)
except (TypeError, AttributeError, UnsupportedOperation):
length = None

body = data

if getattr(body, "tell", None) is not None:
# Record the current file position before reading.
# This will allow us to rewind a file in the event
# of a redirect.
try:
self._body_position = body.tell()
except OSError:
# This differentiates from None, allowing us to catch
# a failed `tell()` later when trying to rewind the body
self._body_position = object()

if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


    is_stream = all(
[
hasattr(data, "__iter__"),
not isinstance(data, (basestring, list, tuple, Mapping)),
]
)

is_stream 이 True되는 조건자체가 data인자만 확인하도록 되어 있습니다. 즉 streaming은 file쪽은 확인하지도 않게되고, 만약 is_stream 으로 들어가서도 files가 존재하게된다면 아래 코드에 의해서 exception이 발생하도록 되어있습니다.

        if files:
raise NotImplementedError(
"Streamed bodies and files are mutually exclusive."
)


해결 방법

이미 다른 분들이 구현해 놓은 모듈이 존재합니다.

https://github.com/requests/toolbelt

사용법은 files로 넘어가는 부분을 data로 옮기는 작업을 해야합니다.

github의 예제를 참고해서 data+files를 MultipartEncoder 넣어서 작업하면 됩니다.

from requests_toolbelt import MultipartEncoder
import requests

m = MultipartEncoder(
    fields={'field0': 'value', 'field1': 'value',
            'field2': ('filename', open('file.py', 'rb'), 'text/plain')}
    )

r = requests.post('http://httpbin.org/post', data=m,
                  headers={'Content-Type': m.content_type})



2023년 10월 22일 일요일

Python list compare (리스트 비교)

 Python에서 List내부에 값을 포함하는 경우 단순히 '==' "비교 연산자 만으로도 잘 될까?"에 대한 궁금증이 생겼습니다.

실제 테스트 해보도록 하겠습니다.


1. 상수 값으로 이루어진 가장 기본적인 테스트 입니다.

>>> a = [ 1, 2, 3]
>>> b = [ 1, 2, 3]
>>> a==b
True

2. list 순서가 변경된다면 비교가 어떨까요?

>>> a = [ 1, 2, 3]
>>> b = [ 3, 2, 1]
>>> a==b
False

리스트는 순서가 있겠죠? 그러니 당연히 안됩니다.


3. list내에 dict 타입이 들어 있는 경우 입니다.

그런데 순서가 뒤죽박죽 되어 있다면?? 이번에도 안될까요?

>>> a = [{'a':1,'b':2}]
>>> b = [{'a':1,'b':2}]
>>> a==b
True
>>> b = [{'b':2,'a':1}]
>>> a==b
True
>>> a
[{'a': 1, 'b': 2}]
>>> b
[{'b': 2, 'a': 1}]

잘 동작 됩니다. key, value 쌍만 잘맞으면 dict 타입은 순서가 중요하지 않습니다.





2023년 10월 14일 토요일

python requests timeout 의 고찰

requests 모듈을 사용하다가 timeout이 발생하여 관련해서 찾아보고 정리해봤습니다.

Timeout 의 기초

문서에서는 아래와 같이 나와있습니다.

https://requests.readthedocs.io/en/stable/user/advanced/#timeouts

보통은 timeout 을 설정하지 않고 사용하는 경우 timeout이 얼마나 발생하는지 궁금해서 문서를 찾아봤는데 정보가 애매하게 되어있습니다.

시간 초과가 없으면 코드가 몇 분 이상 중단될 수 있습니다. 

시간 정보가 구체적이지 않았습니다.

그래서 몇가지 테스트를 해봤습니다. 아래와 같이 코드를 구현해서 timeout이 발생할 수 있는 조건으로 시험해봤습니다.

import requests
import datetime
import traceback
import time


def url_connect(url, timeout=None):
    nowtime = datetime.datetime.now()
    try:
        print(f"********** trying connect {url} *************")
        if timeout != None:
            r = requests.get(url, timeout=timeout)
        else:
            r = requests.get(url)
        print(r)
    except Exception as e:
        print("end time:", datetime.datetime.now() - nowtime)
        time.sleep(1)
        print("EXCEPTION", e)
        traceback.print_exc()


if __name__ == "__main__":
    url_connect("http://1.1.1.1:9999")
    url_connect("http://192.168.0.254")
    url_connect("http://127.0.0.1")
    url_connect("http://192.168.0.254", timeout=10)


********** trying connect http://1.1.1.1:9999 *************
end time: 0:00:21.042910
EXCEPTION HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
TimeoutError: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:21.032674
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
TimeoutError: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://127.0.0.1 *************
end time: 0:00:02.053697
EXCEPTION HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 186, in _new_conn
    raise NewConnectionError(
urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 14, in url_connect
    r = requests.get(url)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 565, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:10.010868
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 179, in _new_conn
    raise ConnectTimeoutError(
urllib3.exceptions.ConnectTimeoutError: (<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout.py", line 12, in url_connect
    r = requests.get(url, timeout=timeout)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 553, in send
    raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

call stack 부분을 빼고 다시 정리해봤습니다.

********** trying connect http://1.1.1.1:9999 *************
end time: 0:00:21.042910
EXCEPTION HTTPConnectionPool(host='1.1.1.1', port=9999): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD096670>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:21.032674
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCCC4A8B0>: Failed to establish a new connection: [WinError 10060] 연결된 구성원으로부터 응답이 없어 연결하지 못했거나, 호스트로부터 응답이 없어 연결이 끊어졌습니다'))
********** trying connect http://127.0.0.1 *************
end time: 0:00:02.053697
EXCEPTION HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x000001EBCD1310D0>: Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
********** trying connect http://192.168.0.254 *************
end time: 0:00:10.010868
EXCEPTION HTTPConnectionPool(host='192.168.0.254', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001EBCD131910>, 'Connection to 192.168.0.254 timed out. (connect timeout=10)'))

test code가 크게 4가지 인데요.

1.1.1.1:9999 의 주소는 응답을 안할꺼라는 가정하에 테스트 해봤습니다.

21초 걸렸고 Max retries exceeded 트라이가 초과되었다? 라는 애매한 표현을 하고 있습니다.

두번째는 내부 공유기 쪽으로 안쓰는 포트에 시도해보았습니다.

마찬가지로 21초가 걸렸습니다.

세번째 127.0.0.1 은 내부 포트로 2초만에 응답이 없습니다. timeout이 아니고 거부 형태입니다.

마지막은 timeout을 10초로 넣고 두번째 테스트 했던 내부 공유기 주소로 사용해봤습니다.

10초에 Max retries exceeded 부분은 동일하나 뒤쪽에 보면 timed out 이 발생함을 알 수 있습니다.

위 테스트에서 21초가 걸리긴 했지만 그것을 내부 기본 timeout이라고 할 수는 없습니다. 내부적으로 소켓에 대한 타임 아웃이 있고 그걸 retry 하는 형태로 구현이 되어 있음을 알 수 있으며, 우리가 설정하는 timeout 과는 Exception 이 다름을 알 수 있었습니다.


사용법

타임 아웃이 필요한경우는

r = requests.get('https://github.com', timeout=5)

위와 같이 사용해주면 되는데, 2가지 종류의 시간을 설정이 가능합니다. connect, read 시간입니다. connect timeout은 일반적으로 listen 하고 있는 서버에 접속하면 accept (연결) 를 하게되는데 그때까지의 시간 이라고 보면 됩니다.

The connect timeout is the number of seconds Requests will wait for your client to establish a connection to a remote machine (corresponding to the connect()) call on the socket.

read 시간은 데이터를 주고 받을때의 시간입니다. 클라이언트가 서버에서 전송된 바이트 사이에 대기하는 시간(초)입니다.

(Specifically, it’s the number of seconds that the client will wait between bytes sent from the server. In 99.9% of cases, this is the time before the server sends the first byte).

r = requests.get('https://github.com', timeout=(3.05, 27))

timeout=(connect timeout, read time) 형태로 설정합니다.

무한정 기다리는 경우 아래와 같이 합니다. 그런데 앞에서 테스트 해봤듯이 꼭 무한정이 되는것은 아닙니다.

r = requests.get('https://github.com', timeout=None)


Timeout 을 넘어서

여기에서 한가지 궁금한 점이 생겼습니다. requests에서 파일을 받거나 web page를 받을때의 timeout은 전체 크기를 받는 부분의 timeout 인가라는 의문입니다.

예를 들어 100MB의 파일을 받는 상황이고 requests.get의 timeout이 1초 일때 100MB를 1초만에 받지 못하면 timeout이 발생하는가입니다.

그래서 예제를 만들어 봤습니다.

웹 서버를 구성해야하는 예제이므로 복잡하지만 간단하게 예제를 위해서 socket 으로 설계했습니다.


import socket


IP = '0.0.0.0'
PORT = 8080
ADDR = (IP, PORT)


def make_dummy_message(len):
    ret = ""
    for i in range(0, len):
        ret = ret + 'A'
    return ret

head_data = f"""HTTP/1.1 200 OK

Server: Werkzeug/2.2.2 Python/3.8.10
Date: Sat, 14 Oct 2023 01:43:25 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 100000000
Connection: close

<!doctype html>
<html lang=en>
"""

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.bind(ADDR)
    server_socket.listen()
    print(f"listen [{ADDR}]")

    while True:
        client_socket, client_addr = server_socket.accept()
        print(f"accept [{client_addr}]")
        # msg = client_socket.recv(SIZE)
        print(head_data)
        client_socket.sendall(head_data.encode())
        for i in range(1, 100):
            print(f"send {i}")
            client_socket.sendall(make_dummy_message(1000000).encode())

        client_socket.close()

web 소켓을 열어서 http 응답을 주는 형태입니다. make_dummy_message 에 의해서 약 1MB의 메세지를 client_socket.sendall 함수로 전달해 줍니다.

        for i in range(1, 100):
            print(f"send {i}")
            client_socket.sendall(make_dummy_message(1000000).encode())

1MB씩 100번 100MB가 전달되게 됩니다.


이제 client 코드 입니다.

import requests
import datetime
import traceback
import time
import socket

def url_connect(url, timeout=None):
    nowtime = datetime.datetime.now()
    try:
        print(f"********** trying connect {url} *************")
        if timeout != None:
            r = requests.get(url, timeout=timeout)
        else:
            r = requests.get(url)
        print(r)
    except Exception as e:
        print("end time:", datetime.datetime.now() - nowtime)
        time.sleep(1)
        print("EXCEPTION", e)
        traceback.print_exc()


if __name__ == "__main__":
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   myip = s.getsockname()[0]
   print(myip)
   s.close()
   url_connect(f"http://{myip}:8080", timeout=1)

나의 ip를 구한다음 8080 포트로 접속해 보는것입니다.

서버를 실행하지 않은 상태는 1초만에 아래 형태로 나타납니다.

192.168.0.35
********** trying connect http://192.168.0.35:8080 *************
end time: 0:00:01.013626
EXCEPTION HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))
Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 174, in _new_conn
    conn = connection.create_connection(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 95, in create_connection
    raise err
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\connection.py", line 85, in create_connection
    sock.connect(sa)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 703, in urlopen
    httplib_response = self._make_request(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 398, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 239, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1252, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1298, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1247, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 1007, in _send_output
    self.send(msg)
  File "C:\Users\jun\AppData\Local\Programs\Python\Python38\lib\http\client.py", line 947, in send
    self.connect()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 205, in connect
    conn = self._new_conn()
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connection.py", line 179, in _new_conn
    raise ConnectTimeoutError(
urllib3.exceptions.ConnectTimeoutError: (<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 489, in send
    resp = conn.urlopen(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
    retries = retries.increment(
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\urllib3\util\retry.py", line 592, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:/Users/jun/Documents/GitHub/sourcecode/python/example/_52_requests/timeout3.py", line 12, in url_connect
    r = requests.get(url, timeout=timeout)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 587, in request
    resp = self.send(prep, **send_kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\sessions.py", line 701, in send
    r = adapter.send(request, **kwargs)
  File "C:\Users\jun\Documents\GitHub\sourcecode\venv\lib\site-packages\requests\adapters.py", line 553, in send
    raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPConnectionPool(host='192.168.0.35', port=8080): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x000001A957F37670>, 'Connection to 192.168.0.35 timed out. (connect timeout=1)'))

서버를 실행한 다음에는 다른 에러가 발생하는데

192.168.0.35
********** trying connect http://192.168.0.35:8080 *************
end time: 0:00:11.814797
EXCEPTION ("Connection broken: ConnectionResetError(10054, '현재 연결은 원격 호스트에 의해 강제로 끊겼습니다', None, 10054, None)", ConnectionResetError(10054, '현재 연결은 원격 호스트에 의해 강제로 끊겼습니다', None, 10054, None))

대략 위와 같이 11초 후에 에러같은 것이 발생합니다.

몇가지 이유가 있는데 서버쪽을 대충 만들어서 그렇습니다. contents크기를 받고 소켓을 닫도록 되어있는데 해당 부분이 적절치 않게 만들었기 때문입니다.

timeout시간만 보려고 테스트 코드를 만든것이라 에러에 대해서는 무시하면됩니다.

11초가 걸렸는데도 1초 timeout이 발생하지 않았다는 의미는 구간 구간 socket read 시 1초 timeout이 발생할때만 멈춤다는 것을 알 수 있습니다.