Automate Uploading with Watchdog and FTP

블루호스팅을 사용하는 경우 로컬에서 원하는 편집기로 파일을 수정하고 파일이 변경됨과 동시에 해당파일을 자동으로 업로드해서 실행결과를 서버에서 볼수 있도록 자동으로 파일을 감시하고 업로드하는 파이썬 스크립트를 만들어 보겠습니다.

일단 필요한 패키지는 watchdog이라는 파이썬 패키지 인데요 아래와 같이 설치해줍니다.

pip install watchdog

코딩에 앞서 필요한 정보를 모아주세요. 일단 변경할 폴더위치와 변경시 업로드할 FTP서버정보를 알고 있어야겠죠?

# ===== FTP 설정 =====
FTP_HOST = "ftp.mydomain.com"
FTP_USER = "your_username"
FTP_PASS = "your_password"
FTP_UPLOAD_DIR = "/remote/path/"  # FTP 서버에서 업로드할 경로

# ===== 감시할 폴더 경로 =====
WATCH_FOLDER = "/path/to/watch"  # 감시할 로컬 디렉토리

혹시 Mac에서 ftp명령이 없다고 나오면 ftp를 설치해주세요.

brew install inetutils

그리고 해당 FTP정보가 올바른지 확인합니다.

gftp ftp.mydomain.com

이제 Watchdog을 이용해서 특정 폴더를 감시하는 프로그램을 만들어볼게요.

가장 먼저 스크립트를 만들 폴더에 .env파일을 생성하고 각 환경변수를 아래와 같이 저장하도록 합니다.

FTP_HOST=ftp.mydomain.com
FTP_USER=your_username
FTP_PASS=your_password
FTP_UPLOAD_DIR=/remote/path/
WATCH_FOLDER=/path/to/watch

그리고 스크립트는 watch_and_upload.py라는 이름으로 생성할게요. 그리고 dotenv패키지를 이용해서 환경변수를 읽어옵니다.

import os
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

# 환경 변수 가져오기
FTP_HOST = os.getenv("FTP_HOST")
FTP_USER = os.getenv("FTP_USER")
FTP_PASS = os.getenv("FTP_PASS")
FTP_UPLOAD_DIR = os.getenv("FTP_UPLOAD_DIR")
WATCH_FOLDER = os.getenv("WATCH_FOLDER")

우선 특정파일을 FTP로 업로드하는 함수를 정의하도록 하겠습니다. ftplib패키지에서 FTP를 import하고, ensure_remote_path함수와 upload_to_ftp함수 두개를 선언합니다. ensure_remote_path함수는 FTP로 파일을 업로드 하려고 할때 매칭되는 파일의 경로가 존재하지 않을 경우 폴더를 생성해주는 역할을 합니다. 그리고 upload_to_ftp함수는 FTP로 서버에 접근하여 로컬의 경로와 서버의 경로를 매칭하여 올바른 폴더에 해당 파일을 업로드하는 기능을 합니다.

from ftplib import FTP

def ensure_remote_path(ftp, remote_dir):
    """FTP 서버에 디렉토리 경로가 없으면 생성한다."""
    parts = remote_dir.strip("/").split("/")
    current_path = ""
    for part in parts:
        current_path += "/" + part
        try:
            ftp.cwd(current_path)
        except:
            try:
                ftp.mkd(current_path)
                ftp.cwd(current_path)
            except Exception as e:
                print(f"❌ Failed to create {current_path}: {e}")
                return False
    return True

def upload_to_ftp(file_path, relative_path):
    try:
        with FTP(FTP_HOST) as ftp:
            ftp.login(FTP_USER, FTP_PASS)

            # 경로 구성
            remote_path = os.path.join(FTP_UPLOAD_DIR, relative_path).replace("\\", "/")
            remote_dir = os.path.dirname(remote_path)
            filename = os.path.basename(file_path)

            if ensure_remote_path(ftp, remote_dir):
                with open(file_path, 'rb') as f:
                    ftp.storbinary(f"STOR {filename}", f)
                print(f"✅ Uploaded: {remote_path}")
            else:
                print(f"❌ Failed to ensure remote path: {remote_dir}")
    except Exception as e:
        print(f"❌ FTP Upload Error: {e}")

이제 파일을 감시하는 코드를 넣을게요. 우선 watchdog 패키지에서 파일을 감시하는 이벤트핸들러, FileSystemEventHandler를 import합니다. 그리고 해당 이벤트 핸들러에 on_modified이벤트가 발생했을때 취해야할 액션을 아래와 같이 재정의 합니다. on_modified는 watchdog을 실행했을때 파일이나 폴더에 변경이 있을 경우 실행되는 함수입니다. 폴더는 스킵하고 파일이 변경된 경우에만 FTP업로드를 하는 것으로 할게요. 바로 여기에서 변경된 파일의 풀경로와 상대경로를 획득해서 FTP에 업로드하도록 upload_to_ftp함수를 호출합니다.

from watchdog.events import FileSystemEventHandler

class ChangeHandler(FileSystemEventHandler):
    def on_modified(self, event):
        if not event.is_directory:
            full_path = event.src_path
            relative_path = os.path.relpath(full_path, WATCH_FOLDER)
            upload_to_ftp(full_path, relative_path)

그럼 이제 스크립트가 실행되면 핸들러를 작동시키도록 코드를 추가하겠습니다. 감시에 필요한 클래스 Observer를 import합니다. 그리고 Observer를 observer로 객체화 합니다. 방금 위에서재정의한 ChangeHandler클래스도 마찬가지로 event_handler에 객체생성을 하고, 해당 이벤트 핸들러를 적용하여 observer의 schedule을 start()시킵니다. 그리고 매초마다 Ctrl+C가 입력이 되었는지 확인하여 Ctrl+C를 누른경우 observer를 종료하도록 합니다.

from watchdog.observers import Observer

if __name__ == "__main__":
    print(f"👀 Watching folder (and subfolders): {WATCH_FOLDER}")
    observer = Observer()
    event_handler = ChangeHandler()
    observer.schedule(event_handler, path=WATCH_FOLDER, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

전체 코드는 다음과 같습니다.

import os
from dotenv import load_dotenv
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from ftplib import FTP
import time

# .env 파일 로드
load_dotenv()

# 환경 변수 가져오기
FTP_HOST = os.getenv("FTP_HOST")
FTP_USER = os.getenv("FTP_USER")
FTP_PASS = os.getenv("FTP_PASS")
FTP_UPLOAD_DIR = os.getenv("FTP_UPLOAD_DIR")
WATCH_FOLDER = os.getenv("WATCH_FOLDER")

# ===== FTP 전송 함수 =====
def ensure_remote_path(ftp, remote_dir):
    """FTP 서버에 디렉토리 경로가 없으면 생성한다."""
    parts = remote_dir.strip("/").split("/")
    current_path = ""
    for part in parts:
        current_path += "/" + part
        try:
            ftp.cwd(current_path)
        except:
            try:
                ftp.mkd(current_path)
                ftp.cwd(current_path)
            except Exception as e:
                print(f"❌ Failed to create {current_path}: {e}")
                return False
    return True

def upload_to_ftp(file_path, relative_path):
    try:
        with FTP(FTP_HOST) as ftp:
            ftp.login(FTP_USER, FTP_PASS)

            # 경로 구성
            remote_path = os.path.join(FTP_UPLOAD_DIR, relative_path).replace("\\", "/")
            remote_dir = os.path.dirname(remote_path)
            filename = os.path.basename(file_path)

            if ensure_remote_path(ftp, remote_dir):
                with open(file_path, 'rb') as f:
                    ftp.storbinary(f"STOR {filename}", f)
                print(f"✅ Uploaded: {remote_path}")
            else:
                print(f"❌ Failed to ensure remote path: {remote_dir}")
    except Exception as e:
        print(f"❌ FTP Upload Error: {e}")


# ===== 변경 감지 핸들러 클래스 =====
class ChangeHandler(FileSystemEventHandler):
    def on_modified(self, event):
        if not event.is_directory:
            full_path = event.src_path
            relative_path = os.path.relpath(full_path, WATCH_FOLDER)
            print(f"📂 Modified: {relative_path}")
            upload_to_ftp(full_path, relative_path)

# ===== 감시 시작 =====
if __name__ == "__main__":
    print(f"👀 Watching folder (and subfolders): {WATCH_FOLDER}")
    event_handler = ChangeHandler()
    observer = Observer()
    observer.schedule(event_handler, path=WATCH_FOLDER, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

실행을 해보면 아래와 같이 필요한 경로에 잘 들어가네요.

시청해주셔서 감사합니다.

워드프레스는 카테고리를 어디에 저장할까?

Overview

안녕하세요. 이번 시간에는 워드프레스가 데이타를 저장하는 방법에 대해서 이야기 해보도록 하겠습니다. 현재 최신 버젼의 워드프레스는 WordPress Version: 6.7.2이고 이하 설명은 해당 버젼을 기준으로 진행됩니다.

게시물이나 페이지를 저장하는 테이블

게시물이나 페이지는 직관적입니다. wp_posts테이블에 저장하고요 테이블 상세는 아래와 같습니다.

mysql> desc wp_posts;
ERROR 2013 (HY000): Lost connection to MySQL server during query
No connection. Trying to reconnect...
Connection id:    30134210
Current database: ???

+-----------------------+---------------------+------+-----+---------------------+----------------+
| Field                 | Type                | Null | Key | Default             | Extra          |
+-----------------------+---------------------+------+-----+---------------------+----------------+
| ID                    | bigint(20) unsigned | NO   | PRI | NULL                | auto_increment |
| post_author           | bigint(20) unsigned | NO   | MUL | 0                   |                |
| post_date             | datetime            | NO   |     | 0000-00-00 00:00:00 |                |
| post_date_gmt         | datetime            | NO   |     | 0000-00-00 00:00:00 |                |
| post_content          | longtext            | NO   |     | NULL                |                |
| post_title            | text                | NO   |     | NULL                |                |
| post_excerpt          | text                | NO   |     | NULL                |                |
| post_status           | varchar(20)         | NO   |     | publish             |                |
| comment_status        | varchar(20)         | NO   |     | open                |                |
| ping_status           | varchar(20)         | NO   |     | open                |                |
| post_password         | varchar(255)        | NO   |     |                     |                |
| post_name             | varchar(200)        | NO   | MUL |                     |                |
| to_ping               | text                | NO   |     | NULL                |                |
| pinged                | text                | NO   |     | NULL                |                |
| post_modified         | datetime            | NO   |     | 0000-00-00 00:00:00 |                |
| post_modified_gmt     | datetime            | NO   |     | 0000-00-00 00:00:00 |                |
| post_content_filtered | longtext            | NO   |     | NULL                |                |
| post_parent           | bigint(20) unsigned | NO   | MUL | 0                   |                |
| guid                  | varchar(255)        | NO   |     |                     |                |
| menu_order            | int(11)             | NO   |     | 0                   |                |
| post_type             | varchar(20)         | NO   | MUL | post                |                |
| post_mime_type        | varchar(100)        | NO   |     |                     |                |
| comment_count         | bigint(20)          | NO   |     | 0                   |                |
+-----------------------+---------------------+------+-----+---------------------+----------------+
23 rows in set (1.94 sec)

주요 필드를 설명하자면 아래와 같습니다:

  • post_type:
    • post: 일반 블로그 글
    • page: 정적 페이지
    • attachment: 이미지, PDF 등 업로드 파일
    • custom_post_type: 커스텀 포스트 타입 (예: product, portfolio)
  • post_status: publish, draft, private, trash 등
  • post_author: 작성자 (user ID)
  • post_parent: 페이지 간 계층 구조에서 부모 ID 지정할 때 사용
  • post_date: 작성일
  • post_content: 본문
  • post_title: 제목

카테고리를 저장하는 방식

wp_terms

우선 게시물을 쓰기 전에 선택하고자 하는 카테고리를 먼저 등록하게 되어 있잖아요? 그 카테고리들을 저장하는 테이블은 바로 wp_terms입니다. 테이블 안에 카테고리 이름(name), 슬러그(slug), 고유 ID(term_id)를 저장하고, 테이블 구조는 아래와 같습니다. 여기서 term_group는 원래 다국어 관련 기능을 위해 설계되었지만 거의 사용되지 않습니다.

mysql> desc wp_terms;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| term_id    | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| name       | varchar(200)        | NO   | MUL |         |                |
| slug       | varchar(200)        | NO   | MUL |         |                |
| term_group | bigint(10)          | NO   |     | 0       |                |
+------------+---------------------+------+-----+---------+----------------+

wp_term_taxonomy

사실 위에서 설명한 wp_terms테이블은 카테고리만을 위해 존재하는 것은 아닙니다. 그 안에 태그도 함께 저장할 수 가 있어요. 그렇다면 해당 레코드가 카테고리인지 태그인지는 어떻게 아느냐? 바로 wp_term_taxonomy테이블에서 해당 term이 어떤 분류 유형(taxonomy)인지 정의 (category, post_tag 등)하고 있습니다. 테이블 상세를 보시면 다음과 같습니다.

mysql> desc wp_term_taxonomy;
+------------------+---------------------+------+-----+---------+----------------+
| Field            | Type                | Null | Key | Default | Extra          |
+------------------+---------------------+------+-----+---------+----------------+
| term_taxonomy_id | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| term_id          | bigint(20) unsigned | NO   | MUL | 0       |                |
| taxonomy         | varchar(32)         | NO   | MUL |         |                |
| description      | longtext            | NO   |     | NULL    |                |
| parent           | bigint(20) unsigned | NO   |     | 0       |                |
| count            | bigint(20)          | NO   |     | 0       |                |
+------------------+---------------------+------+-----+---------+----------------+

term_taxonomy_id에 고유ID를 저장하고, term_id에 wp_terms.term_id를 저장합니다. 그리고 taxonomy에 category인지 post_tag인지를 저장하고, description에 설명을 넣을 수 있어요. 그리고 카테고리인 경우 부모카테고리가 있으면 parent필드에 명시합니다. 그리고 마지막으로 count에는 해당 카테고리에 게시글이 몇개인지가 저장됩니다.

wp_term_relationships

이제 어떤 게시글이 어떤 카테고리에 연결이 되는지를 알아야겠죠? 바로 wp_term_relationships테이블에서 어떤 게시물(post) 이 어떤 카테고리(또는 태그 등)에 연결되어 있는지 저장합니다. 테이블의 내용은 엄청 단순합니다.

mysql> desc wp_term_relationships;
+------------------+---------------------+------+-----+---------+-------+
| Field            | Type                | Null | Key | Default | Extra |
+------------------+---------------------+------+-----+---------+-------+
| object_id        | bigint(20) unsigned | NO   | PRI | 0       |       |
| term_taxonomy_id | bigint(20) unsigned | NO   | PRI | 0       |       |
| term_order       | int(11)             | NO   |     | 0       |       |
+------------------+---------------------+------+-----+---------+-------+

Object_id가 바로 post_id가 되고, term_taxonomy_id가 wp_term_taxonomy테이블의 term_taxonomy_id가 됩니다. 여기서 term_order는 하나의 게시물에 여러개의 태그나 카테고리가 걸려있는 경우 보여주는 순서를 정할 수가 있는데 기본적으로는 사용하지 않습니다.

카테고리 + 태그 모두 포함한 SQL 쿼리

위의 관계들을 종합해보면 게시물목록을 불러올 때 사용되는 쿼리는 다음과 같습니다.

SELECT 
    p.ID AS post_id,
    p.post_title,
    tt.taxonomy,
    t.name AS term_name,
    t.slug AS term_slug
FROM wp_posts p
JOIN wp_term_relationships tr ON (p.ID = tr.object_id)
JOIN wp_term_taxonomy tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)
JOIN wp_terms t ON (tt.term_id = t.term_id)
WHERE p.post_status = 'publish'
  AND p.post_type = 'post'
  AND tt.taxonomy IN ('category', 'post_tag')
ORDER BY p.ID, tt.taxonomy;

그리고 쿼리결과는 아래와 같습니다.

+---------+---------------------+----------+-----------+------------+
| post_id | post_title          | taxonomy | term_name | term_slug  |
+---------+---------------------+----------+-----------+------------+
|       1 | Hello world!        | category | 테크       | tech       |
|       6 | 테스트 포스트           | category | 부동산     | realestate |
|       8 | 뉴스 테스트            | category | 주식       | stocks     |
+---------+---------------------+----------+-----------+------------+

워드프레스의 테이블 형태를 이해하는데 도움이 되셨기를 바랍니다. 그럼 다음시간에 뵐게요!

OpenAI API사용하기

Overview

안녕하세요. 이번 시간에는 OpenAI API를 이용해서 챗GPT앱을 사용하지 않고 파이썬 스크립트로 AI응답을 받아오는 방법에 대해서 공부해 보도록 하겠습니다.

사용 Limit확인

OpenAI의 API를 이용하기 위해서는 본인의 계정에 API를 사용할 수 있는 크레딧이 남아 있어야합니다. 현재 남아있는 크레딧을 확인하시거나 결제를 하시려면 https://platform.openai.com/settings/organization/billing/overview에 접속하시면 아래와 같이 현재 보유하고 있는 크레딧을 확인하 실 수가 있습니다.

결제정보를 한번도 입력하지 않은 경우라면, Add payment details을 눌러서 결제정보를 입력해주세요. 카드정보와 빌링주소등을 적고 Continue버튼을 누릅니다.

그러면 충전화면으로 바로 이동합니다. 만약 결제정보가 이미 등록된 경우라면 화면에 Add to credit balance라는 버튼이 있을거에요. 그거 눌러서 아래 화면을 띄웁니다.

최소 충전 금액은 $5이고 한번에 $95까지 충전이 가능합니다. 충전하시고자 하는 금액을 입력하신 뒤 Continue버튼을 눌러서 충전을 완료해주세요. 충전이 완료되면 화면이 닫히고, 빌링대시보드에 충전된 금액이 표시됩니다.

API Key

크레딧이 충전이 되었다면 이제 API Key를 우선적으로 받아와야하는데요. https://platform.openai.com/api-keys에 들어가시면 아래와 같이 API Key관리화면이 뜨고 중앙에 + Create new secret key라는 버튼이 보일거에요.

+ Create secret key버튼을 누르면 팝업이 뜨고 해당 키의 이름과 사용될 프로젝트, 그리고 사용권한을 입력하는 양식이 나오는데요. 테스트니까 그냥 기본값으로 선택하신뒤에 Create secret key버튼을 누르시면 됩니다.

그러면 바로 API Key를 생성해서 화면에 보여줍니다. 그 Key를 복사해주세요. 여기서 반드시 복사하셔야합니다. 팝업창을 한번 닫으면 API Key를 다시 보여주지 않아요.

파이썬에서 API호출하기

우선 접속에 필요한 패키지를 설치해주세요.

pip install openai
pip install python-dotenv

.env 파일 생성

프로젝트 루트 디렉토리에 .env 파일을 생성하고, 다음과 같이 API 키를 저장합니다:

OPENAI_API_KEY=your-api-key-here

파이썬 코드에서 환경 변수 로드

이제 파이썬 코드에서 dotenv를 사용하여 환경 변수를 로드하고, OpenAI 클라이언트를 생성할 수 있습니다:

import os
from openai import OpenAI
from dotenv import load_dotenv

# .env 파일 로드
load_dotenv()

# 환경 변수에서 API 키 가져오기
api_key = os.getenv("OPENAI_API_KEY")

# OpenAI 클라이언트 생성
client = OpenAI(api_key=api_key)

Chat Completions API 호출 예제

이제 client 객체를 사용하여 Chat Completions API를 호출하는 예제를 보여드리겠습니다:​

response = client.chat.completions.create(
    model="gpt-3.5-turbo",  # 또는 사용 가능한 다른 모델
    messages=[
        {"role": "system", "content": "당신은 유용한 어시스턴트입니다."},
        {"role": "user", "content": "파이썬에서 리스트를 정렬하는 방법을 알려줘."}
    ],
    temperature=0.7,
)

# 응답 출력
print(response.choices[0].message.content)

위 코드에서 temperature 파라미터는 생성되는 응답의 창의성을 조절합니다. 값이 낮을수록 더 보수적인 응답을, 높을수록 더 창의적인 응답을 생성합니다.

위의 파일을 실행해보면 다음과 같은 결과가 나타납니다.

파이썬에서 리스트를 정렬하는 방법은 `sort()` 메서드나 `sorted()` 함수를 사용하는 것입니다   \n\n1   `sort()` 메서드를 사용하는 방법:\n```python\nmy_list = [3, 1, 4, 1, 5, 9, 2, 6, 5]\nmy_list  sort()\nprint(my_list)  # [1, 1, 2, 3, 4, 5, 5, 6, 9]\n```\n\n2   `sorted()` 함수를 사용하는 방법:\n```python\nmy_list = [3, 1, 4, 1, 5, 9, 2, 6, 5]\nsorted_list = sorted(my_list)\nprint(sorted_list)  # [1, 1, 2, 3, 4, 5, 5, 6, 9]\n```\n\n`sort()` 메서드는 원본 리스트를 직접 변경하고, `sorted()` 함수는 정렬된 새로운 리스트를 반환합니다   필요에 따라 적절히 선택하여 사용하시면 됩니다

참고로 response를 출력해보면, print(response), 다음과 같은 결과를 보여줍니다.

ChatCompletion(
  "id=""chatcmpl-BK8PJOCXL1zAp3MXID8Mn3YTCnrp8",
  "choices="[
     "Choice(
         finish_reason=""stop",
      index=0,
      "logprobs=None",
      "message=ChatCompletionMessage(
        content=""파이썬에서 리스트를 정렬하는 방법은 `sort()` 메서드나 `sorted()` 함수를 사용하는 것입니다   \n\n1   `sort()` 메서드를 사용하는 방법:\n```python\nmy_list = [3, 1, 4, 1, 5, 9, 2, 6, 5]\nmy_list  sort()\nprint(my_list)  # [1, 1, 2, 3, 4, 5, 5, 6, 9]\n```\n\n2   `sorted()` 함수를 사용하는 방법:\n```python\nmy_list = [3, 1, 4, 1, 5, 9, 2, 6, 5]\nsorted_list = sorted(my_list)\nprint(sorted_list)  # [1, 1, 2, 3, 4, 5, 5, 6, 9]\n```\n\n`sort()` 메서드는 원본 리스트를 직접 변경하고, `sorted()` 함수는 정렬된 새로운 리스트를 반환합니다   필요에 따라 적절히 선택하여 사용하시면 됩니다  ",
        "refusal=None",
        "role=""assistant",
        "annotations="[],
        "audio=None",
        "function_call=None",
        "tool_calls=None
      )
    )"
  ],
  created=1744138577,
  "model=""gpt-3  5-turbo-0125",
  "object=""chat  completion",
  "service_tier=""default",
  "system_fingerprint=None",
  usage=CompletionUsage(
    completion_tokens=273,
    prompt_tokens=50,
    total_tokens=323,
    completion_tokens_details=CompletionTokensDetails(
      accepted_prediction_tokens=0,
      audio_tokens=0,
      reasoning_tokens=0,
      rejected_prediction_tokens=0
    ),
    prompt_tokens_details=PromptTokensDetails(
      audio_tokens=0,
      cached_tokens=0
    )
  )
)

주의사항

  • API 키 보안: API 키는 개인 정보이므로, 코드에 직접 포함시키기보다는 위에서 소개한 방법처럼 환경 변수를 통해 관리하는 것이 좋습니다.​
  • 모델 선택: 사용하려는 모델(gpt-4o, gpt-3.5-turbo 등)이 계정에서 접근 가능한지 확인하세요.​
  • 에러 핸들링: API 호출 시 발생할 수 있는 예외를 처리하여 프로그램이 안정적으로 동작하도록 구현하는 것이 좋습니다.

오늘 강좌는 여기까지 입니다. 최신 버전의 OpenAI Python 라이브러리를 활용하여 다양한 API를 호출할 수 있는데, 더 자세한 내용은 OpenAI 공식 문서를 참고하시기 바랍니다. 시청해 주셔서 감사합니다.

Google Cloud Translation API

Overview

이번시간에는 Google API를 이용하여 파이썬 스크립트에서 영>한 번역서비스를 이용해보도록 하겠습니다. ​구글 번역 API는 일정 부분 무료로 사용할 수 있지만, 사용량에 따라 요금이 부과됩니다. Google Cloud는 새로운 고객에게 90일 동안 $300의 무료 크레딧을 제공하며, 이를 통해 Cloud Translation API를 포함한 다양한 서비스를 체험할 수 있습니다. ​

또한, 모든 고객은 매월 500,000자까지의 텍스트 번역을 무료로 이용할 수 있습니다. 이 무료 사용량을 초과하면 추가 요금이 발생하며, 자세한 요금 정보는 Cloud Translation 가격 책정 페이지에서 확인할 수 있습니다.​

따라서, 소규모 프로젝트나 제한된 번역 작업의 경우 무료 사용 한도 내에서 비용 없이 구글 번역 API를 활용할 수 있습니다. 그러나 대규모 번역이 필요한 경우에는 추가 비용이 발생할 수 있으므로, 사용 계획에 따라 요금제를 검토하는 것이 좋습니다.

새로운 프로젝트 생성

가장 먼저 https://console.cloud.google.com에 가셔서 Project를 하나 만드세요. 새로운 프로젝트를 만드는 방법은 이전 강좌에서 소개했으므로 프로젝트가 생성되어 있다는 전제하에 강의를 진행하겠습니다.

Cloud Translation API 활성화

새로운 프로젝트를 생성하셨으면 화면 상단에서 생성한 프로젝트를 선택해주세요.

그리고 아래 Quick access에서 APIs & Services를 클릭한 뒤에 좌측메뉴에서 Library를 선택합니다. 그러면 아래와 같은 화면이 뜰거에요. 검색창에 Cloud Translation API를 검색해 주세요.

검색결과에서 Cloud Translation API를 선택해주세요.

Enable버튼을 눌러서 해당 앱을 활성화를 시켜줍니다.

그러면 결제정보를 요구할거에요. Enable billing버튼을 눌러서 결제정보입력을 진행해주세요.

만약 선택할 결제정보를 기존에 가지고 있지 않으면 새로운 결제정보를 입력하셔야합니다. Manage billing accounts를 클릭해주세요.

결제관리화면이 나오면 Create account를 클릭해서 결제계정을 생성해주세요.

결제계좌의 이름을 입력하고, 결제 국가 및 화폐를 선택합니다.

결제계좌에 연결할 카드정보를 선택한 뒤 Submit and enable billing을 클릭해서 결제계정 생성절차를 완료합니다.

그러면 아래와 같이 해당 결제계좌에 관한 정보를 한눈에 볼 수 있는 대시보드가 나타납니다.

다시 아까 Cloud Translation API로 가서 Enable버튼을 누르면 이번에는 어떤 결제계좌를 이용할지를 물어봅니다. 방금 만든 My Billing Account 1을 선택해주세요.

팝업을 닫고 Enable버튼을 다시 누르면 아래와 같이 활성화된 결과를 보여줍니다.

API Key생성

이제 API를 사용하기 위해 필요한 API Key를 생성할 차례입니다. 좌측메뉴에서 Credentials를 클릭하면 아래와 같이 API Key들을 관리하는 화면이 뜹니다.

상단에 + Create credentials링크를 누르면 새창이 뜨면서 API Key를 생성해서 보여줍니다.

주의사항을 보면 API를 도용당할 수 있기때문에 Edit API Key를 눌러서 제한설정을 하라네요. 해당링크를 누르면 다음과 같이 사용제한을 설정할 수 있게 해주는데요.

애플리케이션 제한은 웹서비스나 앱이 없으므로 그냥 넘어갈게요. 그리고 API 제한은 Cloud Translation API로 범위를 좁힐게요. Restrict key옵션을 선택하신뒤에 목록에서 Cloud Translation API를 선택해주세요.

Save버튼을 누르면 다시 API Key관리화면으로 보내줍니다.

Cloud Translation API사용하기

이제 만들어진 API키를 가지고 번역서비스를 이용해볼까요? Google번역 API의 v2를 이용해서 간단한 인사를 한글로 번역해 가지고 오도록 하겠습니다. API출력하는 JSON결과를 예쁘게 보여주기 위해서 json패키지를 사용할게요.

import json
import requests

API_KEY = "여기에 API Key를 넣으세요"
text = "Hello, how are you?"
target = "ko"  # Translate to Korean

url = f"https://translation.googleapis.com/language/translate/v2"

params = {
    'q': text,
    'target': target,
    'format': 'text',
    'key': API_KEY,
}

response = requests.post(url, data=params)
result = response.json()

print(json.dumps(result, indent=2, ensure_ascii=False))

이 파일을 실행하면 다음과 같이 번역결과가 JSON에 저장됩니다.

{
  "data": {
    "translations": [
      {
        "translatedText": "안녕하세요. 어떻게 지내세요?",
        "detectedSourceLanguage": "en"
      }
    ]
  }
}

테스트가 끝나고 해당 API를 더이상 사용할 일이 없으시다면 API Key는 삭제해주세요. API Key관리자화면에서 우측에 Actions 점세개 누르면 Delete API Key를 하실수 있습니다.

그리고 불필요하게 Enabled되어있던 다른 API들도 전부 비활성화 시켜줍니다.

오늘 강의는 여기까지입니다. 도움이 되셨길 바래요. 앱을 만들다 보면 돈을 내고라도 번역서비스를 써야할 때가 있으니까요. 참! 앱이나 웹서비스가 있다면 API Key로 하지 마시고, OAuth 2.0 Client ID를 생성하셔서 인증을 하시기를 추천드립니다. API Key는 유출의 위험이 있고, 유출이 되면 도용되기가 너무 쉽기때문에 특정 앱에서 API를 사용하는 경우에는 반드시 OAuth로 인증받으시기를 강력히 추천드립니다. 그럼 좋은 저녁되세요!

References

뉴스서비스 WordPress Theme만들기

안녕하세요. 오늘은 뉴스서비스를 제공하는 사이트의 WordPress Theme을 만들어 볼건데요. Theme을 만드는게 처음이시라면 초간단 WordPress Theme만들기 강좌를 둘러보신 후 학습하시면 더 쉽게 따라오실 수 있으실거에요.

우선 /wp-content/themes폴더 안에 news라는 폴더를 만들어 주세요. 그리고 그 안에 아래와 같이 파일을 만들거에요.

/news/
  ├── style.css
  ├── index.php
  ├── functions.php
  ├── header.php
  ├── footer.php
  ├── sidebar.php        ← 카테고리 메뉴
  ├── single.php         ← 뉴스 상세
  ├── archive.php        ← 카테고리별 뉴스
  └── screenshot.png     ← 테마 미리보기 이미지

style.css를 생성해서 그 안에 기본 스타일을 넣을게요

/*
Theme Name: NewsTheme
Theme URI: https://yourdomain.com
Author: You
Description: A simple WordPress theme for news/blog style websites.
Version: 1.0
*/
body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background: #f5f5f5;
}
.container {
  width: 90%;
  max-width: 1200px;
  margin: 0 auto;
}
.post {
  background: white;
  padding: 20px;
  margin: 20px 0;
}

그 다음 header.php를 생성해서 HTML을 시작하는 부분을 선언해주세요. 이 파일에는 모든 페이지에 공통으로 들어가는 시작부분을 정의합니다. head태그 안에는 wp_head()만 넣으면 워드프레스가 알아서 필요한 코드를 넣어줍니다. 그리고 페이지 최상단에 사이트이름에 홈링크를 걸어서 언제든지 홈으로 돌아올수 있게 할게요.

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
  <?php wp_head(); ?>
</head>
<body>
  <div class="container">
    <header>
      <h1><a href="<?php echo home_url(); ?>"><?php bloginfo('name'); ?></a></h1>
    </header>

그 다음으로 모든 페이지의 마지막에 들어갈 footer.php도 정의해볼게요. 워드프레스가 생성된 년도와 사이트이름을 카피라이트로 넣어줍니다.

    <footer>
      <p>&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?></p>
    </footer>
  </div>
  <?php wp_footer(); ?>
</body>
</html>

그리고 카테고리목록을 보여줄 sidebar.php도 생성합니다. 여기서는 wp_list_categories함수를 호출해서 카테고리를 나열하고 카테고리를 클릭하면 카테고리별 뉴스 페이지로 이동하도록 합니다.

<sidebar>
    <div>
    <?php
        wp_list_categories(array(
           'orderby' => 'name',
           'title_li' => ''
        ));
    ?>
    </div>
</sidebar>

그리고 테마의 기본이 되는 첫페이지, index.php에서 위에 정의한 헤더와 푸터, 그리고 사이드바를 불러오도록할게요.

<?php get_header(); ?>
<?php get_sidebar(); ?>
<main>
  <?php if (have_posts()) : while (have_posts()) : the_post(); ?>
    <div class="post">
      <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
      <p><?php the_excerpt(); ?></p>
    </div>
  <?php endwhile; else : ?>
    <p>뉴스가 없습니다.</p>
  <?php endif; ?>
</main>
<?php get_footer(); ?>

이제 functions.php를 정의합니다. 아래 코드에서 after_setup_theme라는 액션은 테마를 불러온 후에 실행할 함수명을 지정하는 건데요. add_action('after_setup_theme', 'newstheme_setup');라고 하면 테마를 불러온 후에 newstheme_setup함수를 실행해달라고 하는건데요. 바로 위에 함수 newstheme_setup를 보시면 add_theme_support('title-tag');가 있는데요. 이건 게시글의 제목을 HTML의 Title태그에 넣으라는 뜻입니다.

<?php
function newstheme_setup() {
  add_theme_support('title-tag');
}
add_action('after_setup_theme', 'newstheme_setup');

function newstheme_scripts() {
  wp_enqueue_style('style', get_stylesheet_uri());
}
add_action('wp_enqueue_scripts', 'newstheme_scripts');

single.php는 뉴스제목을 클릭하고 들어와서 상세뉴스를 보는 페이지입니다.

<?php get_header(); ?>
<article class="post">
  <h2><?php the_title(); ?></h2>
  <p><?php the_date(); ?> | <?php the_author(); ?></p>
  <?php the_content(); ?>
</article>
<?php get_footer(); ?>

archive.php는 카테고리별 뉴스목록을 보여주는 페이지인데요. 사이드바에서 카테고리를 클릭하면 이 페이지를 열게 됩니다.

<?php get_header(); ?>
<h2><?php single_cat_title(); ?></h2>
<?php if (have_posts()) : while (have_posts()) : the_post(); ?>
  <div class="post">
    <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
    <p><?php the_excerpt(); ?></p>
  </div>
<?php endwhile; endif; ?>
<?php get_footer(); ?>

저장이 다 되었으면 뉴스와 카테고리를 몇가지 넣어보도록 하겠습니다. 그리고나서 사이트를 확인해보면 아래와 같이 나오는데요. 최상단에 홈으로 갈수 있는 링크와 그 밑에 사이드바에는 카테고리별 뉴스를 볼수 있게 카테고리목록이 나오고 그 밑으로 뉴스가 하나씩나옵니다.

카테고리를 클릭하면 archive.php에 선언한 대로 카테고리별 뉴스목록을 보여줍니다.

참고로 홈페이지를 캡쳐해서 테마폴더에 screenshot.png로 저장하면 테마 목록에서 썸네일로 사용됩니다.

오늘의 강좌를 활용해서 더 복잡하고 유연한 워드프레스 테마를 만들어보는건 여러분의 몫인것 같습니다. 많은 테마 디자인 기대하겠습니다. 시청해주셔서 감사합니다.

초간단 WordPress Theme만들기

안녕하세요. 오늘은 초간단 WordPress theme을 만들어볼게요. 이 강좌를 따라오시려면 우선적으로 WordPress가 설치되어있는 사이트를 가지고 계셔야합니다.

WordPress를 설치하신 뒤에 File Manager에 들어가보시면, 루트폴더에 wp-admin, wp-content, wp-includes, 세개의 폴더가 생성되어 있을거에요.

그중에 wp-content에 들어가면 themes라는 폴더가 있을거에요.

themes에 들어가서 새로운 폴더를 simplest라는 이름으로 하나 만들어 주세요.

그 안에 아래와 같이 파일을 3개 만들어주세요. index.php는 기본 템플릿 파일이고, style.css는 테마정보와 스타일을 정의할거구요. functions.php는 사실 없어도 되는데 있는게 좋아요. 여기다가 각종 테마의 기능들을 정의할거에요.

우선 style.css를 열어서 해당 테마에 대한 정보를 적어줄거에요.

/*
Theme Name: My First Theme
Theme URI: https://example.com
Author: Your Name
Author URI: https://your-site.com
Description: A simple custom WordPress theme.
Version: 1.0
*/

그리고 index.php를 열어서 화면을 구성합니다. 가장 기본적인 HTML구조를 작성하도록 하겠습니다.

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
  <meta charset="<?php bloginfo('charset'); ?>">
  <title><?php bloginfo('name'); ?></title>
  <link rel="stylesheet" href="<?php bloginfo('stylesheet_url'); ?>">
</head>
<body>
  <h1><?php bloginfo('name'); ?></h1>
  <p><?php bloginfo('description'); ?></p>

  <?php
    if (have_posts()) :
      while (have_posts()) : the_post();
        the_title('<h2>', '</h2>');
        the_content();
      endwhile;
    else :
      echo '<p>No content found</p>';
    endif;
  ?>
</body>
</html>

마지막으로 funxtions.php를 열어서 아래와 같이 스타일을 등록합니다.

<?php
function mytheme_enqueue_styles() {
  wp_enqueue_style('main-style', get_stylesheet_uri());
}
add_action('wp_enqueue_scripts', 'mytheme_enqueue_styles');
?>

이제 워드프레스 관리자페이지를 열어보면 아래와 같이 My First Theme이 테마로 선택할 수 있게 뜹니다. Activate버튼을 눌러서 활성화를 시켜주세요.

이제 상단에 Visit site링크를 눌러서 결과를 확인해주세요. index.php에 명시한 대로 최상단에 사이트이름, 내사이트를 출력하고, 그 아래 가장 최근에 올라온 게시물을 한개 보여줍니다. Hello World!라는 게시물은 워드프레스를 설치하면 기본적으로 들어가 있는 게시글입니다.

일단 테마를 만드는 기본 개념을 알려드리기 위해서 사이트가 가져야하는 정상적인 기능들은 모두 배제하고 가장 간단하게 설명해드렸는데 다음 시간에는 좀더 사이트 다운 테마를 만들어 보도록 하겠습니다. 이번시간에는 어디에 테마파일을 저장하고 기본적으로 필요한 파일이 뭔지에 대해서 알아보았습니다. 시청해주셔서 감사합니다.

Audio Silence Trimming

안녕하세요. 이번시간에는 오디오파일에서 아무말도 안하고 있는 부분을 자동으로 지워주는 파이썬 스크립트를 만들어 볼까 합니다.

우선 필요한 라이브러리를 설치해주세요. pydub와 ffmpeg를 설치해주셔야합니다.

pip install pydub
brew install ffmpeg

pydub이 기존에 파이썬 기본패키지로 들어가 있던 audioop라이브러리를 사용하는데요, 혹시 파이썬 3.13버젼을 사용하신다면 새로운 버젼에서는 더이상 audioop을 기본적으로 제공하지 않기때문에 별도로 설치를 해주셔야합니다. audioop아니고요 audioop-lts를 설치해주셔야합니다.

pip install audioop-lts

코드는 다음과 같습니다. 음성파일을 같은 폴더에 저장해주세요.

from pydub import AudioSegment
from pydub.silence import split_on_silence

# Load the audio file
audio = AudioSegment.from_file("input.m4a")

# Split the audio by silence
chunks = split_on_silence(audio, min_silence_len=1000, silence_thresh=-40)

# Combine the chunks back together
clean_audio = AudioSegment.empty()
for chunk in chunks:
    clean_audio += chunk

# Export the cleaned audio file
clean_audio.export("output.mp3", format="mp3")

위의 코드는 input.m4a를 받아서 output.mp3로 중간에 아무런 말도 들리지 않는 경우에 트림을 하는 코드입니다. min_silence_len는 현재 1초로 설정되어 있는데 더 짧은 구간도 트림하고 싶으시면 700이나 그 이하로도 필요에따라 줄이셔도 됩니다. 그리고 silence_thresh는 -40인 경우에 삭제를 하도록 했습니다. output도 m4a로 하고 싶었는데 라이브러리에서 해당 포멧을 지원하지 않네요.

한번 실행해볼게요. 오디오나 비디오는 처리시간이 꽤 소요됩니다.

python audio-silence-trimming.py

결과를 보시면 output.mp3가 같은 폴더에 생성되어있고, 열어서 확인해보시면 공백이 모두 사라져있습니다.

감사합니다.

PDF to Images in Python

Overview

안녕하세요. 오늘은 PDF파일의 페이지들을 이미지로 변환하는 파이썬 스크립트를 만들어 보도록하겠습니다.

Install Package

우선 필요한 파이썬 패키지를 설치하도록 하겠습니다. 여기서 필요한 패키지는 pdf2image라는 패키지에요.

pip install pdf2image

그런데 pdf2image패키지는 poppler라는 라이브러리를 이용해서 돌아가는 패키지거든요. 운영체제에 poppler를 설치하셔야 pdf2image를 사용하실 수 있습니다. 저는 맥을 쓰고 있어서 brew를 통해서 설치합니다. 설치시간이 많이 걸리니까 한참 기다리셔야해요.

brew install poppler

참고로, poppler는 gcc라이브러리를 참조하므로 혹시 맥에 gcc가 이미 설치되어 있지 않다면 gcc를 먼저 설치하신 후에 poppler를 설치해주시기 바랍니다. 맥북에 XCode가 없으면 gcc설치가 안되니까 XCode를 우선적으로 설치하셔야합니다. XCode는 LaunchPad의 Apps에서 설치하실 수 있습니다.

brew install gcc

Windows운영체제를 사용하시는 분들은 최신 Poppler 패키지를 다운로드하고 bin\ 디렉토리를 PATH 환경 변수에 추가합니다.

Linux운영체제를 사용하시는 분들은 아래와 같이 apt-get라이브러리 매니저를 사용하여 라이브러리를 설치합니다.

sudo apt-get update
sudo apt-get install -y poppler-utils

하지만 최신버젼의 MacOS를 설치한 분들은 이런 잡다한 문제가 일어나지는 않을거에요.

poppler가 성공적으로 설치가 되었으면 pdf_to_images.py라는 이름으로 파일을 하나 만들어 주세요.

from pdf2image import convert_from_path

pdf_path = 'input.pdf'
output_folder = 'output'
images = convert_from_path(pdf_path)

for i, image in enumerate(images):
    image.save(f'{output_folder}/page_{i+1}.png', 'PNG')

그리고 이미지를 추출할 PDF파일을 만들어 input.pdf로 저장합니다. 그리고 output이라는 폴더를 만들어주세요. 그 뒤에 아래와 같이 스크립트를 실행합니다.

python pdf_to_images.py

실행이 완료되면 PDF파일 안의 모든 페이지들이 이미지로 변환되어 output폴더에 저장이됩니다.

Initializing MacBook Pro

Install Basic Apps

가장 먼저 필수앱들을 설치해줍니다. iMovie는 유투브 편집용으로 쓰고 있기 때문에 저는 필요한데 필요없으신 분들은 설치하지 않으셔도 됩니다.

  • Google Chrome
  • Sublime Text
  • iTerm2
  • Visual Studio Code
  • iMovie

subl

홈에 bin폴더 만들어서 subl의 심볼릭링크를 저장해줍니다.

cd ~
mkdir bin
cd bin
ln -s /Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl

그리고 .bash_profile을 생성해서 해당 경로를 인식할 수 있게 해주세요

export PATH=$PATH:~/bin

그러면 앞으로 subl라는 명령어로 Sublime Text를 열수 있습니다.

iTerm2

Settings > Appearance > General에 들어가서 Theme을 Dark으로 변경합니다.

그리고 옆에 Profiles > Colors탭을 열어서 배경색, 글자색등을 바꿔줍니다.

그리고 .bash_profile에서 프롬프트도 예쁘게 바꿔줍니다.

DEFAULT='\033[0m'
BLACK='\033[0;30m'
DARKGRAY='\033[1;30m'
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BROWNORANGE='\033[0;33m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
LIGHTGRAY='\033[0;37m'
WHITE='\033[1;37m'

export PS1="${BLUE}\D{%H:%M} ${GREEN}\w ${DEFAULT}\$ "

Basic Alias

.bash_profile를 열어서 자주 쓰는 명령어를 alias해줍니다.

alias l="ls -al"
alias h="history"
alias vialias="subl ~/.bash_profile"
alias sourcealias="source ~/.bash_profile"

현재 사용하고 있는 MacOS가 15.3.1 (24D70)인데 원래대로라면 .zshrc을 사용하는 것으로 알고 있습니다. 하지만 제가 지금 세팅하고 있는 랩탑은 2019년 생산제품이고 버젼 10.14가 디폴트로 설치되는 Macbook입니다. 파티션의 내용을 지우고 기본으로 제공되는 10버젼을 새로 설치한뒤 파티션 데이타 포멧에 오류가 있어서 바로 15로 업그레이드를 하지 못하고 11로 업그레이드한 후에 15를 설치한 경우라서 아마도 bash을 이용하는 것 같습니다.

References

Running Python Flask on Bluehost WordPress Hosting Service

Overview

안녕하세요. 오늘은 약간 편법적인 방법으로 Python서버를 Bluehost의 WordPress 호스팅플랜으로 한번 돌려보려고 해요. 성공여부는 모르겠지만 일단 한번 도전해볼게요!

Connect SSH

일단 SSH로 서버에 접속합니다. 블루호스트의 웹호스팅으로 SSH서버에 접속하는 방법은 여기에 있습니다.

ssh 아이디@도메인.com -p 2222 -i ~/.ssh/bluehost/id_rsa

Python 3.12.8 (권한없음)

Python 버젼 확인

파이썬 버젼을 확인해볼까요? 버젼이 너무 오래되었네요.

# python --version
Python 2.7.5
# python3 --version
Python 3.6.8

파이썬 3.12.8 설치시도 (C compiler없음)

오늘 날짜(2025년 1월 13일)로 가장 최근 버전의 파이썬은 3.12.8입니다.

mkdir ~/python

cd ~/python

wget http://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz

tar zxfv Python-3.12.8.tgz

find ~/python -type d | xargs chmod 0755

cd Python-3.12.8

./configure --prefix=$HOME/python

설치를 위한 C컴파일러가 서버에 없네요.

configure: error: in `/home/__HOMEDIR__/python/Python-3.12.8':
configure: error: no acceptable C compiler found in $PATH
See `config.log' for more details

C Compiler 권한요청

에러수정 안내를 보니까 cPanel사용자가 C compiler group에 속해있지 않다네요.

If the configuration error shown above occurs when you attempt to configure Python with ./configure –prefix=$HOME/python, this means that your cPanel user is not added to the C compiler group on the server. Contact our Support at 888-401-4678 to request you be added to the Compiler group.

888-401-4678에 전화를 해보도록 하겠습니다. 고객센타에서 제 cpanel 계정을 C compiler group에 넣어주었고, 파이썬 버젼도 업그레이드 해주었다네요. 확인해보도록하겠습니다. python은 여전히 2.7이고 대신 python3이 3.6이네요.

$ python --version
Python 2.7.5
$ python3 --version
Python 3.6.8

Python 3.12.8 설치 재시도 (권한없음)

파이썬 설치파일을 실행해볼까요?

./configure --prefix=$HOME/python

권한이 없다고 에러가 납니다.

Python 3.6.8로 서비스 되는지 (되긴 됨)

Run Hello World with Python 3.6.8

일단 버젼 업그레이드는 나중에 하고 2.7은 너무 out dated니까 3.6버젼으로 시도를 계속 해보도록 하겠습니다.

hello_world.py 파이썬 파일을 하나 만들게요.

print("Hello, World!")

파일이 실행은 됩니다.

# python3 hello_world.py
Hello, World!

Run Server with Python 3.6

서버가 띄워지는지 한번 볼까요? Python2는 안되네요.

# python -m http.server
/usr/bin/python: No module named http

Python3로 시도해볼게요.

# python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

8000포트로 서버가 떴어요.

Run Flask with Python 3.6

이번에는 Falsk프레임웤으로 서버가 실행이 되는지 해볼게요

app.py라는 이름으로 파일을 하나 생성하세요.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return '<h1>Flask REST API</h1>'

if __name__=="__main__":
    app.run(debug=True)

해당 앱을 실행해 볼까요? Flask가 없다네요

$ flask --app app
-jailshell: flask: command not found

PIP Install Flask 2.0.3 (Pip install 권한 있음)

pip설치 권한이 있는지 모르겠는데 한번 시도해볼게요. 놀랍게도 설치가 잘되네요. 참고로 제 cpanel 계정을 C compiler group에 추가해주기 전에는 pip install 권한이 없었습니다. 일찌감치 권한요청을 하길 잘한거 같아요.

# pip3 install Flask
Successfully installed Flask-2.0.3 Jinja2-3.0.3 MarkupSafe-2.0.1 Werkzeug-2.0.3 click-8.0.4 dataclasses-0.8 itsdangerous-2.0.1

Encoding문제 해결

그러면 다시 Flask 서버를 실행해볼게요. 에러가 나네요. 파이썬이 ASCII엔코딩을 사용하도록 설정이 되어 있대요.

# flask --app app
RuntimeError: Click will abort further execution because Python was configured to use ASCII as encoding for the environment. Consult https://click.palletsprojects.com/unicode-support/ for mitigation steps.

해당 설정을 바꿔볼게요.

# export PYTHONIOENCODING=utf-8
# echo $PYTHONIOENCODING
utf-8

Consult해준대로 Locale과 언어도 바꿔볼까요?

# locale charmap
ANSI_X3.4-1968
# export LC_ALL=en_US.UTF-8
# echo $LC_ALL
en_US.UTF-8
# export LANG=en_US.UTF-8
# echo $LANG
en_US.UTF-8
# locale charmap
UTF-8

그리고 파이썬 파일의 캐릭터셋도 바꿀게요. 일단 현재 파일을 캐릭터셋을 확인합니다.

# file -i app.py
app.py: text/x-python; charset=us-ascii

ASCII를 UTF-8로 바꿔볼게요. 그런데 바로는 안바뀌네요.

# iconv -f us-ascii -t utf-8 app.py > app-utf8.py
# file -i app-utf8.py
text/x-python; charset=us-ascii

그러면 UTF-16으로 변환을 했다가 다시 UTF-8로 변환을 시도해보도록 하겠습니다.

# iconv -f us-ascii -t utf-16 app.py > app-utf16.py
# file -i app16.py
app16.py: text/x-python; charset=utf-16le
# iconv -f utf-16le -t utf-8 app-utf16.py > app-utf8.py
# file -i app8.py
app8.py: text/x-python; charset=utf-8

Run Flask 2.0.3 Server

이제 Flask서버를 다시 실행해보겠습니다. --app옵션이 없다고 나오네요.

# flask --app app-utf8.py
Usage: flask [OPTIONS] COMMAND [ARGS]...
Try 'flask --help' for help.

Error: No such option: --app

파이썬 3.6버젼에서 설치할 수 있는 Flask버젼은 2.0.3이 최선이었나봅니다.

# flask --version
Python 3.6.8
Flask 2.0.3
Werkzeug 2.0.3

그러면 2.0.3버젼에서 실행하던 명령으로 실행해야겠네요

# flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

일단 로컬에서는 실행이 잘되었습니다

# curl http://127.0.0.1:5000/
<h1>Flask REST API</h1>

Make PHP Wrapping Service

Define Flask Routes

PHP Wrapping Service를 만들기 전에 충분한 테스트를 위하여 Flask에 Route을 몇개 더 만들어 볼게요.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return '<h1>Flask REST API</h1>'

@app.route('/profile')
def profile():
    return '<h1>Profile</h1>'

@app.route('/business')
def business():
    return '<h1>Business</h1>'

if __name__=="__main__":
    app.run(debug=True)

혹시 SSH를 나갔다 다시 들어왔다면 UTF-8로 설정한 값들이 없어져 있을거에요. 앱을 실행하기 전에 다시 설정해주셔야합니다. 매번 이렇게 해줄수는 없으니 권한 문제가 해결되면 Python과 Flask버젼을 업그레이드 하도록 하겠습니다.

# export PYTHONIOENCODING=utf-8
# export LC_ALL=en_US.UTF-8
# export LANG=en_US.UTF-8
# locale charmap
UTF-8
# file -i app.py
app.py: text/x-python; charset=utf-8

Flask를 실행하고 각 Route를 테스트합니다.

# flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [24/Jan/2025 13:58:26] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [24/Jan/2025 13:58:31] "GET /profile HTTP/1.1" 200 -
127.0.0.1 - - [24/Jan/2025 13:58:35] "GET /business HTTP/1.1" 200 -

# curl http://127.0.0.1:5000/
<h1>Flask REST API</h1>
# curl http://127.0.0.1:5000/profile
<h1>Profile</h1>
# curl http://127.0.0.1:5000/business
<h1>Business</h1>

.htaccess

PHP코드를 하기전에 .htaccess에 Route을 설정할게요.

RewriteEngine On
RewriteRule ^profile index.php?c=profile [L,QSA]
RewriteRule ^business index.php?c=business [L,QSA]

PHP Wrapper App

그러면 이제 PHP로 Corresponding URL을 호출하는 앱을 만들어 볼게요. index.php를 생성합니다.

<?php
$path = $_GET['c'];
$url = "http://127.0.0.1:5000/".$path;
echo file_get_contents($url);
?>

자 이제 최종테스트를 해볼까요? 로컬 터미널에서 해당 서비스를 호출합니다.

$ curl https://__YOUR_DOMAIN__/profile
<h1>Profile</h1>
$ curl https://__YOUR_DOMAIN__/business
<h1>Business</h1>

오래된 버젼으로 어찌어찌 서비스가 되기는 합니다만 이렇게 서비스를 하는 것은 추천드리는 바는 아닙니다. 부득이한 경우에 사용할 수는 있지만 안정적으로 사용할 수는 없을 것 같습니다. 특히 문제가 되는것은 Python버젼이 너무 낮아서 Flask를 최신 버젼으로 설치하지 못하는 것인데 그렇게 되면 OEM등의 기능을 전혀 사용하지 못하게 되어 코드가 너무 낡아져 버립니다. 그래도 이게 되는지 확인하고 싶었고, 불편한 수준에 그쳤지만 어쨌든 Rough하게 가능하다는 거는 확인했으니까 이제 버젼 업그레이드 하는 문제만 해결하면 되겠네요.

Python 3.12.8 재시도 (성공)

Python 3.12.8 재설치 (권한받음)

Bluehost에 연락해서 파이썬 버젼을 업그레이드해달라고 부탁했습니다. 제가 권한이 없어서 직접 설치를 하지 못한다고 했더니 권한을 부여해주더라구요. 대신 서버에 있는 버젼은 업그레이드 할수 없고, 제 Home디렉토리에 설치해서 Alias로 우회하는 방법을 사용해야합니다. 오히려 그게 그들에게도 저에게도 안전한것 같아요. 서버의 파이썬 버젼이 바뀌어도 제 폴더의 파이썬 버젼은 제가 업그레이드할때까지 그대로 일테니까요. 너무 잘된것 같습니다.

그럼 다시 일전에 받아두었던 최신버젼을 설치해보도록하겠습니다. 고객센터에서 참고하라고 보내준 링크입니다. 지난번에 Python-3.12.8를 다운받아서 압축을 해제하는 것 까지는 했으니까 오늘은 configure부터 실행하도록 하겠습니다.

mkdir ~/python

cd ~/python

wget http://www.python.org/ftp/python/3.12.8/Python-3.12.8.tgz

tar zxfv Python-3.12.8.tgz

find ~/python -type d | xargs chmod 0755

cd Python-3.12.8

./configure --prefix=$HOME/python

make

make install

위의 configure명령를 실행하겠습니다.

# ./configure --prefix=$HOME/python
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking for Python interpreter freezing... ./_bootstrap_python
checking for python3.12... no
...
Checked 111 modules (31 built-in, 71 shared, 1 n/a on linux-x86_64, 1 disabled, 7 missing, 0 failed on import)

그럭저럭 잘 실행이 되네요. 그러면 이번에는 make을 실행하도록 하겠습니다.

# make
gcc -std=gnu11 -pthread -c -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall    -std=c11 -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Wstrict-prototypes -Werror=implicit-function-declaration -fvisibility=hidden  -I./Include/internal  -I. -I./Include    -DPy_BUILD_CORE -o Programs/python.o ./Programs/python.c
...
Checked 111 modules (31 built-in, 71 shared, 1 n/a on linux-x86_64, 1 disabled, 7 missing, 0 failed on import)

일단 에러는 안났으니까. 그러면 이번엔 진짜 설치를 하도록하겠습니다.

# make install
Creating directory /home/__HOMEDIR__/local/python/bin
Creating directory /home/__HOMEDIR__/local/python/lib
...
WARNING: The scripts pip3 and pip3.12 are installed in '/home/__HOMEDIR__/local/python/bin' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-24.3.1

성공적으로 Python 3.12.8을 설치했습니다.

Make venv with Python 3.12.8

개발에 앞서 해당 프로젝트 폴더에 들어가서 가상환경을 만듭니다. 그래야 다른 프로젝트랑 패키지버젼이 conflic되지 않을테니까요.

# cd ~/www/__PROJECT_DIR__
# ~/python/bin/python3 -m venv .venv
# source ~/www/__PROJECT_DIR__/.venv/bin/activate
(.venv)#

나중에 편리하게 사용하기 위해서 alias를 만들겠습니다.

alias workon="source ~/www/__PROJECT_DIR__/.venv/bin/activate"
alias workoff="deactivate"

PIP Install Flask (SSL기능 안됨)

이제 Flask를 최신버젼으로 설치해볼까요?

(.venv)# pip install Flask
WARNING: Disabling truststore since ssl support is missing
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
...
ERROR: No matching distribution found for Flask

SSL이 왜 안되는지 좀 찾아봐야겠어요.

Install OpenSSL 3.4.0

찾아보니까 새로 설치한 최신버젼의 파이썬이 OpenSSl 1.0.2가 너무 오래되서 더이상 지원하지 않는다고 하네요.

# openssl version
OpenSSL 1.0.2k-fips  26 Jan 2017

현재 서버에 설치되어 있는 OpenSSL 버젼은 1.0.2인데 Python 3.12.8을 사용하려면 최소한 OpenSSL 1.1.1이 설치되어있어야합니다. 일단 제가 이 서버에 루트권한이 없으니까 새로운 버젼의 OpenSSL을 홈디렉토리에 설치하고 해당 OpenSSL을 이용해서 Python을 재설치하면 해결이 될것같아요. 현재시점 OpenSSL의 최신버젼은 3.4.0입니다. OpenSSL 3.4.0을 다운받을게요.

wget https://github.com/openssl/openssl/releases/download/openssl-3.4.0/openssl-3.4.0.tar.gz

다운받은 압축파일을 해제합니다.

tar zxfv openssl-3.4.0.tar.gz

압축이 풀린 폴더를 실행가능하게 권한을 변경할게요

find ~/python -type d | xargs chmod 0755

해당 폴더에 들어가세요

cd openssl-3.4.0

그리고 실행전 필요한 설정을 합니다. 여기서 설정할 것은 prefix라고 최종적으로 설치를 할 폴더를 명시합니다. 그리고 openssldir도 같은 폴더위치를 값으로 할당합니다. prefix와 openssldir은 절대경로로 적어주셔야 설치파일이 실행 됩니다. 공용라이브러리를 사용하지 않을거니까 config할때 shared zlib는 명시하지 않습니다.

./config --prefix=$HOME/local/ssl --openssldir=$HOME/local/ssl

위에서 설정한 대로 설치파일을 컴파일하도록 하겠습니다.

make

컴파일이 완료되면 설치를 진행하기 전에 에러가 없는지 점검을 해본 뒤에 진행할게요

make test

문제가 없으면 실제 설치를 진행합니다.

make install

설치가 완료되면 이제 HOME디렉토리에 설치한 OpenSSL를 실행할건데 이때 OpenSSL을 실행하면 libssl.so.3 모듈을 공용폴더에서 찾으려고 할거에요. 이게 HOME디렉토리에 있다고 미리 알려줘야 OpenSSL이 libssl.so.3를 찾아서 실행할 수가 있어요. 아래와 같이 LD_LIBRARY_PATH경로에 새로 설치한 OpenSSL 라이브러리를 추가합니다.

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/local/ssl/lib64

방금 설치한 OpenSSL의 버젼을 확인해볼까요

# $HOME/local/ssl/bin/openssl version -a
OpenSSL 3.4.0 22 Oct 2024 (Library: OpenSSL 3.4.0 22 Oct 2024)
built on: Mon Jan 27 21:52:41 2025 UTC
platform: linux-x86_64
options:  bn(64,64)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -O3 -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_BUILDING_OPENSSL -DZLIB -DNDEBUG
OPENSSLDIR: "$HOME/local/ssl"
ENGINESDIR: "$HOME/local/ssl/lib64/engines-3"
MODULESDIR: "$HOME/local/ssl/lib64/ossl-modules"
Seeding source: os-specific
CPUINFO: OPENSSL_ia32cap=0xfffa32034f8bffff:0x818d19e4fbb

    Python 3.12.8 재설치 with OpenSSL 3.4.0

    OpenSSL경로가 바뀌었기때문에 파이썬을 재설치해야합니다. 아까 다운받아서 풀어놓은 Python-3.12.8폴더에 들어가서 configure를 하는데 이번엔 뒤에 with-openssl플래그를 주어서 새로 설치되는 파이썬이 우리가 방금 홈에 설치한 OpenSSL을 사용하도록 합니다.

    설치폴더에 들어가세요

    cd Python-3.12.8

    설치를 진행하기 전에 OpenSSL의 변경된 경로를 LDFLAGS와 CPPFLAGS를 설정함으로써 파이썬에 전달할거에요.

    export LDFLAGS="-L$HOME/local/ssl/lib64"
    export CPPFLAGS="-I$HOME/local/ssl/include"

    configure를 할때 –with-openssl와 -with-openssl-rpath에 OpenSSL경로를 명시합니다.

    ./configure --prefix=$HOME/local/python --with-openssl=$HOME/local/ssl --with-openssl-rpath=$HOME/local/ssl/lib64

    그리고 make, make install을 실행하여 파이썬을 설치합니다.

    # make
    ...
    # make install
    ...
    Successfully installed pip-24.3.1

    Make new venv with new Python location

    제가 OpenSSL지원하는 버젼을 ~/local에 새롭게 설치했기 때문에 기존에 ~/python에서 Python을 갖다 쓰던 virtual env는 SSL을 사용할 수가 없습니다. venv을 새로 설치한 Python을 가지고 다시 만들어야합니다. 헷갈리니까 ~/python에 있는 파일들은 전부 삭제하도록 하겠습니다.

    일단 workoff를 실행해서 venv에서 나옵니다.

    # workoff

    그리고 프로젝트 폴더에 들어가서 venv폴더를 삭제합니다.

    # cd ~/www/__PROJECT_DIR__
    # rm -rf .venv

    그리고 새로 설치한 SSL이 지원되는 파이썬을 가지고 다시 venv를 만들어 줍니다.

    # ~/local/python/bin/python3 -m venv .venv

    기존에 정의한 alias는 그대로 사용할 수 있습니다.

    # workon
    (.venv) #

    이제 파이썬이 ssl모듈을 지원하는지 확인해 볼게요

    python -m ssl

    PIP install Flask

    드디어 원점으로 돌아온 느낌입니다. Flask패키지를 설치해볼게요.

    # pip install Flask
    ...
    Successfully installed Flask-3.1.0 Jinja2-3.1.5 MarkupSafe-3.0.2 Werkzeug-3.1.3 blinker-1.9.0 click-8.1.8 itsdangerous-2.2.0
    # pip list
    Package      Version
    ------------ -------
    blinker      1.9.0
    click        8.1.8
    Flask        3.1.0
    itsdangerous 2.2.0
    Jinja2       3.1.5
    MarkupSafe   3.0.2
    pip          24.3.1
    Werkzeug     3.1.3

    너무 잘 되네요. 우여곡절끝에 해낸것 같습니다 ㅎㅎ 코딩하면서 또 문제가 생기겠지만 그때마다 하나씩 풀어나가면 해결이 되겠지요. 오늘은 여기까지 하겠습니다. 수고하셨습니다.

    References