웹 크롤러를 만들거나 html에서 필요한 정보를 검색할 때, BeautifulSoup 라이브러리를 사용하여 편리하게 코딩할 수 있습니다.

설치

$ pip3 install beautifulsoup4

beautifulsoup에는 기본적으로 파이썬 표준라이브러리인 html 파서를 지원하지만, lxml이라는 모듈이 더 빠르게 동작하므로 lxml 모듈도 설치해 줍니다.

$ pip3 install lxml


아래는 beautifulsoup에서 사용할 수 있는 파서의 장단점을 보여주는 테이블 입니다.

Parser선언방법장점단점
파이썬 html.parserBeautifulSoup(markup, 'html.parser')

설치할 필요 없음

적당한 속도


lxml HTML parserBeautifulSoup(markup, 'lxml')매우 빠름lxml 추가 설치 필요
lxml XML parser

BeautifulSoup(markup, 'lxml-xml')

BeautifulSoup(markup, 'xml')

매우 빠름

유일하게 지원되는 xml parser

lxml 추가 설치 필요
html5libBeautifulSoup(markup, 'html5lib')

웹 브라우저와 같은 방식으로 페이지를 파싱

유효한 HTML5 생성

html5lib 추가 설치 필요

매우 느림

사용법

기본 선언 및 테스트 HTML은 아래와 같습니다.

from bs4 import BeautifulSoup

html = """
<!DOCTYPE html>
<html>

<head>
   <title>Page title</title>
</head>

<body>
   <div>
      <p>a</p>
      <p>b</p>
      <p>c</p>
   </div>
   <div class="ex_class">
      <p>d</p>
      <p>e</p>
      <p>f</p>
   </div>
   <div id="ex_id">
      <p>g</p>
      <p>h</p>
      <p>i</p>
   </div>
   <h1>This is a heading</h1>
   <p>This is a paragraph.</p>
   <p>This is another paragraph.</p>
   <a href="http://brownbears.tistory.com" class="a"/>
</body>

</html>
"""


bs = BeautifulSoup(html, 'lxml')

find(name, attrs, recursive, string, **kwargs)

조건에 맞는 태그를 가져옵니다. 만약 조건에 맞는 태그가 1개 이상이면 가장 첫 번째 태그를 가져옵니다.

result = bs.find('p')
print(result)


# <p>a</p>

find_all(name, attrs, recursive, string, limit, **kwargs)

조건에 맞는 모든 태그들을 가져옵니다.

result = bs.find_all('p')
print(result)

# [<p>a</p>, <p>b</p>, <p>c</p>, <p>d</p>, <p>e</p>, <p>f</p>, <p>g</p>, <p>h</p>, <p>i</p>, <p>This is a paragraph.</p>, <p>This is another paragraph.</p>]

class 명으로 찾기

result = bs.find('div', class_='ex_class')
print(result)


# <div class="ex_class">
# <p>d</p>
# <p>e</p>
# <p>f</p>
# </div>

id 명으로 찾기

result = bs.find('div', id='ex_id')
print(result)


# <div id="ex_id">
# <p>g</p>
# <p>h</p>
# <p>i</p>
# </div>

해당 태그명 출력

result = bs.find('div', id='ex_id')
print(result.name)

# div

해당 id명 출력

result = bs.find('div', id='ex_id')
print(result['id'])

# ex_id

해당 class명 출력

result = bs.find('div', class_='ex_class')
print(result['class'])


# ex_class

태그 사이에 있는 내용 출력

result = bs.find('div', class_='ex_class')
print(result.p.text)

# d

result = bs.find('div', class_='ex_class')


# 검색된 div 태그 내의 모든 p태그를 조회
for tag in result.find_all('p'):
    print(tag.text)

# d
# e
# f

태그 내의 속성값 출력

result = bs.find('a', class_='a')
print(result.get('href'))
# http://brownbears.tistory.com


위에서 설명한 방법만으로 충분히 HTML을 파서할 수 있습니다. 더 자세한 내용은 https://www.crummy.com/software/BeautifulSoup/bs4/doc/ 에서 확인할 수 있습니다.

파이썬 스크립트를 개발할 때, 호출 당시 인자값을 줘서 동작을 다르게 하고 싶은 경우가 있습니다. 이때, 파이썬 내장함수인 argparse 모듈을 사용하여 원하는 기능을 개발할 수 있습니다.

아래 설명은 파이썬 3.7 버전 기준으로 작성했습니다.

사용법

간단하게 인자값을 받아 처리하는 로직은 아래와 같습니다.

import argparse

# 인자값을 받을 수 있는 인스턴스 생성
parser = argparse.ArgumentParser(description='사용법 테스트입니다.')

# 입력받을 인자값 등록
parser.add_argument('--target', required=True, help='어느 것을 요구하냐')
parser.add_argument('--env', required=False, default='dev', help='실행환경은 뭐냐')

# 입력받은 인자값을 args에 저장 (type: namespace)
args = parser.parse_args()

# 입력받은 인자값 출력
print(args.target)
print(args.env)


위와 같이 코드를 작성한 다음, 터미널에서 해당 파일을 인자값 없이 실행시키면 아래와 같이 노출이 됩니다.

$ python3 argparse_test.py
usage: argparse_test.py [-h] --target TARGET [--env ENV]
argparse_test.py: error: the following arguments are required: --target

$ python3 argparse_test.py -h
usage: argparse_test.py [-h] --target TARGET [--env ENV]

사용법 테스트입니다.

optional arguments:
  -h, --help       show this help message and exit
  --target TARGET  어느 것을 요구하냐
  --env ENV        실행환경은 뭐냐


다음은 인자값을 target, env에 인자값을 주고 실행시킨 결과입니다.

$ python3 argparse_test.py --target=테스트 --env=local
테스트
local

$ python3 argparse_test.py --target=테스트
테스트
dev

$ python3 argparse_test.py --env=qa
usage: argparse_test.py [-h] --target TARGET [--env ENV]
argparse_test.py: error: the following arguments are required: --target

인자값 설명

위에서 설명한 기능으로도 원하는 동작을 충분히 커버할 수 있습니다. 아래는 객체와 메서드에 어떤 인자가 있는지 간략하게 설명하겠습니다.

ArgumentParser()

해당 객체에는 아래와 같이 입력받고 있습니다.

  • prog: 프로그램의 이름 (기본값: sys.argv[0])
    • 기본값으로 실행한 스크립트파일명을 노출. 작성 시 스크립트 파일 대신 입력한 값이 노출
  • usage: 프로그램 사용법을 설명하는 문자열 (기본값: 파서에 추가된 인자로부터 만들어지는 값)
    • 사용방법을 노출.  기본값으로 실행한 파일 + 입력한 인자값들을 노출
  • description: 인자 도움말 전에 표시할 텍스트 (기본값: none)
    • 스크립트에 -h 옵션을 주어 실행 시, usage 아래에 노출
  • epilog: 인자 도움말 후에 표시할 텍스트 (기본값: none)
  • parents: ArgumentParser 객체들의 리스트이고, 이 들의 인자들도 포함
  • formatter_class: 도움말 출력을 사용자 정의하기 위한 클래스
  • prefix_chars: 선택 인자 앞에 붙는 문자 집합 (기본값: '-').
  • fromfile_prefix_chars: 추가 인자를 읽어야 하는 파일 앞에 붙는 문자 집합 (기본값: None).
  • argument_default: 인자의 전역 기본값 (기본값: None)
  • conflict_handler: 충돌하는 선택 사항을 해결하기 위한 전략 (일반적으로 불필요함)
  • add_help: 파서에 -h/--help 옵션을 추가 (기본값: True)
  • allow_abbrev: 약어가 모호하지 않으면 긴 옵션을 축약할 수 있도록 함. (기본값: True)

add_argument()

해당 메서드는 아래와 같이 입력받고 있습니다.

  • name or flags: 옵션 문자열의 이름이나 리스트, 예를 들어 foo 또는 -f, --foo.
  • action: 명령행에서 이 인자가 발견될 때 수행 할 액션의 기본형.
  • nargs: 소비되어야 하는 명령행 인자의 수.
  • const: 일부 action 및 nargs 를 선택할 때 필요한 상숫값.
  • default: 인자가 명령행에 없는 경우 생성되는 값.
  • type: 명령행 인자가 변환되어야 할 형.
  • choices: 인자로 허용되는 값의 컨테이너.
  • required: 명령행 옵션을 생략 할 수 있는지 아닌지 (선택적일 때만).
  • help: 인자가 하는 일에 대한 간단한 설명.
  • metavar: 사용 메시지에 사용되는 인자의 이름.
  • dest: parse_args() 가 반환하는 객체에 추가될 어트리뷰트의 이름.


해당 인자들과 추가적인 기능에 대한 자세한 설명은 https://docs.python.org/ko/3.7/library/argparse.html 에서 확인할 수 있습니다.

  1. Hoguz 2019.06.26 10:36 신고

    매번 많은 도움 받고 갑니다. 감사합니다

  2. 지나가다 2019.08.06 11:13

    도움 많이 되었습니다 감사합니다!

SELECT 쿼리를 사용하여 DB의 값을 가져올 때, float, double과 같은 타입일 경우에 psycopg2가 Decimal 타입으로 가져오게 됩니다. 가져온 값을 그대로 다른 DB에 넣을 때 Decimal타입을 변경해줘야 하는데 귀찮으니 애초에 DB 값을 가져올 때, 아래와 같이 제거해서 결과를 출력할 수 있습니다.

from psycopg2.extensions import new_type, DECIMAL


DEC2FLOAT = new_type(DECIMAL.values, 'DEC2FLOAT', lambda value, curs: float(value) if value is not None else None)
psycopg2.extensions.register_type(DEC2FLOAT)
conn = psycopg2.connect()
cursor = conn.cursor(cursor_factory=RealDictCursor, )


먼저 우리가 흔히 아는 특수문자는 반각문자 입니다. (키보드에 존재하는 특수문자 !@#$% 등등) 전각문자는 윈도우 한자 키를 사용하여 생성된 특수문자입니다. (123abc?!등등)

전각문자를 사용해도 표현은 되지만 문자 사이의 간격이 반각보다 커서 가독성 문제나 123과 같은 숫자가 전각일 경우, 문자로 인식되는 것등의 문제가 있습니다. 

따라서 파이썬에서 전각문자를 반각문자로 변경하는 방법은 아래와 같습니다.


# 전각문자
full = '!'
# 반각문자
half = '!'
# 전각문자와 반각문자의 차이
diff = '0xfee0'
# 전각문자 블랭크
blank = '0x3000'

# 16진수인 ascii code
hex_ascii_full = ord(full)
hex_ascii_half = ord(half)
hex_ascii_diff = int(diff, 16)
hex_ascii_blank = int(blank, 16)

# 16진수 형태의 string
hex_full = hex(hex_ascii_full)
hex_half = hex(hex_ascii_half)
hex_blank = hex(hex_ascii_blank)

# 전각일 경우 전각 기준인 값을 차감해 반각으로 변경
if hex_ascii_full >= hex_ascii_diff:
    result = hex_ascii_full - hex_ascii_diff
# 빈칸이 전각일 경우는 위 공식에 어긋나므로 강제로 반각형태의 빈칸을 지정
elif hex_ascii_full == hex_ascii_blank:
    result = hex_blank


assert chr(result) == '!'


파이썬3은 string이 전부 unicode입니다. 따라서 ord() 함수를 사용하여 문자열을 아스키코드로 변환할 수 있습니다. (= Hexadecimal Ascii Code) 다음 변환된 아스키 코드를 hex()로 감싸면 원하는 유니코드표(=16진수)의 값을 확인할 수 있게 됩니다. 이러한 작업을 한 다음, 비교문을 통해 0xfee0 값 보다 큰 값은 전부 전각문자이므로 16진수 형태의 문자와 차를 구하게 되면 반각문자를 구하게 될 수 있습니다. 계산된 전각문자는 16진수이므로 내장함수인 chr() 함수를 사용해 우리가 알고있는 문자로 변환을 하면 끝입니다. 

만약 반각 → 전각 문자로 변경하고자 하면 0xfee0 값 보다 작은 값을 찾아, 더해주면 됩니다.


삽질

코드는 다른 언어에 비해 간단하다고 할 수 있는데 가장 기본인 파이썬3의 모든 string이 unicode이다 이 부분을 까먹으면 혼돈에 빠지게 됩니다.


1. 먼저 첫 번째로 한 삽질은 str.encode('unicode_escape') 를 사용해 문자를 unicode 형식으로 변경할 수 있습니다. 하지만 변경된 문자는 b'\\uac00' 와 같은형식으로 바이트 타입입니다. 혹시나 해서 바이트 타입을 벗기면 \uac00 와 같은 string 타입이 나오게 됩니다.. 만약 반각일 경우는 \u가 없는 바이트 타입이 나오게 됩니다. 이러한 방식은 전각 ↔ 반각 변경에 전혀 쓸 수 없습니다. (하려면 할 순 있지만 코드가 방대하고 어려워지게 됩니다.)

2. 두 번째로는 바이트로 변경하여 전각문자와 반각문자의 차이값인 0xfee0 만큼 빼려고 시도했습니다. 하지만 바이트는 더하거나 뺄 수없고 시프트로 자리값을 밀어야 합니다. 이 방법 또한 구현하기 복잡하고 디버깅이 어려워 바로 포기했습니다.



간단한 문제를 다양한 방법으로 삽질을 통해 해결하긴 했지만 가장 기본인 파이썬3의 모든 string이 unicode이다 가 제일 중요합니다.

Postgresql에서는 order by 부분에 null값이 가장 먼저 나올지, 나중에 나올지 설정할 수 있습니다. 

order by의 예는 아래와 같습니다.

SELECT name
FROM test
ORDER BY name DESC NULLS FIRST;


해당 쿼리는 name 컬럼에 null값인 행을 가장 앞으로 정렬하게되는 쿼리입니다. 


아래는 위 쿼리를 Django ORM으로 똑같이 표현한 예입니다.

from django.db.models import F


Test.objects.order_by(F('name').desc(nulls_first=True))

null값을 가장 뒤로 보내고 싶으면 nulls_first 부분을 nulls_last로 변경해주면 됩니다.

쿼리에서 aggregation함수(count(), max(), first() 등등) 와 필요한 컬럼을 출력하는 방법은 아래와 같습니다.

SELECT name AS changed_name, count(count)
FROM test
GROUP BY name;


위와 같은 쿼리는 아래와 같이 장고 ORM으로 표현할 수 있습니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
).values('max_count', 'changed_name')


aggregation 함수를 사용하기 전, group by를 진행할 필드를 지정해 줍니다.

queryset = Test.objects.values('name')


group by를 진행할 필드를 작성한 다음, annotate()를 사용해서 aggregation과 변경할 이름을 작성합니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
)


마지막으로 호출하고자 하는 필드를 지정해 줍니다. 이때는 annotate()에서 변경한 이름을 지정합니다.

queryset = Test.objects.values('name').annotate(
    max_count=Count('count'),
    changed_name=F('name')
).values('max_count', 'changed_name')


+ Random Posts