개인프로젝트를 진행하다가 사진들이 필요해져서 국내 최대 커뮤니티인 디씨인사이드의 사진을 크롤링한
작업을 공유하고자 합니다.
API response를 분석해 크롤링하는 방법도 있지만 저는 비교적 쉽고 간단한 BeautifulSoup를 통해 html parse로 원하는 정보를 얻었습니다.
이번 게시물에서 크롤링하고자하는 대상의 갤러리는 리그오브레전드 갤러리로 하겠습니다.
이 글에서는 글 목록, 단일 게시물, 사진을 크롤링하고자 합니다.
그럼 우선 리그오브레전드 갤러리로 가서, 글 목록의 html 구조를 확인합니다.
개발자 도구를 통해서 확인해보시면, html 태그 내 유일한 <tbody> 태그로 감싸져 있는 것을 확인하실 수 있습니다.
또한, 내부 글 목록의 게시물 하나하나는 <tr> </tr> 태그로 시작과 끝남을 알 수 있습니다.
따라서 우리는 <tbody> 태그 내의 <tr> 태그 리스트들을 통해서 게시물 정보를 알 수 있겠구나, 짐작할 수 있습니다.
여기까지를 python으로 나타내봅시다.
아래는 이번 목표에 필요한 python library들입니다.
일반적인 상황의 경우, requests와 BeatifulSoup library의 경우는 pip를 통해 설치해주셔야 합니다.
import requests
from urllib import request
from bs4 import BeautifulSoup
import time
우선 request 전반에 사용할 BASE_URL을 설정해줍니다.
디씨인사이드의 모든 갤러리 url은 아래에서 정할 base_url 뒤에 id를 쿼리하는 식으로 이루어집니다.
예시 : gall.dcinside.com/board/lists/?id=leagueoflegends4&page=1
따라서 아래와 같이 bsae_url을 정할 수 있습니다
# 디시인사이드 갤러리 base url
BASE_URL = "https://gall.dcinside.com/board/lists"
그리고 request에 같이 보낼 header를 정합니다.
글 목록, 혹은 게시글 request의 경우는 별거없고 User-agent 하나면 충분합니다
자신의 User-agent는 개발자도구에서 request 보낸 파일들 중 아무거나 열어 Request Header쪽을 보시면
아래와 같이나와있습니다.
이 부분을 복사해서 아래 같이 headers 변수를 선언해주세요.
headers = [
{'User-Agent' : '복사한 User-Agent'},
]
headers가 배열 변수인 이유는, 자신의 request로 보낸 User-Agent 뿐만 아니라 여러 개조한 User-Agent도 사용하여 여러번의 request로 인한 디씨인사이드 측의 차단을 최대한 회피하기 위함입니다.
이제 헤더를 선언했으니 어느 곳으로 request를 보낼 지를 정해야합니다.
디씨인사이드 리그오브레전드 갤러리 첫 페이지의 주소는 아래와 같습니다
gall.dcinside.com/board/lists/?id=leagueoflegends4&page=1
여기서 base_url 뒤의 쿼리쪽을 잘 보시면 id, 그리고 page가 있음을 알 수 있습니다.
두 부분을 request의 파라미터로써 넣어줍니다
params = {'id': 'leagueoflegends4','page':i}
그리고 지금까지 변수로써 선언한 헤더와 파라미터, base_url을 조합하여
python의 request library를 통해 request를 보냅니다
response = requests.get(BASE_URL, params=params, headers=headers[0])
request에 따른 respose는 아마도 리그오브레전드 갤러리 첫 페이지의 모든 tag가 담긴 html 문서가 담겨있을 것입니다.
우린 이것을 BeatifulSoup library로 파싱하여 우리가 필요한 부분만 쏙쏙 꺼내어 활용할 것 입니다.
soup = BeautifulSoup(response.content, 'html.parser')
또한, 위에 말했듯이 실질적인 글 목록 부분은 tbody 태그 내에 담겨있고, 내부에는 tr 태그의 리스트로 글 목록들이 존재합니다.
이를 BeatifulSoup의 메소드들로 가져와보면 아래와 같습니다
#실질적 글 목록 부분
article_list = soup.find('tbody').find_all('tr')
article_list에는 tr 태그로 이루어진 글 목록 들이 배열로써 존재한다고 보시면 됩니다
이제 우리는 이 리스트를 반복문으로 검사하여, 제목과, 게시물로 들어갈 수 있는 url을 얻을 수 있습니다.
그런데 또 하나 중요한 점은, 디씨인사이드에서는 사진이 담긴 글은 옆에 따로 표시가 되어있습니다.
옆의 초록색 아이콘, 그리고 말풍선 아이콘으로 사진의 보유 여부를 알려주고 있습니다.
사진이 있는 게시물의 태그는 <em class="icon_img icon_pic"></em>
사진이 없는 게시물의 태그는 <em class="icon_img icon_text"></em>
우리는 사진을 크롤링하길 원하므로, 사진이 없는 게시물에 request를 보낼 필요가 없습니다.
따라서 우리는 tr 태그로 분석 할 때, 위의 class 차이 여부도 생각해서 사진을 가지고 있는 게시물만 request 보내면 됩니다.
이 부분까지를 코드로서 보여드리겠습니다.
import requests
from urllib import request
from bs4 import BeautifulSoup
import time
BASE_URL = "https://gall.dcinside.com/board/lists"
ARTICLE_BASE_URL = "https://gall.dcinside.com"
# 헤더 설정
headers = [
{'User-Agent' : '자신의 User-Agent'},
]
#몇 페이지부터 몇 페이지까지
for i in range(1, 10):
# 파라미터 설정
params = {'id': 'leagueoflegends4','page':i}
response = requests.get(BASE_URL, params=params, headers=headers[0])
soup = BeautifulSoup(response.content, 'html.parser')
#실질적 글 목록 부분
article_list = soup.find('tbody').find_all('tr')
# 한 페이지에 있는 모든 게시물을 긁어오는 코드
for tr_item in article_list:
# 이미지가 있는 게시물일 경우만 탐색 시작 start
image_flag = tr_item.find('em', class_='icon_img icon_pic')
if image_flag is None:
continue
# 이미지가 있는 게시물일 경우만 탐색 시작 end
print('+'*12)
# 제목 추출
title_tag = tr_item.find('a', href=True)
title = title_tag.text
print("제목: ", title)
print("주소: ", title_tag['href'])
사진이 있는 게시물의 제목과 주소를 얻어왔습니다.
※ 사진이 없는 게시물도 원하신다면, 주석을 잘 보시고 어디를 없애면 될 지 판단하시면 됩니다
※ 글목록에서 게시물의 제목, 링크 외에 글쓴이, 조회수, 추천 수 등의 데이터를 얻고 싶으시다면,
위의 태그에서 tr_item 변수와 BeautifulSoup 함수인 find를 이용하여 자신이 얻길 원하는 부분의 태그를 추적해 보시면 됩니다.
이제 사진이 있는 게시물로 request를 보낼 수 있는 정보(title_tag['href] 변수)도 얻었으니,
게시물로 가서 원하는 정보를 얻을 수 있게 되었습니다
변수 title_tag['href']의 예시는 이렇습니다.
/board/view/?id=leagueoflegends4&no=719020&_rk=Zxh&page=1
http 프로토콜을 제외한 url이 써져있는 것을 보니, 어떤 base_url 뒤에 붙을 주소라 추측이 됩니다.
디씨인사이드 글목록에 있는 한 게시물로 가서 url을 보면 아래와 같습니다
https://gall.dcinside.com/board/view/?id=leagueoflegends4&no=719018&_rk=urT&page=1
https://gall.dcinside.com/ 이게시물의 base url 이군요
게시물 request용 base_url을 만들어줍시다
ARTICLE_BASE_URL = "https://gall.dcinside.com"
이제 그럼 base_url과 아까 그 뒷부분의 정보를 얻었으니, 헤더와 함께 url로 전달해주면 됩니다.
# 이미지가 있는 게시물에 request
article_response = requests.get(ARTICLE_BASE_URL + title_tag['href'], headers=headers[0])
예상되시겠지만 article_response에는 게시물이 html 문서로 존재하며, 우린 이것에서 사진 혹은 글을 가져오면 됩니다.
저는 여기서 사진만 원하므로, 글은 위의 게시물을 가져오는 방식처럼 직접 가져와보시기 바랍니다.
힌트를 드리자면,
# 게시물 부분의 태그
article_contents = article_soup.find('div', class_='writing_view_box').find_all('div')
게시물 정보는 이 내부 div 태그의 리스트로써 존재합니다.
사진을 가져오기 위해선, 글 내부의 사진들을 가져오는 방법도 있겠지만 디시인사이드 게시물의 사진은
글 아래 부분에 직접 다운로드가 가능하도록 되어 있습니다
저 파일명을 누르면 다운로드가 시작됩니다. 즉 저 url로 request를 보내면 사진을 얻을 수가 있다는 것 입니다.
해당 부분의 파일명을 얻기 위해선 저 부분의 태그를 봐야겠죠.
<div class="appending_file_box">
<strong>원본 첨부파일 <em class="font_red">1</em></strong>
<ul class="appending_file">
<li>
<a href="이미지 request 주소">이미지 파일명</a>
</li>
</ul>
</div>
ul 태그 내에 li 태그로 이미지 파일명이 리스트로 존재하는 형태입니다.
이 부분을 통해서 이미지를 얻을 수 있습니다.
여기까지를 전체 태그로 보여드리겠습니다
(최종 모습 입니다)
import requests
from urllib import request
from bs4 import BeautifulSoup
import time
BASE_URL = "https://gall.dcinside.com/board/lists"
ARTICLE_BASE_URL = "https://gall.dcinside.com"
# 헤더 설정
headers = [
{'User-Agent' : '자신의 User-Agent'},
]
#몇 페이지부터 몇 페이지까지
for i in range(1, 10):
# 파라미터 설정
params = {'id': 'leagueoflegends4','page':i}
response = requests.get(BASE_URL, params=params, headers=headers[0])
soup = BeautifulSoup(response.content, 'html.parser')
#실질적 글 목록 부분
article_list = soup.find('tbody').find_all('tr')
# 한 페이지에 있는 모든 게시물을 긁어오는 코드
for tr_item in article_list:
# 이미지가 있는 게시물일 경우만 탐색 시작 start
image_flag = tr_item.find('em', class_='icon_img icon_pic')
if image_flag is None:
continue
# 이미지가 있는 게시물일 경우만 탐색 시작 end
print('+'*12)
# 제목 추출
title_tag = tr_item.find('a', href=True)
title = title_tag.text
print("제목: ", title)
print("주소: ", title_tag['href'])
# 이미지가 있는 게시물에 request
article_response = requests.get(ARTICLE_BASE_URL + title_tag['href'], headers=headers[0])
print("url: ", article_response.url)
article_id = (title_tag['href'].split('no=')[1]).split('&')[0]
print("게시물 ID : ", article_id)
article_soup = BeautifulSoup(article_response.content, 'html.parser')
# 게시물 부분의 태그
article_contents = article_soup.find('div', class_='writing_view_box').find_all('div')
# 아래 이미지 다운로드 받는 곳에서 시작
image_download_contents = article_soup.find('div', class_='appending_file_box').find('ul').find_all('li')
for li in image_download_contents:
img_tag = li.find('a', href=True)
img_url = img_tag['href']
print("url : "+img_url)
file_ext = img_url.split('.')[-1]
#저장될 파일명
savename = "test." + file_ext
opener = request.build_opener()
opener.addheaders = [('User-agent', '자신의 User-Agent'), ('Referer', article_response.url)]
request.install_opener(opener)
request.urlretrieve(img_url, savename)
print("url : "+img_url)
이미지 리퀘스트 보내는 부분인
opener = requset.build_opener() 부터 보시게 되면, 헤더 부분에 Referer을 추가적으로 보내는 모습을 보실 수 있습니다.
디시인사이드 이미지 서버는 request에 담긴 Referer을 검사하여, 다르다면 403을 뱉습니다.
따라서 반드시 필수적으로 넣어주셔야하며, 해당 값은 게시물의 url와 같기 때문에 게시물의 주소인
article_response.url을 변수로써 넣어줍니다.
파일명은 test. 확장자로 저장될테지만 timestamp 등을 이용하여 매번 다른 파일명으로 만들어주시면 됩니다
최대한 활용하여 좋은 서비스 만드세요~ 감사합니다