Today_I_Learned

Playwright로 Colab에서 연합뉴스 크롤링하기

CONCAT 2023. 12. 21. 10:27
728x90

Playwright로 Colab에서 연합뉴스 크롤링하기

Colab 노트북

 

231221_Playwright로 Colab에서 연합뉴스 크롤링하기.ipynb

Colaboratory notebook

colab.research.google.com

연합뉴스 최신기자 페이지(https://www.yna.co.kr/news/1)를 크롤링하는 코드를 짜달라는 요청이 들어왔다.
아니 컴공 복전한 사람이 이런 것도 못해? 라고 힐난하고 책망하려던 차에...

---------------------------------------------------------------------------
SSLError                                  Traceback (most recent call last)
/usr/local/lib/python3.10/dist-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    467             try:
--> 468                 self._validate_conn(conn)
    469             except (SocketTimeout, BaseSSLError) as e:

18 frames
SSLError: [SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:1007)

During handling of the above exception, another exception occurred:

SSLError                                  Traceback (most recent call last)
SSLError: [SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:1007)

The above exception was the direct cause of the following exception:

MaxRetryError                             Traceback (most recent call last)
MaxRetryError: HTTPSConnectionPool(host='www.yna.co.kr', port=443): Max retries exceeded with url: /news/1 (Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:1007)')))

During handling of the above exception, another exception occurred:

SSLError                                  Traceback (most recent call last)
/usr/local/lib/python3.10/dist-packages/requests/adapters.py in send(self, request, stream, timeout, verify, cert, proxies)
    515             if isinstance(e.reason, _SSLError):
    516                 # This branch is for urllib3 v1.22 and later.
--> 517                 raise SSLError(e, request=request)
    518 
    519             raise ConnectionError(e, request=request)

SSLError: HTTPSConnectionPool(host='www.yna.co.kr', port=443): Max retries exceeded with url: /news/1 (Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:1007)')))

위와 같은 에러를 만나버렸다. HTTPSConnectionPool(host='www.yna.co.kr', port=443): Max retries exceeded with url: /news/1 (Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] unsafe legacy renegotiation disabled (_ssl.c:1007)'))) 라는 에러로 쉽게 말해 SSL 인증을 할 수 없다, 이런 뜻이다.

이를 해결하기 위해서는 2가지 세팅이 필요한데, 먼저 적절한 Header 세팅이다. 크롤링은 일반적인 브라우저 요청과는 달라서, Header 쪽에 세팅이 좀 빈약하다. (주로 Cookie 혹은 User-agent) 그래서 이 쪽 세팅을 해줘야한다. Local에서 Jupyter Notebook으로 크롤링을 하는 경우에는 이 세팅만 적절히 해주면 requests가 잘 작동한다.

나머지 하나는 연합뉴스 홈페이지에서 사용하는 SSL 알고리즘에 맞는 방식으로 접근이 필요한데... Colab은 뭔가 버전이 안 맞아서 이게 안 된다. 즉, 리눅스 내부에 있는 OpenSSL을 통한 Requests 요청(curl과 유사)은 불가능하다.


그래서 Colab에서도 headless browser를 구동할 수 있는 playwright를 사용하기로 했다.

쉽게 말해 selenium처럼 browser를 구동하는 거라고 보면 되는데, 요새는 무슨 문제인지 colab에서 selenium이 잘 구동이 안되더라... 그래서 갈아탔다. + 여러 세팅이 더 쉽다.

설치

 

Installation | Playwright Python

Introduction

playwright.dev

!pip install pytest-playwright -q # 패키지 설치
!playwright install # 필요한 브라우저 설치

playwright는 자체 install 기능을 통해 굳이 외부에서 필요한 브라우저를 찾기 않아도 알아서 다운로드 받아준다.

구동

 

Playwright | Playwright Python

Playwright module provides a method to launch a browser instance. The following is a typical example of using Playwright to drive automation:

playwright.dev

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()  # Chromium 브라우저 실행
        page = await browser.new_page()  # 새로운 웹 페이지 생성
        await page.goto('https://www.yna.co.kr/news/1')  # 지정한 URL로 이동
        print(await page.title())  # 현재 페이지의 제목 출력
        await browser.close()  # 브라우저 종료

asyncio.run(main())  # 비동기 함수 실행

공식 홈페이지의 예시 코드를 바탕으로, 연합뉴스 최신 기사 페이지의 제목(타이틀)을 출력하는 코드를 작성했다. 파이썬에서의 비동기 처리 (async-await)는 좀 어색하긴 하지만 JS(ECMAScript2016)의 경험이 있으므로 얼추 하고 넘어가려고 했는데...

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-3-e86128b10562> in <cell line: 13>()
     11         await browser.close()
     12 
---> 13 asyncio.run(main())

/usr/lib/python3.10/asyncio/runners.py in run(main, debug)
     31     """
     32     if events._get_running_loop() is not None:
---> 33         raise RuntimeError(
     34             "asyncio.run() cannot be called from a running event loop")
     35 

RuntimeError: asyncio.run() cannot be called from a running event loop

잘 되나 싶더니만 에러와 조우... (RuntimeError: asyncio.run() cannot be called from a running event loop)

다행히도 흔한 에러라서 바로 stackoverflow에서 해결책을 찾을 수 있었다. Colab에서는 nest_asyncio를 통한 추가 세팅이 필요했던 것!

 

Running Playwright on Google colab gives error : asyncio.run() cannot be called from a running event loop

I was trying to run playwright web automation on google colab but can't run the event loop on colab. This is what I tried !pip install playwright from playwright.sync_api import sync_playwright with

stackoverflow.com

import nest_asyncio
nest_asyncio.apply()

설정하고 구동하니까 잘 된다.


이것저것 섞어보자

!pip install pytest-playwright -q # 패키지 설치
!playwright install # 필요한 브라우저 설치

# 필요한 라이브러리 가져오기
from bs4 import BeautifulSoup  # 웹 페이지 파싱을 위한 BeautifulSoup 라이브러리
import pandas as pd  # 데이터 프레임을 생성하기 위한 Pandas 라이브러리
import asyncio  # 비동기 작업을 위한 asyncio 라이브러리
from playwright.async_api import async_playwright  # 웹 브라우징을 위한 Playwright 라이브러리
from google.colab import files  # Google Colab에서 파일 다운로드를 위한 라이브러리
from datetime import datetime as dt  # 현재 날짜 및 시간을 얻기 위한 datetime 라이브러리
from dateutil.relativedelta import relativedelta as rd  # 날짜 연산을 위한 dateutil 라이브러리
import nest_asyncio  # 비동기 작업 루프를 설정하기 위한 nest_asyncio 라이브러리

# 비동기 작업 루프 설정
nest_asyncio.apply()

# HTML 파싱을 위한 함수 정의
def parse_html(html):
    soup = BeautifulSoup(html, 'lxml')  # BeautifulSoup을 사용하여 HTML 파싱
    return soup

# 현재 날짜 및 시간을 가져오는 함수 정의
def get_now():
    now_dt = dt.utcnow() + rd(hours=9)  # 현재 UTC 시간을 가져와서 9시간을 더해 한국 시간으로 변환
    return now_dt.strftime('%Y-%m-%d_%H:%M:%S')  # 날짜 및 시간을 문자열로 반환

# 데이터를 CSV 파일로 저장하는 함수 정의
def save_to_csv(tags):
    items = tags.select('.section01 .item-box01')  # HTML에서 뉴스 아이템 요소 선택
    data = [{
        'created_at': item.select_one('.txt-time').getText(),  # 뉴스 작성 시간 가져오기
        'title': item.select_one('.tit-news').getText(),  # 뉴스 제목 가져오기
        'lead': item.select_one('.lead').getText(),  # 뉴스 요약 가져오기
        'url': 'https:' + item.select_one('.tit-wrap')['href'],  # 뉴스 URL 가져오기
    } for item in items]  # 모든 뉴스 아이템에 대한 정보를 리스트로 저장
    df = pd.DataFrame(data)  # 데이터를 Pandas 데이터 프레임으로 변환
    fn = f'news_{get_now()}.csv'  # 파일 이름 생성 (현재 시간 기반)
    df.to_csv(fn)  # 데이터 프레임을 CSV 파일로 저장
    files.download(fn)  # Google Colab에서 파일 다운로드

# 비동기 함수를 정의하고 실행하는 메인 함수
async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()  # Chromium 브라우저 실행
        page = await browser.new_page()  # 새로운 웹 페이지 생성
        URL = 'https://www.yna.co.kr/news/1'  # 스크랩할 웹 페이지 URL
        await page.goto(URL)  # 웹 페이지로 이동
        tags = parse_html(await page.inner_html('body'))  # 웹 페이지의 HTML을 파싱
        save_to_csv(tags)  # 데이터를 CSV 파일로 저장
        await browser.close()  # 브라우저 종료

# asyncio 라이브러리를 사용하여 비동기 작업 실행
asyncio.run(main())

뭐 어떻게든 데이터만 받아올 수 있으면 그 다음은 쉽다(?)
bs4를 통한 태그 파싱, pandas로 df 만들어서 저장, colab.files로 다운로드 진행 정도 버무려서 마무리.