-
파이썬(Python)으로 보안뉴스 크롤링(Crawling) 하기IT 2023. 12. 12. 15:36반응형
웹 크롤링(Crawling)이란?
웹 크롤링(Web Crawling)은 인터넷상의 웹사이트를 자동으로 탐색하고 정보를 수집하는 과정을 의미한다. 이를 통해 데이터를 수집하거나, 검색 엔진이 웹 페이지를 인덱싱하는 데 사용된다. 크롤러(또는 스파이더, 봇)라고 불리는 자동화된 프로그램이 인터넷을 탐색하면서 웹 페이지의 내용을 다운로드하고, 그 링크를 따라 다른 페이지로 이동한다. 이 과정을 통해 웹 페이지의 내용, 링크, 이미지 등 다양한 데이터가 수집된다.
웹 크롤링은 다음과 같은 목적으로 사용될 수 있다.
- 검색 엔진 최적화(SEO): 검색 엔진이 웹 페이지를 인덱싱하고, 검색 결과를 제공하는 데 필요한 데이터를 수집
- 데이터 분석: 특정 주제나 키워드에 관한 정보를 수집하여 시장 조사, 경쟁 분석 등에 활용
- 콘텐츠 모니터링: 뉴스, 소셜 미디어, 포럼 등에서 특정 콘텐츠나 트렌드를 모니터링
- 데이터 아카이빙: 웹 페이지의 정보를 저장하여 기록을 유지
웹 크롤링은 유용하지만, 웹사이트의 로드를 증가시키거나, 저작권이나 개인정보 보호와 같은 법적 문제를 일으킬 수 있어 주의가 필요하다. 따라서 크롤링을 수행할 때는 웹사이트의 이용 약관을 준수하고, 적절한 속도로 크롤링을 진행하는 것이 중요하다.
보안뉴스 크롤링하기
구글, 네이버 등과 같은 사이트에서 볼 수 있는 수 많은 인터넷 신문 사이트가 존재한다. 이번 글에서 크롤링하려는 사이트는 보안뉴스이다. 먼저 보안뉴스 사이트에서 봇의 접근을 허용하는지 확인해보자. 봇의 접근과 관련된 정보는 robots.txt에 존재한다. 보안뉴스 사이트의 robots.txt를 확인해보면 다음과 같다.
이 robots.txt 파일은 웹 크롤러들이 사이트의 모든 부분에 접근할 수 있도록 허용하고 있으나, /secu_admin/ 디렉토리에는 접근을 금지하고 있다. 여기서 User-Agent: *는 모든 웹 크롤러에 해당하는 설정이고, Allow: /는 사이트의 모든 부분에 대한 접근을 허용한다는 의미이고, Disallow: /secu_admin/는 /secu_admin/ 디렉토리에 대한 접근을 금지한다는 것을 의미한다. 수집하는 대상이 /secu_admin/이 아니기 때문에 봇의 접근을 허용한다고 볼 수 있다.
전체 기사 URL 확인
보안뉴스의 여러 탭 중에서 '#전체기사' 탭의 뉴스기사를 수집하려고한다. 해당 탭의 URL은 https://boannews.com/media/t_list.asp?Page=1&kind= 과 같이 나타난다.
URL을 보면 Page 정보가 있는데, 이번 글에선 1 페이지에 있는 뉴스기사만 크롤링을 할 것이지만, 페이지 값을 수정한다면 나중에 여러 페이지를 크롤링할 수 있을 것이다.
크롤러 개발하기
보안뉴스기사 크롤러 개발에 필요한 라이브러리는 다음과 같다.
from bs4 import BeautifulSoup import requests import pandas as pd from tqdm.auto import tqdm import lxml
- Beautiful Soup (bs4): Beautiful Soup는 Python에서 HTML 및 XML 파일을 구문 분석하고 조작하기 위한 라이브러리. 웹 스크래핑에 주로 사용되며, 웹 페이지로부터 데이터를 추출하고 분석하는데 유용하다. 사용자가 태그와 속성을 쉽게 검색하고 수정할 수 있도록 도와준다.
- Requests: Requests는 Python에서 HTTP 요청을 보내기 위한 간단하고 사용하기 쉬운 라이브러리. 이 라이브러리를 사용하면 웹 페이지의 내용을 가져오고, API 호출을 수행하고, 웹 기반 데이터와 상호 작용하는 등의 작업을 손쉽게 할 수 있다.
- Pandas: Pandas는 데이터 분석과 조작을 위한 고성능, 사용하기 쉬운 데이터 구조와 데이터 분석 도구를 제공하는 Python 라이브러리. 주로 데이터프레임이라고 하는 2차원 테이블 형태의 구조를 사용하여, 복잡한 데이터 조작 및 분석 작업을 간편하게 수행할 수 있다.
- tqdm: tqdm은 Python에서 반복 작업의 진행 상황을 시각적으로 표시하는 데 사용되는 라이브러리. 반복문(예: for 루프)에 쉽게 추가할 수 있으며, 작업의 진행률, 소요 시간, 예상 남은 시간 등을 그래픽 형태로 표시한다.
- lxml: lxml은 Python에서 XML 및 HTML을 처리하기 위한 고성능 라이브러리. Beautiful Soup와 함께 사용되곤 하는데, XML/HTML 문서의 구문 분석과 처리를 위한 강력하고 빠른 도구다. XPath 및 XSLT와 같은 기능을 지원하며, 복잡한 웹 페이지를 효과적으로 파싱할 수 있다.
이제 보안뉴스 사이트의 내용을 가져와보자. requests 라이브러리를 이용해서 #전체기사 탭의 페이지 내용을 가져올 수 있다. 먼저 requests.get을 했을 때 응답 코드를 확인해보자.
URL = 'https://boannews.com/media/t_list.asp?Page=1&kind=' site = requests.get(URL, verify=False) print(site) # <Response [200]>
응답 코드가 200으로 출력된다면 정상이다.
여기서 requests.get의 인자에 verify를 False로 설정한 이유는 SSL 인증서 검증을 무시하기 위해서이다. URL을 자세히보면 https로 시작하는데 verify 옵션을 False로 설정하지 않으면 SSLCertVerificationError가 발생한다. 아래는 오류 메세지이다.
--------------------------------------------------------------------------- SSLCertVerificationError Traceback (most recent call last) File e:\venv\Crawling\lib\site-packages\urllib3\connectionpool.py:703, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw) 702 # Make the request on the httplib connection object. --> 703 httplib_response = self._make_request( 704 conn, 705 method, 706 url, 707 timeout=timeout_obj, 708 body=body, 709 headers=headers, 710 chunked=chunked, 711 ) 713 # If we're going to release the connection in ``finally:``, then 714 # the response doesn't need to know about the connection. Otherwise 715 # it will also try to release it and we'll have a double-release 716 # mess. File e:\venv\Crawling\lib\site-packages\urllib3\connectionpool.py:386, in HTTPConnectionPool._make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw) 385 try: --> 386 self._validate_conn(conn) 387 except (SocketTimeout, BaseSSLError) as e: 388 # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout. File e:\venv\Crawling\lib\site-packages\urllib3\connectionpool.py:1040, in HTTPSConnectionPool._validate_conn(self, conn) ... --> 517 raise SSLError(e, request=request) 519 raise ConnectionError(e, request=request) 521 except ClosedPoolError as e: SSLError: HTTPSConnectionPool(host='boannews.com', port=443): Max retries exceeded with url: /media/t_list.asp?Page=1&kind= (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1131)'))) Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
만약 verify 옵션을 설정하지 않고, requests.get을 하려면 URL의 https를 http로 변경하면 된다.
URL = 'http://boannews.com/media/t_list.asp?Page=1&kind=' site = requests.get(URL) print(site) # <Response [200]>
정상 응답 코드를 확인했다면, 페이지 내용과 텍스트의 데이터 타입을 확인해보자.
print(site.text) print(type(site.text))
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko" lang="ko"> <head> <title>보안뉴스_뉴스</title> <meta http-equiv="Content-Type" content="text/html; charset=euc-kr"> <link rel="shortcut icon" type="image/x-icon" href="https://www.boannews.com/images/favicon.ico" /> <script type="text/javascript" src="/js/jquery-1.12.4.min.js"></script> <script type="text/javascript" src="/js/multiScroll.js"></script> <link href="../css/media.css?version=3.0" rel="stylesheet" type="text/css"> <script type="text/javascript"> <!-- //스크롤업 메뉴, 뉴스 var didScroll; var lastScrollTop = 0; var delta = 5; var navbarHeight = $('#scroll_up').outerHeight(); $(window).scroll(function(event){ didScroll = true; }); setInterval(function() { ... </script> </body> </html> <class 'str'>
페이지의 html이 출력되고, 텍스트의 타입이 'str'인 것을 확인할 수 있다.
사람이 봤을 때는 페이지 내용이 html이라는 것을 알 수 있지만 프로그램에서는 string으로 인식하고 있다. 따라서 페이지의 내용이 html이라는 것을 코드에게 인지시키기 위해 bs4와 lxml을 이용한다. 이 과정을 '파싱'한다고 한다.
site_soup = BeautifulSoup(site.text, 'lxml') print(site_soup) print(type(site_soup))
<!DOCTYPE html> <html lang="ko" xml:lang="ko" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>보안뉴스_뉴스</title> <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/> <link href="https://www.boannews.com/images/favicon.ico" rel="shortcut icon" type="image/x-icon"/> <script src="/js/jquery-1.12.4.min.js" type="text/javascript"></script> <script src="/js/multiScroll.js" type="text/javascript"></script> <link href="../css/media.css?version=3.0" rel="stylesheet" type="text/css"/> <script type="text/javascript"> <!-- //스크롤업 메뉴, 뉴스 var didScroll; var lastScrollTop = 0; var delta = 5; var navbarHeight = $('#scroll_up').outerHeight(); $(window).scroll(function(event){ didScroll = true; }); setInterval(function() { if (didScroll) { hasScrolled(); didScroll = false; ... </script> </body> </html> <class 'bs4.BeautifulSoup'>
페이지의 html과 데이터 타입이 'bs4'인 것을 확인할 수 있다.
이제 본격적으로 뉴스기사를 수집해보자. 먼저, 크롬브라우저를 이용해서 #전체기사 탭의 URL로 접속한다. 그리고 F12 키를 누르면 '개발자도구'창이 나타난다. 빨간색 박스로 표시된 부분을 선택하고 뉴스기사 위치에 마우스를 이동해서 클릭하면 해당 부분이 페이지의 어느 부분인지 알 수 있다.
해당 부분의 selector를 아래 그림처럼 우클릭으로 복사할 수도 있지만, 페이지 구조가 복잡하지 않고 위치를 쉽게 알 수 있으므로 사용하지 않는다.
그림에서 보는 것처럼 뉴스 기사들이 클래스가 news_list인 div 태그라는 것을 알 수 있다. 이 정보를 이용해서 뉴스 기사 리스트를 가져와보자.
news_list = site_soup.select('div.news_list') print(news_list) print(len(news_list))
[<div class="news_list"> <a href="/media/view.asp?idx=124649&page=1&kind=3"> <img alt="" class="news_img" height="70" src="/media/upFiles2/2023/12/20231212spain-s.jpg"/> <span class="news_txt">보수적인 스페인, 첨단기술이 보안산업 트렌드를 바꿨다</span> </a><br/> <a class="news_content" href="/media/view.asp?idx=124649&page=1&kind=3">스페인은 치안이 우수한 편에 속하는 나라로 소매치기를 비롯한 절도・강도 피해가 타 서유럽 국가에 비해 낮은 편이다. 특히, 스페인에서는 절도범...</a> <span class="news_writer">엄호식 기자 | 2023년 12월 12일 13:48</span> </div>, <div class="news_list"> <a href="/media/view.asp?idx=124688&page=1&kind=1"> <img alt="" class="news_img" height="70" src="/media/upFiles2/2023/12/t_nara.jpg"/> <span class="news_txt">조달청 나라장터 사이트가 또? 1시간 넘게 접속 불가</span> </a><br/> <a class="news_content" href="/media/view.asp?idx=124688&page=1&kind=1">조달청 나라장터 서비스가 12일 오전 9시 27분~10시 30분까지 약 1시간 동안 먹통이 됐다. 지난달 23일 해외 특정 IP 접속으로 사이트가 마비된 ...</a> <span class="news_writer">박은주 기자 | 2023년 12월 12일 13:20</span> </div>, <div class="news_list"> <a href="/media/view.asp?idx=124687&page=1&kind=3"> <img alt="" class="news_img" height="70" src="/media/upFiles2/2023/12/t_nnsp.jpg"/> <span class="news_txt">창립 20주년 앤앤에스피, 글로벌 CPS 보안 전문기업으로 도약</span> </a><br/> <a class="news_content" href="/media/view.asp?idx=124687&page=1&kind=3">앤앤에스피(NNSP, 대표 김일용)가 글로벌 사이버물리시스템(CPS: Cyber Physical System) 보안 전문기업으로 도약을 선언했다. 2025...</a> <span class="news_writer">박은주 기자 | 2023년 12월 12일 13:15</span> </div>, <div class="news_list"> <a href="/media/view.asp?idx=124678&page=1&kind=3"> <img alt="" class="news_img" height="70" src="/media/upFiles2/2023/12/20231212etyms535igluabo1sum.jpg"/> <span class="news_txt">전 세계적 불확실성 속, ‘국가 지원 공급망 공격 증가’ 새해 최대 위협 대두</span> </a><br/> <a class="news_content" href="/media/view.asp?idx=124678&page=1&kind=3">2024년 사이버 보안 분야에서는 국가 지원 기반 공급망 공격 증가, 보안 운영 자동화, IT-OT 융합 보안 등이 핵심 이슈로 떠오를 전망이다. 새해 사...</a> <span class="news_writer">김영명 기자 | 2023년 12월 12일 10:03</span> </div>, <div class="news_list"> ... </a><br/> <a class="news_content" href="/media/view.asp?idx=124670&page=1&kind=2">종로구가 전국 최초로 라이다(LiDAR) 센서와 CCTV를 결합한 ‘스마트 인파관리시스템’ 개발을 완료하고 내년부터 관내 전역에 확대 도입한다.</a> <span class="news_writer">박미영 기자 | 2023년 12월 12일 10:24</span> </div>] 30
결과를 보면 뉴스기사 부분만 잘 추출되었고, 뉴스 리스트의 개수가 30개인 것을 알 수 있다.
먼저, 첫번째 뉴스기사의 제목, URL, 날짜 및 작성자 정보를 가져와보자.
print(news_list[0]) news_title = news_list[0].select('span.news_txt')[0].text print(news_title) news_url = news_list[0].find('a').get('href') news_url = 'http://boannews.com' + news_url print(news_url) news_writer = news_list[0].select('span.news_writer')[0].text print(news_writer)
뉴스 리스트의 첫번째 기사의 내용을 확인해보면, 제목은 클래스가 news_txt인 span 태그에 있고, URL은 a 태그의 href 속성에 있고, 날짜 및 작성자는 클래스가 news_writer인 span 태그에 있는 것을 알 수 있다.
<div class="news_list"> <a href="/media/view.asp?idx=124649&page=1&kind=3"> <img alt="" class="news_img" height="70" src="/media/upFiles2/2023/12/20231212spain-s.jpg"/> <span class="news_txt">보수적인 스페인, 첨단기술이 보안산업 트렌드를 바꿨다</span> </a><br/> <a class="news_content" href="/media/view.asp?idx=124649&page=1&kind=3">스페인은 치안이 우수한 편에 속하는 나라로 소매치기를 비롯한 절도・강도 피해가 타 서유럽 국가에 비해 낮은 편이다. 특히, 스페인에서는 절도범...</a> <span class="news_writer">엄호식 기자 | 2023년 12월 12일 13:48</span> </div>
위 코드를 실행하면 아래와 같이 제목, URL, 날짜 및 작성자 정보를 확인할 수 있다. URL의 경우 완전한 URL이 아니기 때문에 접속이 가능하도록 URL 앞에 'http://boannews.com'을 추가했다.
보수적인 스페인, 첨단기술이 보안산업 트렌드를 바꿨다 http://boannews.com/media/view.asp?idx=124649&page=1&kind=3 엄호식 기자 | 2023년 12월 12일 13:48
이제 반복문을 이용해서 30개의 뉴스기사에 대해 제목, URL, 날짜 및 작성자 정보를 추출해보자. 먼저 데이터를 csv로 저장하기 위해 pandas 라이브러리를 이용해서 dataframe을 만든다.
data = pd.DataFrame(columns=['title', 'url', 'writer'])
뉴스 리스트의 모든 기사에 대해 정보를 추출해서 데이터프레임에 저장해보자.
for news in tqdm(news_list): news_title = news.select('span.news_txt')[0].text news_url = news.find('a').get('href') news_url = 'http://boannews.com' + news_url news_writer = news.select('span.news_writer')[0].text new_data = [news_title, news_url, news_writer] data.loc[len(data)] = new_data data
데이터를 확인해보면 아래 그림처럼 30개의 기사에 대한 정보가 잘 수집된 것을 알 수 있다.
수집한 데이터를 아래 코드를 통해 csv로 저장할 수 있다.
data.to_csv('boannews.csv', encoding='utf-8-sig', index=False)
파일명은 boannews.csv로 했고, 인코딩 타입은 주로 사용되는 utf-8로 설정했다. index의 경우 위 그림에서 보는 것처럼 각 데이터 행마다 번호를 추가하려면 True로 설정하고, 필요 없다면 False로 설정하면 된다.
한글의 경우 utf-8-sig로 설정하지 않으면 엑셀에서 확인할 때 깨져서 출력되므로 한글이 포함되어 있다면 반드시 utf-8이 아닌 utf-8-sig로 설정해야한다.
엑셀로 열어서 확인해보면 잘 저장된 것을 확인할 수 있다.
전체 소스코드
from bs4 import BeautifulSoup import requests import pandas as pd from tqdm.auto import tqdm import lxml URL = 'http://boannews.com/media/t_list.asp?Page=1&kind=' site = requests.get(URL) print(site) print(site.text) print(type(site.text)) site_soup = BeautifulSoup(site.text, 'lxml') print(site_soup) print(type(site_soup)) news_list = site_soup.select('div.news_list') print(news_list) print(len(news_list)) print(news_list[0]) news_title = news_list[0].select('span.news_txt')[0].text print(news_title) news_url = news_list[0].find('a').get('href') news_url = 'http://boannews.com' + news_url print(news_url) news_writer = news_list[0].select('span.news_writer')[0].text print(news_writer) data = pd.DataFrame(columns=['title', 'url', 'writer']) for news in tqdm(news_list): news_title = news.select('span.news_txt')[0].text news_url = news.find('a').get('href') news_url = 'http://boannews.com' + news_url news_writer = news.select('span.news_writer')[0].text new_data = [news_title, news_url, news_writer] data.loc[len(data)] = new_data data.to_csv('boannews.csv', encoding='utf-8-sig', index=False)
추가
이번 글에서는 1 페이지의 뉴스 리스트를 수집했지만, 페이지 정보를 이용해서 여러 페이지 수집하는 것도 가능할 것이다. 뿐만 아니라 각 뉴스 기사의 URL을 알고 있기 때문에 위와 같은 방식으로 뉴스 기사의 전체 컨텐츠 내용도 크롤링할 수 있을 것이다.
반응형'IT' 카테고리의 다른 글
파이썬 장고(Django) 템플릿(template) 상속하는 방법 (0) 2023.12.07 Python 가상환경 구축하기(명령 프롬프트, VSCode) (2) 2023.12.07 서버 접속을 위한 IPTIME 공유기 포트포워딩 설정하기 (0) 2023.11.15