[오늘의 문제] 웹 크롤링 확장

shlim55·2025년 11월 12일

코딩테스트

목록 보기
173/223

[크롤링 미션]

모든 페이지에 있는 데이터를 크롤링하여 딕셔너리를 원소로 갖는 리스트 형태로 결과를 출력하세요.

특히 'Win %'와 '+ / -' 의 데이터의 경우 초록색 글씨는 괄호에 success를 붙여서, 빨간색 글씨는 danger를 붙인 결과로 만드세요.
ex)
win : 0.55(success)
win : 0.312(danger)

  • / - : 35(success)
  • / - : -38(danger)

{
Team Name : ...,
Year : ...,
Wins : ...,
Losses : ...,
OT Losses : ...,
Win % : ...,
Goals For(GF) : ...,
Goals Against(GA) : ...,

  • / - : ...
    }

내가 작성한 코드문

import requests # 웹 페이지 요청을 위해
from bs4 import BeautifulSoup # 가져온 문서에서 데이터 추출을 위해

data = []

for page in range(1, 26):
    url = f"https://www.scrapethissite.com/pages/forms/?page_num={page}"
    res = requests.get(url)
    bs = BeautifulSoup(res.text) 
# 모든 페이지에 있는 데이터를 크롤링하여 딕셔너리를 원소로 갖는 리스트 형태로 결과를 출력하세요.
    # print("page: ", page)
    
# 특히 'Win %'와 '+ / -' 의 데이터의 경우 초록색 글씨는 괄호에 success를 붙여서, 빨간색 글씨는 danger를 붙인 결과로 만드세요.

    # 방식은 자유롭게 beautifulsoup selenium 둘다 써도 되고 전자만 써도 됨
    entire = bs.select(".table > .team")
    # print(len(entire))
    # print(entire)

    for team in entire:
        team_name = team.select_one("td.name").get_text(strip=True)
        # print(f"팀 이름: {team_name}")
        year = team.select_one("td.year").get_text(strip=True)
        # print(f"팀 년도: {year}")
        wins = team.select_one("td.wins").get_text(strip=True)
        # print(f"Wins: {wins}")
        losses = team.select_one("td.losses").get_text(strip=True)
        # print(f"Losses: {losses}")
        win_percent = team.select_one("td.pct").get_text(strip=True)
        win_color = team.select_one("td.pct")["class"][1][5:] # class 속성 값중 1인덱스 내에 5인덱스부터 추출 
        win_percent = f"{win_percent}({win_color})"
        # print(f"Win%: {win_percent}")

        gf = team.select_one("td.gf").get_text(strip=True)
        # print(f"Goals For: {gf}")
        
        ga = team.select_one("td.ga").get_text(strip=True)
        # print(f"GA: {ga}")

        plus_minus = team.select_one("td.diff").get_text(strip=True)
        plus_color = team.select_one("td.diff")["class"][1][5:]
        plus_minus = f"{plus_minus}({plus_color})"

        # print(f"+-: {plus_minus}")
        # print("========")
        data.append({
            "TeamName": team_name,
            "Year": year,
            "Wins":wins,
            "Losses": losses,
            "WinPercent":win_percent,
            "WinColor": win_color,
            "GF": gf,
            "GA":ga,
            "plus_minus":plus_minus
        })

print("\n=== 전체 크롤링 결과 (총 {}개 데이터) ===".format(len(data)))
for i in data:
    print(i)

[크롤링 미션2]
특정 장르에 해당하는 책 구매 목록 페이지의 모든 페이지의 데이터를 크롤링하세요.
[필요한 정보]
책 사진 URL
별점 개수
제목
상세 페이지 URL
가격

[데이터 예시]

{
  "genre" : "장르",
  "items" : [
    {
      "page" : 1,
      "data" : [
        {
          "title" : "제목",
          "price" : "가격",
          "stars" : "별점 개수",
          "img_url" : "사진 URL",
          "detail_url" : "상세 페이지 URL"
        },
        {
         }
      ]
    },
    {
      "page" : 2,
      "data" : [
        {
          "title" : "제목",
          "price" : "가격",
          "stars" : "별점 개수",
          "img_url" : "사진 URL",
          "detail_url" : "상세 페이지 URL"
         },
          {
          }
        ]
     },
  ]
}

내가 작성한 코드문

import requests # 웹 페이지 요청을 위해
from bs4 import BeautifulSoup # 가져온 문서에서 데이터 추출을 위해

genre = []
items = []
data = []


# 장르나 페이지를 미리 추출해야 함 

url = "https://books.toscrape.com/index.html"
res = requests.get(url)
bs = BeautifulSoup(res.text)

entire = bs.select("section ul.pager")
genre_list = bs.select("ul.nav li ul li")

# print("genre_list", genre_list)

for li_tag in genre_list:
    
    # 2. 각 li_tag 내부에서 <a> 태그를 찾습니다.
    #    구조: <li> <a> ... </a> </li>
    category_link = li_tag.select_one('a') 
    
    # 3. <a> 태그를 찾았는지 확인 후 텍스트를 추출합니다.
    if category_link:
        # get_text(strip=True)를 사용하여 <a> 태그 내부의 텍스트만 추출하고, 
        # 앞뒤의 모든 공백(줄 바꿈, 탭, 공백)을 제거합니다.
        category_name = category_link.get_text(strip=True)
        
        # 4. 추출된 이름을 리스트에 추가합니다.
        genre.append(category_name)
    else:
        # <a> 태그가 없는 li 항목에 대한 예외 처리
        genre.append("Category Link Not Found")

# print("genre: ", genre)


page_ul = entire[0]

# 2. <li class="current"> 요소를 선택합니다.

element = page_ul.select_one("li.current")# 태그코드 문이 나옴

# 3. 요소 내부의 텍스트를 가져옵니다. (결과: 'Page 1 of 50')
text = element.get_text(strip=True) # 꺽새 사이의 값
# 4. 문자열 가공: 공백으로 분리하고, 'of'로 분리하여 '50'을 얻습니다.
parts = text.split()
# 5. 분리된 리스트에서 '50' (4번째 요소, 인덱스 3)를 선택합니다.
total_pages = int(parts[3])
# print(f"전체 페이지 수: {total_pages}")

for page in range(1, total_pages):
    url = f"https://books.toscrape.com/catalogue/page-{page}.html"
    res = requests.get(url) # 해당 URL에 해당 요청을 하고, 응답을 받는다 
    bs = BeautifulSoup(res.text)# 응답으로 받은 결과 중에서 HTML 문서 텍스트를 가져와서 이것을 bs 객체로 생성
     # 크롤링 작업을 bs 객체를 통해 수행 

    entire = bs.select("ol.row > li.col-xs-6.col-sm-4.col-md-3.col-lg-3")
    # print(entire)
    for book in entire:
        article = book.select_one("article.product_pod")
        img_url = article.select_one("div.image_container img.thumbnail")["src"]# 책 사진 URL
        # print(f"img_url: {img_url}")

        star = book.select_one("p.star-rating")["class"][1]# 별점 개수
        if star == "One":
            star = 1
        elif star == "two":
            star = 2
        elif star == "three":
            star = 3
        elif star == "Four":
            star = 4
        elif star == "Five":
            star = 5
        # print(f"star: {star}")

        title = book.select_one("h3 > a")["title"]# 제목
        # print(f"title: {title}")

        detail_url = article.select_one("div.image_container > a")["href"]
        # print(f"url: {detail_url}")# 상세 페이지 URL

        product_price = book.select_one("div.product_price")
        price = product_price.select_one("p.price_color").get_text(strip=True)# 가격
        # print(f"price: {price}")

        
        items.append({
            "page":page
            })

        data.append({
            "img_url": img_url,
            "star": star,
            "title": title,
            "url": detail_url,
            "price": price
        })
        
        items.append({"data": data})
        genre.append({"items":items})


print("\n=== 전체 크롤링 결과 (총 {}개 데이터) ===".format(len(data)))

for data in items:
    print(data)
    print()

트러블 슈팅 

중첩된 딕셔너리 구조를 가지고 있다. 현재 코드에서 genre, items, data 리스트를 잘못된 위치에서 append하고 중첩시키는 오류가 있어 최종 결과가 원하는 구조가 되지 못한다.

주요 수정 사항

JSON 구조 잡기: 모든 데이터를 담을 최종 딕셔너리 final_data를 선언.

items 리스트 위치 수정: items 리스트는 페이지 루프 밖에서 선언하고, data 리스트는 페이지 루프 안에서 매 페이지마다 초기화.

데이터 누적 방식 수정: data 리스트에 책 데이터를 모은 후, items에 { "page": n, "data": data_list } 형태로 한 번만 추가.

genre 처리 방식 수정: 이 코드는 **특정 카테고리(General)**의 모든 페이지를 크롤링하고 있으므로, genre는 하나의 텍스트 값으로 설정해야 한다. (이 경우에는 메인 페이지를 크롤링하므로, 첫 번째 카테고리를 대표 장르로 설정했다.)

제미나이 코드
import requests
from bs4 import BeautifulSoup
import json # 최종 결과를 JSON 형식으로 출력하기 위해 추가

# 별점 클래스 이름과 숫자 매핑을 위한 딕셔너리
STAR_MAPPING = {
    "One": 1,
    "Two": 2, # 'two' 대신 'Two'로 수정 (사이트 클래스명 확인)
    "Three": 3,
    "Four": 4,
    "Five": 5
}

# 1. 최종 결과 구조를 위한 변수 초기화
final_data = {}
all_pages_data = [] # 요청하신 'items' 리스트에 해당

# ----------------- 초기 메인 페이지 정보 추출 -----------------
url = "https://books.toscrape.com/index.html"
res = requests.get(url)
res.raise_for_status() # 요청 실패 시 에러 발생
bs = BeautifulSoup(res.text, 'html.parser')

# (선택) 장르 이름 추출 (현재는 General 페이지 크롤링이므로 첫 번째 장르 이름을 사용)
# 실제로는 크롤링하는 카테고리의 이름을 사용해야 함.
genre_list_elements = bs.select("ul.nav li ul li")
# 전체 장르 리스트의 첫 번째 항목의 텍스트를 장르 이름으로 설정
if genre_list_elements:
    genre_name = genre_list_elements[0].select_one('a').get_text(strip=True)
else:
    genre_name = "Books (General)"

# 전체 페이지 수 추출 로직 (기존 코드와 동일)
pager_ul = bs.select_one("section ul.pager") # entire 변수 대신 직접 select_one 사용
if pager_ul:
    text = pager_ul.select_one("li.current").get_text(strip=True)
    parts = text.split()
    total_pages = int(parts[3])
else:
    # 페이지네이션이 없는 경우를 대비하여 최소 1페이지로 설정
    total_pages = 1

print(f"장르: {genre_name}, 총 페이지 수: {total_pages}")
# -----------------------------------------------------------

# ----------------- 페이지별 책 데이터 크롤링 -----------------
for page in range(1, total_pages + 1): # 마지막 페이지까지 포함하도록 +1 수정
    # URL은 메인 페이지가 1페이지이므로, 2페이지부터는 catalogue/page-n.html 형식
    if page == 1:
        page_url = "https://books.toscrape.com/index.html"
    else:
        page_url = f"https://books.toscrape.com/catalogue/page-{page}.html"
    
    print(f"➡️ 크롤링 중: Page {page} ({page_url})")

    res = requests.get(page_url)
    res.raise_for_status()
    bs = BeautifulSoup(res.text, 'html.parser')

    # 현재 페이지의 모든 책 목록을 선택
    book_list = bs.select("ol.row > li.col-xs-6") 
    
    # ❗️ 페이지별 데이터를 담을 리스트 (요청하신 "data" 리스트)를 매 페이지마다 초기화
    current_page_data = [] 

    for book in book_list:
        article = book.select_one("article.product_pod")

        # 책 사진 URL
        img_tag = article.select_one("div.image_container img.thumbnail")
        # 상대 경로(e.g., ../../media/...)를 절대 경로로 변환하는 로직 추가
        img_url = requests.compat.urljoin(page_url, img_tag["src"]) if img_tag else "N/A"

        # 별점 개수
        star_class = book.select_one("p.star-rating")["class"][1]
        stars = STAR_MAPPING.get(star_class, star_class) # 매핑 딕셔너리 사용

        # 제목
        title = book.select_one("h3 > a")["title"]

        # 상세 페이지 URL
        detail_link = article.select_one("div.image_container > a")
        detail_url = requests.compat.urljoin(page_url, detail_link["href"]) if detail_link else "N/A"

        # 가격
        price = book.select_one("p.price_color").get_text(strip=True)

        # ❗️ 현재 페이지의 data 리스트에 책 정보 추가
        current_page_data.append({
            "title": title,
            "price": price,
            "stars": stars,
            "img_url": img_url,
            "detail_url": detail_url
        })
    
    # ❗️ 요청하신 'items' 리스트에 페이지별 데이터 구조를 추가
    all_pages_data.append({
        "page": page,
        "data": current_page_data
    })

# ----------------- 최종 JSON 구조 완성 -----------------
final_data = {
    "genre": genre_name,
    "items": all_pages_data
}

# ----------------- 최종 출력 -----------------
print("\n=== 최종 JSON 구조 결과 미리보기 (일부만 출력) ===")

# 전체 데이터를 JSON 형식으로 깔끔하게 출력
print(json.dumps(final_data, indent=2, ensure_ascii=False))

print(f"\n✅ 크롤링 완료: 총 {len(all_pages_data)} 페이지, {len(book_list) * len(all_pages_data)}권 이상의 책 데이터가 추출되었습니다.")
다른 사람의 풀이
import requests
from bs4 import BeautifulSoup
import time

data = []

# 페이지 개수를 구하기 위한 최초 페이지 접속
url = "https://www.scrapethissite.com/pages/forms/"
res = requests.get(url)
soup = BeautifulSoup(res.text)

# 하단의 영역에 있는 페이지 번호의 영역을 가져옴
footer_section = soup.select_one("div.row.pagination-area").select_one("div.col-md-10")
page_lst = footer_section.select("ul.pagination li") # li의 개수 == 페이지 개수
page_count = len(page_lst)

# 페이지 개수만큼 페이지 번호로 요청
for page in range(1, page_count):
    url = f"https://www.scrapethissite.com/pages/forms/?page_num={page}"
    res = requests.get(url)
    soup = BeautifulSoup(res.text)
    
    print(f"==============={page}번째 페이지 크롤링 중입니다==========")
    tb = soup.select("table.table tr.team") 
    # 한개의 Row, 그리고 그것을 여러개를 받음(여러 개의 행)
    for row in tb: # 각 행을 순회
        name = row.select_one("td.name").get_text(strip=True)
        year = row.select_one("td.year").get_text(strip=True)
        wins = row.select_one("td.wins").get_text(strip=True)
        losses = row.select_one("td.losses").get_text(strip=True)
        ot = row.select_one("td.ot-losses").get_text(strip=True)
        # Win % 요소
        pct = row.select_one("td.pct")
        pct_text = pct.get_text(strip=True)
        pct_class = pct["class"][1].split("-")[1] 
        # pct 요소의 클래스 추출(리스트 형태) 그 중 1번째 인덱스의 값이 text-{} 형태
        gf = row.select_one("td.gf").get_text(strip=True)
        ga = row.select_one("td.ga").get_text(strip=True)

        # +/- 요소
        diff = row.select_one("td.diff")
        diff_text = diff.get_text(strip=True)
        diff_class = diff["class"][1].split("-")[1]

        data.append({
            "Team Name" : name,
            "Year" : year,
            "Wins" : wins,
            "Losses" : losses,
            "OT Losses" : ot,
            "Win %" : f"{pct_text}({pct_class})",
            "Goals For(GF)" : gf,
            "Goals Against(GA)" : ga,
            "+/-" : f"{diff_text}({diff_class})"
        })
    # 각 페이지마다 대기 시간 추가 (중요!)
    time.sleep(1)

print(len(data)) # 데이터의 총 개수
print(data) # 데이터 출력
profile
A Normal Programmer

0개의 댓글