ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 혼자 공부하는 데이터 분석 CH 02 웹 스크래핑
    Study/Data Analysis 2025. 9. 22. 10:53

     

    목차

     

    들어가며: API, JSON, XML, 그리고 웹 스크래핑으로 데이터 정복하기

     

    1. API와 데이터 형식 (JSON, XML) 다루기

    • 1-1. JSON 데이터 처리
    • 1-2. XML 데이터 처리
    • 1-3. API 호출 및 응답 처리

    2. 웹 스크래핑으로 웹 데이터 가져오기

    • 2-1. 데이터 로드 및 전처리
    • 2-2. 웹 스크래핑 기초 (requests & BeautifulSoup)
    • 2-3. 스크래핑 함수화 및 데이터프레임 병합

    마치며: 데이터 수집과 가공, 그 다음은?

     


     

    들어가며: API, JSON, XML, 그리고 웹 스크래핑으로 데이터 정복하기

    안녕하세요! 데이터 분석의 핵심은 바로 '데이터'를 얻는 것입니다.

    오늘은 웹에 흩어져 있거나 API를 통해 제공되는 다양한 형태의 데이터를 파이썬으로 가져오고 다루는 방법을 깊이 있게 알아보겠습니다. 특히 JSON, XML 같은 데이터 형식과 웹 스크래핑 기술에 초점을 맞출 예정입니다.

     

    본격적인 실습에 앞서, 웹에서 데이터를 가져오는 두 가지 핵심 방법을 먼저 개념적으로 살펴보겠습니다.

    • 방법 1: API 활용 (정식 창구 이용) 

      API는 프로그램끼리 데이터를 주고받기 위한 
      공식적인 규칙입니다. 
      정해진 요청을 보내면, 서버가 깔끔하게 포장된 데이터를 보내줍니다. 
      이때 데이터는 주로 
      JSON이나 XML형태로 오며, requests 라이브러리로 서버에 요청하고 pandas로 응답받은 데이터를 즉시 표(DataFrame)로 만들 수 있습니다.
    • 방법 2: 웹 스크래핑 (직접 정보 수집) 

      공식 창구인 API가 없을 때 사용하는 방법입니다.
       
      웹사이트에 직접 찾아가서 HTML 코드 속에서 
      필요한 부분만 추출하는 기술입니다.
      r
      equests
      로 웹페이지의 HTML 전체를 가져온 뒤, BeautifulSoup이라는 탐색 도구로 원하는 정보를 쉽게 찾아낼 수 있습니다.

     


    1. API와 데이터 형식 (JSON, XML) 다루기

    API(Application Programming Interface)는 프로그램 간의 통신 규칙이며,

    대부분 데이터를 JSON이나 XML형태로 주고받습니다. 이 두 가지 형식은 웹에서 데이터를 교환하는 데 가장 널리 사용됩니다.

    1-1. JSON 데이터 처리

    JSON(JavaScript Object Notation)은 가볍고 읽기 쉬운 데이터 교환 형식입니다.

    파이썬의 딕셔너리와 리스트 구조와 매우 유사하여 다루기 편리합니다.

    기본 JSON 변환

    # 파이썬 딕셔너리 생성
    d = {"name": "혼자 공부하는 데이터 분석"}
    print(d['name']) # 결과: 혼자 공부하는 데이터 분석
    
    # 딕셔너리를 JSON 문자열로 변환
    import json
    d_str = json.dumps(d, ensure_ascii=False) # 한글이 깨지지 않도록 ensure_ascii=False 설정
    print(d_str)      # 결과: {"name": "혼자 공부하는 데이터 분석"}
    print(type(d_str)) # 결과: <class 'str'> (JSON 문자열은 파이썬에서 문자열 타입)
    
    # JSON 문자열을 다시 딕셔너리로 변환
    d2 = json.loads(d_str)
    print(d2['name'])  # 결과: 혼자 공부하는 데이터 분석 (딕셔너리로 복원됨)
    print(type(d2))    # 결과: <class 'dict'>
    

    복잡한 JSON 구조 처리

    JSON은 중첩된 딕셔너리나 리스트(배열)를 포함할 수 있습니다.

    # 문자열 형태의 JSON 파싱
    json_str1 = '{"name": "혼자 공부하는 데이터 분석", "author": "박해선", "year": 2022}'
    d3_1 = json.loads(json_str1)
    print(d3_1['name'])    # 결과: 혼자 공부하는 데이터 분석
    
    # 배열이 포함된 JSON 파싱
    json_str2 = '{"name": "혼자 공부하는 데이터 분석", "author": ["박해선","홍길동"], "year": 2022}'
    d3_2 = json.loads(json_str2)
    print(d3_2['author'][1])  # 결과: 홍길동 (리스트처럼 인덱스로 접근)
    
    # JSON 배열 파싱 (여러 개의 객체가 리스트로 묶인 형태)
    d4_str = """
    [
      {"name": "혼자 공부하는 데이터 분석", "author": "박해선", "year": 2022},
      {"name": "혼자 공부하는 머신러닝+딥러닝", "author": "박해선", "year": 2020}
    ]
    """
    d4 = json.loads(d4_str)
    print(d4[0]['name'])  # 결과: 혼자 공부하는 데이터 분석 (리스트의 첫 번째 요소에서 'name' 키 접근)
    

    판다스와 JSON 연동

    pandas 라이브러리는 JSON 데이터를 DataFrame으로 쉽게 변환할 수 있는 강력한 기능을 제공합니다.

    import pandas as pd
    
    # JSON 문자열(배열 형태)을 직접 데이터프레임으로 변환
    df_from_json_str = pd.read_json(d4_str)
    print(df_from_json_str)
    # 결과:
    #                     name author  year
    # 0       혼자 공부하는 데이터 분석    박해선  2022
    # 1  혼자 공부하는 머신러닝+딥러닝    박해선  2020
    
    # 파이썬 리스트/딕셔너리(d4)를 데이터프레임으로 변환
    df_from_dict = pd.DataFrame(d4)
    print(df_from_dict)
    # 결과:
    #                     name author  year
    # 0       혼자 공부하는 데이터 분석    박해선  2022
    # 1  혼자 공부하는 머신러닝+딥러닝    박해선  2020
    

    1-2. XML 데이터 처리

    XML(Extensible Markup Language)은 HTML과 유사하게 태그를 사용하여 데이터를 구조화하는 형식입니다.

    파이썬의 xml.etree.ElementTree 모듈로 처리할 수 있습니다.

    단일 XML 요소 파싱

    x_str = """
    <book>
        <name>혼자 공부하는 데이터 분석</name>
        <author>박해선</author>
        <year>2022</year>
    </book>
    """
    
    import xml.etree.ElementTree as et
    book = et.fromstring(x_str)  # XML 문자열을 Element 객체로 변환
    print(type(book))   # 결과: <class 'xml.etree.ElementTree.Element'>
    print(book.tag)     # 결과: book (최상위 태그명)
    
    # 자식 요소의 텍스트 추출 (list()로 자식 요소들을 얻은 후 접근)
    book_childs = list(book)
    name_elem, author_elem, year_elem = book_childs
    print(name_elem.text)    # 결과: 혼자 공부하는 데이터 분석
    print(author_elem.text)  # 결과: 박해선
    
    # findtext()로 더 간편하게 특정 태그의 텍스트 추출
    name = book.findtext('name')
    author = book.findtext('author')
    year = book.findtext('year')
    print(name, author, year) # 결과: 혼자 공부하는 데이터 분석 박해선 2022
    

    중첩된 XML 구조 파싱

    findall() 메서드를 사용하면 특정 태그를 가진 모든 자식 요소를 리스트 형태로 찾을 수 있습니다.

    x2_str = """
    <books>
        <book>
            <name>혼자 공부하는 데이터 분석</name>
            <author>박해선</author>
            <year>2022</year>
        </book>
        <book>
            <name>혼자 공부하는 머신러닝+딥러닝</name>
            <author>박해선</author>
            <year>2020</year>
        </book>
    </books>
    """
    
    books_root = et.fromstring(x2_str)
    print(books_root.tag)  # 결과: books
    
    # findall()로 모든 <book> 요소를 찾아 반복 처리
    for book in books_root.findall('book'):
        name = book.findtext('name')
        author = book.findtext('author')
        year = book.findtext('year')
        print(f"책 이름: {name}, 저자: {author}, 출판 연도: {year}")
        print()
    # 결과:
    # 책 이름: 혼자 공부하는 데이터 분석, 저자: 박해선, 출판 연도: 2022
    #
    # 책 이름: 혼자 공부하는 머신러닝+딥러닝, 저자: 박해선, 출판 연도: 2020
    
    # 판다스로 XML 직접 읽기
    df_from_xml = pd.read_xml(x2_str)
    print(df_from_xml)
    # 결과:
    #                     name author  year
    # 0       혼자 공부하는 데이터 분석    박해선  2022
    # 1  혼자 공부하는 머신러닝+딥러닝    박해선  2020
    

    1-3. API 호출 및 응답 처리

    파이썬의 requests 라이브러리를 사용하여 HTTP 요청을 보내고 API 응답을 받을 수 있습니다.

    requests 라이브러리로 API 호출

    import requests
    
    # 도서관 정보나루 API URL (※ 실제 사용 시 '인증키' 부분을 본인의 인증키로 변경해야 합니다.)
    url = "http://data4library.kr/api/loanItemSrch?format=json&startDt=2021-04-01&endDt=2021-04-30&age=20&authKey=인증키"
    
    # API 호출 및 응답 받기
    # 실제 API 호출 시 인증키가 필요하며, 여기서는 예시로만 보여드립니다.
    # r = requests.get(url)
    # data = r.json() # 응답을 JSON으로 파싱
    
    # 예시 데이터 (API 호출 없이 구조만 보여주기 위함)
    data = {
        "response": {
            "docs": [
                {"doc": {"no": 1, "bookname": "데이터 분석 입문", "authors": "김철수"}},
                {"doc": {"no": 2, "bookname": "파이썬으로 데이터 과학", "authors": "박영희"}}
            ]
        }
    }
    
    
    # API 응답 구조 탐색
    print(data) # 전체 응답 데이터 (예시 데이터 출력)
    # 결과: {'response': {'docs': [{'doc': {'no': 1, 'bookname': '데이터 분석 입문', 'authors': '김철수'}}, {'doc': {'no': 2, 'bookname': '파이썬으로 데이터 과학', 'authors': '박영희'}}]}}
    print(data['response']['docs']) # 실제 도서 정보가 담긴 부분
    # 결과: [{'doc': {'no': 1, 'bookname': '데이터 분석 입문', 'authors': '김철수'}}, {'doc': {'no': 2, 'bookname': '파이썬으로 데이터 과학', 'authors': '박영희'}}]
    

    API 응답 데이터 처리

    API 응답에서 필요한 데이터만 추출하여 DataFrame으로 만들고 파일로 저장할 수 있습니다.

    # 예시 데이터 (위에서 사용한 data 변수)
    # 일반적인 반복문으로 데이터 추출
    books = []
    for d in data['response']['docs']:
        books.append(d['doc'])
    print(books)
    # 결과: [{'no': 1, 'bookname': '데이터 분석 입문', 'authors': '김철수'}, {'no': 2, 'bookname': '파이썬으로 데이터 과학', 'authors': '박영희'}]
    
    
    # 리스트 컴프리헨션으로 더 간결하게 처리
    books_comprehension = [d['doc'] for d in data['response']['docs']]
    print(books_comprehension)
    # 결과: [{'no': 1, 'bookname': '데이터 분석 입문', 'authors': '김철수'}, {'no': 2, 'bookname': '파이썬으로 데이터 과학', 'authors': '박영희'}]
    
    # 데이터프레임으로 변환
    books_df = pd.DataFrame(books)
    print(books_df.head())
    # 결과:
    #    no       bookname authors
    # 0   1  데이터 분석 입문     김철수
    # 1   2  파이썬으로 데이터 과학     박영희
    
    # JSON 파일로 저장 (실제 실행 시 '20s_best_book.json' 파일이 생성됩니다.)
    # books_df.to_json('20s_best_book.json')
    

    2. 웹 스크래핑으로 웹 데이터 가져오기

    웹 스크래핑(Web Scraping)은 웹 페이지의 HTML을 분석하여 원하는 데이터를 추출하는 기술입니다.

     requests로 웹페이지 내용을 가져오고 BeautifulSoup으로 파싱합니다.

    2-1. 데이터 로드 및 전처리

    먼저 스크래핑할 대상 도서 목록을 JSON 파일에서 로드합니다.

    import gdown
    import pandas as pd
    
    # 구글 드라이브에서 JSON 파일 다운로드 (실제 파일이 존재하지 않으면 에러 발생)
    # gdown.download('https://bit.ly/3q9SZix', '20s_best_book.json', quiet=False)
    
    # 다운로드된 JSON 파일에서 데이터프레임 로드
    # 여기서는 예시 데이터프레임을 직접 생성합니다.
    books_df = pd.DataFrame([
        {'no': 1, 'ranking': 1, 'bookname': '우리가 빛의 속도로 갈 수 없다면', 'authors': '김초엽', 'publisher': '허블', 'publication_year': 2019, 'isbn13': 9791190090018},
        {'no': 2, 'ranking': 2, 'bookname': '아몬드', 'authors': '손원평', 'publisher': '창비', 'publication_year': 2017, 'isbn13': 9791186259556}
    ])
    print(books_df.head())
    
    # 필요한 컬럼만 선택하여 새로운 데이터프레임 생성
    books = books_df[['no','ranking','bookname','authors','publisher',
                     'publication_year','isbn13']]
    print(books.head())
    

    데이터프레임 인덱싱 기법

    loc이나 iloc을 사용하여 데이터프레임에서 원하는 행과 열을 선택하는 다양한 방법입니다.

    # loc을 이용한 특정 행과 컬럼 선택
    print(books_df.loc[[0,1], ['bookname','authors']])
    # 결과:
    #                     bookname authors
    # 0  우리가 빛의 속도로 갈 수 없다면     김초엽
    # 1                      아몬드     손원평
    
    # loc을 이용한 범위로 행과 컬럼 선택
    print(books_df.loc[0:1, 'bookname':'authors'])
    # 결과:
    #                     bookname authors
    # 0  우리가 빛의 속도로 갈 수 없다면     김초엽
    # 1                      아몬드     손원평
    
    # 모든 행, 특정 컬럼 범위 선택
    books_subset = books_df.loc[:, 'no':'isbn13']
    print(books_subset.head())
    
    # 짝수 번째 행만 선택 (스텝=2)
    print(books_df.loc[::2, 'no':'isbn13'].head())
    # 결과:
    #    no  ranking                    bookname authors publisher  publication_year       isbn13
    # 0   1        1  우리가 빛의 속도로 갈 수 없다면     김초엽      허블              2019  9791190090018
    

    2-2. 웹 스크래핑 기초 (requests & BeautifulSoup)

    requests로 웹페이지 HTML을 가져오고, BeautifulSoup으로 HTML 구조를 분석합니다.

    SSL 에러 해결 (필요시)

    일부 환경에서 발생하는 SSL 에러를 해결하기 위한 코드입니다.

    # DH_KEY_TOO_SMALL 에러 발생 시 실행 (필요한 경우에만)
    import requests
    # requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL'
    # try:
    #     requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST += 'HIGH:!DH:!aNULL'
    # except AttributeError:
    #     pass
    

    웹페이지 요청 및 응답

    requests.get() 함수로 특정 URL의 HTML 내용을 가져옵니다.

    import requests
    
    isbn = 9791190090018      # '우리가 빛의 속도로 갈 수 없다면'의 ISBN
    url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
    
    # requests.get()으로 ISBN을 넣어 검색 페이지 요청
    r = requests.get(url.format(isbn))
    # print(r.text) # HTML 응답 내용이 길어서 주석 처리. 실제 실행 시 웹 페이지 HTML이 출력됩니다.
    

    BeautifulSoup을 이용한 HTML 파싱

    BeautifulSoup은 HTML/XML 문서를 파싱하여 원하는 요소를 쉽게 찾을 수 있도록 돕습니다.

    from bs4 import BeautifulSoup
    
    # HTML 파서로 파싱
    soup = BeautifulSoup(r.text, 'html.parser')
    # 'a' 태그 중 class가 'gd_name'인 첫 번째 요소 찾기
    prd_link = soup.find('a', attrs={'class':'gd_name'})
    print(prd_link)         # 결과: <a class="gd_name" href="/Product/Goods/74261416">우리가 빛의 속도로 갈 수 없다면</a> (전체 태그 출력)
    print(prd_link['href']) # 결과: /Product/Goods/74261416 (href 속성값 추출)
    

    상세 페이지 정보 추출

    검색 결과에서 얻은 상세 페이지 URL로 이동하여 추가 정보를 추출합니다.

    # 도서 상세 페이지로 이동
    detail_url = 'http://www.yes24.com'+prd_link['href']
    r_detail = requests.get(detail_url)
    soup_detail = BeautifulSoup(r_detail.text, 'html.parser')
    
    # 상품 상세정보 div(id가 infoset_specific) 찾기
    prd_detail = soup_detail.find('div', attrs={'id':'infoset_specific'})
    
    # 상세정보 테이블의 모든 tr 태그 추출
    prd_tr_list = prd_detail.find_all('tr')
    
    # '쪽수, 무게, 크기' 정보 찾아서 추출
    page_td_text = ''
    for tr in prd_tr_list:
        if tr.find('th').get_text() == '쪽수, 무게, 크기':
            page_td_text = tr.find('td').get_text()
            break
    
    print(page_td_text.split()[0])  # 결과: 360쪽 (첫 번째 단어만 추출)
    

    2-3. 스크래핑 함수화 및 데이터프레임 병합

    스크래핑 로직을 함수로 만들고, pandas의 apply()나 merge()를 활용하여 데이터프레임에 적용합니다.

    쪽수 추출 함수 정의

    def get_page_cnt(isbn):
        url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
        r = requests.get(url.format(isbn))
        soup = BeautifulSoup(r.text, 'html.parser')
    
        prd_info = soup.find('a', attrs={'class':'gd_name'})
        if prd_info is None: # 검색 결과가 없으면 빈 문자열 반환
            return ''
    
        detail_url = 'http://www.yes24.com'+prd_info['href']
        r_detail = requests.get(detail_url)
        soup_detail = BeautifulSoup(r_detail.text, 'html.parser')
    
        prd_detail = soup_detail.find('div', attrs={'id':'infoset_specific'})
        if prd_detail is None: # 상세 정보 div가 없으면 빈 문자열 반환
            return ''
    
        prd_tr_list = prd_detail.find_all('tr')
    
        for tr in prd_tr_list:
            if tr.find('th').get_text() == '쪽수, 무게, 크기':
                return tr.find('td').get_text().split()[0] # 첫 단어(쪽수) 반환
        return '' # 쪽수 정보를 찾지 못하면 빈 문자열 반환
    
    # 함수 테스트
    print(get_page_cnt(9791190090018)) # 결과: 360쪽
    print(get_page_cnt(9791190090000)) # 결과: (존재하지 않는 ISBN 가정 시 빈 문자열 또는 에러 처리)
    

    apply() 함수로 일괄 처리

    데이터프레임의 각 행에 위에서 정의한 함수를 적용하여 일괄적으로 쪽수 정보를 가져옵니다.

    Python
     
    top10_books = books.head(10) # 예시로 상위 10개 도서만 사용
    
    # 람다 함수를 사용하여 get_page_cnt 함수를 각 행의 'isbn13'에 적용
    page_count_series = top10_books.apply(lambda row: get_page_cnt(row['isbn13']), axis=1)
    page_count_series.name = 'page_count' # 시리즈에 이름 부여
    print(page_count_series)
    # 결과 (예시):
    # 0    360쪽
    # 1    336쪽
    # Name: page_count, dtype: object
    

    데이터프레임 병합 (merge())

    스크래핑으로 얻은 쪽수 정보를 기존 도서 데이터프레임에 병합합니다.

    # 인덱스 기준으로 병합
    top10_with_page_count = pd.merge(top10_books, page_count_series,
                                     left_index=True, right_index=True)
    print(top10_with_page_count.head())
    # 결과 (예시):
    #    no  ranking                    bookname authors publisher  publication_year       isbn13 page_count
    # 0   1        1  우리가 빛의 속도로 갈 수 없다면     김초엽      허블              2019  9791190090018       360쪽
    # 1   2        2                      아몬드     손원평      창비              2017  9791186259556       336쪽
    
    # merge() 함수의 다양한 옵션들 (예제 데이터프레임으로 설명)
    df1 = pd.DataFrame({'col1': ['a','b','c'], 'col2': [1,2,3]})
    df2 = pd.DataFrame({'col1': ['a','b','d'], 'col3': [10,20,30]})
    
    # inner join (기본값): 공통된 키(col1)만 병합 (교집합)
    print(pd.merge(df1, df2, on='col1'))
    # 결과:
    #   col1  col2  col3
    # 0    a     1    10
    # 1    b     2    20
    
    # left join: 왼쪽 데이터프레임(df1) 기준으로 병합. 일치하는 값이 없으면 NaN
    print(pd.merge(df1, df2, how='left', on='col1'))
    # 결과:
    #   col1  col2  col3
    # 0    a     1  10.0
    # 1    b     2  20.0
    # 2    c     3   NaN
    
    # outer join: 모든 키를 포함하여 병합 (합집합)
    print(pd.merge(df1, df2, how='outer', on='col1'))
    # 결과:
    #   col1  col2  col3
    # 0    a   1.0  10.0
    # 1    b   2.0  20.0
    # 2    c   3.0   NaN
    # 3    d   NaN  30.0
    

    마치며: 데이터 수집과 가공, 그 다음은?

    오늘은 웹에서 데이터를 수집하는 두 가지 강력한 방법인 API 활용 웹 스크래핑에 대해 깊이 있게 알아보았습니다. 

     

    JSON XML 데이터를 파싱하고,

     requests, BeautifulSoup, pandas 같은 라이브러리를 통해 데이터를 원하는 형태로 가공하는 과정을 실습했습니다.

     

    이제 여러분은 웹의 방대한 데이터에 접근하고,

    이를 파이썬으로 가져와 분석 가능한 형태로 만드는 기초 역량을 갖추게 되었습니다.

     

    데이터 분석의 첫걸음인 데이터 수집과 전처리는 매우 중요하니, 오늘 배운 내용들을 꼭 반복 해보시기를 바랍니다.

    다음 시간에는 이렇게 얻은 데이터를 실제로 분석하고 시각화하는 방법을 다뤄보겠습니다.

Designed by Tistory.