Categories
아무말대잔치

맥북

나는 노트북이 3대 있다. 2010년 맥북 프로 13인치, 2015년 맥북 프로 15인치, 삼성 노트북 9 (2016년형). 이 중 2010년 맥북 프로는 사실상 사용하지 않아 동면중이다. 어디 팔기에도 이미 10년 전 제품이라 민망하고, 나도 딱히 팔아야 겠다는 생각도 들지 않아서 그냥 가지고만 있다. 아마 언젠가 그냥 폐기할 거 같다.

삼성 노트북 9은 해외 출장을 나갈 때 챙겨간다. 일단 다니는 회사에 노트북 라인업도 있는만큼 대외 활동할 때는 그 만한 노트북이 없다. 890g의 무게는 출장 나갈 때의 무거운 어깨를 좀 덜어주는 효과도 있다. 주로 오피스 제품만 사용해서 별 다른 기능도 필요 없고, Micro HDMI to HDMI 변환 케이블을 몇 개씩 들고 다니면서 발표할 때 주로 쓴다.

마지막 맥북 프로 15인치가 아직까지 현역으로 여러 방면에서 사용중인 노트북이다. 상해 난징동루 애플스토어에 가서 카드로 긁어서 샀다. 그 때 2010년형 13인치 맥북 프로로 아주 허접한 동영상 편집을 하고 있을 때였는데, 도저히 편집 하다가 속 뒤집어져서 앞뒤 안재고 그냥 질렀다. 터치바 달린 모델이 막 나온 시점이었는데, 썬더볼트 달랑 4개 달린 포트를 보고 실망해서 작년 모델을 그냥 가져온 기억이 난다. 포트도 포트지만 나비식 키보드 이슈를 생각하면 정말 잘한 결정이라 생각한다.

이 노트북으로 주로 Final Cut Pro X를 이용해서 영상 편집을 하고 있다. 요즘은 가끔 파이썬 프로그래밍도 하고, 여기저기 들고 다니면서 밖에서 코딩하는 즐거움도 만끽하고 있다. 맥북 들고 카페에서 뭔가 하고 있으면 허세 부리는 것처럼 보여질수도 있는데, 그 허세라는 느낌을 이 맥북은 준다.

분명 이 노트북 보다 비싸거나 얇거나 고성능인 노트북은 세상에 많다. 하지만 아무도 ThinkPad를 펼쳐놓고 코딩하는 사람에게 허세 부린다고 생각하진 않는다. 그건 뭐랄까, 진짜 일이 급해서 노트북을 펼쳐놓고 코딩하는 그런 느낌이다. 나도 내 삼성 노트북 9을 펼쳐놓고 코딩을 하거나 웹서핑을 하고 있으면, 누가 보더라도 일하고 있는 사람처럼 보일 것이다.

하지만 맥북은 다르다. 사과 마크를 펼쳐놓고 코딩을 하던 웹서핑을 하던 정말로 급박하게 보고서를 쓰던 멀리서 볼 땐 허세의 느낌이 난다. 뭔가 단순한 보고서가 아닌 창의적인 무언가를 하는 느낌도 들고 왠지 음악 아니면 영상 아니면 사진 관련 일인거 같고 괜히 허세 부리는 것 처럼 보인다.

다른 노트북들은 성능이 어떻고, 디자인이 어떻고, 두께가, 무게가 어쩌고 저쩌고 하면서 많은 것들을 비교 당한다. 하지만 맥북은 그냥 맥북이다. 나비식 키보드가 거지 같아도 맥북이고, ESC를 터치바에 넣어도 맥북이다. (둘 다 다행히 개선되었다.) 맥북은 엄연히 ‘한낱’ 노트북이면서도 ‘같은’ 노트북들과의 비교를 거부한다. 달라서 따돌림 당하는 게 아니라 달라서 특별한 것임을 강조한다. 그리고, 그 뭔가 다른 것을 쓰는 사용자 까지도 특별한 것 처럼 착각하게 만들어준다.

몇 가지 비법이 있을 것이다. 맥북을 설계한 기업의 철학, 디자인, 그리고 Windows 가 아닌 Mac OS, 실제로 음악/사진/영상 등의 예술 분야에서 많이 쓰인다는 점, 그리고 사양 대비 높은 가격으로 인한 약간의 명품 효과…

어느 것 하나 콕 집어서 말할 수 없지만, 맥북이 그냥 노트북들과 다르다는 점은 누구나 쉽게 인지할 수 있다. 그리고 그 다른 게 불편할 순 있어도 결코 부끄럽지 않고 오히려 자랑스럽다. 내가 이렇게 다른(이상한) 것도 잘 쓴다. 나는 특별하다.

마치 단순한 전자 기기가 아닌 어떤 명품 브랜드를 사는 듯한 느낌을 소비자에게 줄 수 있다는 게 애플의 놀라운 점이다. 맥북라고 해서 특별할 게 없는데, 그냥 노트북일 뿐인데, 그걸 쓰는 소비자까지 특별하게 만들어주는 것. 이게 어쩌면 애플이 가지고 있는 가장 큰 자산이고 그들이 끝까지 지키고 싶어하는 브랜드 가치가 아닐까.

Categories
아무말대잔치

주식

나는 주식을 하지 않는다. 2012년 지금의 회사에 입사한 이래로 다행히도 아직까지 월급은 꼬박꼬박 받고 있지만, 주식을 한 적은 맹세코 단 한번도 없다. 그 흔하다는 우리 사주도 사지 않았다. 나는 그냥 직원으로 남고 싶을 뿐, 주주가 되어서 회사의 경영에 간섭하고 싶거나 배당금을 노리거나 하지도 않았고 할 생각도 없다.

거기엔 몇 가지 이유가 있다. 가장 큰 이유는 부모님의 밥상머리 교육. 부모님은 없이 살 때나 지금이나 한결같이 주식은 도박이라는 생각에 변함이 없으시다. 건전한 경제활동으로 볼 수도 있고 정말로 그 회사의 잠재 가치 실현을 위해 기꺼이 가진 재산 일부를 투자할 수도 있겠지만, 부모님은 살면서 그 동안 주식으로 한탕 해보겠다고 덤볐다가 말아먹은 사람들을 부지기수로 봤다. Deep learning 관점에서 말하자면, 학습이 된거다.

그래서 나도 어렸을 적 부터 주식은 도박과 마찬가지로 절대 해서는 안되는 걸로 배웠고, 지금도 실천하고 있다. 뭐 물론 이 거 말고 핵심은 주식에 투자할 돈도 없다는 아주 명확한 이유도 있다…

요즘 주식 시장이 엄청 뜨겁다고 한다. 코로나19로 인해 실물경제에 타격이 오면서, V자나 U자 반등을 기대하고 지금 “저평가”된 주식들을 있는 돈 없는 돈 모아서 사는 사람들이 많다고 한다. 마치 비트코인 광풍이 불었을 때 모습과 유사해 보인다. 외국인들이 던진 걸 개미들이 받아내고 있다고 해서 #동학개미운동 이라고 표현하던데, 풍자는 우리가 세계제일인듯.

물론 해당 기업들이 다른 이유도 아닌 코로나19 때문에 저평가가 되었고, 특히 우리나라 주식 시장의 1/3을 쥐고 있는 외국인 투자자들이 공포에 질려 현금화해서 나가고 있기 때문에 일견 그러한 접근은 틀리지 않아 보인다. 저평가된 게 맞고 언젠가 오를 거라는 확신이 있다면, 떨어졌을 때 사는 게 틀린 생각은 아닐 것이다.

근데, 하나 궁금한 건… 나같은 경알못조차도 그렇게 생각하고 있을 정도면 전국민이 이미 그렇게 생각하고 있을텐데, 너무나 뻔해 보이는 이 답안이 과연 정답일까? 2008년 리먼 사태때도 뭔가 비슷한 일이 있었던 거 같은데, 비트코인때도 그렇고… 왜 이번에는 다르다고 생각하지?

이번에는 금융 위기가 아닌 다른 위기라서? 정부가 나서서 돈을 풀테니까? 기업들이 잘못한 게 아니니 코로나19가 사라지면 금방 회복될 걸로 보여서?

인간의 욕심은 끝이 없고, 늘 똑같은 실수를 반복한다는 말은 꽤 높은 확률로 적중했다. 묻지마 투자에 퇴직금까지 털어 넣고 있다는데, 과연 이 많은 사람들에게 해피엔딩이 찾아올까? 항상 승자는 소수였는데… 부디 지금 주식 하시는 분들이 그 소수의 승자 중 한 사람이 되시길…

Categories
아무말대잔치

Raspberry Pi 4 웹서버 설정하기

주말동안 라즈베리파이4를 웹서버로 설정하는 작업을 진행했고, 이 글을 쓰는 현재 대부분의 세팅이 완료되었다. 아직 세부적으로는 더 손을 봐야겠지만, nginx 서버의 파일 업로드 크기 제한을 풀어놓는 것까지 해둔 지금은 뭐 그럭저럭 쓸만한 듯.

하다보니 배보다 배꼽이 더 큰 작업이었다. sirini.blog 도메인은 충동적으로 구매했다. 그냥 sirini.asuscomm.com 으로 계속 써도 되는데, 주소가 너무 길고 내가 어떤 무선 공유기를 쓰는지 광고하는 것 처럼 보이는 것도 싫어서 샀다.

너무 오랫만에 세팅이었고, 나는 LAMP(Linux Apache MySQL PHP) 세대인데 지금 대세는 LEMP(Linux (E)Nginx MariaDB PHP) 였다. 관성대로 해서는 여전히 과거에만 머물 수 밖에 없다. 즐거운 마음으로 간만에 삽을 꺼내들고 내가 참고할만한, 삽질을 먼저하신 선배님들의 발자취를 구글에서 찾아보았다.

https://ryan-han.com/post/server/raspberry_server_1/

그리고 발견한 링크가 위의 링크이다. 정말로 대부분의 내용이 잘 정리되어 있어서 솔직히 그냥 따라 하기만 해도 99%는 문제 없었다. 문제 있었던 부분은 Nginx 설정에 익숙하지 못한 나의 잘못과, 도메인을 DDNS 도메인으로 연결하는 부분을 잘 몰랐던 내 탓일 뿐.

정말로 많이 배웠는데, 그 중에서도 웹서버를 Nginx로 대체한 것은 탁월한 선택이다. Apache는 안정적이지만 무겁고 라즈베리파이같은 초소형 서버에는 걸맞지 않다. Request 요청이 왔을 때 Thread나 Process를 늘리는게 아닌 비동기 이벤트 방식으로 처리하는 건 Nginx를 선택해야하는 중요한 이유다. 덤으로 설정이 나에겐 좀 생소했지만 어렵진 않았다. 덕분에 꽤 만족스런 웹서버 구성을 했다.

sirini.net을 운영할때도 하지 않았던 SSL 설정을 위 링크의 설명을 보면서 했다. 아마 라즈베리파이로 웹서버 만들겠다는 생각을 하지 않았더라면 영영 몰랐으리라. 그 동안 배울 생각은 안하고 안주했던 스스로를 반성한다.

집에서 라즈베리파이4로 웹서버를 구성하는 것은 위의 링크에 정성껏 작성된 글을 보고 따라하면 된다. 여기서 굳이 중언부언 할 필요는 없겠지. 모든 걸 외울 필요 없이 필요할 때 구글신에게 물어보면 된다는 주의이긴 하지만, 저 링크 속 글들은 내 것으로 만들고 싶다.

Categories
아무영상공유

[유튜브] 라즈베리파이4 언박싱

영상 편집을 취미로 시작한지 벌써 햇수로는 3년차에 접어드는 거 같다. 중국에서 살 때 정말 허접하게 그냥 컷 편집 정도만 iMovie로 해서 했었는데 아직도 그 수준에서 크게 발전하진 않았다. 바뀐 건 iMovie가 Final Cut Pro X로, Macbook Pro 13-inch (2010)이 Macbook Pro 15-inch (2015)로 바뀐 정도다. (2020년형으로 바꾸고 싶다… -_ㅠ)

여전히 내가 전면에 나서는 건 부끄럽고, 목소리를 넣는 것도 싫다. 그냥 편집이 재밌고 내가 원하는 구도와 느낌을 잘 살리면 그걸로 만족한다. 음성이 들어가지 않으니 BGM이 중요하고, BGM을 넣다보니 거기 맞춰서 컷편집이 좀 더 달라지긴 하지만 예전이나 지금이나 내맘대로 촬영하고 편집하는 건 변함없다.

주변에서 돈도 안되는 유튜브 채널을 왜 운영 하냐고 많이들 물어보신다. 그냥 취미라고 답해도 미심쩍어하는 눈초리도 여럿 있다. 근데 내가봐도 돈을 벌만한 채널은 아니다. 일단 내가 그만한 정성을 들이고 있지도 않고, 돈을 벌면 그 때부턴 재밌지 않을테니 하고 싶어지지 않을 것이다. 마치 프로그래밍처럼.

그래서, 그냥 재미로 운영하는 채널에 새로 영상 하나 올린김에, 마침 이 블로그를 돌려주고 있는 라즈베리파이4 언박싱 영상이라서 여기에도 공유하고자 한다. 돈은 직장 생활 하면서 열심히 벌면 되니까, 여기선 그냥 재미로.

https://youtu.be/NEswRIYV81A

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 이 되리라.

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