Python

유통상품 표준DB 조회

T-Blog-000 2025. 8. 11. 11:23

유통상품 표준DB 조회

개요

유통상품 표준DB는 국내에서 유통되는 상품의 정보를 표준화하여 제공하는 국가 주도 데이터베이스입니다. 대한상공회의소한국상품정보관리원이 공동으로 운영하며, 상품의 바코드, 제품명, 이미지, 카테고리 등 다양한 정보를 포함하고 있습니다.

특징

  • 표준화된 상품 정보: 일관된 형식으로 바코드, 제품명, 카테고리 등 제공
  • 방대한 데이터베이스: 국내 유통되는 대부분의 상품 정보 포함
  • 이미지 정보: 제품 이미지 URL 제공 (일부 제한적 접근성)
  • 계층적 카테고리: 상품 분류를 계층 구조로 제공

사용법

 

제공된 Python 코드는 대한민국 유통상품 표준DB에서 상품 정보를 검색하고 추출하는 스크래퍼입니다. 다음은 이 도구의 사용 방법입니다.

Google Colab 소스코드 파일

AllProductKoreaScraper.ipynb
0.04MB

 

요구 사항

  • Python 3.6 이상
  • 필요 라이브러리: requests, beautifulsoup4, lxml, urllib3

설치 방법

  1. 필요 라이브러리 설치:
pip install requests beautifulsoup4 lxml urllib3
  1. 스크래퍼 코드 저장:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import json
import urllib.parse
import urllib3
import requests
import argparse
import re
from bs4 import BeautifulSoup
from datetime import datetime

# SSL 경고 무시
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

SEARCH_URL = "https://www.allproductkorea.or.kr/products/search"

class AllProductKoreaScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.verify = False  # SSL 검증 비활성화
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
            "Accept-Encoding": "gzip, deflate",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive"
        }

    def convert_to_original_image(self, image_url: str) -> str:
        """작은 이미지 URL을 원본 이미지 URL로 변환"""
        if not image_url or not image_url.strip():
            return image_url

        # Firebase Storage URL인 경우만 변환
        if "firebasestorage.googleapis.com" not in image_url:
            return image_url

        try:
            # 1단계: _숫자_w 패턴 제거
            step1 = re.sub(r'_\d+_w(?=\.[a-zA-Z]{2,5})', '', image_url)

            # 2단계: 토큰 파라미터 제거
            step2 = re.sub(r'&token=[a-f0-9\-]+', '', step1)

            return step2
        except Exception as e:
            print(f"이미지 URL 변환 오류: {e} (URL: {image_url})", file=sys.stderr)
            return image_url

    def test_image_accessibility(self, image_url: str) -> dict:
        """이미지 URL이 실제로 로드되는지 테스트"""
        test_result = {
            "url": image_url,
            "accessible": False,
            "status_code": None,
            "content_type": None,
            "content_length": None,
            "error": None
        }

        if not image_url or not image_url.strip():
            test_result["error"] = "URL이 비어있음"
            return test_result

        try:
            response = self.session.head(image_url, timeout=10)
            test_result["status_code"] = response.status_code
            test_result["accessible"] = response.status_code == 200
            test_result["content_type"] = response.headers.get("content-type", "")
            test_result["content_length"] = response.headers.get("content-length")

            if test_result["content_length"]:
                test_result["content_length"] = int(test_result["content_length"])

        except Exception as e:
            test_result["error"] = str(e)

        return test_result

    def build_search_url(self, main_keyword: str, sub_keyword: str = "", page: int = 1, size: int = 10) -> str:
        """검색 URL 생성"""
        q = json.dumps({
            "mainKeyword": main_keyword, 
            "subKeyword": sub_keyword
        }, ensure_ascii=False)

        params = {
            "q": q,
            "page": page,
            "size": size
        }
        return f"{SEARCH_URL}?{urllib.parse.urlencode(params)}"

    def fetch_html(self, url: str) -> str:
        """HTML 페이지 가져오기"""
        response = self.session.get(url, headers=self.headers, timeout=30)
        response.raise_for_status()
        response.encoding = 'utf-8'
        return response.text

    def extract_pagination_info(self, soup: BeautifulSoup) -> dict:
        """페이지네이션 정보 추출"""
        pagination_info = {
            "current_page": 1,
            "total_pages": 1,
            "page_size": 10,
            "total_count": 0,
            "result_count": 0,
            "has_next": False,
            "has_prev": False
        }

        try:
            # 총 결과 수 추출
            total_element = soup.select_one(".pl_total span")
            if total_element:
                total_text = total_element.get_text(strip=True)
                # "총 : 30건" 형태에서 숫자 추출
                total_match = re.search(r'총\s*:\s*(\d+)건', total_text)
                if total_match:
                    pagination_info["total_count"] = int(total_match.group(1))

            # 페이지 정보 추출
            page_info = soup.select_one(".per_pageinfo")
            if page_info:
                page_text = page_info.get_text(strip=True)
                # "총 3페이지" 형태에서 숫자 추출
                page_match = re.search(r'총\s*(\d+)페이지', page_text)
                if page_match:
                    pagination_info["total_pages"] = int(page_match.group(1))

            # 현재 페이지 추출 (스크립트에서)
            script_tags = soup.find_all('script')
            for script in script_tags:
                if script.string and 'pagination' in script.string:
                    # JavaScript에서 페이지 정보 추출 시도
                    page_match = re.search(r'"page"\s*:\s*(\d+)', script.string)
                    if page_match:
                        pagination_info["current_page"] = int(page_match.group(1))

                    size_match = re.search(r'"size"\s*:\s*(\d+)', script.string)
                    if size_match:
                        pagination_info["page_size"] = int(size_match.group(1))

                    count_match = re.search(r'"resultCount"\s*:\s*(\d+)', script.string)
                    if count_match:
                        pagination_info["result_count"] = int(count_match.group(1))

            # 이전/다음 페이지 존재 여부
            pagination_info["has_prev"] = pagination_info["current_page"] > 1
            pagination_info["has_next"] = pagination_info["current_page"] < pagination_info["total_pages"]

        except Exception as e:
            print(f"페이지네이션 정보 추출 오류: {e}", file=sys.stderr)

        return pagination_info

    def extract_search_metadata(self, soup: BeautifulSoup) -> dict:
        """검색 메타데이터 추출"""
        metadata = {
            "title": "",
            "breadcrumb": [],
            "search_input_placeholder": "",
            "page_description": ""
        }

        try:
            # 페이지 제목
            title_element = soup.select_one("title")
            if title_element:
                metadata["title"] = title_element.get_text(strip=True)

            # 브레드크럼
            breadcrumb_elements = soup.select(".loc_wrap li")
            for element in breadcrumb_elements:
                text = element.get_text(strip=True)
                if text and text != "홈":
                    metadata["breadcrumb"].append(text)

            # 검색 입력창 플레이스홀더
            search_input = soup.select_one("#searchText")
            if search_input:
                metadata["search_input_placeholder"] = search_input.get("placeholder", "")

            # 페이지 설명
            h3_element = soup.select_one(".h3_wrap h3")
            if h3_element:
                metadata["page_description"] = h3_element.get_text(strip=True)

        except Exception as e:
            print(f"메타데이터 추출 오류: {e}", file=sys.stderr)

        return metadata

    def extract_products(self, soup: BeautifulSoup) -> list:
        """제품 리스트 추출"""
        products = []

        for li in soup.select(".spl_list li[data-prd-no]"):
            try:
                product = {
                    "prdNo": li.get("data-prd-no", "").strip(),
                    "barcode": "",
                    "name": "",
                    "image": "",
                    "categoryPath": "",
                    "categoryHierarchy": [],
                    "link": ""
                }

                # 이미지 URL (원본 이미지로 변환)
                img = li.select_one(".spl_img img")
                if img:
                    original_image_url = img.get("src", "")
                    # 작은 이미지를 원본 이미지로 변환
                    converted_image_url = self.convert_to_original_image(original_image_url)

                    product["image"] = converted_image_url

                    # 원본과 변환된 URL이 다르면 둘 다 저장
                    if original_image_url != converted_image_url:
                        product["original_small_image"] = original_image_url
                        product["image_conversion_applied"] = True
                    else:
                        product["image_conversion_applied"] = False

                # 바코드와 제품명
                pt = li.select_one(".spl_pt")
                if pt:
                    barcode_elem = pt.select_one("em")
                    if barcode_elem:
                        product["barcode"] = barcode_elem.get_text(strip=True)

                    name_elem = pt.select_one("strong")
                    if name_elem:
                        product["name"] = name_elem.get_text(strip=True)

                # 카테고리 경로
                pm = li.select_one(".spl_pm")
                if pm:
                    category_path = pm.get_text(strip=True)
                    product["categoryPath"] = category_path
                    # 카테고리를 배열로도 저장
                    if category_path:
                        product["categoryHierarchy"] = [cat.strip() for cat in category_path.split(">")]

                # 링크 (추정)
                if product["prdNo"]:
                    product["link"] = f"/products/info/korcham/{product['prdNo']}"

                products.append(product)

            except Exception as e:
                print(f"제품 정보 추출 오류: {e}", file=sys.stderr)
                continue

        return products

    def scrape(self, main_keyword: str, sub_keyword: str = "", page: int = 1, size: int = 10) -> dict:
        """메인 스크래핑 함수"""
        url = self.build_search_url(main_keyword, sub_keyword, page, size)

        print(f"검색 파라미터:", file=sys.stderr)
        print(f"   - 주요 키워드: {main_keyword}", file=sys.stderr)
        print(f"   - 보조 키워드: {sub_keyword}", file=sys.stderr)
        print(f"   - 페이지: {page}", file=sys.stderr)
        print(f"   - 사이즈: {size}", file=sys.stderr)
        print(f"요청 URL: {url}", file=sys.stderr)

        html = self.fetch_html(url)
        print(f"HTML 가져오기 완료 ({len(html):,} bytes)", file=sys.stderr)

        soup = BeautifulSoup(html, "lxml")

        # 모든 데이터 추출
        products = self.extract_products(soup)
        pagination = self.extract_pagination_info(soup)
        metadata = self.extract_search_metadata(soup)

        print(f"스크래핑 결과: {len(products)}개 제품", file=sys.stderr)

        # 이미지 URL 변환 및 접근성 테스트
        image_test_results = []
        conversion_count = 0
        accessible_count = 0

        for i, product in enumerate(products[:3]):  # 처음 3개 제품만 테스트
            if product.get("image"):
                test_result = self.test_image_accessibility(product["image"])
                test_result["product_index"] = i
                test_result["product_name"] = product.get("name", "이름 없음")
                test_result["conversion_applied"] = product.get("image_conversion_applied", False)

                if test_result["conversion_applied"]:
                    conversion_count += 1

                if test_result["accessible"]:
                    accessible_count += 1

                image_test_results.append(test_result)

                content_length = test_result.get('content_length') or 0
                status = "OK" if test_result['accessible'] else "FAIL"
                print(f"이미지 테스트 [{i+1}] {product.get('name', '이름없음')[:20]}: {status} ({content_length:,} bytes)", file=sys.stderr)

        if image_test_results:
            print(f"URL 변환: {conversion_count}/{len(image_test_results)}개", file=sys.stderr)
            print(f"접근 가능: {accessible_count}/{len(image_test_results)}개", file=sys.stderr)

        # 결과 구성
        result = {
            "timestamp": datetime.now().isoformat(),
            "request": {
                "url": url,
                "parameters": {
                    "mainKeyword": main_keyword,
                    "subKeyword": sub_keyword,
                    "page": page,
                    "size": size
                }
            },
            "metadata": metadata,
            "pagination": pagination,
            "products": {
                "count": len(products),
                "items": products
            },
            "image_tests": {
                "tested_count": len(image_test_results),
                "conversion_count": conversion_count,
                "accessible_count": accessible_count,
                "results": image_test_results
            },
            "summary": {
                "scraped_count": len(products),
                "total_available": pagination.get("total_count", 0),
                "current_page": pagination.get("current_page", page),
                "total_pages": pagination.get("total_pages", 1),
                "has_more_pages": pagination.get("has_next", False)
            }
        }

        return result

def main():
    parser = argparse.ArgumentParser(
        description="AllProductKorea.or.kr 제품 검색 스크래퍼",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
사용 예시:
  python allproductkorea_scraper.py "빙그레 더단백"
  python allproductkorea_scraper.py "삼양라면" --page 2 --size 20
  python allproductkorea_scraper.py "커피" --sub-keyword "원두" --page 1 --size 50
  python allproductkorea_scraper.py --url "https://www.allproductkorea.or.kr/products/search?q=..."
        """
    )

    # 파라미터 그룹 1: 검색어 방식
    search_group = parser.add_argument_group('검색어 파라미터')
    search_group.add_argument('main_keyword', nargs='?', help='주요 검색어')
    search_group.add_argument('--sub-keyword', '-s', default='', help='보조 검색어 (기본값: 빈 문자열)')
    search_group.add_argument('--page', '-p', type=int, default=1, help='페이지 번호 (기본값: 1)')
    search_group.add_argument('--size', '-z', type=int, default=10, help='페이지당 결과 수 (기본값: 10)')

    # 파라미터 그룹 2: URL 직접 입력
    url_group = parser.add_argument_group('URL 직접 입력')
    url_group.add_argument('--url', '-u', help='완성된 검색 URL 직접 입력')

    # 출력 옵션
    output_group = parser.add_argument_group('출력 옵션')
    output_group.add_argument('--pretty', action='store_true', help='JSON 출력을 예쁘게 포맷')
    output_group.add_argument('--output', '-o', help='결과를 파일로 저장')

    args = parser.parse_args()

    # 파라미터 검증
    if not args.url and not args.main_keyword:
        print("오류: 검색어 또는 URL을 제공해야 합니다.", file=sys.stderr)
        parser.print_help()
        sys.exit(1)

    try:
        scraper = AllProductKoreaScraper()

        if args.url:
            # URL에서 파라미터 추출
            parsed_url = urllib.parse.urlparse(args.url)
            query_params = urllib.parse.parse_qs(parsed_url.query)

            if 'q' in query_params:
                q_data = json.loads(query_params['q'][0])
                main_keyword = q_data.get('mainKeyword', '')
                sub_keyword = q_data.get('subKeyword', '')
            else:
                main_keyword = ''
                sub_keyword = ''

            page = int(query_params.get('page', [1])[0])
            size = int(query_params.get('size', [10])[0])
        else:
            main_keyword = args.main_keyword
            sub_keyword = args.sub_keyword
            page = args.page
            size = args.size

        # 스크래핑 실행
        result = scraper.scrape(main_keyword, sub_keyword, page, size)

        # 결과 출력
        if args.pretty:
            json_output = json.dumps(result, ensure_ascii=False, indent=2)
        else:
            json_output = json.dumps(result, ensure_ascii=False)

        if args.output:
            with open(args.output, 'w', encoding='utf-8') as f:
                f.write(json_output)
            print(f"결과를 {args.output}에 저장했습니다.", file=sys.stderr)
        else:
            print(json_output)

    except Exception as e:
        print(f"오류 발생: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc(file=sys.stderr)
        sys.exit(1)

if __name__ == "__main__":
    main()

위의 코드를 allproductkorea_scraper.py 파일로 저장합니다.

기본 사용법

명령줄에서 다음과 같이 실행합니다:

python allproductkorea_scraper.py "검색어"

고급 옵션

다양한 매개변수를 사용하여 검색을 정교화할 수 있습니다:

# 기본 검색
python allproductkorea_scraper.py "빙그레 더단백"

# 페이지 및 결과 수 지정
python allproductkorea_scraper.py "삼양라면" --page 2 --size 20

# 주요 키워드와 보조 키워드 사용
python allproductkorea_scraper.py "커피" --sub-keyword "원두" --page 1 --size 50

# 직접 URL 사용
python allproductkorea_scraper.py --url "https://www.allproductkorea.or.kr/products/search?q=..."

# 결과를 JSON 파일로 저장
python allproductkorea_scraper.py "라면" --output results.json --pretty

출력 결과 해석

python allproductkorea_scraper.py --pretty "더단백"

검색 파라미터:
   - 주요 키워드: 더단백
   - 보조 키워드: 
   - 페이지: 1
   - 사이즈: 10
요청 URL: https://www.allproductkorea.or.kr/products/search?q=%7B%22mainKeyword%22%3A+%22%EB%8D%94%EB%8B%A8%EB%B0%B1%22%2C+%22subKeyword%22%3A+%22%22%7D&page=1&size=10
HTML 가져오기 완료 (21,994 bytes)
스크래핑 결과: 10개 제품
이미지 테스트 [1] 자연주의 한번 쪄내어 더 부드러운 고: OK (2,336,020 bytes)
이미지 테스트 [2] 빙그레 건강tft 더단백 워터 프로틴: OK (2,558,638 bytes)
이미지 테스트 [3] 빙그레 건강 tft 더:단백 워터 프: OK (2,011,492 bytes)
URL 변환: 3/3개
접근 가능: 3/3개

{
  "timestamp": "2025-08-11T11:14:59.911933",
  "request": {
    "url": "https://www.allproductkorea.or.kr/products/search?q=%7B%22mainKeyword%22%3A+%22%EB%8D%94%EB%8B%A8%EB%B0%B1%22%2C+%22subKeyword%22%3A+%22%22%7D&page=1&size=10",
    "parameters": {
      "mainKeyword": "더단백",
      "subKeyword": "",
      "page": 1,
      "size": 10
    }
  },
  "metadata": {
    "title": "유통상품 표준DB",
    "breadcrumb": ["상품 검색"],
    "search_input_placeholder": "상품명 / 바코드/ 회사명을 입력해 주세요.",
    "page_description": "상품정보 검색"
  },
  "pagination": {
    "current_page": 1,
    "total_pages": 4,
    "page_size": 10,
    "total_count": 34,
    "result_count": 10,
    "has_next": true,
    "has_prev": false
  },
  "products": {
    "count": 10,
    "items": [
      {
        "prdNo": "102171669",
        "barcode": "8809020767885",
        "name": "자연주의 한번 쪄내어 더 부드러운 고단백 유기농 서리태볶음 400g",
        "categoryHierarchy": ["가공식품", "농산가공식품", "기타농산가공식품", "기타농산가공식품"]
      },
      {
        "prdNo": "102184523",
        "barcode": "8801104949231",
        "name": "빙그레 건강tft 더단백 워터 프로틴 청사과 400mL",
        "categoryHierarchy": ["가공식품", "음료류", "기능성/건강음료", "기타기능성/건강음료"]
      },
      /* 이하 8개 제품 정보 생략 */
    ]
  },
  "image_tests": {
    "tested_count": 3,
    "conversion_count": 3,
    "accessible_count": 3
    /* 테스트 세부 결과 생략 */
  },
  "summary": {
    "scraped_count": 10,
    "total_available": 34,
    "current_page": 1,
    "total_pages": 4,
    "has_more_pages": true
  }
}

 

스크래퍼는 JSON 형식으로 결과를 출력합니다. 주요 구성 요소:

  • timestamp: 검색 시점의 타임스탬프
  • request: 요청 정보 (URL, 검색 매개변수)
  • metadata: 검색 메타데이터
  • pagination: 페이지네이션 정보 (총 페이지, 현재 페이지 등)
  • products: 검색된 상품 목록
    • count: 검색된 상품 수
    • items: 상품 상세 정보 배열
  • image_tests: 이미지 접근성 테스트 결과
  • summary: 검색 결과 요약

주의사항

  • 교육 및 연구 목적으로만 사용: 이 도구는 교육 및 연구 목적으로만 사용해야 합니다.
  • 불법적 활용 금지: 상업적 목적이나 불법적인 활동에 이 도구를 사용하는 것은 엄격히 금지됩니다.