본문 바로가기

파이썬 & 머신러닝과 딥러닝

영화 추천 시스템, 동적 크롤링

영화 추천 시스템의 기본 원리 구현

행렬 분해(Matrix Factorization) 기법을 사용하여 유저와 영화 간의 평점을 예측하는 방식

 

 

1. 행렬 A 정의 및 누락값 포함

A = np.array([[4,1,5,np.nan,1],
              [2,3,np.nan,2,3],
              [1,np.nan,4,1,3],
              [np.nan,2,4,np.nan,2],
              [1,np.nan,4,1,3]])

A는 영화 추천 시스템에서 유저와 영화 간의 평점을 나타내는 행렬

np.nan은 누락된 값(즉, 유저가 특정 영화에 대한 평점을 제공하지 않은 경우)을 나타냄.

 

 

2. 마스크 생성

mask = ~np.isnan(A)

mask는 A의 각 요소가 누락값이 아닌지(True) 또는 누락값인지(False) 나타내는 불리언 배열

np.isnan(A)는 A에서 nan이 있는 위치에 True를 반환하므로, ~ 연산자를 통해 반전시킴

 

 

3. 행렬 분해를 위한 초기화

k = 2
m, n = A.shape

U = np.random.rand(m, k)  # m행 k열의 랜덤 행렬
V = np.random.rand(n, k)  # n행 k열의 랜덤 행렬

U와 V는 A를 분해하기 위한 두 개의 행렬

U는 유저의 특성을, V는 영화의 특성을 나타냄

 

 

4. 경사하강법을 통한 최적화

for i in range(5000):
    A_pred = U @ V.T  # 예측값 구함

    E = np.zeros((m, n))
    E[mask] = A_pred[mask] - A[mask]  # 예측값과 실제값 간의 오차 계산

    U_grad = E @ V
    V_grad = E.T @ U

    U -= U_grad * lr
    V -= V_grad * lr

    loss = np.sum(E[mask] ** 2)
    print(loss)  # 손실값 출력

경사하강법을 사용하여 U와 V를 최적화

각 반복에서 예측값(A_pred)을 계산하고, 오차(E)를 업데이트하고 손실(loss)은 오차 제곱의 합으로 계산됨

이 값이 감소하는 것을 보면서 모델의 성능이 개선되는지를 확인

 

 

5. 누락값 채우기

A_filled = A.copy()  # A의 복사본 생성

zeros = np.zeros(A_pred.shape)
zeros[~mask] += A_pred[~mask]  # mask가 False인 위치에 A_pred 값을 추가

 

A_filled는 누락값을 채운 결과를 담을 배열

zeros는 A_pred와 같은 모양의 0으로 채워진 배열이며,

mask가 False인 위치에 A_pred의 값을 추가하여 누락값을 예측함

 

 

6. 추천 생성

for i in range(len(zeros)):
    a = zeros[i]
    for j in range(len(a)):
        if a[j] >= 4:
            print(f'{i}번째 유저, {j}번째 영화 추천!')

각 유저에 대해 예측된 평점이 4 이상인 영화에 대해 추천 메시지를 출력

즉, 예측값이 4 이상인 영화는 해당 유저에게 추천할 영화로 간주됨

 

 

7. 누락값 채우기

A_filled[~mask] = A_pred[~mask]  # mask가 False인 위치에 예측값 채우기

최종적으로 A_filled의 누락값(mask가 False인 위치)을 A_pred의 값으로 채움

이렇게 하면 원래 행렬 A에서 누락된 값을 예측된 값으로 대체하게 됨

 

 

8. 유저에게 영화 추천

zeros = np.zeros(A_pred.shape)
zeros[~mask] += A_pred[~mask]

누락된 값이 있는 위치에서만 예측값 A_pred를 zeros 배열에 넣기

 

for i in range(len(zeros)):
    a = zeros[i]
    for j in range(len(a)):
        if a[j] >= 4:
            print(f'{i}번째 유저, {j}번째 영화 추천!')

평점이 4 이상인 영화가 발견되면 해당 유저와 영화를 출력

 

 

→ 영화 추천 시스템에서 유저와 영화 간의 평점을 예측하여 누락된 값을 채우고,

    예측된 평점에 따라 추천할 영화를 결정하는 과정

경사하강법을 통해 U와 V를 최적화하여, 예측의 정확성을 높이고, 유저에게 추천할 영화를 출력함.

 

 


 

 

ratings 데이터로 행렬 인수분해를 통해서 누락값들을 예측하고 영화 추천

import pandas as pd
import numpy as np

# 1. ratings 데이터 로드
ratings = pd.read_csv('ratings.csv')

# 2. pivot_table을 사용하여 사용자-영화 행렬 생성
# 'userId'를 인덱스로, 'movieId'를 컬럼으로, 'rating'을 값으로 가지는 행렬 생성
df2 = ratings.pivot_table(index='userId', columns='movieId', values='rating').values

# 3. NaN이 아닌 값의 마스크 생성
mask = ~np.isnan(df2)

# 4. 잠재 요인 수 설정
k = 2
m, n = df2.shape  # m: 사용자 수, n: 영화 수
lr = 0.0005  # 학습률 설정

# 5. 사용자 행렬 U와 영화 행렬 V 초기화
U = np.random.randn(m, k)  # m x k 행렬
V = np.random.randn(n, k)  # n x k 행렬

# 6. 행렬 분해를 위한 반복 과정
for i in range(5000):
    # 6-1. 예측된 평점 행렬 계산
    df_pred = U @ V.T

    # 6-2. 오차 행렬 E 계산
    E = np.zeros((m, n))
    E[mask] = df_pred[mask] - df2[mask]  # NaN이 아닌 위치에 대해서만 오차 계산

    # 6-3. U와 V의 그래디언트 계산
    U_grad = E @ V  # 사용자 행렬 U에 대한 그래디언트
    V_grad = E.T @ U  # 영화 행렬 V에 대한 그래디언트

    # 6-4. U와 V 업데이트
    U -= U_grad * lr
    V -= V_grad * lr

    # 6-5. 손실 계산 및 출력
    loss = np.sum(E[mask] ** 2)  # NaN이 아닌 값에 대한 오차 제곱합
    print(loss)

# 7. 원래 행렬을 복사하여 결측값을 채우기 위한 행렬 생성
df_filled = df2.copy()

# 8. 예측된 평점으로 NaN 값을 대체
df_filled[~mask] = df_pred[~mask]

# 9. NaN이 아닌 평점으로 채워진 예측 결과 행렬 생성
zeros = np.zeros(df_pred.shape)
zeros[~mask] += df_pred[~mask]  # 예측된 평점으로 NaN이 아닌 위치만 업데이트

# 10. 영화 정보 로드
movies = pd.read_csv("movies.csv")
movie_names = {}
for i in range(len(movies)):
    a = movies.iloc[i]
    movie_names[a['movieId']] = a['title']  # 영화 ID를 키로, 제목을 값으로 저장

# 11. 영화 ID 가져오기
df = ratings.pivot_table(index="userId", columns="movieId", values="rating", aggfunc="sum")
columns = df.columns  # 영화 ID 리스트

# 12. 0번째 사용자에게 추천할 영화 선정
user = 0
a = zeros[user]
for i in range(len(a)):
    if a[i] > 4.8:  # 예측 평점이 4.8 이상인 경우
        idx = columns[i]  # 영화 ID
        name = movie_names[idx]  # 영화 제목 가져오기
        print(f'{name} 추천')  # 추천 출력

 

 

 


 

동적 크롤링

정적 사이트
동적 사이트 : 매크로(인스타 자동 좋아요, 콘서트 티켓, 자동 블로그 포스팅)
Selenium, Pyautogui, pyperclib

 

!pip install selenium
!pip install undetected-chromedriver
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import time

driver = webdriver.Chrome()  # Chrome 웹 드라이버 인스턴스 생성
driver.get('https://www.naver.com/')  # Naver 웹사이트로 이동
time.sleep(5)  # 페이지 로드를 기다리기 위해 5초 대기

search = driver.find_element(By.CSS_SELECTOR, 'input.search_input')  
# CSS 선택자를 사용하여 클래스 속성이 'search_input'인 input 요소를 찾는다.

search.send_keys('동대구')  # 검색창에 '동대구'라는 텍스트를 입력
time.sleep(1)  # 1초 대기
search.send_keys(Keys.RETURN)  # 엔터 키를 입력하여 검색 실행

 


 

인터파크 전시 '기대평' 찾기

import undetected_chromedriver as uc  # undetected_chromedriver 모듈을 가져옵니다.

# undetected_chromedriver를 사용하여 Chrome 브라우저 인스턴스를 생성합니다.
driver = uc.Chrome()

# CSS 선택자로 'li.navItem' 요소를 찾고, 각 요소에 대해 반복합니다.
# '기대평'을 찾은 경우 클릭 수행.
for i in driver.find_elements(By.CSS_SELECTOR, 'li.navItem'):
    if '기대평' in i.text:
        i.click()

 

 

기대평 페이지 1부터 10까지 클릭

import undetected_chromedriver as uc

driver = uc.Chrome()

인터파크 전시 사이트 들어가서 원하는 전시 클릭

 

for i in driver.find_elements(By.CSS_SELECTOR, 'li.navItem'):
    if '기대평' in i.text:
        i.click()

웹 페이지에서 '기대평'이라는 텍스트를 포함한 모든 li 요소를 찾아서 클릭

 

for i in driver.find_element(By.CSS_SELECTOR,'ol.pageNumWrap').find_elements(By.CSS_SELECTOR, 'li'):
    i.click()
    time.sleep(1)

웹 페이지에서 페이지 번호를 포함하는 목록을 찾고, 각 페이지 번호를 클릭하는 작업을 수행

클릭 후에는 페이지가 로딩될 시간을 주기 위해 1초간 대기

 

driver.find_element(By.CSS_SELECTOR, 'a.pageNextBtn.pageArrow')

웹 페이지에서 "다음 페이지" 버튼을 찾기 위한 코드


 

기대평 제목과 날짜만 스크래핑

import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
import time
import pandas as pd

# 드라이버 초기화
driver = uc.Chrome()

# 주어진 URL 열기
driver.get('https://tickets.interpark.com/goods/24011915')
time.sleep(5)  # 페이지 로딩 대기

# 기대평 탭 클릭
for i in driver.find_elements(By.CSS_SELECTOR, 'li.navItem'):
    if '기대평' in i.text:
        i.click()  # 기대평 클릭
        time.sleep(2)  # 클릭 후 잠시 대기
        break  # 한 번 클릭한 후 반복 종료

# 결과를 저장할 리스트 초기화
results = []

# 1부터 10까지 페이지 순회
for page in range(1, 11):
    # 제목과 날짜 수집
    title_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsTitleText')
    date_elements = driver.find_elements(By.CSS_SELECTOR, '.rightSide .bbsItemInfoList')

    for title, date in zip(title_elements, date_elements[1::4]):
        results.append({'제목': title.text, '날짜': date.text})  # 제목과 날짜 정보 저장

    # 다음 페이지 클릭
    if page < 10:
        page_button = driver.find_element(By.LINK_TEXT, str(page + 1))
        page_button.click()
        time.sleep(2)  # 페이지 로딩 대기

# 10페이지가 끝난 후, 화살표 클릭
try:
    next_button = driver.find_element(By.CSS_SELECTOR, "a.pageNextBtn.pageArrow")
    next_button.click()
    time.sleep(2)  # 페이지 로딩 대기
except Exception as e:
    print(f"화살표 클릭 중 오류 발생: {e}")

# 11부터 17까지 페이지 순회
for page in range(11, 18):
    # 제목과 날짜 수집
    title_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsTitleText')
    date_elements = driver.find_elements(By.CSS_SELECTOR, '.rightSide .bbsItemInfoList')

    for title, date in zip(title_elements, date_elements[1::4]):
        results.append({'제목': title.text, '날짜': date.text})  # 제목과 날짜 정보 저장

    # 다음 페이지 클릭
    if page < 17:
        page_button = driver.find_element(By.LINK_TEXT, str(page + 1))
        page_button.click()
        time.sleep(2)  # 페이지 로딩 대기

# 결과를 DataFrame으로 변환
df = pd.DataFrame(results)

# 엑셀 파일로 저장
df.to_excel('기대평_제목과_날짜_전체3.xlsx', index=False, engine='openpyxl')  # 인코딩 관련 설정은 필요 없음

# 드라이버 종료
driver.quit()

print("데이터 수집이 완료되었습니다.")

 

+) 전체 내용까지

import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
import time
import pandas as pd

# 드라이버 초기화
driver = uc.Chrome()

# 주어진 URL 열기
driver.get('https://tickets.interpark.com/goods/24011915')
time.sleep(5)  # 페이지 로딩 대기

# 기대평 탭 클릭
for i in driver.find_elements(By.CSS_SELECTOR, 'li.navItem'):
    if '기대평' in i.text:
        i.click()  # 기대평 클릭
        time.sleep(2)  # 클릭 후 잠시 대기
        break  # 한 번 클릭한 후 반복 종료

# 결과를 저장할 리스트 초기화
results = []

# 페이지 순회 시작
while True:
    # 현재 페이지의 제목, 날짜, 본문, 조회수, 댓글 수 수집
    title_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsTitleText')  # 제목 선택
    date_elements = driver.find_elements(By.CSS_SELECTOR, '.rightSide .bbsItemInfoList')  # 날짜 요소 선택
    content_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsText')  # 본문 내용 선택
    info_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsItemInfoList')  # 조회수와 댓글 수 요소

    # 제목, 날짜, 본문, 조회수, 댓글 수 수집
    for title, date, content in zip(title_elements, date_elements[1::4], content_elements):
        try:
            # 조회수와 댓글 수 추출
            view_count = ""
            comment_count = ""
            
            for info in info_elements:
                text = info.text
                if '조회' in text:
                    view_count = text
                elif '댓글' in text:
                    comment_count = text
            
            # 저장할 정보 추가
            results.append({
                '제목': title.text,
                '날짜': date.text,
                '본문': content.text,
                '조회수': view_count,
                '댓글 수': comment_count
            })
        except Exception as e:
            print(f"데이터 수집 중 오류 발생: {e}")

    # 페이지 번호 버튼 클릭
    try:
        page_buttons = driver.find_element(By.CSS_SELECTOR, "ol.pageNumWrap").find_elements(By.CSS_SELECTOR, "li")
        for button in page_buttons[1:]:  # 첫 번째 버튼은 현재 페이지이므로 제외
            button.click()
            time.sleep(2)  # 페이지 로딩 대기

            # 제목, 날짜, 본문 내용, 조회수, 댓글 수 다시 수집
            title_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsTitleText')
            date_elements = driver.find_elements(By.CSS_SELECTOR, '.rightSide .bbsItemInfoList')
            content_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsText')
            info_elements = driver.find_elements(By.CSS_SELECTOR, '.bbsItemInfoList')

            for title, date, content in zip(title_elements, date_elements[1::4], content_elements):
                try:
                    # 조회수와 댓글 수 추출
                    view_count = ""
                    comment_count = ""
                    
                    for info in info_elements:
                        text = info.text
                        if '조회' in text:
                            view_count = text
                        elif '댓글' in text:
                            comment_count = text
                    
                    # 저장할 정보 추가
                    results.append({
                        '제목': title.text,
                        '날짜': date.text,
                        '본문': content.text,
                        '조회수': view_count,
                        '댓글 수': comment_count
                    })
                except Exception as e:
                    print(f"데이터 수집 중 오류 발생: {e}")

        # '다음' 버튼 클릭
        next_button = driver.find_element(By.CSS_SELECTOR, "a.pageNextBtn.pageArrow")
        next_button.click()
        time.sleep(2)  # 페이지 로딩 대기

    except Exception as e:
        print("더 이상 페이지가 없습니다.")
        break

# 결과를 DataFrame으로 변환
df = pd.DataFrame(results)

# 엑셀 파일로 저장
df.to_excel('기대평_전체_데이터.xlsx', index=False, engine='openpyxl')

print("데이터 수집이 완료되었습니다.")

# 드라이버 종료
driver.quit()

 


 

블로그

driver.switch_to.frame("mainFrame")

from selenium.webdriver.common.by import By
driver.find_element(By.CSS_SELECTOR, "div.pcol1").text

driver.find_element(By.CSS_SELECTOR, "div.se-main-container").text