通过 HTTP 读取照片目录

PythonBeginner
立即练习

介绍

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。页面明确表示这只是一页,而不是整个集合。图片用于提供背景;本课程导出元数据,不下载图片。

Harbor walks 的实际三条记录预览

将插画下方的 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 和 >,以原样 Python 文本替换 client.py。执行完整代码块,包括最后的 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 是两个精确筛选值。Shell 引号使含有特殊字符的 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 没有出现,因为它是家居类别的竖向照片。第一个标题中的逗号和署名中的重音字符仍与各自记录正确对应。

现在观察一次有意触发的 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。两者都不是合法的零匹配结果。准备好的命令接口会捕获异常、说明原因,并以非零状态退出。恢复方法是回到允许的大小,不要更改服务:

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 进行自检。你已经制作了可复用的单页样例,而非空续页令牌仍说明这不是完整目录。

总结

你发出了真实 HTTP 请求,将 JSON 页面外层对象与记录分开,按精确字段值筛选,并写出了正确处理引号的 UTF-8 CSV。HTTP 拒绝、连接失败与合法空结果含义不同。这份两条记录的页面样例已经可用,下个单元将沿剩余游标完成整个导出。