[python] 웹 크롤링(beautifulsoup)

728x90

1. 기본 설치 파일 및 코드 

먼저 requests 와 beautifulsoup를 설치합니다.

 

pip install requests
pip install beautifulsoup4

 

naver.py로 파이썬 파일을 만들어 아래 코드를 입력합니다.

import requests
from bs4 import BeautifulSoup

url = "https://www.naver.com/"

req = requests.get(url)

html = req.text

soup = BeautifulSoup(html,"html.parser") # html을 html_parser로 분석한다

 

2.사람이 접속한 것처럼 보이게 하기

 

접속한 사이트에 접속하여 개발자 도구를 실행합니다(예시로는 네이버를 사용하지만 다른 사이트라도 상관없습니다.)

 

개발자 도구의 네트워크 탭을 클릭한 후 F5를 눌러 새로고침을 합니다.

 

그리고 가장 위의 url 주소를 클릭하면 아래와 같은 화면이 나타납니다.

 

위의 빨간색 네모박스의 User-Agent를 복사합니다.

 

requests의 여러 기능들 중 headers를 찾는 기능도 있습니다.

print(req.request.headers)

파이썬에서 위 코드를 작성하고 실행하면 아래와 같이 나올 것입니다.

 

즉 아무런 설정없이 get(url)을 보내게 되면 파이썬으로 실행했다는 정보를 보낸다는 것입니다.

 

따라서 아래 코드를 추가합니다.

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

그리고 req 변수를 변경합니다.

req = requests.get(url,headers=headers)

 

실행을 시켜보면 아래와 같이 변경된 것을 확인할 수 있습니다.

 

 

3.원하는 경로를 입력하여 URL 이동하기

먼저 네이버에서 검색을 합니다 저는 뉴진스를 검색했습니다.

 

 

주소창을 보시면 query 뒤에 검색 내용이 입력되는 것을 확인할 수 있습니다. 

 

url을 전부 복사합니다.

 

파이썬 코드에서 url을 변경합니다

url = "https://search.naver.com/search.naver?where=view&sm=tab_jum&query=%EB%89%B4%EC%A7%84%EC%8A%A4"

 

그대로 복사해서 붙여넣었지만 한글부분이 위와 같이 알아볼수 없는 언어로 적힐 것입니다.

 

그러나 걱정하실것없이 해당 내용을 지우고 코드를 수정하겠습니다.

 

url을 base_url로 변경하고 입력란을 추가하겠습니다. 즉 아래와 같이 코드를 수정하겠습니다.

 

base_url = "https://search.naver.com/search.naver?where=view&sm=tab_jum&query="

keyword = input("검색어를 입력하세요: ")

url = base_url+keyword

print(url)

 

해당 내용을 실행하겠습니다. 손흥민을 검색하니 아래와 같이 나타나는 것을 볼 수 있습니다.

url을 전부 복사해서 크롬창의 url에 붙여넣기하면 view탭의 검색내용은 손흥민으로 가는 것을 볼 수 있습니다.

 

VSCODE에서는 하이퍼링크가 바로 생성된다고 들었는데 파이참에서는 검색내용을 포함한 하이퍼링크가 생성되지는 않았습니다.

 

4.정보 가져오기

 

네이버의 view 탭에서 정보를 가져와보겠습니다

 

뉴진스를 검색해서 개발자 코드를 열어보시면 제목부분에 아래와 같은 클래스로 되어있는 것을 볼 수 있습니다.

 

 

여러내용을 검색해서 공통된 부분을 찾는 것이 좋습니다.

 

해당 클래스를 전부 찾는 코드를 추가합니다.

html = req.text

soup = BeautifulSoup(html,"html.parser") # html을 html_parser로 분석한다

result = soup.select(".api_txt_lines.total_tit")
#selectone이 아닌 select는 해당 클래스를 전부 가져온다. select내부는 클래스명이며 띄어쓰기가 되어있으면 .으로 대체해준다.

print(result)

 

코드를 작성했으면 실행하겠습니다. 

 

 

 

view 제목들을 가져오는 것을 확인할 수 있습니다.

 

해당 코드를 반복문으로 출력해보겠습니다. result 코드를 복수형으로 변환하고 for반복문을 추가합니다.

 

results = soup.select(".api_txt_lines.total_tit")
#selectone이 아닌 select는 해당 클래스를 전부 가져온다. select내부는 클래스명이며 띄어쓰기가 되어있으면 .으로 대체해준다.

for result in results:
    print(result)
    print() #알아보기 쉽게 빈줄 추가

아래와 같이 출력되는 것을 확인할 수 있습니다.

 

태그없이 내용만 출력하고 싶다면 result변수에 .text를 추가하면됩니다.

 

for result in results:
    print(result.text)
    print() #알아보기 쉽게 빈줄 추가

 

결과는 아래와 같이 출력됩니다.

 

해당 내용을 접속할 url을 찾고 싶다면 아래와 같이 코드를 추가하면 됩니다.

 

for result in results:
    print(result['href'])
    print(result.text)
    print() #알아보기 쉽게 빈줄 추가

또는

for result in results:
    print(result.get('href'))
    print(result.text)
    print() #알아보기 쉽게 빈줄 추가

 

아래와 같이 접속할 수 있는 경로가 나타납니다.

 

 

5. 여러 정보를 한번에 가져오기

1) zip 사용

이번엔 제목과 작성자를 가져오겠습니다. 그리고 아래와 같이 코드를 수정합니다.

 

titles = soup.select(".api_txt_lines.total_tit")
#selectone이 아닌 select는 해당 클래스를 전부 가져온다. select내부는 클래스명이며 띄어쓰기가 되어있으면 .으로 대체해준다.

names = soup.select(".sub_txt.sub_name")

for result in zip(names,titles): #zip 함수는 여러변수를 묶어서 튜플로 만들어준다.
    print(result[0].text) # names 정보
    print(result[1].text) # titles 정보
    print() #알아보기 쉽게 빈줄 추가

 

출력하면 아래와 같은 결과가 나타납니다.

 

다만 주의해야 할점은 zip함수를 사용했을 때 작성자나 제목이 없는 list가 있다면 해당 수만큼 밀리는 경우가 발생합니다.

 

2) 공통된 상위태그를 사용

먼저 아이유를 검색하여 개발자도구를 열어보겠습니다.

 

 

이번에는 박지성을 검색하여 개발자 도구를 열어보겠습니다

 

아이유는 timeline_area 이지만 박지성은 total_area로 나오는 것을 볼 수 있습니다. 물론 박지성을 검색해도 timeline_area로 나올때도 있습니다.

 

저 두개의 상황이 나온다고 가정했을 때 코드를 수정하겠습니다.

 

total_area = soup.select(".total_area")
timeline_area = soup.select(".timeline_area")

if total_area:
    areas = total_area
elif timeline_area:
    areas = timeline_area
else:
    print("확인요망")

for area in areas:
    title = area.select_one(".api_txt_lines.total_tit") # select_one은 하나만 가져오는 것
    name = area.select_one(".sub_txt.sub_name")
    print(name.text)
    print(title.text)
    print()

위의 두 상황외의 상황이 존재한다면 '확인요망'이라는 글자가 나타날 것이므로 케이스를 추가해주면 됩니다.

 

6.광고 제거하기

광고 요소의 클래스를 찾아서  반복문 부분을 아래처럼 수정합니다.

 

for area in areas:
    ad = area.select_one(".link_ad") #광고요소의 클래스
    if ad:
        print("광고")
        continue
    title = area.select_one(".api_txt_lines.total_tit") # select_one은 하나만 가져오는 것
    name = area.select_one(".sub_txt.sub_name")
    print(name.text)
    print(title.text)
    print()

 

7.동적타입에서의 beatifulsoup

 beatifulsoup애서는 검색 당시의 정보를 가져오기때문에 네이버 처럼 스크롤시 정보가 더 생성되는 동적타입에서는 모든 정보를 가져올 수 없습니다.

 

이는 태그의 개수로 확인해 볼 수 있습니다.

처음 검색했을 때 total_area태그의 개수를 확인해보겠습니다.

 

빨간색 네모상자를 보시면 총 30개의 태그가 있습니다.

 

그러면 스크롤을 내린 후 다시 확인해보겠습니다.

 

 

이번엔 60개로 늘어난 것을 확인할 수 있습니다.

 

그러면 코드상에서는 몇개가 출력되는지 확인해보겠습니다.

print(len(areas))

 

 

30개가 출력되는 것을 확인할 수 있습니다.

 

즉 동적태그는 beautifulsoup로 크롤링 할 수 없기 때문에 셀레니움을 사용해야합니다.

 

8. 사용예제

자세한 사용방법은 주석을 확인하시면 됩니다. 현재 네이버에서는 BeautifulSoup만으론 크롤링이 안되고 셀레니움을 병행해야 합니다.

 

1) 네이버 검색

import requests
from bs4 import BeautifulSoup

url = "https://www.naver.com/"

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

req = requests.get(url,headers=headers)

html = req.text

soup = BeautifulSoup(html,"html.parser")

print(soup.title) # 타이틀 태그를 찾는다.
print(soup.h1) # h1 태그를 찾는다.
print()

h1 = soup.find('h1') # h1태그를 찾아 h1변수에 저장한다.
print("h1: "+str(h1)) #문자열을 더했을 때는 str로 감싸준다.
print()

h1 = soup.select_one('h1') # h1태그를 찾아 h1변수에 저장한다.
print(h1)
print()

h2 = soup.find(class_='search_logo') # search_logo라는 클래스를 찾아 h2변수에 저장한다.

print("h2: "+str(h2))
print()

h3 = soup.find(id='special-input-logo') # id가 special-input-logo인 값을 찾아 h3에 저장한다.
print("h3: "+str(h3))
print()

service_name = soup.find(class_='service_name',string='증권') #class가 link_service이면서 문자열은 증권인 값을 가져온다.
#현재 네이버에서는 사용이 안됨 -> 셀레니움과 혼용하면 사용가능

service_name_a = soup.find('a',string='증권') # a태그이면서 문자열은 증권인 값을 가져온다.
#현재 네이버에서는 사용이 안됨 -> 셀레니움과 혼용하면 사용가능
print(service_name_a)

service_name_all = soup.find_all(class_='service_name') # service_name란 클래스를 가진 모든 값을 가져온다.
print(service_name_all)
print(len(service_name_all))

for service in service_name_all:
    print(service.text)
    print(service['href'])
    print()

 

 

2) melon 차트

import requests
from bs4 import BeautifulSoup

url = "https://www.melon.com/chart/index.htm"


# "javascript:melon.link.goAlbumDetail('11286070');"

# a태그의 자바스크립트 안의 숫자를 가져오는 함수
def get_song_nums(song_num_text):
    # song_num=[]
    # for num in song_num_text:
    #     if num.isdigit(): # num이 숫자인지 판단
    #         song_num.append(num)
    # song_num = "".join(song_num) # 리스트를 결합
    #

    song_num = "".join([num for num in song_num_text if num.isdigit()]) # 리스트 컴프리헨션 -> 위 주석내용을 한줄로

    return song_num

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

req = requests.get(url,headers=headers)

html = req.text

soup = BeautifulSoup(html,"html.parser")

##### 방법1 #######
# lst50 = soup.select(".lst50")
# 
# lst100 =soup.select(".lst100")
# 
# lst = lst50+lst100

##### 방법2 #######
# lst = soup.select(".lst50, .lst100") # 위 주석처리된 내용은 이와같이 처리 가능


##### 방법3 #######
lst = soup.find_all(class_=["lst50","lst100"])

for rank,i in enumerate(lst,1):
    title = i.select_one(".ellipsis.rank01 a") # ellipsis rank01클래스 아래의 a태그를 찾는다.
    singer = i.select_one(".ellipsis.rank02 > a") # ellipsis rank02 클래스 바로 아래의 a태그를 찾는다.
    singer_link = get_song_nums(singer['href']) # singer에서 href 속성을 가져온다.

    album = i.select_one(".ellipsis.rank03 > a")
    album_link = get_song_nums(album['href'])

    print(f"{rank} : {title.text}")
    print(f"{singer.text} : https://www.melon.com/artist/timeline.htm?artistId={singer_link}")
    print(f"{album.text} : https://www.melon.com/album/detail.htm?albumId={album_link}")

 

3) 다음뉴스

import requests
from bs4 import BeautifulSoup

url = "https://news.daum.net/"

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

req = requests.get(url,headers=headers)

html = req.text

soup = BeautifulSoup(html,"html.parser")

item_issues = soup.select(".item_issue")

for item in item_issues:
    # 언론사
    #### 방법1 ###
    # press = item.select(".thumb_g")[1]["alt"]
    #### 방법2 ###
    press = item.select_one(".logo_cp img")["alt"] # 
    category = item.select_one(".txt_category").text

    link_txt = item.select_one(".link_txt")
    link = link_txt['href']
    txt = link_txt.text.strip() # 맨앞과 맨끝의 공백제거

    print(f"{press} - {category}")
    print(f"{txt} : {link}")

 

4) ssg.com 이벤트 정보 가져오기

import requests
from bs4 import BeautifulSoup

url = "https://www.ssg.com/event/eventMain.ssg"

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

req = requests.get(url,headers=headers)

html = req.text

soup = BeautifulSoup(html,"html.parser")

li = soup.select_one(".evt_osmu_lst") #해당 클래스에서 여러건이 있을 때 select_one을 하면 가장 첫번째 값만 가져온다.

units = li.select(".eo_link")

for unit in units:
    link = unit["href"]
    if link.startswith("https"):
        print(link)
    else:
        print(f"https://event.ssg.com{link}")

    eo_in = unit.select_one(".eo_in")

    text_list = eo_in.find_all(string=True) # string=True 는 string이 있는 것을 가져오라는 뜻

    for text in text_list:
        if text != "\n":
            print(text)
    print()

 

5) cgv 정보 가져오기

import requests
from bs4 import BeautifulSoup


url = "http://www.cgv.co.kr/movies/"

headers ={
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
}

req = requests.get(url,headers=headers)

html = req.text

soup = BeautifulSoup(html,"html.parser")

sect_movie_chart= soup.select_one(".sect-movie-chart")

movie_chart = sect_movie_chart.select("li")

for rank,movie in enumerate(movie_chart,1):
    title = movie.select_one(".title")
    score = movie.select_one(".score")
    ticketing = score.select_one(".percent")
    egg_gage = score.select_one(".egg-gage.small > .percent")
    info = movie.select_one(".txt-info > strong").next_element # txt-info 안에 있는 strong 태그에서 텍스트인지, 태그인지 상관없이 바로 다음 요소만 가져온다. 예) <strong>문자열1<span>문자열2</span></strong> -> 문자열1만 출력

    print(f"<<<{rank}>>>")
    print(title.text)
    print(ticketing.get_text(" : ")) # 텍스트로 가져온 문자열이 포함되어 있는 태그가 있다면 둘 사이를 연겷해줄 수 있다. 예) <td>문자열1<span>문자열2</span></td> 일때 문자열1 : 문자열2
    print(egg_gage.text)
    print(f"{info.strip()} 개봉")
    print()

 

6)쿠팡 정보 가져오기

쿠팡은 크롤링을 자주하면 접속이 막히게 되므로 많이 하지 않는 것이 좋습니다.

import requests
from bs4 import BeautifulSoup

base_url = "https://www.coupang.com/np/search?component=&q="

keyword = input("검색어를 입력하세요: ")

url = base_url+keyword


headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
    "accept-language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"
}

cookie = {"a":"b"} # 아무 의미없는 쿠키를 넣는다.

req = requests.get(url,timeout=5,headers=headers,cookies=cookie) # tiemout 은 대기시간 설정하는 것

# print(req.status_code) # 접속 가능한지 상태확인

html = req.text

soup = BeautifulSoup(html,"html.parser")

items = soup.select("[class=search-product]") # class가 search-product인것만 찾는다.

rank =1
for item in items:
   # print(item['class']) # class 속성을 가져온다.
    badge_rocket = item.select_one(".badge.rocket")
    if not  badge_rocket:
        continue

    name = item.select_one(".name")
    price = item.select_one(".price-value")
    thumb = item.select_one(".search-product-wrap-img")
    link = item.a['href'] # item 요소 하위에 있는 a태그의 href 속성을 가져온다.

    print(f"{rank}위")
    print(name.text)
    print(f"{price.text}원")
    print(f"https://coupang.com{link}")
    if thumb.get("data-img-src"):
        img_url = f"http:{thumb.get('data-img-src')}"

    else:
        img_url = f"{thumb.get('src')}"

    print(img_url)
    print()

    img_req = requests.get(img_url)

    with open(f"upload/{rank}.jpg","wb") as f: #upload 폴더안에 1.jpg,2.jpg .. 형태로 저장
        f.write(img_req.content) # 파일 내용 다운로드

    rank +=1

# print(len(items))