카탈로그의 모든 페이지 따라가기

PythonBeginner
지금 연습하기

소개

카드 세 개의 미리보기는 둘러보기에 편리하지만 편집자는 일치하는 모든 레코드가 필요합니다. 같은 로컬 데모 API의 클라이언트를 완성하여 연속 토큰을 따르고 페이지 사이의 ID를 보존하며 전체 배치가 성공한 뒤에만 CSV를 공개하세요. 빈 페이지나 짧은 페이지 뒤에도 더 많은 페이지가 있을 수 있습니다.

이 단원은 독립적으로 초기화되며 첫 번째 가이드 실습의 요청 함수와 선택 함수가 준비되어 있습니다. 해당 실습 또는 동등한 HTTP/JSON/CSV 지식이 필요합니다. WebIDE에서 Terminal → New Terminal을 사용하고, 소스와 저장한 결과 및 공개된 Demo controls는 Web 8080으로 엽니다. client.py와 storage.py만 수정하세요. 표준 라이브러리와 파일 내부 보조 함수만 사용하고 추가 모듈이나 리소스는 사용하지 않습니다. CSV는 작업 공간의 실제 일반 파일이어야 하며 심볼릭 링크는 허용하지 않습니다. catalog.py, /opt/commonlight, 합성 그림과 허구의 원본 레코드는 그대로 두세요. 읽기와 실습의 55분은 저자의 예상이며 초보자 실측 시간이 아닙니다.

프로토콜이 끝날 때까지 따라가기

이 단계에서는 각 ID를 한 번만 보존하면서 빈 페이지와 짧은 데이터 페이지를 포함한 모든 페이지를 수집합니다. WebIDE 터미널에서 독립적으로 준비된 작업 공간으로 이동하세요.

cd /home/labex/project

요청 함수는 이미 JSON, 인코딩된 커서, 블로킹 작업의 3초 제한을 처리합니다. cat으로 준비된 로직을 확인하세요.

cat client.py

현재 collect는 한 페이지만 반환합니다. page_size가 3이면 최대 세 레코드라는 뜻이지 모든 페이지가 세 레코드여야 한다는 뜻은 아닙니다. 작은 순서를 예상해 보세요. 처음에는 세 레코드, 다음에는 레코드가 없지만 또 다른 커서, 그다음에는 한 레코드와 또 다른 커서입니다. 빈 페이지는 아무것도 더하지 않지만 완료를 증명하지도 않습니다. 반환된 커서만 사용해 이 순서를 실행하세요.

python3 - <<'PY'
import client
url = 'http://127.0.0.1:8080/api/harbor'
first = client.request_page(url, 3)
second = client.request_page(url, 3, first['next_cursor'])
third = client.request_page(url, 3, second['next_cursor'])
print('Page 1 items:', len(first['items']))
print('Page 2 items:', len(second['items']))
print('Page 2 finished:', second['next_cursor'] is None)
print('Page 3 items:', len(third['items']))
print('Page 3 finished:', third['next_cursor'] is None)
PY
Page 1 items: 3
Page 2 items: 0
Page 2 finished: False
Page 3 items: 1
Page 3 finished: False

정상 배치를 끝내는 조건은 next_cursor is None뿐입니다. items가 비었거나 페이지가 짧거나 추측한 페이지 수에 도달했다는 이유로 멈추지 마세요.

같은 사진 ID가 다른 페이지에서 동일한 값으로 다시 나타날 수 있습니다. photo_id를 키로 하는 딕셔너리는 ID와 모든 필드의 관계를 유지합니다. setdefault(key, value)는 키가 없을 때만 값을 넣습니다. 따라서 복사본 하나가 남으며, 제목이 같다고 ID가 중복된 것은 아닙니다. 충돌하는 수정본은 이 데모 계약의 범위 밖입니다. Blue hour라는 제목의 서로 다른 ID 두 개는 모두 보존해야 합니다.

집합은 고유한 값을 기록합니다. set()은 빈 집합을 만들고, cursor in seen_cursors는 토큰이 이미 있는지 확인하며, seen_cursors.add(cursor)는 새 토큰을 기록합니다. 이 집합이 이미 본 연속 토큰을 기억합니다. 먼저 정상적인 null 종료를 확인한 뒤 반복된 토큰을 거부하세요. 무한 반복하거나 순환을 완료로 취급하면 안 됩니다. raise ValueError는 불완전한 결과를 저장하기 전에 수집을 중단합니다. 정상적으로 준비된 카탈로그는 유한하며, 고정 페이지 수 상한이나 자동 재시도 또는 이어받기는 없습니다.

client.py를 다음의 전체 모듈로 교체합니다. 앞에서처럼 >는 이 파일을 바꾸고, 따옴표로 지정한 here-document는 내용을 그대로 유지합니다. 요청과 필터는 같으며 collect가 새 부분입니다.

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):
    photos = {}
    cursor = None
    seen_cursors = set()
    while True:
        page = request_page(endpoint, page_size, cursor)
        for photo in page['items']:
            photos.setdefault(photo['photo_id'], photo)
        cursor = page['next_cursor']
        if cursor is None:
            return list(photos.values())
        if cursor in seen_cursors:
            raise ValueError('Repeated cursor; catalog did not complete')
        seen_cursors.add(cursor)
PY

전체 수집을 실행합니다. --first-page는 의도적으로 샘플만 생성하므로 생략하세요.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv
Saved 5 photos to catalog.csv

숫자만 믿지 말고 저장된 ID를 확인하세요.

python3 - <<'PY'
import csv
with open('catalog.csv', encoding='utf-8', newline='') as stream:
    for photo in csv.DictReader(stream):
        print(photo['photo_id'], '|', photo['title'])
PY
p01 | Morning, market
p02 | Blue hour
p04 | The "quiet" lane
p08 | Blue hour
p09 | Last tram — 帰り道

Blue hour의 두 ID와 마지막 페이지 레코드가 포함된 완전한 저장 CSV

마지막 페이지의 p09이 있고 p02는 한 번만 나타나며, p08은 다른 ID로 같은 제목을 유지합니다. Web 8080 → Your exports → catalog.csv에서 같은 값을 살펴보세요. 이 표는 CSV만 읽으므로 불완전한 파일은 여기에서도 불완전합니다. WebIDE로 돌아가 자체 검사를 수행하세요. 파일 이름이나 디렉터리가 틀렸다면 pwd, ls, 앞의 cd로 복구한 뒤 재시도합니다.

완전한 결과만 공개하기

이 단계에서는 이후 요청, 프로토콜 순환, 파일 쓰기가 실패해도 마지막 정상 출력을 보호합니다. 수집은 이미 저장 전에 끝납니다. 작성기에도 완전한 성공 경계가 필요합니다. 최종 경로를 w로 열면 새 쓰기가 성공하기 전에 올바른 파일이 잘릴 수 있습니다.

Path(output)은 전달된 파일 이름 문자열을 경로 객체로 바꿉니다. .parent는 상위 디렉터리, .name은 최종 파일 이름입니다. NamedTemporaryFile은 catalog.csv.tmp처럼 예측 가능한 이웃 파일을 빌려 쓰는 대신 새로운 고유 이름을 만듭니다. dir=output.parent는 대상 디렉터리에 두어 같은 파일 시스템 안에서 교체하게 합니다. delete=False는 파일을 닫은 뒤에도 교체할 수 있도록 남겨 둡니다. stream.name은 할당된 이름이며 Path로 저장합니다. finally 블록은 성공한 뒤에도 예외가 발생한 뒤에도 실행됩니다. exists()는 임시 경로가 남아 있는지 확인하고, unlink()는 남아 있을 때 이번에 새로 만든 파일만 삭제합니다. 교체가 성공하면 임시 경로는 사라지므로 삭제할 것이 없습니다. 예외는 계속 명령 인터페이스로 전달되어 0이 아닌 종료를 만듭니다. Path.replace는 with 블록이 파일을 닫은 뒤 완성된 파일을 공개합니다. 성공한 POSIX 교체는 원자적이지만, 전원 장애 시 영속성이나 여러 파일의 트랜잭션, 동시 작성자 조정을 보장하지 않습니다. 관계없는 기존 .tmp 파일은 덮어쓰거나 삭제하면 안 됩니다.

WebIDE 터미널에서 storage.py를 교체하세요. CSV의 다섯 필드는 헤더에 각각 정확히 한 번 나타나야 합니다. 동등한 구현에서는 유효한 인용 방식, 행 순서, 열 순서가 달라도 됩니다.

cat > storage.py <<'PY'
import csv
import tempfile
from pathlib import Path

FIELDS = ['photo_id', 'title', 'category', 'orientation', 'credit']

def save_csv(output, photos):
    output = Path(output)
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(
            mode='w', encoding='utf-8', newline='',
            dir=output.parent, prefix='.' + output.name + '.',
            suffix='.tmp', delete=False
        ) as stream:
            temporary = Path(stream.name)
            writer = csv.DictWriter(stream, fieldnames=FIELDS)
            writer.writeheader()
            writer.writerows(photos)
        temporary.replace(output)
    finally:
        if temporary is not None and temporary.exists():
            temporary.unlink()
PY

문제를 재현하기 전에 정상 배치를 한 번 실행합니다. sha256sum은 내용 지문과 파일 이름을 출력합니다. 지문을 기억하거나 터미널 출력을 보이는 상태로 두세요. 다음 실패 배치에서도 바뀌지 않아야 합니다. 쓰기 실패도 마지막 완전한 파일을 보존해야 하며, 검사는 작업 공간을 바꾸지 않는 격리된 테스트로 이를 확인합니다.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv
sha256sum catalog.csv

Web 8080으로 전환해 Demo controls, Enable middle-page failure를 선택하고 Middle-page failure ON을 확인합니다.

중간 페이지 오류는 활성화되고 커서 반복 제어는 비활성화된 상태

활성화한 오류에 해당하는 제어 카드를 사용하세요. 데모 API의 메모리만 바꾸는, 명시적으로 허용된 되돌릴 수 있는 변경입니다. 소스 파일은 읽기 전용으로 남습니다. WebIDE로 돌아가 의도적인 실패를 실행합니다. printf가 즉시 $?를 기록합니다.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv
printf 'Exit status: %s\n' "$?"
sha256sum catalog.csv
HTTP request refused: 503. No complete export published.
Exit status: 1
...

여기서 HTTP 503은 데모 API가 중간 페이지 요청을 거부했다는 뜻입니다. 연결 실패도, 정상인 빈 결과도 아닙니다. 지문은 이전과 같아야 하며, 예전의 다섯 행 출력은 계속 볼 수 있어야 합니다. 프로그램이 이미 파일을 손상했다면 종료 코드가 0이 아니라는 사실만으로는 충분하지 않습니다.

Web 8080 → Demo controls에서 중간 페이지 오류의 Restore normal pages를 선택하고 OFF를 확인합니다. WebIDE로 돌아가 컬렉션 URL에서 새 유한 배치를 시작하세요. 이전 커서를 재사용하지 않습니다.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv

다음으로 Web 8080 → Demo controls에서 Enable repeated cursor를 선택합니다. Repeated cursor ON이며 오류 제어는 OFF인지 확인하고 WebIDE로 돌아갑니다.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv
printf 'Exit status: %s\n' "$?"
Export failed: Repeated cursor; catalog did not complete
Exit status: 1

이 응답의 HTTP 상태는 200이지만 반복된 커서는 완료가 아닌 프로토콜 오류입니다. 집합 검사가 무한 반복과 공개를 막습니다. Web 8080 → Demo controls의 Repeated cursor 카드 안에서 Restore normal pages를 선택하고 OFF를 확인하세요. 이제 두 제어가 모두 OFF여야 합니다. WebIDE로 돌아가 성공하는 명령을 다시 실행한 뒤, 새 출력 이름으로 유효한 일치 항목 0개 필터도 시험합니다.

python3 catalog.py 'http://127.0.0.1:8080/api/harbor' street landscape 3 catalog.csv
python3 catalog.py 'http://127.0.0.1:8080/api/harbor' missing landscape 2 empty.csv
Saved 5 photos to catalog.csv
Saved 0 photos to empty.csv

두 번째 결과는 성공했으며 헤더만 있습니다. 앞의 두 오류와 다릅니다. Web 8080 → Your exports에서 empty.csv를 열어 다섯 헤더와 데이터 행이 없음을 확인하고, catalog.csv를 열어 Download CSV를 선택하세요. 실제 저장된 다섯 레코드 파일이 제출물입니다. WebIDE로 돌아가 자체 검사를 수행합니다. 프로그램과 준비된 소스 파일을 보존하세요. 검사는 실제 결과를 읽고 변경하지 않은 복사본을 격리된 실제 HTTP 테스트에서 실행하여 다른 URL, 필터, 크기, 쓰기 오류도 확인합니다.

요약

불투명한 연속 토큰을 명시적인 null 종료까지 따르고, 제목이 같아도 서로 다른 ID를 보존했으며 프로토콜 순환을 거부했습니다. 고유한 임시 파일로 완전한 UTF-8 CSV를 공개하고 실패 후에도 이전 정상 결과를 보호했습니다. 같은 프로그램은 다른 컬렉션과 선택 조건도 전달받을 수 있습니다.