반응형

정식 이름은 Assignment expression operator인데 walrus operator라고도 불린다.

walrus는 “바다코끼리”라는 뜻으로 operator가 바다 코끼리의 눈과 이빨을 닮아서 이렇게 부른다.
때론 colon(:) equals(=) operator라고도 한다.

Python 3.8버전부터 새로 등장했다.

 

https://dev.to/davidarmendariz/python-walrus-operator-j13

 

Statement vs Expression in Python

바다코끼리 연산자의 정식 이름을 보면 Assignment expression operator로, expression이라는 단어가 나온다. 

Python에서 statement와 expression이라는 표현이 비슷해 혼동스러운데 간단히 정리하면, 아래와 같다.

  • statement: 코드를 구성할 수 있는 단위 혹은 모든 것
  • expression: 값을 평가하는 statement로 연산자와 피연산자의 조합으로 구성됨

예시

x = 25          # a statement
x = x + 10      # an expression
  • statement는 변수를 생성하는데 사용된다.
  • expression은 x값에 10을 더하는 연산이 수행된 후 결과가 x에 할당되었다.
>>> walrus = False # (1)
>>> walrus
False

>>> (walrus := True) # (2)
True
>>> walrus
True
  1. walrus = False는 값 False가 walrus에 할당된다. (traditional statement)
  2. (walrus := True) 는 assignment expression으로 walrus에 값 True를 할당한다.

둘의 미묘한 차이중 하나는 walrus = False는 값을 반환하지 않지만 (walrus := True)는 값을 반환한다는 것이다!

>>> walrus = False
>>> (walrus := True)
True

 

등장한 이유

PEP 572에 Abstract에 아래와 expression 내에서 변수에 할당하는 방법을 제안하고 있다. 

creating a way to assign to variables within an expression using the notation NAME := expr.

C언어에서는 변수에 값을 할당하는 statement도 expression인데 강력하지만 찾기 힘든 버그를 생산하기도 한다.

int main(){
	int x = 3, y = 8;
	if (x = y) {
	    printf("x and y are equal (x = %d, y = %d)", x, y);
	}
	return 0;
}

x와 y값을 비교후 값이 같으면 두 값을 출력하는 코드지만 x와 y값이 다르기 때문에 아무것도 출력 안되길 기대되지만 실제 코드 실행 결과는 아래와 같이 print 문이 출력된다. 왜일까?

x and y are equal (x = 8, y = 8)

문제는 위 코드 세번째 줄 if (x = y) 에서 equality comparison operator(==) 대신 assignment operator(=) 를 사용하고 있기 때문이다. if 문의 조건에는 expression이 와야하는데 C언어에서는 x = y를 expression으로 x값이 8로 할당되고 1이상의 값으로 True로 판단되서 print문이 출력된다.

그럼 Python에서는? 

x, y = 3, 8
if x = y:
    print(f"x and y are equal ({x = }, {y = })")
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
  • Syntax Error를 내뱉는데 expression이 아닌 statement이기 때문이다. 파이썬은 이를 분명히 구분하고 walrus operator에도 이러한 설계 원칙이 반영되었다. 그래서 walrus operator를 이용해서 일반적인 assignment를 할 수 없다.
>>> walrus := True
  File "<stdin>", line 1
    walrus := True
           ^
SyntaxError: invalid syntax

이를 해결하기 위해 많은 경우에 assignment expression 에 괄호를 추가해 python에서 syntax error를 피할 수 있다.

>>> (walrus := True)  # Valid, but regular assignments are preferred
True

 

사용 예시

walrus operator는 반복적으로 사용되는 코드를 간단히 하는데 유용하게 사용될 수 있다.

(1) 수식 검증

예로 복잡한 수식을 코드로 작성하고 이름 검증하고 debugging할 때 walrus operator가 유용할 수 있다.

아래와 같은 수식이 있다고 하자 (참고: haversine formula, 지구 표면의 2점 사이의 거리를 구하는 식)

$$
2 \cdot \text{r} \cdot \arcsin\left(
    \sqrt{
        \sin^2\left(\frac{\phi_2 - \phi_1}{2}\right)
        + \cos(\phi_1) \cdot \cos(\phi_2) \cdot \sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)
    }
\right)
$$ 

  • ϕ: 위도(latitude), λ: 경도(longitude)

위 수식을 이용해  오슬로(59.9°N 10.8°E) 와 밴쿠버(49.3°N 123.1°W)  사이의 거리를 구하면,

from math import asin, cos, radians, sin, sqrt
# Approximate radius of Earth in kilometers
rad = 6371
# Locations of Oslo and Vancouver
ϕ1, λ1 = radians(59.9), radians(10.8)
ϕ2, λ2 = radians(49.3), radians(-123.1)
# Distance between Oslo and Vancouver
print(2 * rad * asin(
    sqrt(
        sin((ϕ2 - ϕ1) / 2) ** 2
        + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
    )
))

# 7181.7841229421165 (km)
  • 위 수식을 검증하기 위해서 수식의 일부 값을 확인해야할 수 있는데 수식의 일부를 복&붙으로 확인할 수 있다.
  • 이때 walrus operator를 이용하면,
2 * rad * asin(
    sqrt(
        **(ϕ_hav := sin((ϕ2 - ϕ1) / 2) ** 2)**
        + cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
    )
)

# 7181.7841229421165

ϕ_hav
# 0.008532325425222883
  • 전체 expression의 값을 계산하면서 동시에 ϕ_hav값을 계속 확인할 수 있어서 copy & paste로 인해 발생할 수 있는 오류의 가능성을 줄일 수 있다.

 

(2) Lists 에서 활용될 수 있는 walrus operator

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

위 list에서 길이, 합계, 평균 값을 dictionary에 저장한다고 가정해보자

description = {
    "length": len(numbers),
    "sum": sum(numbers),
    "mean": sum(numbers) / len(numbers),
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • description에서 numbers의 len과 sum이 각각 두번씩 호출된다
  • 짧은 list에서는 큰 문제가 되지 않지만 길이가 더 긴 list나 연산이 복잡할 경우에는 최적화할 필요가 있다

 

물론 아래처럼 len_numbers, sum_numbers 변수를 dictionary 밖에서 선언 후 사용할 수도 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

len_numbers = len(numbers)
sum_numbers = sum(numbers)

description = {
    "length": len_numbers,
    "sum": sum_numbers,
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}

 

 

하지만 walrus operator를 이용해 len_numbers, sum_numbers 변수를 dictionary 내부에서만 사용하여 code를 최적화할 수 있다

numbers = [2, 8, 0, 1, 1, 9, 7, 7]

description = {
    "length": (len_numbers := len(numbers)),
    "sum": (sum_numbers := sum(numbers)),
    "mean": sum_numbers / len_numbers,
}

print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
  • 이 경우 코드를 읽는 사람들에게 len_numbers와 sum_numbers 변수는 계산을 최적화하기 위해 dictionary 내부에서만 사용했고 다시 사용되지 않음을 명확히 전달 할 수 있다

 

(3) Text 파일에서 lines, words, character 수 세는 예시

# wc.py
import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = (
        path.read_text().count("\\n"),  # Number of lines
        len(path.read_text().split()),  # Number of words
        len(path.read_text()),  # Number of characters
    )
    print(*counts, path) # 11 32 307 wc.py
  • wc.py 파일은 11줄, 32단어, 307 character로 구성되어있다
  • 위 코드를 보면 path.read_text() 가 반복적으로 호출되는걸 알 수 있다 ⇒ walrus operator를 이용해 개선해보면,
import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    counts = (
        **(text := path.read_text()).count("\\n"),  # Number of lines**
        len(text.split()),  # Number of words
        len(text),  # Number of characters
    )
    print(*counts, path)

 

물론 아래처럼 text 변수를 이용하면 코드는 한줄 늘어나지만 readability를 훨신 높일 수 있다.

import pathlib
import sys

for filename in sys.argv[1:]:
    path = pathlib.Path(filename)
    text = path.read_text()
    counts = (
        text.count("\\n"),  # Number of lines
        len(text.split()),  # Number of words
        len(text),  # Number of characters
    )
    print(*counts, path)

그러므로 walrus operator가 코드를 간결하게 해주더라도 readability를 고려해야 한다.

 

(4) List Comprehensions

  • List comprehension과 함께 연산이 많은 함수를 사용하게 될 때, walrus operator의 사용은 효과적일 수 있다.
import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = [slow(num) for num in numbers if slow(num) > 4]

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 30.01522707939148

  • numbers 리스트의 각 element에 slow 함수를 적용 후 3보다 큰 경우에만 results에 slow 호출 결과를 저장하는 코드
  • 문제는 slow 함수가 2번 호출됨
    • slow 호출 후 반환 결과가 3보다 큰지 확인할 때
    • results 리스트에 저장하기 위해 slow 호출할 때

가장 일반적인 해결책은 list comprehension 대신 for loop을 사용하는 것이다.

import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = []
for num in numbers:
    slow_num = slow(num)
    if slow_num > 4:
        results.append(slow_num)

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 25.021725063323975

  • slow 함수가 모든 경우에 한번씩만 호출됨
  • 하지만 코드 양이 늘어나고 가독성이 떨어짐

walrus operator를 사용하면 list comprehension을 유지하면서 가독성을 높일 수 있음

import time

t_start = time.time()

def slow(num):
    time.sleep(5)
    return num

numbers = [4, 3, 1, 2, 5]

results = [slow_num for num in numbers if (slow_num := slow(num)) > 4]
print(results)

t_end = time.time()

print("elapsed time: ", t_end - t_start)

elapsed time: 25.018176908493042

 

(5) While Loop

question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

user_answer = input(f"\n{question} ")
while user_answer not in valid_answers:
    print(f"Please answer one of {', '.join(valid_answers)}")
    user_answer = input(f"\n{question} ")
  • 위 코드는 사용자의 입력을 받는 input 함수가 두번 반복됨
  • 이를 개선하기 위해 While True 와 break를 사용하여 코드를 다시 작성하는 것이 일반적임
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

while True:
    user_answer = input(f"\n{question} ")
    if user_answer in valid_answers:
        break
    print(f"Please answer one of {', '.join(valid_answers)}")

 

walrus operator를 이용해서 while loop을 간결하게 할 수 있음

question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}

while (user_answer := input(f"\n{question} ")) not in valid_answers:
    print(f"Please answer one of {', '.join(valid_answers)}")
  • 사용자로부터 받은 input 입력을 user_answer 변수에 저장하고 동시에 valid_answers 내에 포함되어있는지를 체크하여 가독성을 높일 수 있음

 

Reference


https://realpython.com/python-walrus-operator/

 
반응형
반응형

 

 

파이썬에서 URL을 다루기 위한 패키지로 크게 3가지 종류가 있음; urllib, urllib3, requests

  • urllib은 built-in package이고 나머지 2개는 third party

 

사용방법

 

1. 기본 사용방법

from urllib.request import urlopen                     # (1)

with urlopen("<https://www.example.com>") as response: # (2)
    body = response.read()                             # (3)
    print(type(body))                                  # (4)

(1) urllib.request는 built-in package로 따로 설치하지 않아도 됨. HTTP request를 위해 urlopen을 사용

(2) context manager with 문을 통해 request 후 response를 받을 수 있음

(3) response 는 <http.client.HTTPResponse> 객체

  • read 함수를 통해 bytes로 변환할 수 있음

(4) 실제 body의 type을 print해서 bytes 타입임을 확인

 

2. GET request for json format response

  • API 작업시 response가 json format인 경우가 많음
from urllib.request import urlopen
import json                                            # (1)

url = "<https://jsonplaceholder.typicode.com/todos/1>" # (2)
with urlopen(url) as response:
    body = response.read()

print("body: ", body)                                  # (3)
# body:  b'{\\n  "userId": 1,\\n  "id": 1,\\n  "title": "delectus aut autem",\\n  "completed": false\\n}'

# json bytes to dictionary
todo_item = json.loads(body)                           # (4)
print(todo_item)
# {'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}

(1) urllib 패키지와 함께 json 포맷을 다루기 위해 json package 추가

(2) JSON 형태의 데이터를 얻기 위한 샘플 API 주소

(3) 응답을 print 해보면 json 형태의 bytes format. 이를 dictionary 형태로 변경해주기 위해 json 패키지 필요

(4) json bytes를 파이썬 객체인 dictionary로 변경하기 위해 json.loads 함수 사용

 

3. Response의 header 정보 얻는 방법

from urllib.request import urlopen
from pprint import pprint

with urlopen("<https://www.example.com>") as response:
    pprint(response.headers.items())                       # (1)
    pprint(response.getheader("Connection")) # 'close'     # (2)
  • response의 headers.items()를 통해 header 정보를 얻을 수 있음

(1) pretty print(pprint)를 이용해 header 정보를 보기 좋게 출력하면 아래와 같음

[('Accept-Ranges', 'bytes'), ('Age', '78180'), ('Cache-Control', 'max-age=604800'), ('Content-Type', 'text/html; charset=UTF-8'), ('Date', 'Sat, 24 Aug 2024 18:10:20 GMT'), ('Etag', '"3147526947"'), ('Expires', 'Sat, 31 Aug 2024 18:10:20 GMT'), ('Last-Modified', 'Thu, 17 Oct 2019 07:18:26 GMT'), ('Server', 'ECAcc (lac/5598)'), ('Vary', 'Accept-Encoding'), ('X-Cache', 'HIT'), ('Content-Length', '1256'), ('Connection', 'close')]

(2) header의 개별 정보는 getheader 메소드를 이용해 얻을 수 있음

 

4. bytes를 string으로 변환

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()                            
    print(type(body)) # <class 'bytes'>                       # (1)

decoded_body = body.decode("utf-8")                           # (2)
print(type(decoded_body)) # <class 'str'>                     # (3)
print(decoded_body[:30])

(1) body의 type을 확인해보면 bytes 이고 아래와 같은 형태이다

b'<!doctype html>\n<html>\n<head>\n

(2) bytes를 string으로 변환하기 위해 decode method를 이용 (”utf-8”을 파라미터로 전달)

(3) decoded_body의 type을 확인해보면 string인걸 확인할 수 있고 decoded_body의 일부를 표시하면 아래와 같은 형태

<!doctype html>
<html>
<head>

 

5. Bytes를 file로 변환

크게 2가지 방법이 있음

encoding & decoding 없이 바로 file로 작성

from urllib.request import urlopen

with urlopen("<https://www.example.com>") as response:
    body = response.read()

with open("example.html", mode="wb") as html_file:
    html_file.write(body)
  • write binary(wb) mode로 파일을 열어 bytes를 바로 example.html 파일에 작성
  • 코드를 실행하면 example.html 파일이 생성됨

 

contents를 file로 encoding해야하는 경우

from urllib.request import urlopen

with urlopen("<https://www.google.com>") as response:
    body = response.read()

character_set = response.headers.get_content_charset()        # (1) 
content = body.decode(character_set)                          # (2)

with open("google.html", encoding="utf-8", mode="w") as file: # (3)
    file.write(content)

(1)&(2): 구글같은 홈페이지는 location에 따라 다른 encoding 방식을 사용하기도 한다. 그래서 get_content_charset () 메소드를 이용해서 encoding 방식을 확인 후 bytes를 string으로 decoding 함

(3) decoded string을 다시 html에 utf-8 모드로 encoding해서 google.html 파일에 저장함

References


https://realpython.com/lessons/python-urllib-request-overview/

 
반응형
반응형

현상

 

1) 핸드폰을 세로로 길게 세운 상태에서 사진을 찍었다 (iPhone 12, iOS: 16.6.1)

전기구이통닭 시켜서 서비스로 받은 콜라

2) Python에서 pillow 이용해서 이미지를 열었는데 자동으로 반시계방향으로 90도 회전이 되어 읽어진다

from PIL import Image, ImageFile

ImageFile.LOAD_TRUNCATED_IMAGES = True # OSError: image file is truncated 에러때문에 추가했다

image_path = 'service_coke.jpeg'

img = Image.open(image_path)
img.show(title='test')

 

왜 자동으로 이미지를 돌릴까?

해결책은 github pillow repo의 issue에서 찾을 수 있었다. https://github.com/python-pillow/Pillow/issues/4703


I would imagine that the images you are dealing with have an EXIF orientation tag. This means that the image data is saved in one position, and then the image instructs the viewer to rotate it another way.

Pillow does not automatically apply this transformation to the data. However, it has a method to do so - https://pillow.readthedocs.io/en/stable/reference/ImageOps.html#PIL.ImageOps.exif_transpose

Here is an example using it. You can try this and check if it resolves your problem.

from PIL import Image, ImageOps
im = Image.open("file.jpg")
im = ImageOps.exif_transpose(im)

 

최종 코드

from PIL import Image, ImageFile, ImageOps

ImageFile.LOAD_TRUNCATED_IMAGES = True

image_path = 'service_coke.jpeg'

img = Image.open(image_path)
img = ImageOps.exif_transpose(img)
img.show(title='test')

짜잔

 

EXIF orientation tag는 뭘까?

먼저, EXIF는 Exchangeable image file format의 약자로 digital camera로 찍힌 이미지에 대한 다양한 meta 정보를 저장하는 프로토콜이다. EXIF 는 실제 이미지와 함께 저장되는데 shutter speed, focal length, orientation, shooting time 정보들이 있다. 

그중 내가 관심있는 orientation tag는 1~8까지의 값을 가진다. 

https://jdhao.github.io/2019/07/31/image_rotation_exif_info/

이미지에서 orientation tag를 얻는 방법

from PIL.ExifTags import TAGS
from PIL import Image

image_path = 'service_coke.jpeg'

img = Image.open(image_path)
exif = img.getexif()

for k, v in exif.items():
    print('{}: {}'.format(TAGS[k], v))

orientation tag:  6
TileWidth: 512
TileLength: 512
ResolutionUnit: 2
ExifOffset: 240
Make: Apple
Model: iPhone 12
Software: 16.6.1
Orientation: 6
DateTime: 2024:01:14 23:25:06
YCbCrPositioning: 1
XResolution: 72.0
YResolution: 72.0
HostComputer: iPhone 12

이런 정보를 확인할 수 있고 이 중 orientation tag 값은 6을 보여준다.

6은 위 이미지에서 볼 떄 image viewer에서 보여줄 때 이미지를 시계 방향으로 90도 회전해서 보여줘야된다는 의미이고 이를 반영하여 image를 보여주기 위해서는 위의 최종 코드에서 ImageOps.exif_transpose를 사용해야한다. 

from PIL import Image, ImageFile, ImageOps

image_path = 'service_coke.jpeg'
img = Image.open(image_path)
img = ImageOps.exif_transpose(img)
img.show(title='test')

 

exif_transpose 함수 정의

def exif_transpose(image, *, in_place=False):
    """
    If an image has an EXIF Orientation tag, other than 1, transpose the image
    accordingly, and remove the orientation data.

    :param image: The image to transpose.
    :param in_place: Boolean. Keyword-only argument.
        If ``True``, the original image is modified in-place, and ``None`` is returned.
        If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
        with the transposition applied. If there is no transposition, a copy of the
        image will be returned.
    """

- exif_transpose 함수는 이미지에 EXIF Orientation tag 정보가 있으면 이를 이용하여 image를 보여준다. 

 
반응형
반응형

상황


  • Google에서 이미지를 크롤링하는 파이썬 스크립트를 쿠버네티스 클러스터에 pod로 띄우려고 했다
  • Script → Docker container 과정으로 테스트 후 정상 동작하는 걸 확인 후 Pod로 띄웠는데 실패…
  • 그동안 Docker container 에서 동작 → Pod에서 동작으로 이해하고 있었다.
  • 결론: 실행환경을 고려할 때 CPU 아키텍쳐도 고려를 해야한다!

 

과정


1. Google에서 이미지 크롤링하는 파이썬 스크립트 (크롬사용)

Selenium은 웹 테스트를 할 때 사용하는 프레임워크인데 BeautifulSoup과 더불어 크롤링할 때 많이 사용되는 도구들 중 하나다.

실제로 우리가 구글에서 검색할때처럼 검색창에 키워드를 입력하고 기다린 후 스크롤을 내리는 과정들을 코드로 작성한다. 코드를 보면 어릴적 게임할 때(라스트킹덤 광물캘때) 사용하던 매크로같은 느낌이 든다.

https://www.browserstack.com/guide/selenium-webdriver-tutorial

 

위 그림처럼 Selenium 패키지를 통해 파이썬 코드로 Browser driver를 통해 실제 browser로 명령/요청을 전달하고 응답을 받는다. 이 과정을 위해 아래의 3가지가 갖춰진 환경이 필요하다.

  1. 크롭 웹 브라우저
  2. 크롬 드라이버
  3. 파이썬 Selenium 패키지

이 때 중요한건 크롬 웹브라우저와 크롬 드라이버의 버전의 호환성이다. 웹 브라우저는 자주 업데이트가 되는데 크롬 드라이버의 버전이 업데이트가 되지 않으면 어느 순간 크롤링이 안되는 문제가 발생한다. 본 글에서는 특정한 버전으로 맞춰서 진행한다.

진행했던 환경은 아래와 같다.

더보기
  • x86_64, Ubuntu 20.04.6 LTS,1CPU, 1GB
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal

$ uname -a 
Linux ubuntu-s-1vcpu-512mb-10gb-sfo3-01 5.4.0-122-generic #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

 

 

 

1) 크롭 웹 브라우저 설치

현재 날짜 기준(2023.12.25) Chrome 웹 브라우저의 stable 버전은 120.0.6099.109

(https://googlechromelabs.github.io/chrome-for-testing/#stable

# 크롬 웹브라우저 다운로드 및 설치 (working directory: /root/crawling)
$ wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.109/linux64/chrome-linux64.zip
$ unzip chrome-linux64.zip

 

2) 크롬 드라이버 설치

위 버전에 맞춰서 크롬 드라이버도 설치

# 크롬 드라이버 다운로드 및 설치 (working directory: /root/crawling)
$ wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.109/linux64/chromedriver-linux64.zip
$ unzip chromedriver-linux64.zip

 

3) Python selenium 패키지 설치

pip install "selenium == 4.15.1"

크롤링하는 코드는 본 유투브 링크를 참고했고, apple이라는 키워드로 검색했을 때 나오는 이미지들을 저장하는 코드이다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys  # 엔터처리용
import time
import urllib.request
import os

options = webdriver.ChromeOptions()

options.add_argument("--headless") # '창이 없는’: 서버에서는 웹브라우저 창을 못띄우니 필요
options.add_argument("--no-sandbox") 
options.add_argument("--disable-dev-shm-usage") # 공유메모리 사용하지 않음
options.add_argument("--single-process")
options.binary_location = '/root/crawling/chrome-linux64/chrome'

service = webdriver.ChromeService(executable_path='/root/crawling/chromedriver-linux64/chromedriver')

driver = webdriver.Chrome(service=service, options=options)

URL = "<https://www.google.co.kr/imghp>"
KEYWORD = "apple"
driver.get(url=URL)

# time.sleep은 fixed, implicity는 flexible로 time_to_wait이 maximum time
driver.implicitly_wait(time_to_wait=10)

keyElement = driver.find_element(By.NAME, "q")
keyElement.send_keys(KEYWORD)
keyElement.send_keys(Keys.RETURN)  # 키보드 엔터

bodyElement = driver.find_element(By.TAG_NAME, "body")
time.sleep(5)  # 엔터치고 이미지 나오는 시간 기다림

image_candidates = []

print("Crawling Images Start!")

for i in range(1):
    bodyElement.send_keys(Keys.PAGE_DOWN)
    time.sleep(0.2)

    images = driver.find_elements(
        By.XPATH, '//*[@id="islrg"]/div[1]/div/a[1]'
    )  # XPATH는 변경될 수 있고 web browser의 개발자도구 이용해서 확인필요

    image_candidates.append(images)

for images in image_candidates:
    for idx, image in enumerate(images):
        image.send_keys(Keys.ENTER)
        time.sleep(0.5)

        high_images = driver.find_elements(
            By.XPATH,
            '//*[@id="Sva75c"]/div[2]/div[2]/div[2]/div[2]/c-wiz/div/div/div/div/div[3]/div[1]/a/img[1]', # XPATH는 변경될 수 있고 web browser의 개발자도구 이용해서 확인필요
        )
        try:
            real_image = high_images[0].get_attribute("src")
        except Exception as e:
            print(f"Exception: {e}")
            continue
        try:
            urllib.request.urlretrieve(
                real_image,
                os.path.join(os.getcwd(), str(idx)) + ".jpg",
            )
        except Exception as e:
            print(e)

정상적으로 동작하는걸 확인했고 Docker 이미지로 만들어서 쿠버네티스 파드로 띄우려고 했는데 아래와 같은 에러가 발생했다.

OSError: [Errno 8] Exec format error: '~~/chromedriver-linux64/chromedriver’

에러메세지가 분명하진 않다.

이것저것 시도 후 파악한 결과는 CPU 아키텍처에 맞는 chrome 웹브라우저와 드라이버를 다운받아야했다. 알아보니 쿠버네티스 pod는 AWS graviton 기반의 인스턴스에 뜨게 되는데 이는 arm64기반의 cpu였다!

결국 arm64 기반의 크롬 웹브라우저와 드라이버를 다운로드 후 해결했다.

ARM은 intel이나 amd와 호환되지 않는 cpu 아키텍쳐로 저전력 고효율을 목적으로 하는 곳(ex) 스마트폰)에 많이 사용되었는데 많이 발전해서 이제 PC용으로도 사용하는 시도가 많아지고 있다고 한다. 앞서 이야기한 AWS의 Graviton이나 Apple의 M1, M2칩이 그 예이다. Cloud환경에서 작업할 때 CPU 아키텍쳐도 고려해야한다는 걸 알 수 있는 경험이었다. 

참고


 
 
반응형
반응형

공식문서에 따르면 Python에서는 함수나 변수 타입의 annotation을 강제하지 않는다. annotation이란 type hint를 생각하면 된다.

[c++]

int do_something(int x)
{
	return x + 1;
{

do_something이란 함수에 대해서 input parameter와 함수의 return 타입은 모두 int형으로 이를 어길시 에러가 발생한다.

[Python]

def do_something(x: int) -> int:
	return x + 1


print(do_something(1.1))  # 2.1

하지만 python의 경우 type hint를 어겨도 에러가 발생하지 않고, 위의 예와 같이 input parameter의 float 인 1.1을 넣어줘도 함수의 내용에 따라 2.1이 반환된다. 

 

하지만 annotation으로 에러가 발생하는 경우도 있는데 아래와 같이 forward reference와 연관된 경우이다. Forward reference란 메소드에서 변수를 먼저 사용하고, 그 이후에 변수를 정의하는 것이다. 

class A:
    def __init__(self, a: A):
        pass

# NameError: name 'A' is not defined

class A를 정의할 때, __init__ 메소드에서 parameter로 a를 받고 이는 class A라고 annotation이 표시되어있다. 하지만 위의 코드와 같이 작성하면 NameError가 발생하는데 그 이유는 class A의 정의가 끝나지 않는 상태에서 __init__ 메소드에서 사용하기 때문이다. 

위 문제를 해결하기 위해 크게 2가지가 있다.

1) type hint를 string으로 작성

class A:
    def do_something(self, a: 'A'):
        pass

아래의 코드를 통해서 해당 메소드의 annotation을 확인할 수 있다. 

print(A().do_something.__annotations__)  # {'a': 'A', 'return': None}

 

2) Python 3.7 이상 버전에서 from __future__ import annotations 추가

from __future__ import annotations

class A:
    def do_something(self, a: A) -> None:
        pass

class A의 정의가 끝나지 않는 상황에서 A를 annotation으로 사용하지만 에러가 발생하지 않는다.

아래의 코드를 통해 해당 메소드의 annotation들을 확인해보면 자동으로 string으로 변환된 것을 알 수 있다.

print(A().do_something.__annotations__) # {'a': 'A', 'return': None}

 

반응형

+ Recent posts