스파르타 웹개발 종합반 프로젝트 만들기 - 데이터 크롤링 및 출력코드 작성

사용자가 항목과 타입을 모두 선택한 뒤에 dessert 버튼을 누르면 데이터를 불러와야한다.

데이터 크롤링

우선 항목관련 데이터베이스에 데이터를 삽입하고 난 조건에 맞는 데이터를 모은뒤 무작위로 하나를 보내는 기능을 구현한다.

데이터 크롤링은 파리바게트 메뉴 목록의 이름과 이미지를 크롤링했다.

selector로 원하는 값을 복사해서 원하는 데이터가 있는 경로를 확인했다.
이번 경우는 prodect-body내부의 category-block에 있는 이미지와 텍스트를 가져왔다.
우선 category-block의 경로를 복사했다.


import requests
from bs4 import BeautifulSoup

from pymongo import MongoClient
import certifi
ca = certifi.where()

client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert


#유저가 부른 것처럼 하기 위한 header 속성
headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
data = requests.get('https://www.paris.co.kr/products/?cat1=%EC%A0%84%EC%B2%B4',headers=headers)
soup = BeautifulSoup(data.text, 'html.parser')


category_block = soup.select('#main > div.page-body > div > div > div > section > div > div > div > div > div > div > div > div > div.product-body > div')

그다음 내부의 데이터를 반복수행으로 가져오는 코드를 작성했다.


for bread in category_block:
    desc_list = bread.select('div > div > ul > li')
    for desc in desc_list:
        br_list = desc.select_one('a > div.desc > h3')
        br_name = br_list.text
        img_list = desc.select_one('a > div.img > img.product-tb')
        img_name = img_list.get("src")

br_name : 빵 메뉴 이름
img_name: img url

해당 데이터를 bread라는 이름의 데이터베이스에 추가했다.

  doc = {'name' : br_name,
               'img' : img_name}
        db.bread.insert_one(doc)

현재 상황

성공적으로 이미지와 메뉴 이름이 추가되었다.

데이터 분류와 무작위 선택후 전송

사용자가 선택한 타입에 따른 데이터를 불러오는 기능을 구현한다.
먼저 선택한 항목의 값과 타입의 값을 선언한다. 이때 사용자가 항목은 선택하되, 아무런 항목을 선택하지 않으면 전체 타입을 선택한 것으로 간주한다.

  let dessert = $(":input:radio[name=type]:checked").val()
        var chk_arr = [];

        $("input[name=type_chkbox]:checked").each(function () {
            var chk = $(this).val();
            chk_arr.push(chk);
        });
        if(chk_arr = null){
            $('input:checkbox[name=type_chkbox]').each(function(){
                var chk = $(this).val();
                chk_arr.push(chk)
            })
        }

ajax POST로 dessertchk_arr를 서버로 보낸다.

$.ajax({
            type: "POST",
            url: "/Dessert",
            data: {dessert_give: dessert, type_give : chk_arr},

이때 서버는 값들을 받아와서 해당 항목의 이름과 일치하는 데이터베이스에서 타입 조건을 만족하는
메뉴 이름들을 출력한다.

항목 마다 데이터 베이스를 생성했으므로 해당 경우에 따라 각각 작성했다(bread, icecream, drink, snack). 우선 크롤링으로 데이터를 추가한 빵메뉴를 작성한다.

selecteddb = request.values['dessert_give']
    type_list = request.form.getlist('type_give[]')
    print(type_list)

selecteddb:사용자가 선택한 항목의 값. 데이터를 탐색할 데이터베이스의 이름
type_list: 사용자가 선택한 타입 목록

현재 데이터의 타입 값은 작성자가 직접 데이터에 추가했다.


type값으로 배열을 사용하여 복수로 들어갈수 있게 했다.

만약 selecteddb의 값이 bread일 경우, type_list 값 하나하나 마다 find를 사용해서 해당 조건을 만족하는 데이터를 추가한 listdata_list를 생성했다.

 if selecteddb == 'bread':
        for seltype in type_list:
            data_list = list(ddb.bread.find({'type': seltype},{'_id':False}))
            
   return jsonify({'name': data_list})

ajax에는 데이터 불러오기가 완료되었다는 것을 알리는 동시에 console.log로 데이터를 출력하게 작성했다.

 $.ajax({
            type: "POST",
            url: "/Dessert",
            data: {dessert_give: dessert, type_give : chk_arr},
            success: function (response) {

                for (let i = 0; i < tp.length; i++) {
                     let type = tp[i]['name']
               
                    console.log(type)
                }



                alert('dataload complete')

            }
        });

데이터 목록이 출력된다.

문제점 - 원하는 값이 나타나지 않는다.

단일 선택으로는 문제 없는듯 했으나, 복수의 타입을 선택하면 원하는 값이 나오지 않았다.

예를 들어, 빵에서 복수의 항목을 선택하면 그 항목들중에서 하나라도 만족하는 메뉴가 출력되게 해야하지만, 그렇게 출력되지 않는다. 모든 타입을 선택하고 버튼을 누르면

이렇게 2개 메뉴밖에 나오지 않는다.

해결 방법: 내가 반복분을 사용해서 해당 타입 값을 만족하는 데이터를 데이터베이스에서 find한것 까지는 좋았는데, data_list자체에 반복적으로 넣어서 값이 갱신이 되어버린것 같아서, 다른 빈 리스트에 집어넣어봤다.

    if selecteddb == 'bread':
        for seltype in type_list:

            data_list = list(ddb.bread.find({'type': seltype},{'_id':False}))
            for i in range(len(data_list)):
                  a.append(data_list[i])
    print(data_list)
    print(a)

결과

해당 조건을 하나라도 만족하는 메뉴들이 출력된 모습이지만, 중복된 항목들이 보여서 중복을 제거해야했다. 따라서 a리스트를 name값의 중복을 없앤뒤 다른 리스트에 값을 부여했다.
result = list({item['name']: item for item in a}.values())
그리고 result를 출력한 결과

중복까지 모두 사라졌지만, 내가 원하는 것은 랜덤으로 딱 '하나만' 출력하는 것이었으므로, random을 사용해서 선택된 데이터를 선언하여 리턴값으로 지정했다.
fin_result = random.choice(result)

그리고 이와 동시에 index.html의 ajax response를 수정했다. 보내지는 값이 리스트 딕셔너리가 아니어서 response값을 할당받는 변수를 수정해야 하며, 하나의 값에서 메뉴의 이름과, 이미지 url을 불러와야하므로

  let menu_name = response['name']['name']
                let menu_img = response['name']['img']
                console.log(menu_name,menu_img)

이렇게 수정하고 다시 실행했다.

성공적으로 무작위 메뉴의 이름과 그에 해당하는 이미지를 불러오는데 성공했다.

사용자가 만약 아무 타입도 선택하지 않은 경우

사용자가 아무런 타입을 선택하지 않은 경우는 그 항목에 해당하는 모든 타입을 선택한것으로 간주하고 메뉴를 선택하는 방식으로 구현했다.

index에서 전송하는 chk_arr의 길이가 0일 경우에 checkbox의 모든 val값을 담은 리스트empty_arr를 보내게 했다.


        $("input:checkbox[name=type_chkbox]").each(function () {
            var chk = $(this).val();
            empty_arr.push(chk);
        });

ajax에서 보내는 type_give에 조건을 부여해서 그에 따른 전송값을 다르게 지정했다.

data: {dessert_give: dessert, type_give : (chk_arr.length === 0) ? empty_arr : chk_arr},

html 에서 보낸 데이터를 서버에서 사용하면, html의 모든 checkbox의 val값이 선택되어버린다.
['sweet', 'stuff', 'soft', 'chrispy', 'hot', 'cold', 'sweet', 'bitter', 'fresh', 'bar', 'corn', 'cup', 'pack', 'box']
bread에 해당하는 타입은 4개 ('sweet','stuff','soft','chrispy')이므로 리스트를 나눠서 지정했다.

    if selecteddb == 'bread':
        sl_list = type_list[0:4]
        for seltype in sl_list:
            data_list = list(ddb.bread.find({'type': seltype},{'_id':False}))
            for i in range(len(data_list)):
                  a.append(data_list[i])

html에게서 전달받은 type_list의 3번째 인덱스 까지만 분리시킨 sl_list만큼 반복분을 수행하게 된다. 이렇게 하면 아무런 타입을 입력하지 않았을때 전체 항목을 선택한 것과 같은 결과를 나타나게 해준다.

type_listsl_list를 비교하면 이렇게 출력된다.

성공적으로 4개만 선택된 것을 볼수 있으며, 그에 해당하는 항목중 하나도 성공적으로 출력되었다.

그럼 사용자가 아무것도 안하고 버튼만 누르면?

그렇게 하면 오류 메세지를 alert함과 동시에 modalstyle을 수정하여 display값을 none으로 변경할 것이다.

먼저 오류를 인식하게 하기 위해 사용자가 아무메뉴(빵,음료,아이스크림,과자)를 선택하지 않은 경우 서버로 보내는 데이터를 따로 지정했다.

dessert_give로 전송되는 dessert 값이 null 이라면 dessert대신에 0이라는 값을 보내게 했다.

 data: {dessert_give: (dessert == null) ? 0 : dessert, type_give : (chk_arr.length === 0) ? empty_arr : chk_arr},

그 다음 서버에서 html로부터 전송받은 값이 0일경우에 리턴하는 데이터를 따로 지정했다. 작성자의 경우는 fin_result대신에 'error'라는 데이터를 보내게 했다.

app.py

  if selecteddb == '0':
        return jsonify({'name' : 'error'})

    print(type_list)

클라이언트가 response로 받은 데이터의 ['name'] 의 값이 error 라면 alert로 오류 문구를 나타내고, modalstyle을 수정한다.

  if(response['name']== 'error'){
                    alert('적어도 메뉴 하나는 골라주셔야 해요')
                    modal.style.display = 'none'
                }

현재 상황

아무것도 선택하지 않고 버튼만 누르면 이제 오류가 뜸과 동시에 이전 화면으로 돌아오게 되었다.

이제 나머지 항목의 데이터 크롤링과 type의 값을 부여하고 모두 제대로 동작하는지 확인하는 과정을 거친다.

음료수 데이터는 스타벅스 홈페이지의 메뉴에서 가져왔다. 빵 데이터와는 달리 javascript를 통해서 동적으로 생성되는 방식이므로 bs4 만으로는 크롤링이 어려웠기 때문에 셀레니움을 사용했다. 셀레니움을 사용하기 위해 chromewebdriver를 다운로드 했고 해당 프로젝트 내부에 저장했다.

from bs4 import BeautifulSoup
from pymongo import MongoClient
import certifi
from selenium import webdriver


ca = certifi.where()
client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert

# Webdriver 실행
dr = webdriver.Chrome('./chromedriver_win32/chromedriver.exe')


dr.get('https://www.starbucks.co.kr/menu/drink_list.do')

html_source = dr.page_source

soup = BeautifulSoup(html_source,  'html.parser')

drink_list = soup.select('.product_list dd a')

for drink in drink_list:
    drink_name = [drink.find('img')['alt']]
    img_name = [drink.find('img')['src']]

    doc = {
        'name': drink_name[0],
        'img' : img_name[0]
    }
    db.drink.insert_one(doc)

현재 상황

성공적으로 데이터를 추가했다.

아이스크림 데이터는 3곳에서 크롤링하여 데이터베이스를 생성했다.

  1. 롯데제과
from bs4 import BeautifulSoup
from pymongo import MongoClient
import certifi
from selenium import webdriver
import time

ca = certifi.where()
client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert

# Webdriver 실행
dr = webdriver.Chrome('./chromedriver_win32/chromedriver.exe')

dr.implicitly_wait(5)
dr.get('https://www.lotteconf.co.kr/brand/product?searchType1=LC700')

html_source = dr.page_source

soup = BeautifulSoup(html_source,  'html.parser')

#더 보기 항목을 클릭해서 항목을 모두 표시한다.
while True:
    try:
        btn = dr.find_element_by_css_selector("#content > div.max-1600 > div.list-type1.num4 > div > a.btn-more")
        btn.click()
        time.sleep(4)

    except:
        break
# 그 다음에 모든 항목의 이미지와 텍스트를 크롤링했다.
icecream_list = dr.find_elements_by_css_selector('a.button-more')
for icecream in icecream_list:
    img_list = icecream.find_element_by_css_selector('div.img > img')
    icecream_name = icecream.text
    img_name = img_list.get_attribute('src')
    print(img_name, icecream_name)

    doc = {
            'name': icecream_name,
            'img' : img_name
        }
    db.icecream.insert_one(doc)

2.빙그레

from bs4 import BeautifulSoup
from pymongo import MongoClient
import certifi
from selenium import webdriver
import time

from selenium.webdriver.common.by import By

ca = certifi.where()
client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert

# Webdriver 실행
dr = webdriver.Chrome('./chromedriver_win32/chromedriver.exe')

dr.implicitly_wait(5)
dr.get('http://www.bing.co.kr/brand/icecream')

html_source = dr.page_source

soup = BeautifulSoup(html_source,  'html.parser')

while True:
    try:
        btn = dr.find_element_by_css_selector("#more-brand-load")
        btn.click()
        time.sleep(3)


    except:
        break

icecream_list = dr.find_elements(By.CSS_SELECTOR, value='#product-normal-wrap > div')

for icecream in icecream_list:
    icecream_name = icecream.find_element_by_css_selector('a > div.product >div.product_tit strong')
    img_list = icecream.find_element_by_css_selector('div.img_wrap > img')
    ice_name = icecream_name.text
    img_name = img_list.get_attribute('src')

    doc = {
        'name': ice_name,
        'img': img_name
    }
    db.icecream.insert_one(doc)

3.롯데푸드

from bs4 import BeautifulSoup
from pymongo import MongoClient
import certifi
from selenium import webdriver
import time

from selenium.webdriver.common.by import By

ca = certifi.where()
client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert

# Webdriver 실행
dr = webdriver.Chrome('./chromedriver_win32/chromedriver.exe')
dr.implicitly_wait(5)


dr.get('https://www.lottefoods.co.kr/ko/brand/b2c')

while True:
    try:
        btn = dr.find_element(By.CSS_SELECTOR, value='button.btnMore.aniBtn')
        btn.click()
        time.sleep(3)

    except:
        break

html_source = dr.page_source

soup = BeautifulSoup(html_source,  'html.parser')


icecream_list = soup.select('a > span.img > span.img-inner')

for icecream in icecream_list:
    ice_name = icecream.find('img')['alt']
    img_name = icecream.find('img')['src']



    doc = {'name': ice_name,
           'img': img_name}
    db.icecream.insert_one(doc)

과자

롯데제과

from bs4 import BeautifulSoup
from pymongo import MongoClient
import certifi
from selenium import webdriver
import time

from selenium.webdriver.common.by import By

ca = certifi.where()
client = MongoClient('mongodb+srv://test:[email protected]/Cluster0?retryWrites=true&w=majority', tlsCAFile=ca )
db = client.dessert

# Webdriver 실행
dr = webdriver.Chrome('./chromedriver_win32/chromedriver.exe')

dr.implicitly_wait(5)
dr.get('https://www.lotteconf.co.kr/brand/product?searchType1=LC400')
# https://www.lotteconf.co.kr/brand/product?searchType1=LC500 파이
# https://www.lotteconf.co.kr/brand/product?searchType1=LC400 스낵
html_source = dr.page_source

soup = BeautifulSoup(html_source,  'html.parser')


while True:
    try:
        btn = dr.find_element_by_css_selector("#content > div.max-1600 > div.list-type1.num4 > div > a.btn-more")
        btn.click()
        time.sleep(2)


    except:
        break

snack_list = dr.find_elements(By.CSS_SELECTOR, value='a.button-more')
for snack in snack_list:
    img_list = snack.find_element_by_css_selector('div.img > img')
    snack_name = snack.text
    img_name = img_list.get_attribute('src')
    print(img_name, snack_name)

    doc = {
            'name': snack_name,
            'img' : img_name
        }
    db.snack.insert_one(doc)

데이터 베이스 설정은 모두 끝나서 다시 테스트를 해봤다.

오류 발생: index 범위가 맞지 않음

정리를 해보자. 현재 나는 체크된 항목(메뉴)의 정보를 체크된 radio의 val값을 보내고, 체크박스(타입)값을 리스트의 형태로 서버에 전송했다. 또한 사용자가 아무런 체크박스를 선택하지 않을 경우 작성한 모든 타입의 값을 리스트로 전송했다.
정상적으로 사용자가 원하는 타입을 선택해서 보냈다면 해당 메뉴가 보유하는 특성을 제외한 다른 특성들을 제외시킨 상태로 반복작업을 실시해야했다. 그랬기 때문에 sl_list = type_list[0:4] 와 같이 분리시켜서 작업을 했었다...
만 첫번째 메뉴인 빵의 경우는 제대로 작동하는듯 했으나 두번째 부터는 index 범위가 맞지 않는다는 문구가 나타나며 데이터를 출력하지 못했다.

나타난 이유는 아래와 같다. 내가 slice를 사용하기전에 전체값으로 받아온 type_list
['sweet', 'stuff', 'soft', 'chrispy', 'hot', 'cold', 'sweet', 'bitter', 'fresh', 'bar', 'cone', 'cup', 'pack', 'box'] 이렇게 4가지 메뉴가 보유하고 있는 특성을 순서대로 나열했다. 빵의 경우는 0에서 3번째 인덱스, 음료수는 4~8번째 인덱스, 아이스크림은 9~11번째, 과자는 나머지 인덱스를 slice한 값을 sl_list로 정의했다.
type_list의 값은 사용자가 선택한 항목을 list 형태로 전송하고, 만약 사용자가 빵 항목 선택을 하고 보냈다면(예를 들어 2개의 타입만 선택하고 보냈다면) 0번째에서 4번째까지는 어찌저찌 되겠지만, 사용자가 아이스크림의 항목중 2개만 선택하고 보내게 되면 slice 방식이 적용되지 않는다. 아이스크림의 sl_listtype_list의 9~11번째 인덱스를 slice한 리스트지만, 정작 type_list는 2개밖에 전송되지 않았기 때문이다.

따라서 index의 범위가 맞지 않다는 오류가 나타난 것

해결 방법

slice를 사용하지 않고, 각 항목마다 제거해야하는 리스트를 따로 정의해서 거기에 해당하지 않는항목만 추가시킨 리스트를 만들었다.

예를 들어 빵의 경우 필요하지 않는 type_list의 값은 'sweet', 'stuff', 'soft', 'chrispy'를 제외한 나머지 값이므로

remove_set={'hot', 'cold','bitter', 'fresh', 'bar', 'cone', 'cup', 'pack', 'box'}

이렇게 필요하지 않은 remove_set 을 만들었고, sl_listtype_list의 항목과 remove_set의 원소를 비교해서 같지 않으면 sl_list에 추가되는 반복문을 작성했다.

sl_list = [ i for i in type_list if i not in remove_set]
# type_list 내 항목중 remove_set 에 없는 항목만 sl_list에 들어간다.

위와 같이 나머지 메뉴들도 변경했다.

#음료수
 remove_set = {'stuff', 'soft', 'chrispy', 'bar', 'cone', 'cup', 'pack', 'box'}
        sl_list = [i for i in type_list if i not in remove_set]
#아이스크림        
 remove_set = {'sweet', 'stuff', 'soft', 'chrispy', 'hot', 'cold', 'sweet', 'bitter', 'fresh', 'pack', 'box'}
        sl_list = [i for i in type_list if i not in remove_set]
#과자
 remove_set = {'sweet', 'stuff', 'soft', 'chrispy', 'hot', 'cold', 'sweet', 'bitter', 'fresh', 'bar', 'cone', 'cup'}
        sl_list = [i for i in type_list if i not in remove_set]
 

이렇게 반복 작업을 수행하는 길이(sl_list)수정하고 실행한 결과.

이제 데이터베이스에서 데이터를 출력하는것 까지 모두 구현했다.

좋은 웹페이지 즐겨찾기