호쌤
호쌤 Just For Fun

[Python 데이터 수집] Selenium을 활용한 인스타그램 이미지 수집

크리에이티브 커먼즈 라이선스 ITPAPER(호쌤,쭈쌤)에 의해 작성된 ≪[Python 데이터 수집] Selenium을 활용한 인스타그램 이미지 수집≫은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.
이 라이선스의 범위 이외의 이용허락을 얻기 위해서는 leekh4232@gmail.com으로 문의하십시오.

[Python 데이터 수집] Selenium을 활용한 인스타그램 이미지 수집

파이썬 데이터 분석 관련 수업을 진행하면서 만난 패션회사에 근무하시는 수강생 분께서 인스타그램에서 특정 해시태그에 대한 이미지를 수집하는 업무를 매일 수행하고 있다는 말씀을 하셨습니다. 그 말을 듣고 수업시간에 인스타그램에서 이미지를 수집하고 그 결과를 하나의 압축 파일로 저장하도록 하는 예제를 즉흥적으로 만들어 보았습니다.

01. 필요한 모듈 참조

이 예제에서는 인스타그램에서 수집한 이미지들을 하나의 압축 파일로 만들기 위해 zipfile 모듈도 함께 import 한다.

1
2
3
4
5
6
7
8
9
10
11
# 설치가 필요한 패키지
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from bs4 import BeautifulSoup
import requests

# 파이썬에 내장되어 있는 패키지
import datetime as dt
import os
import zipfile   
import shutil

#02. 브라우저 제어를 위한 객체 생성

1) 크롬이 모바일 장치로 인식되도록 속성을 변경

인스타그램의 PC사이트는 모바일용 웹 사이트보다 구조가 복잡하기 때문에 HTML DOM을 취득하기 다소 어려운 면이 있다. ChromeDriver에 옵션을 지정하여 객체를 생성하면 대상 웹 사이트가 모바일 브라우저로 인식하도록 지정할 수 있다.

1
2
3
4
5
6
7
8
options = webdriver.ChromeOptions()
mobile_emulation = {"deviceName": "Nexus 5"}
options.add_experimental_option("mobileEmulation", mobile_emulation)

# 크롬 브라우저를 백그라운드 프로세스 형태로 실행시키고자 하는 경우 아래의 옵션도 추가한다.
#options.add_argument('headless')
#options.add_argument('window-size=1920x1080')
#options.add_argument("disable-gpu")

2) 준비된 옵션을 적용한 상태로 크롬브라우저 열기

표시되는 경고메시지는 무시

1
2
3
4
# 맥이나 리눅스의 경우 파일 확장자가 없다. 윈도우의 경우 exe 확장자까지 명시해야 한다.
driver = webdriver.Chrome('./chromedriver', chrome_options=options)
# 모든 동작마다 크롬브라우저가 준비될 때 까지 최대 5초씩 대기
driver.implicitly_wait(5)
1
2
c:\users\leekh\appdata\local\programs\python\python37\lib\site-packages\ipykernel_launcher.py:1: DeprecationWarning: use options instead of chrome_options
  """Entry point for launching an IPython kernel.

#03. 이미지 수집

1) 수집을 원하는 페이지로 이동하기

인스타그램의 페이지 URL은 아래와 같은 형식을 띄고 있다. 특정 사용자의 feed인지, 특정 해시태그에 대한 피드인지를 구분하여 URL을 지정할 수 있다.

  • https://www.instagram.com/아이디/feed
  • https://www.instagram.com/explore/tags/태그
1
2
3
driver.get("https://www.instagram.com/explore/tags/python")
# 브라우저가 표시될 때 까지 3초간 프로그램 대기
time.sleep(3)

크롬브라우저에 모바일 인스타 페이지가 표시된다.

insta01

2) 스크롤을 진행하면서 이미지 컨텐츠의 URL 수집하기

스크롤 강제 이동

크롬브라우저에게 window.scrollTo(0, document.body.scrollHeight); 자바스크립트 구문을 실행하도록 명령하면 강제로 다음 페이지 컨텐츠를 로드할 수 있다.

HTML DOM 취득하기

웹 브라우저에서 직접 스크롤을 이동해서 컨텐츠를 로드해 보면 새로운 컨텐츠가 불러와지는 시점에 화면에서 사라진 이전 컨텐츠의 HTML DOM은 삭제되는 것을 알 수 있다.

insta01

그러므로 HTML DOM을 마지막에 한 번만 취득하는 것이 아니라 반복문 안에서 스크롤을 이동하는 동안 매 반복마다 취득하여 미리 준비한 리스트에 병합해야 한다.

이 과정에서 중복 이미지가 표시될 수 있기 때문에 집합 자료형(set)으로 변환한 후 다시 리스트로 되돌리는 방법을 사용하여 반복된 데이터를 제거해야 한다.

구현하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 이미지 목록을 저장할 빈 리스트
img_list = []

# 지정된 회차 동안 반복하면서 스크롤을 화면 맨 아래로 이동한다.
for i in range(0, 5):
    
    # 현재 브라우저에 표시되고 있는 소스코드 가져오기
    soup = BeautifulSoup(driver.page_source, 'html.parser')
    
    # srcset이라는 속성을 포함하는 모든 이미지 태그 가져오기 --> 리스트형으로 반환됨
    img = soup.select("img[srcset]")
    
    # 미리 준비한 리스트에 결합시킴
    img_list += img
    
    # 동일한 항목에 대한 중복제거
    img_list = list(set(img_list))
    
    # 수집 과정을 출력한다.
    print("%04d번째 페이지에서 %02d건 수집함 >> 누적 데이터수: %05d" % (i+1, len(img), len(img_list)))
    
    # 스크롤을 맨 아래로 이동
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

    # 다음 컨텐츠가 로딩되는 동안 1초씩 대기
    time.sleep(1)
    
img_list
▶ 출력결과
1
2
3
4
5
6
7
8
0001번째 페이지에서 06건 수집함 >> 누적 데이터수: 00006
0002번째 페이지에서 11건 수집함 >> 누적 데이터수: 00017
0003번째 페이지에서 08건 수집함 >> 누적 데이터수: 00025
0004번째 페이지에서 09건 수집함 >> 누적 데이터수: 00034
0005번째 페이지에서 04건 수집함 >> 누적 데이터수: 00038

[<img class="FFVAD" decoding="auto" sizes="360px" src="https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/e35/p1080x1080/91593480_596911587570977_8274429251861143431_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&amp;_nc_cat=105&amp;_nc_ohc=Bs8x1bgdtoAAX9Eenfh&amp;oh=c9c84187c414f83fad3f24e1c828add1&amp;oe=5EC00947" srcset="https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/p640x640/91593480_596911587570977_8274429251861143431_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&amp;_nc_cat=105&amp;_nc_ohc=Bs8x1bgdtoAAX9Eenfh&amp;oh=da76a14ff98ff6c2e7f6ab2fee46c4d2&amp;oe=5EC2CBA4 640w,https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/p750x750/91593480_596911587570977_8274429251861143431_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&amp;_nc_cat=105&amp;_nc_ohc=Bs8x1bgdtoAAX9Eenfh&amp;oh=ae862aa682ad7b29e8b15bbee73ef1a5&amp;oe=5EC25524 750w,https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/e35/p1080x1080/91593480_596911587570977_8274429251861143431_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&amp;_nc_cat=105&amp;_nc_ohc=Bs8x1bgdtoAAX9Eenfh&amp;oh=c9c84187c414f83fad3f24e1c828add1&amp;oe=5EC00947 1080w" style="object-fit: cover;"/>,
 ... 생략 ...]

2) 이미지 태그에 대한 리스트로 반복처리 수행

일반적인 src속성이 사용되지 않고 srcset 속성 안에 해상도별 이미지 URL이 정의되어 있다. 이 속성을 취득하여 맨 마지막 값을 얻기 위한 문자열 처리를 수행해야 한다.

insta01

1
2
3
4
5
6
7
8
9
10
11
12
# 이미지의 주소만을 담기 위한 
src_list = []
for t in img_list:
    srcset = t.attrs['srcset']              # srcset 속성 가져오기
    srcset_list = srcset.split(",")         # 쉼표 단위로 추출
    item = srcset_list[len(srcset_list)-1]  # 이미지 해상도가 가장 큰 마지막 원소를 선택
    url = item[:item.find(" ")]             # 첫 번째 글자부터 마지막 공백문자 전까지 잘라냄
    src_list.append(url)                    # 준비한 리스트에 추출결과 넣기
    
# 중복제거를 위해 집합으로 변경 후 리스트로 다시 변환
src_list = list(set(src_list))
src_list
▶ 출력결과
1
2
3
4
['https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/e35/91210875_655194788636460_7159464562839561219_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=100&_nc_ohc=cRHihJ91szIAX8m_KKe&oh=87d233b63a05955986e0be65733d907b&oe=5EC21F20',
 'https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/e35/72465194_207728523556644_2153826408530624467_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=100&_nc_ohc=Ox_cFAPbviYAX97UN12&oh=f0ce9ec027c66cbe85d7ec7731961d16&oe=5EC0DC63',
 'https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-15/e35/69186322_184966085849480_3445684836164959880_n.jpg?_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=108&_nc_ohc=R_0nEwDwHGUAX9d9seN&oh=d64d432c7a64c6118862ba54d41ab8dd&oe=5EC3614A'
 ... 생략 ...]

#04. 이미지 저장하기

1) 이미지가 저장될 폴더의 이름 만들기

1
2
datetime = dt.datetime.now().strftime("%y%m%d_%H%M")
dirname = "insta_%s" % (datetime)

2) 접속 세션 만들기

1
2
3
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"
session = requests.Session()
session.headers.update( {'User-agent': user_agent, 'referer': None} )

3) 이미지 저장하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 저장된 이미지 수를 카운트하기 위한 변수
count = 0

# 폴더 생성하기
if not os.path.exists(dirname):
    os.mkdir(dirname)
    
# 폴더 이름으로 빈 압축파일 생성
zipfile_name = '%s.zip' % dirname
insta_zip = zipfile.ZipFile(zipfile_name, 'w')

# 이미지 URL수 만큼 반복
for image_url in src_list:
    # 카운트 증가
    count += 1

    # 파일이 저장될 경로 생성
    path = "%s/%04d.jpg" % (dirname, count)

    # 예외처리를 동반한 이미지 다운로드
    try:
        # 이미지 주소를 다운로드를 위해 stream 모드로 가져온다.
        r = session.get(image_url, stream=True)

        # HTTP 상태코드가 성공을 의미하는 값이 아니라면 에러로 간주하고 except 블록으로 강제 이동
        if r.status_code != 200:
            raise Exception
            
        # 추출한 데이터를 바이너리(이진값) 쓰기 모드로 저장 -> 'wb'
        with open(path, 'wb') as f:
            f.write(r.raw.read())
            print( "[Ok] %s(이)가 저장되었습니다." % path )
        
        # 압축파일에 다운로드 된 이미지 추가
        insta_zip.write(path)
        
    except:
        # 이미지 다운로드 실패시 다음 이미지를 시도하기 위해 반복의 조건식으로 이동함
        print( "[Error] %s(은)는 저장에 실패했습니다." % path )
        continue

# 압축파일 닫기
insta_zip.close()
print( "%s 압축파일이 생성되었습니다." % zipfile_name )

# 이미지가 수집된 폴더 삭제
shutil.rmtree(dirname)
print( "수집 폴더가 삭제되었습니다." )
▶ 출력결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Ok] insta_200416_1439/0001.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0002.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0003.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0004.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0005.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0006.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0007.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0008.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0009.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0010.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0011.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0012.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0013.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0014.jpg(이)가 저장되었습니다.
... 생략 ...
[Ok] insta_200416_1439/0037.jpg(이)가 저장되었습니다.
[Ok] insta_200416_1439/0038.jpg(이)가 저장되었습니다.
insta_200416_1439.zip 압축파일이 생성되었습니다.
수집 폴더가 삭제되었습니다.
Rating:

크리에이티브 커먼즈 라이선스 ITPAPER(호쌤,쭈쌤)에 의해 작성된 ≪[Python 데이터 수집] Selenium을 활용한 인스타그램 이미지 수집≫은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.
이 라이선스의 범위 이외의 이용허락을 얻기 위해서는 leekh4232@gmail.com으로 문의하십시오.

comments powered by Disqus