Skip to main content

Various convenient features related to httpx and BeautifulSoup.

Project description

[!CAUTION] This package is no longer maintained and replaced with httpc.

hxsoup

PyPI - Python Version PyPI - Downloads

Various convenient features related to httpx and BeautifulSoup. (httpx + BeautifulSoup)

hxsoup는 httpx를 기반으로 추가적인 기능을 추가한 라이브러리입니다.

Getting started

파이썬을 설치하고 터미널에 다음과 같은 명령어를 입력하세요.

pip install -U hxsoup

httpx와 bs4는 같이 설치되지만 BeatifulSoup의 추가적인 parser인 lxml와 html5lib는 기본으로 제공하지 않습니다.

How to use

[!NOTE] 예시에서는 get 요청을 위주로 설명하지만, 다른 모든 메소드(options/head/post/put/patch/delete)에서도 동일하게 작동합니다.

attempts

어떤 경우에서든 서버와의 연결이 실패할 수 있습니다. 이유는 다양할 수 있으나, 그저 다시 시도하는 것만으로도 해결되는 경우가 태반입니다.

만약 attempts를 1보다 큰 정수로 설정하면 연결을 실패했을 때 해당 숫자만큼 재시도합니다. 그리고 만약 결국 attempts 만큼 도전했는데도 불구하고 연결에 실패했을 경우 오류를 re-raise합니다.

연결에 바로 성공했을 경우:

>>> import hxsoup.dev as hd
>>> hd.get("https://python.org", attempts=3)
<Response [200 OK]>

[!NOTE] hxsoup.dev는 hxsoup와 거의 같지만 일부 기본값을 조정한 모듈입니다. hxsoup와 같다고 생각하셔도 무관합니다. 자세한 내용은 뒤에서 설명합니다.

연결에 끝까지 실패했을 경우:

>>> import hxsoup.dev as hd
>>> hd.get("https://unreachable-service.com", attempts=3)
WARNING:root:Retrying...
WARNING:root:Retrying...
Traceback (most recent call last):
    ...
httpx.ConnectError: [Errno 11001] getaddrinfo failed

첫 연결에 실패하고 다시 몇 번 시도했을 때 성공했을 경우:

>>> import hxsoup.dev as hd
>>> hd.get('https://www.webtoons.com/en/', attempts=4)
WARNING:root:Retrying...
WARNING:root:Retrying...
WARNING:root:Successfully retrieved: 'https://www.webtoons.com/en/'
<Response [200 OK]>

raise_for_status

httpx에는 raise_for_status라는 기능이 있습니다. response.raise_for_status()를 이용하면 상태 코드가 일반적이지 않을 때 오류를 냅니다.

>>> import hxsoup.dev as hd
>>> response = hd.get("https://httpbin.org/status/404")
>>> response.raise_for_status()
Traceback (most recent call last):
    ...
httpx.HTTPStatusError: Client error '404 NOT FOUND' for url 'https://httpbin.org/status/404'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404

hxsoup에서는 raise_for_status를 파라미터에서 그대로 사용할 수 있도록 합니다.

>>> import hxsoup.dev as hd
>>> response = hd.get("https://httpbin.org/status/404", raise_for_status=True)
Traceback (most recent call last):
    ...
httpx.HTTPStatusError: Client error '404 NOT FOUND' for url 'https://httpbin.org/status/404'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404

Client에 raise_for_status를 추가하면 해당 클라이언트에서는 모두 raise_for_status가 적용됩니다.

>>> import hxsoup.dev as hd
>>> with hd.Client(raise_for_status=True) as client:
...     try:
...         client.get("https://httpbin.org/status/404")
...     except Exception as e:
...         print(e)
...     try:
...         client.get("https://httpbin.org/status/404")
...     except Exception as e:
...         print(e)
...
Client error '404 NOT FOUND' for url 'https://httpbin.org/status/404'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
Client error '404 NOT FOUND' for url 'https://httpbin.org/status/404'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404

일반적으로 상태 코드 이상은 모든 연결이 안정적으로 동작한 경우가 많기 때문에 raise_for_status에 의한 오류는 attempts에 의해 필터링되지 않습니다.

Client

hxsoup.Client는 httpx.Client에 대응하는 기능입니다. 위에서 설명되었던 모든 추가 파라미터들(attempts, raise_for_status)는 Client에서도 사용할 수 있으며 Client를 initialize할 때 사용한다면 해당 client의 모든 통신에서 적용시킬 수 있습니다.

>>> with hxsoup.Client(raise_for_status=True) as client:
...     # Client를 initialize할 때 raise_for_status를 True로 했기 때문에
...     # raise_for_status를 직접 적지 않았다면 raise_for_status가 자동을 적용됨.
...     client.get("https://httpbin.org/status/404")
...
Traceback (most recent call last):
    ...
httpx.HTTPStatusError: Client error '404 NOT FOUND' for url 'https://httpbin.org/status/404'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404

하지만 개별 request에 해당 파라미터와 다른 파라미터를 적용했다면 해당 파라미터가 적용됩니다.

SoupTools, SoupedResponse

SoupToolsBeautifulSoup에 몇 가지 편리한 기능을 추가하고, SoupedResponseBeautifulSoup를 respone 안에 멋지게 통합합니다.

SoupedResponsehttpx.Response의 subclass이면서 SoupTools의 subclass이며, hxsoup.dev를 포함하여 hxsoup의 모든 response는 SoupedResponse입니다.

The SoupTools methods that correspond to methods in BeautifulSoup

SoupTools에는 BeautifulSoup의 메소드에 대응하는 메소드들이 있습니다.

  • SoupTools.soup(): BeautifulSoup(...)에 대응합니다.
  • SoupTools.soup_select(selector): BeautifulSoup(...).select(selector)에 대응합니다.
  • SoupTools.soup_select_one(selector): BeautifulSoup(...).select_one(selector)에 대응합니다.

no_empty_result

BeautifulSoup.soup_select_one()에는 한 가지 문제가 있습니다. 바로 해당하는 element를 찾는데 실패했을 때 오류가 아닌 None을 내밷는다는 점입니다. 이는 어쩔 때는 편리할지는 몰라도 typing도 어렵고 .soup_select().text와 같은 chaining도 어렵게 합니다.

hxsoup의 response.soup_select_one()에는 이러한 문제를 해결하기 위해 no_empty_result라는 파라미터를 도입했습니다. 아래는 python.orgnever-gonna-selected라는 선택자에 맞는 태그가 있는지 확인하고 있습니다. python.org에서는 그런 태그는 없기 때문에 일반적으로는 None을 반환받게 되는데, no_empty_result가 True라면 None이 아닌 오류를 raise하게 됩니다.

>>> import hxsoup.dev as hd
>>> res = hd.get("https://python.org")
>>> res.soup_select_one("never-gonna-selected", no_empty_result=True)
Traceback (most recent call last):
    ...
hxsoup.exceptions.EmptyResultError: Selecting result is None. This error happens probably because of invalid selector or URL. Check whether selector and URL are both valid.
status code: HTTP 200 OK, URL: https://www.python.org/, selector: 'never-gonna-selected'

같은 방식이 response.soup_select()에도 적용됩니다. 이 경우 빈 리스트가 리턴될 때 EmptyResultError가 나게 됩니다.

>>> import hxsoup.dev as hd
>>> res = hd.get("https://python.org")
>>> res.soup_select("never-gonna-selected", no_empty_result=True)
Traceback (most recent call last):
    ...
hxsoup.exceptions.EmptyResultError: Selecting result is empty list("[]"). This error happens probably because of invalid selector or URL. Check whether selector and URL are both valid.
status code: HTTP 200 OK, URL: https://www.python.org/, selector: 'never-gonna-selected'

response.soup_select()에도 no_empty_result가 있다는 점을 잊지 마세요. 따라서 Clienthxsoup.get 자체에 no_empty_result를 사용할 때에는 이 기능이 response.soup_select()에도 적용된다는 점을 명심하세요.

parser

parser을 설정할 수 있도록 합니다. BeautifulSoup의 용어로는 feature입니다.

기본적으로는 'html.parser'를 사용하도록 되어 있습니다.

Broadcasting

soup_select()의 결과는 리스트입니다. Tag 관련 처리를 할 때에는 리스트 컴프리헨션을 이용해야 하는데, 여간 귀찮은 일이 아닙니다.

hxsoup에서 soup_select()의 결과는 BroadcastList이며, 이는 여러 문제를 해결합니다.

BroadcastList는 .bc를 붙이면 브로드캐스팅 가능한 상황이 되고 그 뒤에 어떤 것을 붙이던 브로드캐스팅이 일어납니다.

예를 들면 다음의 코드는

>>> import hxsoup.dev as hd
>>> res = hd.get("https://python.org")
>>> [tag.text for tag in res.soup_select("strong")]
['Notice:', 'A A', 'relaunched community-run job board']

아래의 코드로 대체될 수 있습니다.

>>> import hxsoup.dev as hd
>>> res = hd.get("https://python.org")
>>> res.soup_select("strong").bc.text
['Notice:', 'A A', 'relaunched community-run job board']

브로드캐스팅을 더 하고 싶다면 .bc를 또 붙이면 됩니다.

>>> import hxsoup.dev as hd
>>> res = hd.get("https://python.org")
>>> res.soup_select("strong").bc.text.bc[::2]
['Ntc:', 'AA', 'rluce omnt-u o or']

ClientOptions

고정적으로 여러 request에 대해 같은 키워드를 사용해야 하는 경우가 있습니다. 대부분은 Client를 이용하면 해결되지만, AsyncClient와 Client를 같이 사용하거나, httpx.get처럼 클라이언트 없이 사용하고 싶은 경우도 있을 것입니다. 이럴 경우 이 클래스를 사용할 수 있습니다.

(.venv) C:\Users\USER\Programming\vscode\git\hxsoup>py -3.12 -m asyncio
asyncio REPL 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from hxsoup import ClientOptions
>>>
>>> options = ClientOptions(follow_redirects=True)
>>> print(options.get("https://python.org"))
<Response [200 OK]>
>>> with options.build_client() as client:
...     print(client.get("https://python.org"))
...
<Response [200 OK]>
>>> async with options.build_async_client() as async_client:
...     print(await client.get("https://python.org"))
...
<Response [200 OK]>

hxsoup.dev

일부 기본값을 재조정하고 caching이 포함된 모듈입니다.

Adjusted defaults

개발 중에는 일부 파라미터가 흔히 많이 사용됩니다. 예를 들어 follow_redirects 파라미터는 httpx에서 기본적으로 꺼져 있지만 이른 개발 사이클에 있는 경우 켜져 있는 것이 편한 경우가 많습니다.

개발자의 편의를 위해 follow_redirects를 비롯한 몇몇 파라미터들은 httpx의 기본 설정값과는 다른 값을 이용합니다.

파라미터 이름 hxsoup 기본값 hxsoup.dev 기본값
follow_redirects False True
headers None DEV_HEADERS
>>> import hxsoup
>>> hxsoup.get("https://python.org")
<Response [301 Moved Permanently]>
>>>
>>> import hxsoup.dev as hd
>>> # follow_redirects가 True임.
>>> hd.get("https://python.org")
<Response [200 OK]>

DEV_HEADERS의 내용은 Chrome 브라우저의 기본 헤더이며 내용은 다음과 같습니다.

{
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
    "Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
    "Sec-Ch-Ua-Arch": '"x86"',
    "Sec-Ch-Ua-Bitness": '"64"',
    "Sec-Ch-Ua-Full-Version-List": '"Not_A Brand";v="8.0.0.0", "Chromium";v="120.0.6099.130", "Google Chrome";v="120.0.6099.130"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Model": '""',
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Ch-Ua-Platform-Version": '"15.0.0"',
    "Sec-Ch-Ua-Wow64": "?0",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}

cached requests

활발한 개발 중에는, 특히 Jupyter를 이용할 때는, 일부 requests를 계속해서 보내게 되는 경우가 있습니다. 이는 서버에 부담을 주고 개발 속도를 늦춥니다.

각 메소드의 앞에 c를 붙이면 응답이 캐싱됩니다.

예를 들어 시간을 비교하면 아래와 같이 됩니다.

>>> from timeit import timeit
>>> timeit('hd.get("https://python.org")', setup="import hxsoup.dev as hd", number=10)
0.7851526000013109
>>> timeit('hd.cget("https://python.org")', setup="import hxsoup.dev as hd", number=10)
0.061434000002918765

options/head/post/put/patch/delete들도 마찬가지로 대응되는 coptions/chead/cpost/cput/cpatch/cdelete가 있습니다.

>>> from timeit import timeit
>>> timeit('hd.post("https://httpbin.org/post")', setup="import hxsoup.dev as hd", number=10)
9.307660000005853
>>> timeit('hd.cpost("https://httpbin.org/post")', setup="import hxsoup.dev as hd", number=10)
0.8557240999944042

캐시는 lru_cache 기본값을 사용하기 때문에 메소드 구분 없이 128개까지 저장됩니다.

Client/ClientOptions

Client와 ClientOptions에도 마찬가지로 기본 옵션을 사용 가능합니다.

>>> import hxsoup.dev as hd
>>> import hxsoup
>>>
>>> client = hxsoup.Client()
>>> client.get("https://python.org")
<Response [301 Moved Permanently]>
>>>
>>> dev_client = hd.Client()
>>> dev_client.get("https://python.org")
<Response [200 OK]>
>>>
>>> options = hxsoup.ClientOptions()
>>> options.get("https://python.org")
<Response [301 Moved Permanently]>
>>>
>>> dev_options = hd.ClientOptions()
>>> dev_options.get("https://python.org")
<Response [200 OK]>

License information

이 프로그램의 일부는 resoup(본인 제작) 라이브러리에 있던 코드를 포함합니다. Some part of this program contains code from resoup(created and developed by me) library.

이 프로그램의 일부는 httpx(BSD-3-Clause license) 라이브러리에 있던 코드를 포함합니다. Some part of this program contains code from httpx library.

Motivation and blathers

이전에 requests의 불편한 점을 느끼고 관련 내용을 입맛에 맞게 수정한 resoup라는 라이브러리를 만들어 사용하고 있었습니다.

그러던 어느 날 ArjanCodes의 영상에서 httpx에 대해 다시 보게 되고 실제로 사용해보니 requests와 상위 호환되는 라이브러리라는 점을 알게 되었습니다.

따라서 httpx을 앞으로의 프로젝트들에서 사용하기로 결정했고, resoup의 기능을 포기할 수 없었기에 requests의 경우와 마찬가지로 resoup에 대응하는 httpx에 대한 유틸리티를 만들기로 했으며 그 결과가 hxsoup입니다.

resoup과 비교했을 때 개발 경험은 hxsoup 쪽이 압도적으로 좋았는데, requests는 type hint가 나오기 전 라이브러리라 그런지 효율적이지만 type hint를 적용하기에는 최악이었던 반면, httpx는 따로 type stub나 typing.overload를 거의 사용하지 않았을 정도로 매우 안정적이고 typing을 적용하면서 개발하기에도 좋았습니다. (물론 resoup을 만들면서 생긴 노하우도 많이 도움이 되었겠지만요.)

Changelog

  • 0.5.1 (2024-10-02): 빌드 시스템에 uv 사용, attempt 대상에 SSLError도 추가
  • 0.5.0 (2024-06-22): logger 사용, 빌드 현대화, 의존성 업그레이드, 기타 문서 및 코드 개선
  • 0.4.1 (2024-01-24): caching 제거, broadcasting 마저 제거.
  • 0.4.0 (2024-01-23): NotEmptySoupTools와 NotEmptySoupedResponse 추가, soup_select와 soup_select_one에 **kwargs 추가, 버그 수정
  • 0.3.0 (2024-01-04): ClientOptions 추가, 코드 및 버그 수정
  • 0.2.0 (2024-01-01): 여러 기능을 추가하고 수정.
  • 0.1.0 (2023-12-28): 첫 (프리)릴리즈

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hxsoup-0.6.0.tar.gz (83.1 kB view hashes)

Uploaded Source

Built Distribution

hxsoup-0.6.0-py3-none-any.whl (28.0 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page