본문 바로가기

Python/Crawler

Python과 Google API를 이용하여 인스타그램 크롤링 이후 이미지를 분석해보기

크롤링만 백날하면 무얼하나

크롤링은 파알못인 내가 봐도 어렵지 않은 존재다. 물론 천재들이 만들어준 라이브러리들이 다 해주는거지만 말이다. 모종의 이유로 인스타그램을 크롤링해보다가 넘쳐나는 이미지들을 어떻게 하면 괜찮은 정보로 만들 수 있을까 생각하게 되었다. 구글링해보면 좋은 방법들이 넘쳐나지만 내 방식대로 하고 싶은 마음이 생겼다. 이렇게 저렇게 시도해보다가 DATE도 가져올 수 있고, TAG도 가져올 수 있고, IMG도 가져올 수 있게 되었다. 그런데 이걸로 뭘 하지?

일단 매일 하던 일을 해봤다

인스타그램은 트위터와 달리 크롤링하기가 만만치 않다. 웹에 대해선 잘 모르지만 스크롤에 따라 동적으로 DIV가 변하기 때문인데 첫 번째 포스팅을 찾아간다고 모든 DIV가 남아있지 않다. 스크롤에 따라 사라졌다가, 나타났다가 하는 것이다.
첫번째 방법으로 포스팅을 클릭한 뒤 이를 따올 수 있게 했다. 방식은 다음과 같다.
셀레늄을 통해 인스타그램을 띄운다 > 첫 번째 포스팅을 클릭한다 > 현재 사진 크롤링 > 다음 버튼 누르기 > 사진 크롤링 > N번 반복
이 방법의 단점은 딱 봐도 보이지만 엄청 느리다. sleep 준 만큼 오래 걸린다고 생각하면 되는데 1초만 줘도 게시글이 100개면 100초다. 1분 40초란 시간이 얼마 걸리지 않는 것처럼 느껴지지만 롤로치면 게임 시작 후 블루가 나올때까지 걸리는 시간이다. 이 때에 많은 일들이 일어난다는 것을 생각하면 엄청나게 긴 것이다.

그래도 일단 만들었으니 코드는 공유해본다.

from seleniumrequestshtml import Chrome
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import NoSuchElementException

import urllib.parse
import urllib.request
import time
import datetime
import csv

#instagram 사진 크롤링

def scroll_down(webdriver):
    webdriver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(5)

#검색을 원하는 insta ID
insta_id = "id"
url = "https://www.instagram.com/" + insta_id

webdriver = Chrome('위치')
webdriver.get(url)
session = webdriver.requests_session
response = session.get(url)

webdriver.find_element_by_class_name("v1Nh3").click()

while True:
    time.sleep(1)

    overlapphoto = webdriver.find_elements_by_class_name("Yi5aA")

    if(len(overlapphoto) == 0):
        photo = webdriver.find_element_by_xpath("/html/body/div[3]/div[2]/div/article/div[1]/div/div/div[1]/img").get_attribute('src')
        photo_time = webdriver.find_element_by_class_name("_1o9PC").get_attribute('datetime')
        photo_time = photo_time.split(".")
        photo_time = photo_time[0].replace("-","_").replace("T","_").replace(":","_")
        urllib.request.urlretrieve(photo,photo_time+"_"+insta_id+".jpg")
    else :
        for n in range(len(overlapphoto)):
            temp = n+1
            photo = webdriver.find_element_by_xpath("/html/body/div[3]/div[2]/div/article/div[1]/div/div/div/div[2]/div/div/div/ul/li["+str(temp)+"]/div/div/div/div[1]/img").get_attribute('src')
            photo_time = webdriver.find_element_by_class_name("_1o9PC").get_attribute('datetime')
            photo_time = photo_time.split(".")
            photo_time = photo_time[0].replace("-","_").replace("T","_").replace(":","_")
            urllib.request.urlretrieve(photo,photo_time+"_"+str(temp)+"_"+insta_id+".jpg")

            try :
                webdriver.find_element_by_class_name("coreSpriteRightChevron").click()
            except NoSuchElementException :
                break
    try:
        nextbtn = webdriver.find_element_by_class_name("HBoOv")
        if (nextbtn.text == "다음") :
            nextbtn.click()
    except:
        break


방식을 사진으로 생각해보자


셀레니움으로 첫 번째 게시물을 선택하게 한다. 이는 element로 select 해서 찾아내면 된다.

이후 게시물의 사진을 크롤링하고, 다음 버튼을 클릭하게 한다. 그리고 이 과정을 반복한다. 다음 버튼이 없으면 끝. 한 게시글에 여러 사진이 있는 경우 문제가 발생하는데 이 때에는 사진 내에 버튼이 있는지 확인하게 한다.

있으면 마찬가지로 처리한다.

속도를 올려보자

이 전 방법은 확실하긴 하지만 느려도 너무 느렸다. 빠르게 처리하고 싶었기 때문에 썸네일을 이용하자고 생각했다. 동영상이나 여러장의 사진은 무시했다. 동영상은 따오는게 귀찮았고, 여러장의 사진은 사실상 같은 주제라고 봐도 무방했기 때문이다. 로직 짜기가 짜증났던 이유도 있다. 그래서 두번째 방법으로 스크롤을 내리며 미리보기의 JPG만 가져오게 했다. 다 가져왔는지는 포스팅 개수와 비교해서 맞으면 끝내고, 맞지 않으면... 영원히 돌아간다. 천 건이 넘는 계정도 해봤는데 괜찮았다. 그냥 일단 해보는거지 뭐. 팔 것도 아닌데.

from seleniumrequestshtml import Chrome
from selenium.webdriver.chrome.options import Options
from selenium.common.exceptions import NoSuchElementException

import urllib.parse
import urllib.request
import time
import datetime
import csv

def scroll_down(webdriver):
    webdriver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(3)

def add_photo(webdriver) :
    temp_list = []
    one_photo = webdriver.find_elements_by_class_name("FFVAD")
    for n in one_photo:
        temp = {}
        temp['alt'] = n.get_attribute('alt')
        temp['src'] = n.get_attribute('src')
        temp_list.append(temp)
    return temp_list
#넘어오는 형식은 [{alt:내용, src:주소1},{alt:내용, src:주소}]

#검색을 원하는 insta ID
insta_id = input("Input Your Insta ID : ")
url = "https://www.instagram.com/" + insta_id

webdriver = Chrome('위치')
webdriver.get(url)
session = webdriver.requests_session
response = session.get(url)

#포스트의 총 개수
len_post = webdriver.find_element_by_class_name('g47SY').text

photo_list = []
try:
    while True:
        for n in add_photo(webdriver):
            if n in photo_list:
                pass
            else:
                photo_list.append(n)

        scroll_down(webdriver)

        if(int(len_post) == len(photo_list)):
            break
except:
    pass
for i, n in enumerate(photo_list):
    urllib.request.urlretrieve(n['src'],str(i)+'.jpg')

방식은 간단하다. 그냥 스크롤을 주욱 내리면서 div를 가져오고, 리스트를 확인하여 중복될 경우 제낀다. 시간 조정이 중요할 것 같은데 3초정도로 무난했다. 시간은 훨씬 빨라졌다.

나는 무슨 사진을 많이 올릴까

술스타그램인건 모두가 다 아는 사실이지만 실제로 그런지 판단하고 싶었다. 이것을 일일히 눈으로 보고 분류하는 방법도 있겠지만 크롤링 한 데이터를 구글 이미지 검색을 이용해서 사진의 주제를 찾아낸다면 어떨까하고 생각했다.
무려 구글님은 URL도 제공해준다구! 하지만 일일히 셀레늄으로 붙여넣게 할 바에야 눈으로 보는게 빠르다. 이런 검색 결과를 어떻게 해야 빠르게 얻을 수 있을까 30분 정도 고민한 것 같다.

여기서 등장하는 API가 갓구글님의 Google Cloud Vision이다. 방금 이미지 검색한 사진을 API에 넣으면 JSON으로 결과를 떨궈준다.

API를 사용하는 방법은 간단하다. 구글 클라우드에 API 인증키를 발급받고, PATH에 등록해준 후 PYTHON에서 import하면 끝.

발급받은 API를 적당한 곳에 놔둔다.

환경변수를 등록해준다.

py 파일에서 불러와준다.

테스트는 구글에서 제공하는 코드를 그대로 따라하면 된다.

import io
import os

# Imports the Google Cloud client library
from google.cloud import vision
from google.cloud.vision import types

# Instantiates a client
client = vision.ImageAnnotatorClient()
#google 분석
# The name of the image file to annotate
file_name = os.path.join(
    os.path.dirname(__file__),
    'conssolee/1.jpg')

# Loads the image into memory
with io.open(file_name, 'rb') as image_file:
    content = image_file.read()

image = types.Image(content=content)

# Performs label detection on the image file
response = client.label_detection(image=image)
labels = response.label_annotations

print('Labels:')
for label in labels:
    print(label.description)

이렇게 LABEL 값이 출력된다. 나는 이 작업을 로컬 사진이 아닌 인스타그램 URL로 할 것이기 때문에 원격 연결을 했다.

def detect_labels_uri(uri):
    client = vision.ImageAnnotatorClient()
    image = vision.types.Image()
    image.source.image_uri = uri
    #detect방식에 따라 다름 label은 이미지에 포함된 사물을 인식해준다
    response = client.label_detection(image=image)
    labels = response.label_annotations
    # for label in labels:
        # print(label.description)
    #들어가는 요소는 mid, description, score
    return labels

바로 이렇게!
API 요청 한 번당 사진 16개까지 가능하다고(무료 기준) 써져 있는데 16개를 한번에 날리면 아마 더 빠르게 가능할 것이다. 이러한 라벨을 label list에 푸시하여 count를 세준다.
문제는 가중치를 무시하고 있고 비슷한 단어들을 묶어주지 않았다는 건데 일단은 라벨 태그만 확인하기로 했다. 분석 쪽은 잘 모를 뿐더러 월루 중이였기 때문에 ...
만들어진 라벨 리스트는 이런 형식이다.

#label list 태그를 카운트
    for n in label_list: 
        try : count_label_list[n] += 1
        except : count_label_list[n] = 1

    sort_label = sorted(count_label_list.items(), key=lambda x: x[1], reverse=True)

상위 20개의 라벨만 가져와서 시각화 해줬다.

    for n in sort_label:
        sort_label_list[n[0]] = n[1]
        if(len(sort_label_list) > 20):
            break
    plt.figure
    plt.plot(sort_label_list.keys(),sort_label_list.values())
    plt.xlabel('Label')
    plt.xticks(rotation=90)
    plt.ylabel('Label Count')
    plt.title(insta_id +"'s Label Count")
    plt.show()

글씨가 누워있어서 보기 힘들지만 난 FOOD LABEL이 많다. 74개의 포스팅 중 절반정도라고 생각할 수 있는 것이다. 어디선가 보이는 BEER가 자기 주장이 강해보이는데 아마 DRINK류와 묶어서 표현하면 순위가 더 올라갈 것 같다.

실제로 친구들의 다른 인스타그램도 분석해보니 계정주의 목적과 비슷한 결과가 나왔다. 주말에는 이 결과를 조금 더 신뢰성 있게 만들어봐야겠다.