반응형

시작하기

첫시도

Taling(https://taling.me)을 접속하면 놀랍게도 등록된 클래스들을 전체적으로 조회 할 수 있는 메뉴가 존재 하지 않았다.

그래도 희망을 가지고 해당 사이트를 분석을 해보았다.

taling 메인 페이지

먼저 메인 페이지에 접속해서 해당 네모칸에 검색어를 넣으면 검색된 강의들이 나오는데...

[https://taling.me/Home/Search/?query=노래] >> 해당 Url로 페이지가 조회된다. 이때, [query=] 해당 쿼리뒤에 아무 문자도 적지 않으면 전체 강의가 조회된다는 것을 발견했다!!!! 역시 인생은 잔머리!

더 분석을 해본 결과 Class101때와는 다르게 하단으로 스크롤링 할때마다 강좌가 더 조회되는 것이 아니라, 하단의 페이지 넘버가 존재한다. 한 페이지에 15개의 강좌가 조회되고 다음 번호를 누를때 마다 15개씩 반복적으로 조회된다.

그 후, 이제 조회할 데이터를 분석하였다.

1. 할인률

2. 강좌명

3. 원가, 할인 가격, 강의 방식(시간당)

4. 강의 위치

5. 별점

6. 강사 정보

이러한 데이터가 조회가능하다! 자 이제 크롤링 시자아아악!!!!!!

.....

1번부터 6번까지 데이터에 강좌별 카테고리 데이터가 없다! 쓸모 없는 데이터가 되버려따.. 어떻하지....

 

두번째 시도

마음을 가다듬고 다시한번 페이지를 분석해 보았다.

상단에 커다란 대메뉴(뷰티/헬스)가 있고 그 아래 소메뉴(메이크업/퍼스트컬러/패션/셀프케어...등)이 있고 해당 소메뉴를 클릭하면 그 메뉴에 맞는 데이터가 조회된다. 즉 카테고리 별로 강좌의 조회할려면 해당 방법으로 메뉴들을 클릭하면서 조회한 후, 저장을 해야한다는 것이다.

 

자, 이제 다시 개발할 내용을 정리해 보았다.

 

1. 한페이지당 15개씩 조회되는 강좌를, 마지막 페이지 까지 자동으로 넘어가면서 데이터를 수집하는  코드

2. 상단의 존재하는 대메뉴 > 소메뉴를 자동으로 클릭 및 이동하는 소스 코드

 

1번의 경우 첫시도 시 개발을 다 했음으로... 2번만 개발 하면된다!

 

개발환경

  • MAC OS Big Sur 11.2.3
  • Python3
  • Chrome 버전 90.0.4430.93(공식 빌드) (x86_64)
  • Chrome Driver 90.0.44

개발준비

사용한 패키지 및 도구

  • selenium, BeautifulSoup, pandas
pip3 install selenium pip3 install BeautifulSoup pip3 install pandas

 

  • ChromeDriver 설치 (자신이 로컬에 설치된 Chrome버전과 동일한 버전으로...)
    ChromeDriver - WebDriver for Chrome - Downloads
    Current Releases If you are using Chrome version 91, please download ChromeDriver 91.0.4472.19 If you are using Chrome version 90, please download ChromeDriver 90.0.4430.24 If you are using Chrome version 89, please download ChromeDriver 89.0.4389.23 If you are using Chrome version 88, please
    chromedriver.chromium.org

개발내용

다시한번 알려드리지만 필자는 파이썬 개발자가 아님. 그리고... 하다가..너무 귀찮아서 코드질이 많이 많이 떨어진다... 눈갱 주의

전체 소스 코드(펼치기 시 스크롤 압박 주의 ;;;;)

더보기
# 목표 - 탈잉에 등록된 모든 강의들의 정보(클래스 명, 카테고리, like, 가격, 정보 등)
# https://taling.me/Home/Search/?query= 접속 시, 전체 강의목록을 조회할 수 있다.
# 한 화면에 15개의 강의가 나오게 되고, 하단 다음 페이지 번호를 눌러서 15개씩 조회가 가능하다.
# 따라서 현재 화면의 15강의씩 가장 마지막 페이지 번호까지 넘어가면서 저장을 해야함.
# 단순 화면의 강의 데이터로는 카테고리 분류가 불가능한 상황임 > 상단 메뉴를 옮겨 가면서 카테고리 데이터를 가져와야함.
from selenium import webdriver
import time
import re
from bs4 import BeautifulSoup
import pandas as pd
from selenium.webdriver.common.action_chains import ActionChains


#####상수#####
#뷰티/헬스	 	- sub1 , #cate2
#액티비티		- sub2 , #cate3
#라이프		    - sub3 , #cate4
#취미/공예	 	- sub4 , #cate5
#머니 			- sub5 , #cate6ㅣ,ㅣㅏ,,ㅏㅣㅡㅓㅏ mkkkkkkk
#커리어	 		- sub6 , #cate7
#디자인/영상	 - sub7 , #cate8
#외국어 		- sub8 , #cate9
# 하위 메뉴 선택시 sub로 시작하는 숫자 시작과 #cate 숫자 시작값이 달라서 이를 보정하기 위한 상수
MATCH_FOR_CATE_AND_SUB_NUMBER = 1
TEMP_SAVE_POINT = 100


# 전체 수강 강좌 조회 사이트 열기.
driver = webdriver.Chrome("./chromedriver")
driver.get('https://taling.me/Home/Search/?query=')
time.sleep(3)

pageNumber = [0,1,2,3,4,5,6,7,8,9,10,11,12]
goNextCss = '#container > div.main3_cont > div.page > a:nth-child'
moveCategoriQuery = '//*[@id="sub@subNumber"]/div/li[@childNumber]/a'
#'#sub@subNumber > div > li:nth-child(@childNumber) > a'
# str.replace('@subNumber', '8')
# str.replace('@childNumber', '1')

result = [ ]
subList = []
cateList = []

currentSubName = ''
currentSubCate = ''


######################## 전체 메뉴 및 메뉴 이동 관련 로직 ###########################

#메뉴 클릭 로직 정리
# action = ActionChains(driver)
# driver.find_element_by_css_selector('#cate2').click() # 큰 메뉴 클릭 한 후
# action.move_to_element(driver.find_element_by_css_selector('#cate2')).perform() # 마우스 올리고 
# driver.find_element_by_css_selector('#sub1 > div > li:nth-child(4) > a').click() # 소메뉴 클릭

#전체 메뉴 리스트 (큰 subList) 가져오기
def getSubMenuList():
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	_subList = soup.select('li.cate > a > div')
	numOfSub = len(_subList)
	for i in range(numOfSub):
		subList.append(_subList[i].text.strip())
	print(subList)

def setcurrentSubName(idx):
	currentSubCate = subList[idx]


# 전체 소메뉴? 카테고리 조회
def getCategori():
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	sublen = len(subList)
	for i in range(sublen) :
		cate = soup.select('#sub' + str(i) + ' > div > li')
		catelen = len(cate)
		for j in range(catelen):
			cateList.append(cate[j].text)
	print(cateList)

#소메뉴 갯수 가져오기
def getLenCategoriInSub(subIdx):
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	cate = soup.select('#sub' + str(subIdx) + ' > div > li')
	return len(cate)


#대메뉴 이동
def clickSubMenu(idx):
	global currentSubName
	try:
		driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).click() # 큰 메뉴 클릭 한 후
	except:
		saveFile()
	time.sleep(0.5)
	####메뉴위로 마우스 이동 하기
	action = ActionChains(driver)
	action.move_to_element(driver.find_element_by_css_selector('#cate' + str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER))).perform() # 마우스 올리기
	currentSubName = driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).text
	print('**************************************  currentSubName>>>>>>>  ' + currentSubName)
	time.sleep(0.5)

#소메뉴 이동
def clickCate(subNum, cateNum):
	global currentSubCate
	base = moveCategoriQuery
	base = base.replace('@subNumber', str(subNum))
	base = base.replace('@childNumber', str(cateNum))
	print('Move Cate Query>>>>>>>>>>>>>   ' + base)
	try:
		currentSubCate = driver.find_element_by_xpath(base).text
		print('currentSubCate >>>>>>  '+ currentSubCate)
	except:
		saveFile()
	
	try:
		driver.find_element_by_xpath(base).click()
	except:
		saveFile()

	time.sleep(0.5)

def saveFile():
	global result
	_result = pd.DataFrame(result)
	_result.to_csv('/Users/kimyoungho/python/taling.csv', index=False, encoding="utf-8-sig")



################################################데이터 수집 소스 코드 #############################################

#아래 3개는 존재할 수도 없을 수도 있으니 예외처리 필수임.
# soup.select('div.cont2 > div')[0].select('div.day')[0].text  '1DAY수업'
# soup.select('div.cont2 > div')[0].select('div.d_day')[0].text.strip()    'D-50'
# soup.select('div.cont2 > div')[0].select('div.soldoutbox')[0].text.strip() 'SOLDOUT'
# soup.select('div.cont2 > div')[0].select('div.sale')[0].text	'50%할인'
# soup.select('div.cont2 > div')[0].select('span.reward_badge')[0].text   '30일 무료코칭'  , 리뷰 100건
# soup.select('div.cont2 > div')[0].select('div.title')[0].text.strip()   '[재무설계/저축/재테크/투자/주식] 미래설계를 위해 "돈"을 알려드립니다'
# soup.select('div.cont2 > div')[0].select('div.name')[0].text  '나종길'   이름
# soup.select('div.cont2 > div')[0].select('div.nick')[0].text    '미래설계사'  닉네임


####가격 데이터
#  soup.select('div.cont2 > div')[0].select('div.price1')[0].text.strip()   '\n₩11,000\n'
#  soup.select('div.cont2 > div')[0].select('div.price2')[0].text.strip()   '\n₩5,500\n'
#  soup.select('div.cont2 > div')[0].select('div.price > div.sale')[0].text.strip()   '50%'
#   soup.select('div.cont2 > div')[0].select('span.hour_unit')[0].text  '/시간'

#########평점 평가수 지역
#  soup.select('div.cont2 > div')[0].select('div.info2 > div.star')[0].text.strip()  '★★★★★'
#  soup.select('div.cont2 > div')[0].select('div.info2 > div.review')[0].text.strip()   '(26)'
# soup.select('div.cont2 > div')[0].select('div.info2 > div.location')[0].text.strip()   '온라인 Live'



# 현제 화면에 15개의 강의데이터 뽑는 쿼리.
#soup.select('div.cont2 > div')
def setClassList(driver, result) :
	global currentSubCate
	global currentSubName
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	classList = soup.select('div.cont2 > div')
	numOfClass = len(classList)

	classPeriod = ''
	classDday = ''
	classSale = ''
	classReward = ''
	classSoldout = ''
	classTitle = ''
	lectureName = ''
	lectureNick = ''

	originPrice =''
	salePrice =''
	saleRate = ''
	classunit = ''

	classStar = ''
	classReview = ''
	classLocation = ''

	for i in  range(numOfClass) :
		try:
			classPeriod = classList[i].select('div.day')[0].text
		except:
			classPeriod = ''
		
		try:
			classDday = classList[i].select('div.d_day')[0].text.strip()
		except:
			classDday = ''
		
		try:
			classSoldout = classList[i].select('div.soldoutbox')[0].text.strip()
		except:
			classSoldout = ''
		
		try:
			classSale = classList[i].select('div.sale')[0].text
		except:
			classSale = ''

		try:
			classReward = classList[i].select('span.reward_badge')[0].text
		except:
			classReward = ''

		try:
			classTitle = classList[i].select('div.title')[0].text.strip() 
		except:
			classTitle = ''
		
		try:
			lectureName = classList[i].select('div.name')[0].text
		except:
			lectureName = ''

		try:
			lectureNick = classList[i].select('div.nick')[0].text
		except:
			lectureNick = ''

		try:
			originPrice = classList[i].select('div.price1')[0].text.strip()
		except:
			originPrice = ''

		try:
			salePrice = classList[i].select('div.price2')[0].text.strip()
		except:
			salePrice = ''

		try:
			saleRate = classList[i].select('div.price > div.sale')[0].text.strip()
		except:
			saleRate = ''

		try:
			classunit = classList[i].select('span.hour_unit')[0].text
		except:
			classunit = ''

		try:
			classStar = classList[i].select('div.info2 > div.star')[0].text.strip()
		except:
			classStar = ''

		try:
			classReview = classList[i].select('div.info2 > div.review')[0].text.strip()
		except:
			classReview = ''

		try:
			classLocation = classList[i].select('div.info2 > div.location')[0].text.strip()
		except:
			classLocation = ''
		result.append([currentSubCate, currentSubName, classTitle, lectureName, lectureNick, classPeriod, classDday, classSale, classReward, originPrice, salePrice, saleRate, classunit, classStar,classReview, classLocation, classSoldout])
		
		# result.append(classList[i])
	print(len(result))
	global TEMP_SAVE_POINT
	if len(result) > TEMP_SAVE_POINT:
		print('temp save >>>>>>>>>>>>')
		saveFile()
		TEMP_SAVE_POINT = TEMP_SAVE_POINT + 1000


# 하단에 화면 번호를 클릭하는 쿼리
# 첫화면은  a:nth-child(2) 의 번호가 2 > 3 > 4 .... > 10 > 11(11은 다음으로)
# 그 다음부터는 "이전 버튼이 활성화 되어서" 3 > 4 > 5 > 6 ... > 11 > 12 (12는 다음으로)
# 위와 같은 형식으로 조회가 돠어야 함.
# 다음 조회목록으로 이동하기 위해 클릭하는 로직.
# driver.find_element_by_css_selector('#container > div.main3_cont > div.page > a:nth-child(2)').click()
def nextList(idx,driver):
	try:
		nextButton = driver.find_element_by_css_selector(goNextCss + '(' + str(pageNumber[idx]) + ')')
	except:
		return True
	nextButton.click()
	time.sleep(2)
	return False

def getBottomPageNum() :
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	return len(soup.select('div.page > a'))

def startCrawling():
	pageNum = getBottomPageNum()
	if pageNum < 11 :
		for i in range(2, 2 + pageNum): 
			setClassList(driver, result)
			nextList(i,driver)
	else :
		#첫페이지만 따로 10번 돌린다... 인덱스가 달라서
		#TODO 처음 페이지 수가 
		for i in range(2, 12): 
			setClassList(driver, result)
			nextList(i,driver)
		goNextCssNumber = 3
		while(1):
			setClassList(driver, result)
			if(nextList(goNextCssNumber,driver)) :
				break
			else :
				goNextCssNumber = goNextCssNumber + 1
				if(goNextCssNumber > 12) :
					goNextCssNumber = 3	

	print("Crawling Complete....")
	# time.sleep(2)



##################################### Main Code ##################################################


getSubMenuList()
getCategori()
numOfSumList = len(subList)
for subMenuNum in range(1, numOfSumList):
	print('NumOf MENU>>>>>>>>>>' + str(subMenuNum))
	cateLen = getLenCategoriInSub(subMenuNum)
	clickSubMenu(subMenuNum)
	for cate in range(1, 1 + cateLen):
		clickCate(subMenuNum, cate)
		startCrawling()
saveFile()





 

간략하게 로직을 ... 아니 절대 간략하지 않다!

1. 탈잉에 접속한다.

2. 대 메뉴 클릭 > 대 메뉴 위에 마우스 오버

3. 소 메뉴 클릭

4. 15개의 강좌 크롤링 > 다음 페이지 넘기기 반복

5. 다음 소 메뉴 or 대 메뉴 클릭

 

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 이것만 보면 아주 단순해 보인다...하지만 ㅠㅠㅠㅠㅠㅠ 쉽지 않았다. 아니 솔직히 어렵지는 않았는데.. 노가다가 엄청 났다.

[2. 대 메뉴 클릭 > 대 메뉴 위에 마우스 오버] 해당 로직의 경우 좀 설명이 필요한게... 

두 이미지를 봐보자. 차이는 첫 번째 화면은 필터 기능을 제공한다는 것이고 두번째 이미지는 소 메뉴가 보인다는 것이다.

마우스가  대메뉴 위에 있을때 Drop Down으로 소메뉴가 화면에 보이게 되고 다른 곳을 클릭하면 필터링 기능을 제공한다.

이때 문제가 발생하는것이.. 우리는 소메뉴를 클릭을 해야한다. 하지만 소메뉴가 화면에 나타나지 않을 경우 

"selenium.common.exceptions.ElementNotInteractableException: Message: element not interactable"

이러한 오류에 마딱트리고 만다. 즉 화면에 보이는 Element만 클릭이 가능하다는 것이다. 그러기 위해서는 클릭 전에 대메뉴에 마우스를 올려서 소메뉴가 보이도록 해야한다는 말이다..그러한 로직을 구현한게 아래 부분이다.

#대메뉴 이동
def clickSubMenu(idx):
	global currentSubName
	try:
		driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).click() # 큰 메뉴 클릭 한 후
	except:
		saveFile()
	time.sleep(0.5)
	####메뉴위로 마우스 이동 하기
	action = ActionChains(driver)
	action.move_to_element(driver.find_element_by_css_selector('#cate' + str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER))).perform() # 마우스 올리기
	currentSubName = driver.find_element_by_css_selector('#cate'+ str(idx + MATCH_FOR_CATE_AND_SUB_NUMBER)).text
	print('**************************************  currentSubName>>>>>>>  ' + currentSubName)
	time.sleep(0.5)

그리고 메뉴 이동의 경우 버튼들의 Xpath를 분석해 보니 아래 규칙으로 움직이는 것을 파악했다.

moveCategoriQuery = '//*[@id="sub@subNumber"]/div/li[@childNumber]/a'
# str.replace('@subNumber', '8') 해당 데이터를 replace해가면서 이동
# str.replace('@childNumber', '1') 해당 데이터를 replace해가면서 이동

 

실행결과

탈잉 크롤링

크롤링 하는 로직을 제외하고 화면 이동 하는 것만 동영상으로 찍어 봤다. 해당 방식으로 메뉴를 이동하면서 데이터를 가져오는 것이다.

나도 처음 해보는거라서...진짜 신기하긴 했다. 이렇게 크롤링을 하니 수집한 데이터를 대메뉴 > 소메뉴 별로 나눌 수 있었다.

 

마무리

점점...크롤링 실력이 늘어가는 것은 착각인가...?

이번 탈잉 사이트를 크롤링하면서 다른 사이트보다 훨씬 강좌수가 많이 있다는것에 놀랐다. 다른 사이트보다 강좌들이 온라인 보다 오프라인 위주라는 것, 원데이 클래스가 많이 있다는 것도 알게 되었다. 그리고...이제 또 그 많은 데이터들로 어떠한 유의미한 정보를 파악할 수 있을지도 스터디 중(사실 이것부터 시작이다) 

사이트를 보니 약 6500개의 강좌가 있는거 같은데... 내가 크롤링한 데이터는 6000개 정도 이다..(500개 어디 갔지???)

이번에 작성한 프로그램 정확도가 100프로도 아니고, 코드 질도 많이 떨어 지지만... 그래도 결과 자체는.. 나름 너무 뿌듯 >_< 헤헤

시간 날때 천천히 코드 질도 올리고 사라진 500개의 데이터도 찾아 봐야겠다... 찡긋 >_o

반응형

'개발 > Python' 카테고리의 다른 글

[Python] Class101 Crawling  (0) 2021.05.18
[Python] Instagram Crawling  (0) 2021.05.13
반응형

시작하기

Class101의 경우, 로그인 후 "https://class101.net/search?state=sales&types=klass" 접속 시, 현재 시점으로 바로 수강 가능한 강의 목록을 가져 올 수 있다. 하지만 바로 모든 강의가 조회되는 것이 아닌 스크롤링을 아래로 향할때 마다 일정 갯수 만큼 받아 오도록 화면이 구성되어 있다. 따라서, 모든 데이터를 크롤링 하기 위해선 반복적으로 하단 스크롤링을 한 후, 모든 데이터가 조회가 된 후 크롤링 작업을 시작해야 한다.

 

개발환경

  • MAC OS Big Sur 11.2.3
  • Python3
  • Chrome 버전 90.0.4430.93(공식 빌드) (x86_64)
  • Chrome Driver 90.0.44

개발준비

사용한 패키지 및 도구

  • selenium, BeautifulSoup, pandas
pip3 install selenium pip3 install BeautifulSoup pip3 install pandas
  • ChromeDriver 설치 (자신이 로컬에 설치된 Chrome버전과 동일한 버전으로...)
    ChromeDriver - WebDriver for Chrome - Downloads
    Current Releases If you are using Chrome version 91, please download ChromeDriver 91.0.4472.19 If you are using Chrome version 90, please download ChromeDriver 90.0.4430.24 If you are using Chrome version 89, please download ChromeDriver 89.0.4389.23 If you are using Chrome version 88, please
    chromedriver.chromium.org

개발내용

소스코드를 확인하기 전에 먼저 어떠한 방법으로 어떠한 데이터를가져올지를 파악해야한다.

class101에 등록된 신사임당 강의

1. 온라인 쇼핑 몰 / 신사임당 :  카테고리 및 강사 명

2. [🔥부활] 스마트스토어로 월 100만원 만들기, 평범한 사람이 돈을 만드는 비법 : 강좌 명

3. [39322]  / [98%] : 관심도 및 만족도

4. [월 23,740] / [5개월] : 가격및 기간

5. [199,000] / [40%] : 원가 및 할인률

 

대략적인 스크롤링 방법을 구상 한 후, 어떠한 데이터를 모일지도 파악완료 했다.ㄱㄱㄱ

참고로 필자는...파이썬 개발자가 아니고... 잠시 일적으로 필요해서... 파이썬으로 해당 프로그램을 작성했다.. 그래서 파이썬의 문법이나 코드가 매끄럽지 못하고 난잡할 수 있다는걸...참고..

전체 PythonCode

더보기
from selenium import webdriver
import time
import re
from bs4 import BeautifulSoup
import pandas as pd

# 목표 - class 101에 등록된 모든 강의들의 정보(클래스 명, 카테고리, like, 가격, 정보 등)
# http://class101.net/search?state=sales&types=klass 접속 시, 전체 강의목록을 조회할 수 있다.
# 하지만 스크롤링이 하단에 갈때마다 특정 갯수 만큼 다시 조회를 하는 형식이라서 전체 강의가 완전하게 조회 될때까지 계속해서 하단으로 스크롤링을 해야함
# 따라서 현재 화면의 강의수와 스클롤링 후 화면에 보이는 강의 수가 같아 질떄까지 계속해서 [스크롤링 > 강의 갯수 조회 ] 해당로직을 무한 반복한다.

# 스크롤링 하는 소스 코드
from selenium.webdriver.common.keys import Keys
# bg = driver.find_element_by_css_selector('body')
# bg.send_keys(Keys.SPACE)


import datetime
    
def doScrollDown(driver):
	start = datetime.datetime.now()
	end = start + datetime.timedelta(seconds=10)
	while True:
		driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
		time.sleep(1)
		if datetime.datetime.now() > end:
			break


# 현재까지 조회된 class 갯수를 가져 온다.
def getCurrentClassNum(driver) :
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	return len(soup.select('div.sc-citwmv > ul li'))

#classList[0].select('strong.SellingPrice-yffb6l-0')[0].text  '월 25,740원' main price
#classList[0].select('span.InstallmentText-v6zzig-0')[0].text  ' (5개월)' main date
#classList[0].select('div.CardTags-sc-1fo9d2v-0')[0].text '온라인쇼핑몰・신사임당' categori and 강사명
#classList[0].select('div.sc-dOSReg')[0].text '[🏆BEST특가] 스마트스토어로 월 100만원 만들기, 평범한 사람이 돈을 만드는 비법'  MainTitle
#classList[0].select('div.CountTag__Container-rjlblo-0')[0].text '39225' 하트그림 옆에 있는 숫자
#classList[0].select('div.CountTag__Container-rjlblo-0')[1].text  '98%'  따봉 수
#classList[0].select('span.OriginalPrice-fb3m0l-0')[0].text '199,000원'  original price
#classList[0].select('span.DiscountPercent-sc-1phvwfl-0')[0].text  '35% '  discount rate
def makeClassData(driver) :
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	classList = soup.select('div.sc-citwmv > ul li')
	numOfClass = len(classList)
	classData = ' '
	data = []
	for i in range(numOfClass) :
		classData = classList[i]
		try:
			categoriAndLecturer = classData.select('div.CardTags-sc-1fo9d2v-0')[0].text #'온라인쇼핑몰・신사임당' categori and 강사명
			categori = categoriAndLecturer.split('・')[0]
			lecturer = categoriAndLecturer.split('・')[1]
		except:
			categori = ''
			lecturer = ''
		
		try:
			mainTitle = classData.select('div.sc-dOSReg')[0].text #'[🏆BEST특가] 스마트스토어로 월 100만원 만들기, 평범한 사람이 돈을 만드는 비법'  MainTitle
		except:
			mainTitle = ''

		try:
			mainPrice = classData.select('strong.SellingPrice-yffb6l-0')[0].text # '월 25,740원' main price
		except:
			mainPrice = ''

		try:
			period = classData.select('span.InstallmentText-v6zzig-0')[0].text # ' (5개월)' main date
		except:
			period = ''

		try:
			attention = classData.select('div.CountTag__Container-rjlblo-0')[0].text # '39225' 하트그림 옆에 있는 숫자
		except:
			attention = ''

		try:
			satisfaction = classData.select('div.CountTag__Container-rjlblo-0')[1].text # '98%'  따봉 수
		except:
			satisfaction = ''

		try:	
			originalPrice = classData.select('span.OriginalPrice-fb3m0l-0')[0].text #'199,000원'  original price
		except:
			originalPrice = ''
		
		try:
			discountRate = classData.select('span.DiscountPercent-sc-1phvwfl-0')[0].text # '35% '  discount rate
		except:
			discountRate = ''
		data.append([categori, lecturer, mainTitle, mainPrice, period, attention, satisfaction, originalPrice, discountRate])
	return data


#스크롤링 3번하자`
def scrollDown(bg) : 
	bg.send_keys(Keys.SPACE)
	bg.send_keys(Keys.SPACE)
	bg.send_keys(Keys.SPACE)
	bg.send_keys(Keys.SPACE)
	bg.send_keys(Keys.SPACE)
	bg.send_keys(Keys.SPACE)

#https://class101.net/search?state=sales&types=klass
# 전체 수강 강좌 조회 사이트 열기.
driver = webdriver.Chrome("./chromedriver")
driver.get('http://class101.net/search?state=sales&types=klass')
bg = driver.find_element_by_css_selector('body')

# 현재 화면 html 정보 가져오기
#html = driver.page_source
#soup = BeautifulSoup(html, 'lxml')
# 강좌 정보 list 가져 오는 값
# soup.select('div.sc-citwmv > ul li')

currentNum = getCurrentClassNum(driver)
afterNum = 0
while(1) :
	doScrollDown(driver)
	afterNum = getCurrentClassNum(driver)
	if currentNum == afterNum :
		break
	else :
		currentNum =afterNum
		print(currentNum)

result = makeClassData(driver)
result = pd.DataFrame(result)
result.to_csv('/Users/kimyoungho/python/class.csv', index=False, encoding="utf-8-sig")
print(result)

 

간략하게 로직을 설명하자면...

1. ChromeDriver를 통해 Chrome실행 후, Class101 접속

2. 현재화면의 나타난 강의들의 목록을 찾고 갯수를 센다.

# 현재까지 조회된 class 갯수를 가져 온다.
def getCurrentClassNum(driver) :
	html = driver.page_source
	soup = BeautifulSoup(html, 'lxml')
	return len(soup.select('div.sc-citwmv > ul li'))

3. 하단으로 10초동안 스크롤링을 반복한다.( 이때, 강의 목록이 추가적으로 더 불러 오게 됨.)

def doScrollDown(driver):
	start = datetime.datetime.now()
	end = start + datetime.timedelta(seconds=10)
	while True:
		driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
		time.sleep(1)
		if datetime.datetime.now() > end:
			break

4. 이전의 샌 갯수와 조회 후 갯수를 비교한다. 하단 스크롤링 후 샌 갯수가 더 많다면 위 과정을 반복한다.

5. 만약 스크롤링 후에도 이전의 갯수와 스크롤링 후의 갯수가 같다면 더이상 조회할 강의가 없다는 뜻임으로... 크롤링으로 데이터를 수집 후 종료 한다.

currentNum = getCurrentClassNum(driver)
afterNum = 0
while(1) :
	doScrollDown(driver)
	afterNum = getCurrentClassNum(driver)
	if currentNum == afterNum :
		break
	else :
		currentNum =afterNum
		print(currentNum)

 

실행결과

실행 결과로 나온데이터로 아직 어떠한 유의미한 작업을 하지 않은 상태라 날것의 데이터 그대로가 있다. 작업을 하기 전에 이러한 데이터에서 어떤유의미한 결과를 도출해야 하는지 파악해야 하는데..이게 쉽지 않다. 그래서 저번 파이썬 크롤링때 처럼 워드클라우드라던가..하는건 추후에 올리도록 하겠다.

(뭔가...크롤링한 결과 여기다가 올리면 안될거 같아...알아서...하쇼...)

 

마무리

Python 문법도 재대로 모르는 놈이 파이썬으로 이것저것 크롤링을 하고 있는데... 나름..점점 재밌어 지고 있다.. 잡탕개발자의 길을 걷는듯..

인스타그램 크롤링 작업을 하면서 고생을 좀 많이 해서인가.. 이번 프로그램은 3시간정도 걸린거 같다. 크롤링 하는 방식도 단순하고 한번에 모든 강의를 조회할 수 있는 페이지가 있어서 정말 편한듯 ㅇ.ㅇ...

다음은 "탈잉" 사이트 크롤링 작업을 해볼 생각이다. 대충 화면 봤는데...난이도가 상당해 보이는게 벌써 망삘이 ...ㅠㅠ

반응형

'개발 > Python' 카테고리의 다른 글

[Python] Taling.me Crawling  (0) 2021.05.18
[Python] Instagram Crawling  (0) 2021.05.13
반응형

시작하기

Instagram의 경우, 로그인 후 "https://www.instagram.com/explore/tags/[태그문자]" 접속 시, 해당 [태그문자]로 올라온 게시물들을 확인 할 수 있다. 따라서 특정태그문자에 올라온 다양한 게시물들의 정보를 확보하여 다른 태그문자들과의 연관성 및 해당 게시물들의 이미지를 확보 할 수 있다.

개발환경

  • MAC OS Big Sur 11.2.3
  • Python3
  • Chrome 버전 90.0.4430.93(공식 빌드) (x86_64)
  • Chrome Driver 90.0.44

개발준비

사용한 패키지 및 도구

  • selenium, BeautifulSoup, pandas
pip3 install selenium pip3 install BeautifulSoup pip3 install pandas

 

  • ChromeDriver 설치 (자신이 로컬에 설치된 Chrome버전과 동일한 버전으로...)
    ChromeDriver - WebDriver for Chrome - Downloads
    Current Releases If you are using Chrome version 91, please download ChromeDriver 91.0.4472.19 If you are using Chrome version 90, please download ChromeDriver 90.0.4430.24 If you are using Chrome version 89, please download ChromeDriver 89.0.4389.23 If you are using Chrome version 88, please
    chromedriver.chromium.org

     

개발내용

소스코드를 확인하기 전에 먼저 어떠한 방법으로 어떠한 데이터를가져올지를 파악해야한다.

https://www.instagram.com/explore/tags/이직/ 를 통해 #카페 라는 해시태그를 사용한 게시물들을 가져온 화면이다.
(약 113,255개의 게시물)

여기서 특정 게시물을 클릭 하면 해당 게시물의 상세정보를 가져 올 수 있다.

 

해당 게시물에서 가져오길 원하는 데이터를 정리한다. (빨간색 테두리)

  1. 게시물의 이미지
  2. 게시물의 Description
  3. Hash Tag
  4. Like 수.
  5. 게시물의 위치정보(게시물에 따라서 위치정보를 입력한 게시물은 또 가져 올 수 있다.)

자! 이제 어떻게, 어떠한 데이터를 가져올지 파악했다. 실전으로 가즈아!

전체 Python 소스코드
더보기
from selenium import webdriver
import time
import re
from bs4 import BeautifulSoup
import pandas as pd


def searching(word):
    url = 'https://www.instagram.com/explore/tags/'+word
    return url

driver = webdriver.Chrome("./chromedriver")

def click_first(driver):
    first = driver.find_element_by_css_selector('#react-root > section > main > article > div:nth-child(3) > div > div:nth-child(1) > div:nth-child(1) > a > div.eLAPa > div._9AhH0')
    first.click()
    time.sleep(3)

def next_page(driver):
    next_page = driver.find_element_by_css_selector('body > div._2dDPU.CkGkG > div.EfHg9 > div > div > a._65Bje.coreSpriteRightPaginationArrow')
    next_page.click()
    time.sleep(3)

def get_content(driver):

    # 1. 현재 게시글 html 정보 가져오기
    html = driver.page_source
    soup = BeautifulSoup(html, 'lxml')


    # 2. 본문 내용 가져오기
    # 본문 내용이 없을 수 있으므로 예외 처리구문을 이용
    try:
        content = soup.select('div.C4VMK > span')[0].text
    except:
        content = ''

    # 3. 작성 일시, 좋아요 수 , 위치 정보 가져오기
    # 해쉬 태그는 정규 표현식을 이용해 가져온다.
    tags = re.findall(r'#[^\s#,\\]+', content)  

    #  작성일자 정보 가져오기
    date = soup.select('time._1o9PC.Nzb55')[0]['datetime'][:10]

    #  좋아요 수 가져오기
    # 예외처리구문.
    try:
        # print(soup.select('div.Nm9Fw.zV_Nj > span')[0].text)
        like = soup.select('div.Nm9Fw > a > span')[0].text   
    except:
        like = 0
    # 위치정보
    # 예외 처리구문
    try: 
        place = soup.select('div.M30cS')[0].text
    except:
        place = ''
    # 위치정보
    # 예외 처리구문
    try: 
        img = img = soup.select('div._97aPb img')[0]['src']
        print(img)   
    except:
        img = ''
    # 4. 저장하기
    data = [content, date, like, place, tags, img]
    return data


# 인스타그램 접속하기
driver.get('http://www.instargram.com')

#로딩하는데 시간이 걸릴 수 있으므로 3초간 대기
time.sleep(3)
insta_id = 'ID'   
input_id = driver.find_elements_by_css_selector('input._2hvTZ.pexuQ.zyHYP')[0]

password = 'PW!' 
input_pw = driver.find_elements_by_css_selector('input._2hvTZ.pexuQ.zyHYP')[1]

input_id.send_keys(insta_id)
input_pw.send_keys(password)

first = driver.find_element_by_css_selector("#loginForm > div.Igw0E.IwRSH.eGOV_._4EzTm.kEKum > div:nth-child(3)")
first.click()

time.sleep(5)

word = 'pet'
url = searching(word)

# 검색페이지 접속
driver.get(url)
time.sleep(3)

# 첫 번째 게시글 열기
click_first(driver)

#크롤링 결과를담을 리스트 생성
result = [ ]


# 여러 게시글 수집하기
target = 1000     # 크롤링할 게시글 수
for i in range(target):
    # 게시글 수집에 오류 발생시 5초 대기후, 다음 게시글로 넘어가도록 예외처리 구문 활용
    try:
        data = get_content(driver)    # 게시글 정보 가져오기
        result.append(data)
        next_page(driver)
    except:
        time.sleep(5)
        next_page(driver)
result = pd.DataFrame(result)
result.to_csv('/Users/kimyoungho/python/sample.csv', index=False, encoding="utf-8-sig")
print(result)

 

 

개발 완료?된 소스코드이다. 방법이 생소한거 뿐 작성된 소스코드는 간단한 편이다.

내용은 소스코드 실행 시, 자동으로 크롬을 실행하고 인스타를 접속및 로그인, 크롤링까지 자동적으로 되도록 하는 내용이다.

소스코드와 개발시 난항을 겪은 부분에 대해서 설명해보겠다.

실행 Flow

  1. Chrome실행 후, 인스타그램(http://www.instargram.com) 접속
  2. 로그인 후, https://www.instagram.com/explore/tags/이직/ 접속
  3. 첫 번째 게시물 클릭 → 데이터 수집 → 다음 게시물로 넘어가기 → 반복
#크롬 드라이버를 통해 크롬을 실행하고 인스타를 접속
driver = webdriver.Chrome("./chromedriver")
#이때 https로 접속하면...  크롬에서 보안관련 팝업이 나타나서..자동진행에 방해가 되니 http로 접속하자
driver.get('http://www.instargram.com')


......

#크롬 드라이버를 통해 크롬을 실행하고 인스타를 접속
driver = webdriver.Chrome("./chromedriver")
#이때 https로 접속하면...  크롬에서 보안관련 팝업이 나타나서..자동진행에 방해가 되니 http로 접속하자
driver.get('http://www.instargram.com')

실행결과

 

자동으로 로그인 하는 장면도 넣고 싶으나...이건...내 개인정보 ㅠㅠ.... 넣을 수 가 없었다. 
자동으로 게시물을 넘기는 장면만 넣었다. 동영상에서 볼 수 있듯 하나의 게시물을 3초에 한번씩 넘기면서 데이터를 수집하는 중이다.

아래 파일은 pet이라는 검색어로 얻은 데이터이다.(이거 올려도 되는건가...?ㅎㅎ)

 

sample_pet.csv
0.23MB

 

 

마무리

필자는 인스타를 하지 않아서.. 사실 인스타 자체를 파악하는데 더 많은 시간이 들었던거 같다. 인스타의 구조를 좀더 잘 파악하고 있었으면 시간을 낭비하지 않았을텐데...

크롤링이라는 것도 사실 개념만 알고 있었지 이렇게 실전에서 사용하는게 처음이라서 사실 처음에 앞길이 막막했다...(잘 사용하지 않는 언어 + 잘 모르는 개념 = 지옥)

또한 크롤링 시, 해당 html의 class나 태그명을 통해서 크롤링을 하는게..기본인데.. Instagram이 업데이트를 열심히 하는지 자주 바뀌는거 같다...

driver.find_element_by_css_selector('body > div._2dDPU.CkGkG > div.EfHg9 > div > div > a._65Bje.coreSpriteRightPaginationArrow')
soup.select('div._97aPb img')
#div._97aPb img 이런 값이 업데이트 되면 다시 찾아서 수정해줘야 함 ㅡㅡ...

바뀌고 나서 해당 값을 찾을때 개발자 도구로 찾아도 좋지만 Python3의 디버깅 모드로 한줄한줄 디버깅하면서 찾는게 정확한거 같다...(시간은 많이 들지만.. 확실)

 

마지막으로 여러가지 키워드로 얻은 데이터를 워드클라우드로 시각한 자료까지 공유하면서 마무리 찡긋 >_o

이직 키워드
서울 시청 키워드(카메라 사러 많이 오나 보다 ㅇ,ㅇ)

반응형

'개발 > Python' 카테고리의 다른 글

[Python] Taling.me Crawling  (0) 2021.05.18
[Python] Class101 Crawling  (0) 2021.05.18

+ Recent posts