Categories
아무코드저장

Let’s encrypt 갱신하기

https로 사이트를 운영하기 시작하면서 (이 사이트는 여전히 라즈베리파이4로 운영되고 있고, 한 번도 다운되거나 한 적이 없다.) 무료로 SSL 인증서를 사용하다보니 가끔씩 갱신 주기를 놓쳐서 새로 발급 받아야 하는 경우가 생긴다. 물론 자동화 스크립트를 걸어두고 주기적으로 알아서 동작하도록 하면 되는데, 나의 미천한 리눅스 관리 실력으로는 아직 제대로 되지 않아서 문제가 생기면 그제서야 다시 스크립트를 실행해둔다. (나는 서버 관리자가 안되길 잘한 것 같다. 물론 지금은 도커가 세상은 몰라도 서버 관리자는 구해준 것 같다만)

여하간 Let’s encrypt로 새로 SSL 인증서를 발급 받거나 내용을 갱신해야 할 경우에 아래와 같은 순서로 처리하면 된다. 그냥 내가 잊어버리더라도 여기서나마 찾기 쉽게 하려고 보관해둔다.

sudo service nginx stop

먼저 Nginx 서버를 내려야 한다. Let’s encrypt가 80포트를 통해서 인증서를 갱신한다고 하는데 뭐 나 처럼 서버 사용이 극히 적은 사용자라면 언제든지 내려도 괜찮겠다.

그 후 핵심이 되는 부분은 아래와 같다.

sudo letsencrypt certonly --standalone -d sirini.blog

위에서 sirini.blog에 자신의 도메인을 입력하면 된다. 즉 이 서버로 연결되는 도메인 중에 https 적용을 하고자 하는 도메인이다. 자세한 내용은 이전에 소개한 아래 사이트를 방문하여 자세한 내용을 확인하는 것이 도움이 된다. https://ryan-han.com/post/server/raspberry_server_1/

참고로 기존 인증서를 갱신하기 위해서는 아래 명령어로도 충분하다. 위에 소개한 것은 새로 발급 받을 때 쓰는 것이다.

certbot renew

SSL 인증서를 발급 받았으면 아래 명령어로 확인할 수 있다.

certbot certificates

저장 경로도 알려주는데, 인증서가 어디에 저장되었는지를 알아야 Nginx 서버 설정에 인증서 경로를 포함해서 웹서버가 인증서를 이용할 수 있다. 자세한 건 위의 링크에 있고, 현재 내 라즈베리파이에 nginx.conf 설정은 아래와 같다. (http { 이 안에 내용})

ssl_certificate /etc/letsencrypt/live/sirini.blog/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sirini.blog/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;

SSL이 갱신 되었다면 이제 다시 Nginx 서버를 올려준다. Nginx는 /etc/nginx/nginx.conf에 http 블럭에서 SSL 인증서 경로 및 설정을 참조하여 http 및 https 연결을 듣기 시작할 것이다.

개발 업무를 할 때만 해도 이렇게까지 게으르지는 않았는데, 이제는 정말 개발에서 손을 떼고 있다보니 이지경까지 오게된 것 같다. WWDC 2020도 얼마 남지 않았는데 나도 이제 정신 좀 차려야 하지 않나 싶다…!!!

Categories
아무코드저장

GSMARENA 스마트폰 정보 가져와 엑셀로 저장하기

회사에서 하던 업무가 변경되어서 이제는 더 이상 코딩할 일이 없을 줄 알았지만, 다시 코딩을 해야 할 순간을 만났다. 그건 다름 아닌 정보 수집…!

GSMARENA 사이트에 가 보면 거의 뭐 전세계에 출시된 스마트폰 정보들을 다 볼 수 있는데, 가끔 여기서 뭔가 찾아야 하거나 혹은 한 두가지 정보들의 추세를 확인하거나 해야 할 경우가 생긴다. 근데 이 가끔이 때론 생각지도 못한 순간에 찾아와 사람을 허탈하게 만들기도 한다.

다행인지 불행인지 나도 어느 덧 고참 직원이 되었고, 후배님이 나 대신 이런 수고를 하는 모습을 우연히 관찰만 했었는데, 그야말로 인간 크롤러처럼 페이지 열고 Ctrl + C / Ctrl + V 해서 엑셀에 붙여넣고 그 지난한 시간 후에 다시 수집된 데이터들을 놓고 해석하는 작업을 보다가 이건 뭔가 아니지 않나 싶어서 간만에 다시 파이썬 코드를 잡았다.

import requests 
from bs4 import BeautifulSoup 
from time import sleep 
from random import uniform 
from openpyxl import Workbook 
 
# Welcome message 
print('-----------------------------------------------------------') 
print('                      G S M A R E N A') 
print('-----------------------------------------------------------') 
print(' !Please make sure that you have the following libraries:') 
print(' 1) BeautifulSoup4 2) openpyxl 3) requests') 
print(' I recommend to install Anaconda3 for easy to use')
print(' and you should install *Tor* for secure networking') 
print('_______________Python script by Heegeun Park_______________\n\n') 

# Get target brands and limitation of product number from prompt
print('\n[>] Please input target brands (e.g: samsung (or) samsung apple google)')
t = input('>>> ')
targets = t.lower()
if len(targets) < 2:
    print('[!] Please input target brands... your input is:', t)
    exit()
l = input('>>> Set limitation of product number (1~200): ')
limit = int(l)
if limit < 1 or limit > 200:
    print('[!] Please input a valid number for setting limitation of product number... your input is:', l)
    exit()
 
# Set common variables 
_gsmarena = 'https://www.gsmarena.com/' 
_target_brands = targets.split(' ')
_prod_selector = 'div#review-body div.makers ul li a' 
_page_selector = 'div.review-nav div.nav-pages a' 
_excel_save_path = 'gsmarena_{}.xlsx'.format('_'.join(_target_brands)) 
_excel_workbook = Workbook() 
_excel_default_sheet = _excel_workbook.active 
_gathering_product_limitation = limit
_min_wait_second = 30
_max_wait_second = 90
_session = requests.Session() 
_headers = { 
    'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit 537.36 (KHTML, like Gecko) Chrome", 
    'Accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 
} 
_proxies = { 
    'http': 'socks5://127.0.0.1:9050', 
    'https': 'socks5://127.0.0.1:9050' 
} 
 
# Check my proxy IP for secure network 
real_ip = requests.get('http://icanhazip.com') 
fake_ip = requests.get('http://icanhazip.com', proxies=_proxies) 
print('[@] Real IP: ', real_ip.text.strip(), ', Tor IP : ', fake_ip.text.strip()) 
 
# It needs for server 
def wait_a_second(): 
    f = uniform(_min_wait_second, _max_wait_second) 
    sleep(f) 
 
# Get a page by using Tor network 
def get_page(url): 
    page = _session.get(url, headers=_headers, proxies=_proxies) 
    if page.status_code != 200: 
        print('[!] Failed to connect at ', url) 
        return None 
    wait_a_second() 
    print('[#] Get a page from: ', url)
    return page 

# Get a page and parsing html DOM
def html_select(url, pick):
    page = get_page(url)
    if page == None:
        return None
    dom = BeautifulSoup(page.content, 'html.parser')
    sel = dom.select(pick)
    return sel

# Parsing camera information at given url
def parse_camera_info(url):
    targets = {'cam1modules': '', 'cam1features': '', 'cam1video': '', 'cam2modules': '', 'cam2features': '', 'cam2video': ''}
    page = get_page(url)
    if page == None:
        return None
    dom = BeautifulSoup(page.content, 'html.parser')
    keys = targets.keys()
    for key in keys:
        sel = dom.select('td[data-spec=' + key + ']')
        if len(sel) == 0:
            continue
        value = sel[0].text.replace('<br />', '').strip().lower()
        if len(value) < 3:
            continue
        targets[key] = value
    
    # If this page is not a smartphone, just skip 
    k_len = 0
    for k in keys:
        if targets[k] == '':
            k_len += 1
    if k_len == len(keys):
        print('[?] This device might be not a smartphone... ', url)
        return None

    return targets

# Get a brand page
brands = html_select(_gsmarena, "div.brandmenu-v2 ul li a")
if brands == None:
    print('[!] Failed to connect GSMARENA... T_T)')

# Gathering product list for each smartphone brand
for b in brands:
    brand_name = b.text.lower().strip()
    brand_link = _gsmarena + b['href']

    # Check brand white list
    if brand_name not in _target_brands:
        print('[~] [', brand_name, '] is not my favorite... Just skip.')
        continue

    brand_all_pages = []
    brand_first_page = get_page(brand_link)
    if brand_first_page == None:
        continue

    brand_first_list = BeautifulSoup(brand_first_page.content, 'html.parser')
    phones_in_first_page = brand_first_list.select(_prod_selector)
    brand_all_pages.append(phones_in_first_page)

    # Get product lists from list page on GSMARENA
    brand_pages = brand_first_list.select(_page_selector)
    for p in brand_pages:
        page_link = _gsmarena + p['href']
        phones_in_other_page = html_select(page_link, _prod_selector)
        brand_all_pages.append(phones_in_other_page)

    print('[v] All [', brand_name, '] product links have been updated!')

    # Initialize Excel object
    _excel_workbook.create_sheet(title=brand_name)
    _xls = _excel_workbook[brand_name]

    # Gathering all phone specifications in $brand_name up to _gathering_product_limitation
    limit = 1
    row = 5
    for phones_in_page in brand_all_pages:
        for phone in phones_in_page:

            phone_url = _gsmarena + phone['href']
            cam_info = parse_camera_info(phone_url)
            if cam_info == None:
                continue
            keys = cam_info.keys()

            _xls['D3'] = 'MAIN CAMERA'
            _xls['G3'] = 'FRONT CAMERA'
            _xls['C4'] = 'PRODUCT'
            _xls['D4'] = 'MODULES'
            _xls['E4'] = 'FEATURES'
            _xls['F4'] = 'VIDEO'
            _xls['G4'] = 'MODULES'
            _xls['H4'] = 'FEATURES'
            _xls['I4'] = 'VIDEO'

            # Writing informations to given row
            srow = str(row)
            icol = ord('D')
            i_ = 0
            _xls['C'+srow] = phone.text
            for key in keys:
                point = chr(icol + i_) + srow
                _xls[point] = cam_info[key]
                i_ += 1
            row += 1

            print('[+] The (', phone.text.strip(), ') information has been added to Excel. [{}/{}]'.format(limit, _gathering_product_limitation))

            # check the limitation of gathering information
            if limit >= _gathering_product_limitation:
                print('[~] It has been exceeded the limitation.')
                break
            else:
                limit += 1
        
        # check the limitation and get out of loop if already exceeded
        if limit >= _gathering_product_limitation:
            break

    print('[*] [', brand_name, '] products have been updated!')

# Save all information to Excel file
_excel_workbook.remove(_excel_default_sheet)
_excel_workbook.save(_excel_save_path)
print('[Done] Please check this file: ', _excel_save_path)

위의 코드는 아직 완성본은 아니나, 기능 상에는 거의 문제가 없도록 구성되어 있다. 정말로 단순하게 처음에 만들었는데, 이거 너무 간만에 해서 감이 다 떨어져 버렸는지 생각지도 못한 문제들이 있어서 애를 먹었다.

첫번째 문제는 Too many request…가 뜨면서 막히는 문제. 이건 서버 입장에서 공격 당하는 거나 마찬가지니까 최소한의 방어를 위해 당연히 발동되어야 하는 거지만, 크롤러 성능 테스트를 하다가 알 수 없는 이유로 예외가 줄줄줄 터져 나가는 걸 보다 보니 이걸 아무래도 해결해야겠다 싶었다.

그래서 고심끝에, 지금 이 블로그를 돌려주고 있는 라즈베리파이에 Tor 네트워크를 설치하고, 파이썬에서는 프록시로 Tor 네트워크에 접속하여 다시 변경된 IP로 GSMARENA에 접속해서 페이지를 가져 오도록 변경했다. 프록시에 대한 개념만 수박 겉핥기식으로 알고 있다가 이런 일을 겪고서야 정신 차리고 공부를 했다니 부끄럽다.

두번째 문제는 수집된 데이터들의 중간 저장이다. 이건 생각만 해두고 아직 반영을 안했다. 왜냐면 저 코드로 데이터를 수집해서 원하는 결과는 일단 만들어서 보고까지 끝냈기 때문이다. ㅋㅋㅋ

중간 저장이 필요한 이유는 매번 새롭게 수집할 필요가 없기 때문이다. 기존에 수집된 내용들은 정말 특별한 이유가 없는 한 변경되지 않는다. 따라서 수집한 데이터를 우선 임시로 텍스트 파일에 저장해두고, 다음 번 크롤링에서는 임시 파일이 있으면 페이지를 읽지 말고 그냥 그 임시 파일을 읽어서 분석하면 그만이다. 제일 좋은 건 DB에 연동해서 저장해 두는 거겠지만…

이번 코딩을 하면서 간만에 도전 정신에 불타 올랐다. 프록시를 제대로 써 본 첫 케이스이고, 파이썬 beautifulsoup4의 위엄과 requests 대단함, 거기에 openpyxl의 편리함까지 정말로 감동 먹었다. 내 스크립트 언어의 시작점은 PHP 이지만, 그 끝은 단연코 Python 이 되리라.

코드 백업도 해둘 겸, 혹시 누군가가 필요할까 싶어서 이 곳 새로운 블로그에도 첫 코딩 글로 남겨 둔다. 크롤링은 정말 필요할 때만 하도록 하자.