본문 바로가기
[업무 지식]/Crawling

[크롤러 만들기] 파이썬으로 크롤러 만들기

by 에디터 윤슬 2025. 1. 1.

목록 페이지에서 URL 목록 추출하기

import requests
import lxml.html

response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
root = lxml.html.fromstring(response.content)
for a in root.cssselect('.view_box a'):
    url = a.get('href')
    print(url)

코드 설명

  • 코드에서 `root.cssselect('.view_box a')`는 HTML 문서에서 특정 요소들을 선택하기 위해 CSS 선택자를 사용하는 부분입니다. 여기서 `.view_box a`는 다음과 같은 의미를 가집니다:
    • 1. `.view_box`: 클래스 이름이 `view_box`인 요소를 선택합니다. 클래스 선택자는 점(`.`)으로 시작하며, 해당 클래스가 적용된 모든 요소를 대상으로 합니다.
    • 2. `a`: `view_box` 클래스 내부에 있는 `<a>` 태그(링크 요소)를 선택합니다. 이는 후손 선택자로, `view_box` 클래스 안에 포함된 모든 `<a>` 태그를 찾습니다.
  • 이유
    • 특정 링크 요소를 타겟팅: `view_box`라는 특정 클래스 내의 `<a>` 태그만을 대상으로 작업하기 위해 사용됩니다. 예를 들어, 페이지 내 다른 위치에 있는 `<a>` 태그는 제외됩니다.
    • HTML 구조 탐색: 웹 페이지에서 새 책 목록과 관련된 링크들이 `.view_box`라는 클래스를 가진 컨테이너 안에 있을 가능성이 높습니다. 이 구조에 맞춰 데이터를 효율적으로 추출하려는 의도입니다.
    • CSS 선택자의 활용: CSS 선택자는 HTML 문서에서 원하는 요소를 정확히 지정할 수 있는 강력한 도구로, 이 경우에도 필요한 링크만 추출하기 위해 사용됩니다.
  • 동작 원리
    • 1. `requests.get()`으로 해당 URL의 HTML 콘텐츠를 가져옵니다.
    • 2. `lxml.html.fromstring()`을 통해 HTML 콘텐츠를 파싱하여 DOM 트리로 변환합니다.
    • 3. `.cssselect('.view_box a')`로 DOM 트리에서 `.view_box` 클래스 내부의 모든 `<a>` 태그를 선택합니다.
    • 4. 반복문을 통해 각 `<a>` 태그의 `href` 속성을 가져와 출력합니다.

절대 URL 변환

  • 코드 결과를 보니 'javascript:'라는 부분이 있습니다. 개발자 도구를 확인해보니, '.view_box .book_tit a'로 찾으면 이를 제외 가능함을 알 수 있습니다.
  • 또한 현재 결과는 모두 상대 URL입니다. 따라서 이를 절대 URL로 변환해야 합니다. make_links_absolute() 메서드를 활용하여 문서 내부의 모든 링크의 href 속성을 절대 URL로 변환해 줍니다.
import requests
import lxml.html

response = requests.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
root = lxml.html.fromstring(response.content)

# 모든 링크를 절대 URL로 변환합니다.
root.make_links_absolute(response.url)

# 선택자를 추가해서 명확한 선택을 할 수 있게 합니다.
for a in root.cssselect('.view_box .book_tit a'):
    url = a.get('href')
    print(url)

 

함수를 사용하여 리팩토링

  • main() 함수에서 scrape_list_page() 함수를 호출하는 형태로 변환
  • scrape_list_page() 함수의 반환값은 list처럼 반복 가능한 제너레이터
import requests
import lxml.html
def main():
    """
    크롤러의 메인 처리
    """
    # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
    session = requests.Session()  
    # scrape_list_page() 함수를 호출해서 제너레이터를 추출합니다.
    response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
    urls = scrape_list_page(response)
    # 제너레이터는 list처럼 사용할 수 있습니다.
    for url in urls:
        print(url)

def scrape_list_page(response):
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselect('.view_box .book_tit a'):
        url = a.get('href')
        # yield 구문으로 제너레이터의 요소 반환
        yield url

if __name__ == '__main__':
    main()

 

 

import requests
import lxml.html
def main():
    """
    크롤러의 메인 처리
    """
    # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
    session = requests.Session()  
    # scrape_list_page() 함수를 호출해서 제너레이터를 추출합니다.
    response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
    urls = scrape_list_page(response)
    # 제너레이터는 list처럼 사용할 수 있습니다.
    for url in urls:
        print(url)

def scrape_list_page(response):
    """
    책 제목을 추출하는 함수
    """
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselect('.view_box .book_tit a'):
        title = a.text_content().strip()
        # yield 구문으로 제너레이터의 요소 반환
        yield title

if __name__ == '__main__':
    main()

 

상세 페이지에서 스크레이핑하기

추출하고 싶은 요소 CSS 선택자
타이틀 .store_product_info_box h3
가격 .pbr strong
목차 #tabs_3 .haabit_edit_view 내부의 p 태그들

 

import requests
import lxml.html

def main():
    # 여러 페이지에서 크롤링할 것이므로 Session을 사용합니다.
    session = requests.Session()  
    response = session.get('http://www.hanbit.co.kr/store/books/new_book_list.html')
    urls = scrape_list_page(response)
    for url in urls:
        response = session.get(url)  # Session을 사용해 상세 페이지를 추출합니다.
        ebook = scrape_detail_page(response)  # 상세 페이지에서 상세 정보를 추출합니다.
        print(ebook)  # 책 관련 정보를 출력합니다.
        break  # 책 한 권이 제대로 되는지 확인하고 종료합니다.

def scrape_list_page(response):
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselect('.view_box .book_tit a'):
        url = a.get('href')
        yield url

def scrape_detail_page(response):
    """
    상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
    """
    root = lxml.html.fromstring(response.content)
    ebook = {
        'url': response.url,
        'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
        'price': root.cssselect('.pbr strong')[0].text_content(),
        'content': [p.text_content()\
            for p in root.cssselect('#tabs_3 .hanbit_edit_view p')]
    }
    return ebook

if __name__ == '__main__':
    main()

 

  • 1. `#tabs_3`:
    • ID가 `tabs_3`인 요소를 선택합니다. ID 선택자는 `#`로 시작하며, HTML 문서에서 고유한 요소를 타겟팅합니다.
    • 이 경우, `tabs_3`는 책 상세 페이지에서 책의 상세 설명을 담고 있는 탭이나 섹션일 가능성이 큽니다.
  • 2. `.hanbit_edit_view`:
    • 클래스 이름이 `hanbit_edit_view`인 요소를 선택합니다. 클래스 선택자는 점(`.`)으로 시작하며, 해당 클래스가 적용된 모든 요소를 대상으로 합니다.
    • 이 클래스는 상세 설명이 포함된 특정 컨테이너를 나타낼 수 있습니다.
  • 3. `p`:
    • `hanbit_edit_view` 클래스 내부에 있는 모든 `<p>` 태그(단락)를 선택합니다.
    • `<p>` 태그는 일반적으로 텍스트 내용을 담는 데 사용되며, 책의 상세 설명이 여러 단락으로 나뉘어 있을 경우 이를 모두 가져오기 위해 사용됩니다.
{'url': 'https://www.hanbit.co.kr/store/books/look.php?p_code=B7597652225', 'title': 'Vue 3와 타입스크립트로 배우는 프런트엔드 개발', 'price': '32,400', 'content': ['\r\n                                                                    ', '도입 편', '\xa0', '1장 프런트엔드 개발 흐름과 Vue_1.1 자바스크립트의 변천과 프런트엔드 개발의 등장\xa0_1.2 프런트엔드 프레임워크와 Vue', '\xa0', '2장 Vi

상세 페이지 크롤링하기(1부터 10페이지까지)

import time # time 모듈을 임포트합니다.
import re 
import requests
import lxml.html
from bs4 import BeautifulSoup

def main():
    session = requests.Session()
    base_url = "https://www.hanbit.co.kr/store/books/new_book_list.html?page="
    max_pages = 10
    for page_num in range(1, max_pages+1):
        url = f'{base_url}{page_num}'
        response = session.get(url)
        urls = scrape_list_page(response)
        for url in urls:
            time.sleep(1) # 1초 동안 휴식합니다.
            response = session.get(url)
            ebook = scrape_detail_page(response)
            print(ebook)

def scrape_list_page(response):
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselect('.view_box .book_tit a'):
        url = a.get('href')
        yield url

def scrape_detail_page(response):
    """
    상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
    """
    root = lxml.html.fromstring(response.content)
    ebook = {
        'url': response.url,
        'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
        'price': root.cssselect('.pbr strong')[0].text_content(),
        'content': [normalize_spaces(p.text_content())
            for p in root.cssselect('#tabs_3 .hanbit_edit_view p')
            if normalize_spaces(p.text_content()) != '']
    }
    return ebook

def normalize_spaces(s):
    """
    연결돼 있는 공백을 하나의 공백으로 변경합니다.
    """
    return re.sub(r'\s+', ' ', s).strip()

if __name__ == '__main__':
    main()

스크레이핑 데이터 저장하기_to MySQL

import time
import re
import requests
import lxml.html
import pymysql

def main():
    """
    크롤러의 메인 처리
    """
    # MySQL 데이터베이스 연결 설정
    db_config = {
        "host": "localhost",  # MySQL 서버 주소
        "user": "root",  # MySQL 사용자 이름
        "password": "sqlstudent",  # MySQL 비밀번호
        "database": "scraping",  # 사용할 데이터베이스 이름
        "charset": "utf8mb4"  # 문자 인코딩 설정
    }
    
    # MySQL 연결 생성
    conn = pymysql.connect(**db_config)
    cursor = conn.cursor()

    # 테이블 생성 (필요시)
    create_table_query = """
    CREATE TABLE IF NOT EXISTS ebooks (
        id INT AUTO_INCREMENT PRIMARY KEY,
        url VARCHAR(255) NOT NULL,
        `key` VARCHAR(50) NOT NULL UNIQUE,
        title VARCHAR(255) NOT NULL,
        price VARCHAR(50) NOT NULL,
        content TEXT
    )
    """
    cursor.execute(create_table_query)
    conn.commit()

    # 페이지네이션 처리: 1페이지부터 10페이지까지 크롤링
    session = requests.Session()
    base_url = "https://www.hanbit.co.kr/store/books/new_book_list.html?page="
    max_pages = 3
    
    for page_num in range(1, max_pages + 1):
        url = f"{base_url}{page_num}"
        response = session.get(url)
        urls = scrape_list_page(response)
        
        for url in urls:
            # URL로 키를 추출합니다.
            key = extract_key(url)
            
            # MySQL에서 key에 해당하는 데이터가 있는지 확인합니다.
            cursor.execute("SELECT * FROM ebooks WHERE `key` = %s", (key,))
            ebook = cursor.fetchone()
            
            # MySQL에 존재하지 않는 경우만 상세 페이지를 크롤링합니다.
            if not ebook:
                time.sleep(1)  # 1초 동안 휴식합니다.
                response = session.get(url)
                ebook_data = scrape_detail_page(response)
                
                # 책 정보를 MySQL에 저장합니다.
                insert_query = """
                INSERT INTO ebooks (url, `key`, title, price, content)
                VALUES (%s, %s, %s, %s, %s)
                """
                cursor.execute(insert_query, (
                    ebook_data['url'],
                    ebook_data['key'],
                    ebook_data['title'],
                    ebook_data['price'],
                    ebook_data['content']
                ))
                conn.commit()
            
                # 책 정보를 출력합니다.
                print(ebook_data)

    # 연결 종료
    cursor.close()
    conn.close()

def scrape_list_page(response):
    """
    목록 페이지의 Response에서 상세 페이지의 URL을 추출합니다.
    """
    root = lxml.html.fromstring(response.content)
    root.make_links_absolute(response.url)
    for a in root.cssselect('.view_box .book_tit a'):
        url = a.get('href')
        yield url

def scrape_detail_page(response):
    """
    상세 페이지의 Response에서 책 정보를 dict로 추출합니다.
    """
    root = lxml.html.fromstring(response.content)
    
    ebook = {
        'url': response.url,
        'key': extract_key(response.url),
        'title': root.cssselect('.store_product_info_box h3')[0].text_content(),
        'price': root.cssselect('.pbr strong')[0].text_content(),
        'content': normalize_spaces(" ".join(
            [p.text_content() for p in root.cssselect('#tabs_3 .hanbit_edit_view p') if normalize_spaces(p.text_content()) != '']
        ))
    }
    
    return ebook

def extract_key(url):
    """
    URL에서 키(URL 끝의 p_code)를 추출합니다.
    """
    m = re.search(r"p_code=(.+)", url)
    return m.group(1)

def normalize_spaces(s):
    """
    연결돼 있는 공백을 하나의 공백으로 변경합니다.
    """
    return re.sub(r'\s+', ' ', s).strip()

if __name__ == '__main__':
    main()