소개
Commonlight의 편집자는 먼저 작은 한 페이지 샘플부터 사용할 수 있는 사진 카탈로그를 만들려고 합니다. 실제 HTTP 요청으로 JSON을 받아 필드에 따라 레코드를 선택하고 CSV로 저장합니다. Commonlight는 직접 만든 로컬 데모 API입니다. 레코드와 크레딧은 허구이며, 예시 이미지는 합성 일러스트입니다. 기록 사진이나 외부 사진 서비스의 콘텐츠가 아닙니다.
Python 함수, 리스트와 딕셔너리 같은 컨테이너 타입, 반복문, 예외, 기본 파일 출력을 알아야 합니다. HTTP, URL, JSON, CSV는 여기에서 설명합니다. WebIDE에서 Terminal → New Terminal을 선택하세요. 좁은 화면에서는 Terminal 메뉴가 More 안에 있을 수 있습니다. 모든 터미널 명령은 여기에서 실행합니다. 준비된 웹사이트는 Web 8080으로 엽니다. client.py와 storage.py만 수정하고, 표준 라이브러리와 두 파일 안에 정의한 보조 함수만 사용하세요. 추가 보조 모듈이나 리소스는 사용하지 않습니다. CSV는 작업 공간의 실제 일반 파일로 제출하고 심볼릭 링크로 만들지 마세요. catalog.py와 /opt/commonlight는 그대로 둡니다. 읽기와 실습에 걸리는 40분은 저자의 예상이며, 초보자를 대상으로 측정한 시간이 아닙니다.
카탈로그 한 페이지 요청하고 이해하기
이 단계에서는 API 레코드 세 개를 화면의 카드 세 개와 대응시킨 다음 요청 함수를 구현합니다. Web 8080으로 전환하세요. Harbor walks 미리보기에는 Morning, market, Blue hour, Window light가 있습니다. 전체 컬렉션이 아닌 한 페이지라는 점을 명시합니다. 그림은 맥락을 제공하며, 이 과정에서는 이미지를 다운로드하지 않고 메타데이터를 내보냅니다.

그림 아래의 ID를 곧 요청할 응답과 대응시켜 보세요.
WebIDE와 터미널로 돌아갑니다. cd는 작업 디렉터리를 바꾸고 python3는 Python을 실행합니다. 정확히 다음 경로를 사용하세요.
cd /home/labex/project
HTTP는 이 로컬 API가 사용하는 요청·응답 프로토콜입니다. URL은 주소를 나타냅니다. http는 프로토콜, 127.0.0.1은 현재 환경 자체, 8080은 서비스 포트이며 /api/harbor는 컬렉션을 선택합니다. ? 뒤의 쿼리 page_size=3은 레코드를 최대 세 개 요청합니다. 이 주소는 터미널에서 사용합니다. Web 8080은 같은 서비스에 대해 플랫폼이 제공하는 브라우저 주소를 엽니다.
다음 here-document는 작은 Python 프로그램을 실행합니다. <<'PY'는 마지막 PY 앞까지의 줄을 그대로 Python 표준 입력에 전달하므로 구분자 두 줄도 함께 복사하세요. urlopen은 실제 HTTP 요청을 보냅니다. timeout=3은 연결이나 소켓 대기 같은 블로킹 작업을 제한하며, 가능한 모든 전체 작업의 총 실행 시간을 제한하는 것은 아닙니다. with는 사용 후 응답을 닫습니다. JSON은 구조화된 텍스트이며, json.load는 응답을 Python 데이터로 디코딩합니다.
python3 - <<'PY'
import json
from urllib.request import urlopen
with urlopen('http://127.0.0.1:8080/api/harbor?page_size=3', timeout=3) as response:
print('HTTP status:', response.status)
page = json.load(response)
for photo in page['items']:
print(photo['photo_id'], photo['title'])
print('More pages:', page['next_cursor'] is not None)
PY
HTTP status: 200
p01 Morning, market
p02 Blue hour
p03 Window light
More pages: True
상태 200은 요청 성공을 뜻합니다. 바깥쪽 JSON 객체는 딕셔너리가 됩니다. 그 안의 items 값은 레코드 딕셔너리들의 리스트이지 응답 전체가 아닙니다. JSON의 null은 Python의 None이 됩니다. 여기서 next_cursor는 아직 문자열이므로 다음 페이지가 있습니다. 이번 단원은 의도적으로 한 페이지만 내보내며, 다음 단원에서 전체 순회를 완성합니다.
각 레코드에는 photo_id, title, category, orientation, credit이 있습니다. 안정적인 ID가 레코드를 식별하고, 제목은 서로 다른 사진이 공유할 수 있는 표시용 라벨입니다. 준비된 계약은 /opt/commonlight/api-contract.md에서 읽을 수 있습니다. cat은 텍스트 파일을 표시합니다.
cat /opt/commonlight/api-contract.md
재사용할 수 있는 요청 함수와 선택 함수를 추가합니다. urlencode는 딕셔너리를 쿼리 문자열로 인코딩합니다. 커서는 내부 의미를 해석하지 않는 불투명한 연속 토큰입니다. 숫자를 추측하거나 문자를 해석하지 말고 정확히 복사하세요. 선택 인자 cursor=None은 첫 요청에서 해당 쿼리 매개변수를 생략한다는 뜻입니다. 뒤쪽 페이지에는 인코딩이 필요한 +, /, = 같은 문자가 들어갈 수 있습니다. 한 번의 순회에서는 URL과 페이지 크기를 바꾸지 마세요.
선택 과정은 카테고리와 방향을 모두 대소문자를 구분하여 정확히 비교합니다. 일치하는 항목이 없으면 빈 리스트가 올바른 결과입니다. 리스트 컴프리헨션에서 for photo in photos는 각 레코드를 방문하고, if는 필터를 통과한 것만 남기며, and는 두 비교가 모두 참이어야 함을 뜻합니다. 받은 레코드를 바꾸지 않고 새 리스트를 만듭니다.
다음 블록은 cat과 >로 client.py를 Python 텍스트 그대로 교체합니다. 마지막 PY까지 블록 전체를 실행하세요. WebIDE에서 파일을 열어 내용을 교체하고 Ctrl+S로 저장해도 됩니다. macOS에서는 Cmd+S를 사용합니다. 이 블록은 해당 파일만 변경합니다.
cat > client.py <<'PY'
"""The request and selection functions are completed in the first guided lab."""
import json
from urllib.parse import urlencode
from urllib.request import urlopen
def request_page(endpoint, page_size, cursor=None):
params = {'page_size': page_size}
if cursor is not None:
params['cursor'] = cursor
with urlopen(endpoint + '?' + urlencode(params), timeout=3) as response:
return json.load(response)
def select_photos(photos, category, orientation):
return [photo for photo in photos
if photo['category'] == category and photo['orientation'] == orientation]
def collect(endpoint, page_size):
return request_page(endpoint, page_size)['items']
PY
준비된 명령 인터페이스가 이 함수들을 호출합니다. 현재 collect는 명시적으로 한 페이지용 시작 구현입니다. 자체 검사는 별도의 실제 로컬 HTTP 소스로 요청을 시험하므로 고정된 카탈로그 응답 대신 전달된 URL을 사용해야 합니다. 검사가 CSV를 대신 만들어 주지는 않습니다.
Python 파일을 열 수 없다는 메시지가 나오면 pwd로 현재 디렉터리를, ls로 파일 이름을 확인하세요. 앞서 배운 cd로 /home/labex/project에 돌아와 파일 이름을 고친 뒤 재시도합니다. VM을 초기화할 필요는 없습니다.
한 페이지를 필터링하고 실제 CSV 저장하기
이 단계에서는 첫 페이지의 가로 방향 거리 사진을 선택하고 실제 필드 값이 담긴 CSV를 엽니다. CSV는 쉼표로 구분한 값이지만, 제목 안의 쉼표는 한 필드에 속합니다. 올바른 CSV 작성기는 이를 따옴표로 감쌉니다. 문자열을 단순 연결하거나 모든 쉼표에서 나누면 이 관계가 깨집니다. UTF-8은 Noé 같은 이름과 비라틴 문자를 보존합니다. newline=''은 csv 모듈이 CSV 줄바꿈을 관리하도록 합니다.
FIELDS는 다섯 헤더 이름을 정의합니다. 각 이름은 한 번씩 나타나며, 동등한 구현에서는 레코드나 열 순서가 달라도 됩니다. DictWriter는 딕셔너리 값을 같은 이름의 열에 대응시킵니다. writeheader는 헤더를 쓰고 writerows는 선택한 레코드를 씁니다. 빈 선택이라면 헤더만 있는 파일이 생성됩니다. 파일을 w로 열면 내용이 교체됩니다. 첫 단원에서는 페이지 샘플을 저장하며, 여러 페이지의 완전한 결과를 안전하게 공개하는 방법은 다음에 배웁니다.
WebIDE 터미널에서 storage.py만 교체하세요.
cat > storage.py <<'PY'
"""Single-page CSV writer; safe publication is taught in the second guided lab."""
import csv
FIELDS = ['photo_id', 'title', 'category', 'orientation', 'credit']
def save_csv(output, photos):
with open(output, 'w', encoding='utf-8', newline='') as stream:
writer = csv.DictWriter(stream, fieldnames=FIELDS)
writer.writeheader()
writer.writerows(photos)
PY
준비된 catalog.py는 컬렉션 URL, 카테고리, 방향, 최대 페이지 크기, 출력 파일 이름의 위치 인자 다섯 개를 받습니다. --first-page는 샘플 모드를 명시적으로 선택합니다. street와 landscape가 두 개의 정확한 필터 값입니다. 셸 따옴표는 특수 문자가 포함된 URL이나 파일 이름을 하나의 인자로 유지합니다.
python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 page.csv --first-page
Saved 2 photos to page.csv
따옴표로 둘러싼 필드를 복원하는 DictReader로 결과를 읽습니다. API가 아닌 실제 저장 파일을 읽는 것입니다.
python3 - <<'PY'
import csv
with open('page.csv', encoding='utf-8', newline='') as stream:
for photo in csv.DictReader(stream):
print(photo['photo_id'], '|', photo['title'], '|', photo['credit'])
PY
p01 | Morning, market | Mina Chen
p02 | Blue hour | Noé Silva
p03이 없는 이유는 home 카테고리의 세로 방향 이미지이기 때문입니다. 첫 제목의 쉼표와 크레딧의 악센트 문자는 각각의 레코드에 연결된 채 유지됩니다.
이제 의도적인 HTTP 거부를 관찰합니다. API가 허용하는 페이지 크기는 1–5이므로 9는 성공한 빈 컬렉션이 아니라 400을 반환합니다. 이 블록을 실행하세요. 첫 명령은 실패하는 것이 정상입니다. $?는 바로 앞 명령의 종료 상태이며, printf는 다른 명령이 덮어쓰기 전에 이를 표시합니다.
python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 9 rejected.csv --first-page
printf 'Exit status: %s\n' "$?"
HTTP request refused: 400. No complete export published.
Exit status: 1
HTTPError는 이 HTTP 거부를 뜻합니다. 연결 실패는 별도로 Connection failed라고 보고합니다. 둘 다 유효한 일치 항목 0개의 결과가 아닙니다. 준비된 명령은 예외를 잡아 설명하고 0이 아닌 상태로 종료합니다. 허용 크기로 돌아가 복구하고, 서비스는 변경하지 마세요.
python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 page.csv --first-page
Web 8080으로 전환하여 Your exports, page.csv를 차례로 선택합니다. 표에는 위의 실제 레코드 두 개가 표시되어야 합니다. Download CSV를 선택한 뒤 다운로드 파일을 텍스트나 CSV 읽기 도구로 여세요. 다운로드는 저장된 파일 바이트 그대로이며, 페이지가 소스에서 누락 레코드를 가져오지 않습니다. 페이지 탐색으로 Catalog에 돌아간 뒤 WebIDE로 전환하여 자체 검사를 진행하세요. 재사용 가능한 한 페이지 샘플을 만들었지만, null이 아닌 연속 토큰은 아직 완전한 카탈로그가 아님을 보여 줍니다.
요약
실제 HTTP 요청을 보내고, JSON 페이지의 바깥 구조와 레코드를 구분하고, 정확한 필드 값으로 선택하여 따옴표 처리가 올바른 UTF-8 CSV를 작성했습니다. HTTP 거부, 연결 실패, 유효한 빈 결과는 의미가 다릅니다. 레코드 두 개의 페이지 샘플은 사용할 수 있으며, 다음 단원에서 남은 커서를 따라 전체 결과를 완성합니다.



