Python 综合实战,从网络机器人抓取数据,到 Pandas 分析,再到 FastAPI 接口

前面学的知识,如果只停留在单个语法点,很容易忘。

真正能把 Python 学扎实的方式,是做一个完整流程。

这篇文章把三个项目能力串起来:

  1. 网络机器人获取网页数据。
  2. Pandas 读取 CSV 并做统计分析。
  3. FastAPI 把统计结果做成接口。

最终你会得到一条完整的数据应用链路。
#mermaid-svg-LvK2rq3Vpt3sFLJc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LvK2rq3Vpt3sFLJc .error-icon{fill:#552222;}#mermaid-svg-LvK2rq3Vpt3sFLJc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LvK2rq3Vpt3sFLJc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .marker.cross{stroke:#333333;}#mermaid-svg-LvK2rq3Vpt3sFLJc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LvK2rq3Vpt3sFLJc p{margin:0;}#mermaid-svg-LvK2rq3Vpt3sFLJc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster-label text{fill:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster-label span{color:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster-label span p{background-color:transparent;}#mermaid-svg-LvK2rq3Vpt3sFLJc .label text,#mermaid-svg-LvK2rq3Vpt3sFLJc span{fill:#333;color:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .node rect,#mermaid-svg-LvK2rq3Vpt3sFLJc .node circle,#mermaid-svg-LvK2rq3Vpt3sFLJc .node ellipse,#mermaid-svg-LvK2rq3Vpt3sFLJc .node polygon,#mermaid-svg-LvK2rq3Vpt3sFLJc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .rough-node .label text,#mermaid-svg-LvK2rq3Vpt3sFLJc .node .label text,#mermaid-svg-LvK2rq3Vpt3sFLJc .image-shape .label,#mermaid-svg-LvK2rq3Vpt3sFLJc .icon-shape .label{text-anchor:middle;}#mermaid-svg-LvK2rq3Vpt3sFLJc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .rough-node .label,#mermaid-svg-LvK2rq3Vpt3sFLJc .node .label,#mermaid-svg-LvK2rq3Vpt3sFLJc .image-shape .label,#mermaid-svg-LvK2rq3Vpt3sFLJc .icon-shape .label{text-align:center;}#mermaid-svg-LvK2rq3Vpt3sFLJc .node.clickable{cursor:pointer;}#mermaid-svg-LvK2rq3Vpt3sFLJc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .arrowheadPath{fill:#333333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LvK2rq3Vpt3sFLJc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LvK2rq3Vpt3sFLJc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LvK2rq3Vpt3sFLJc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster text{fill:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc .cluster span{color:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LvK2rq3Vpt3sFLJc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LvK2rq3Vpt3sFLJc rect.text{fill:none;stroke-width:0;}#mermaid-svg-LvK2rq3Vpt3sFLJc .icon-shape,#mermaid-svg-LvK2rq3Vpt3sFLJc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LvK2rq3Vpt3sFLJc .icon-shape p,#mermaid-svg-LvK2rq3Vpt3sFLJc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LvK2rq3Vpt3sFLJc .icon-shape .label rect,#mermaid-svg-LvK2rq3Vpt3sFLJc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LvK2rq3Vpt3sFLJc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LvK2rq3Vpt3sFLJc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LvK2rq3Vpt3sFLJc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网页 HTML
requests 请求
lxml + XPath 解析
CSV 文件
Pandas 分析
Matplotlib 图表
FastAPI JSON 接口

先讲边界,网络机器人不是想抓就抓

网络机器人,也就是常说的爬虫。

技术流程并不复杂,但边界很重要。

抓取前要确认网站规则,查看 robots.txt 和服务条款,控制访问频率,不要给对方服务造成压力。

这篇文章为了让代码稳定可运行,会先用一段本地 HTML 演示抓取和解析流程。你理解流程后,再替换成合规的真实网页。

requests 获取网页

安装:

bash 复制代码
pip install requests lxml pandas matplotlib fastapi uvicorn

最小请求:

python 复制代码
import requests

url = "https://www.python.org/"
response = requests.get(url, timeout=10)
response.raise_for_status()

print(response.status_code)
print(response.text[:200])

几个关键点:

timeout=10 避免网络卡住时一直等待。

raise_for_status() 会在 4xx 或 5xx 状态码时抛出异常。

response.text 是网页文本。

如果需要加请求头:

python 复制代码
headers = {
    "User-Agent": "Mozilla/5.0"
}

response = requests.get(url, headers=headers, timeout=10)

不要伪装成浏览器后就觉得什么都能抓。请求头只是技术细节,合规边界仍然要遵守。

HTML 是树,不是一坨字符串

看一段 HTML:

python 复制代码
html_text = """
<html>
  <body>
    <div class="movie">
      <a href="/movie/1">电影 A</a>
      <span class="year">2020</span>
      <span class="score">8.8</span>
    </div>
    <div class="movie">
      <a href="/movie/2">电影 B</a>
      <span class="year">2021</span>
      <span class="score">8.5</span>
    </div>
  </body>
</html>
"""

解析:

python 复制代码
from lxml import html

document = html.fromstring(html_text)
movie_nodes = document.xpath("//div[@class='movie']")

print(len(movie_nodes))

XPath 常见写法:

XPath 含义
//a 找所有 a 标签
//a/text() 取 a 标签文本
//a/@href 取 a 标签 href 属性
//div[@class='movie'] 找 class 为 movie 的 div
./span[@class='score']/text() 从当前节点向下找评分

解析电影数据并保存 CSV

python 复制代码
import csv
from lxml import html

html_text = """
<html>
  <body>
    <div class="movie">
      <a href="/movie/1">电影 A</a>
      <span class="year">2020</span>
      <span class="score">8.8</span>
    </div>
    <div class="movie">
      <a href="/movie/2">电影 B</a>
      <span class="year">2021</span>
      <span class="score">8.5</span>
    </div>
    <div class="movie">
      <a href="/movie/3">电影 C</a>
      <span class="year">2020</span>
      <span class="score">9.0</span>
    </div>
  </body>
</html>
"""

document = html.fromstring(html_text)
movie_nodes = document.xpath("//div[@class='movie']")

movies = []

for node in movie_nodes:
    titles = node.xpath("./a/text()")
    links = node.xpath("./a/@href")
    years = node.xpath("./span[@class='year']/text()")
    scores = node.xpath("./span[@class='score']/text()")

    movies.append({
        "title": titles[0] if titles else "",
        "link": links[0] if links else "",
        "year": years[0] if years else "",
        "score": scores[0] if scores else ""
    })

with open("movies.csv", "w", encoding="utf-8", newline="") as file:
    writer = csv.DictWriter(file, fieldnames=["title", "link", "year", "score"])
    writer.writeheader()
    writer.writerows(movies)

print("CSV 保存完成")

这里最重要的不是 XPath 语法本身,而是字段缺失处理。

python 复制代码
titles[0] if titles else ""

真实网页上,字段可能缺失。你不能默认每个 XPath 都一定能取到结果。

正则只处理局部文本

如果有时长文本:

text 复制代码
2h 20m

可以用正则转成分钟:

python 复制代码
import re

text = "2h 20m"

hour_match = re.search(r"(\d+)h", text)
minute_match = re.search(r"(\d+)m", text)

hours = int(hour_match.group(1)) if hour_match else 0
minutes = int(minute_match.group(1)) if minute_match else 0

total_minutes = hours * 60 + minutes

print(total_minutes)

但不要用正则硬解析整个 HTML。

HTML 是树结构,优先用 lxml 和 XPath。

Pandas 读取 CSV

python 复制代码
import pandas as pd

data = pd.read_csv("movies.csv")

print(data.head())
print(data.info())

DataFrame 可以理解成表格。

Series 可以理解成一列。

python 复制代码
scores = data["score"]

print(type(data))
print(type(scores))

类型转换和缺失值

CSV 里读出来的数据不一定是你想要的类型。

python 复制代码
import pandas as pd

data = pd.read_csv("movies.csv")

data["year"] = pd.to_numeric(data["year"], errors="coerce")
data["score"] = pd.to_numeric(data["score"], errors="coerce")

data = data.dropna(subset=["year", "score"])

print(data)

errors="coerce" 表示无法转换的值变成缺失值。

dropna(subset=[...]) 表示指定字段缺失时删除这一行。

数据分析里,清洗数据比画图更重要。

groupby 分组统计

按年份统计电影数量:

python 复制代码
count_by_year = data.groupby("year")["title"].count()

print(count_by_year)

按年份计算平均分:

python 复制代码
score_by_year = data.groupby("year")["score"].mean()

print(score_by_year)

groupby 可以理解成三步:
#mermaid-svg-Tlg8T5cUYkSi7E2t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tlg8T5cUYkSi7E2t .error-icon{fill:#552222;}#mermaid-svg-Tlg8T5cUYkSi7E2t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tlg8T5cUYkSi7E2t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .marker.cross{stroke:#333333;}#mermaid-svg-Tlg8T5cUYkSi7E2t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tlg8T5cUYkSi7E2t p{margin:0;}#mermaid-svg-Tlg8T5cUYkSi7E2t .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster-label text{fill:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster-label span{color:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster-label span p{background-color:transparent;}#mermaid-svg-Tlg8T5cUYkSi7E2t .label text,#mermaid-svg-Tlg8T5cUYkSi7E2t span{fill:#333;color:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .node rect,#mermaid-svg-Tlg8T5cUYkSi7E2t .node circle,#mermaid-svg-Tlg8T5cUYkSi7E2t .node ellipse,#mermaid-svg-Tlg8T5cUYkSi7E2t .node polygon,#mermaid-svg-Tlg8T5cUYkSi7E2t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .rough-node .label text,#mermaid-svg-Tlg8T5cUYkSi7E2t .node .label text,#mermaid-svg-Tlg8T5cUYkSi7E2t .image-shape .label,#mermaid-svg-Tlg8T5cUYkSi7E2t .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tlg8T5cUYkSi7E2t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .rough-node .label,#mermaid-svg-Tlg8T5cUYkSi7E2t .node .label,#mermaid-svg-Tlg8T5cUYkSi7E2t .image-shape .label,#mermaid-svg-Tlg8T5cUYkSi7E2t .icon-shape .label{text-align:center;}#mermaid-svg-Tlg8T5cUYkSi7E2t .node.clickable{cursor:pointer;}#mermaid-svg-Tlg8T5cUYkSi7E2t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .arrowheadPath{fill:#333333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tlg8T5cUYkSi7E2t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tlg8T5cUYkSi7E2t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tlg8T5cUYkSi7E2t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster text{fill:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t .cluster span{color:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tlg8T5cUYkSi7E2t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tlg8T5cUYkSi7E2t rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tlg8T5cUYkSi7E2t .icon-shape,#mermaid-svg-Tlg8T5cUYkSi7E2t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tlg8T5cUYkSi7E2t .icon-shape p,#mermaid-svg-Tlg8T5cUYkSi7E2t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tlg8T5cUYkSi7E2t .icon-shape .label rect,#mermaid-svg-Tlg8T5cUYkSi7E2t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tlg8T5cUYkSi7E2t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tlg8T5cUYkSi7E2t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tlg8T5cUYkSi7E2t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始 DataFrame
按 year 拆分成多组
每组选择 score
对每组执行 mean/count/sum
合并成统计结果

这就是 Pandas 文档里常说的 split-apply-combine。

Matplotlib 画图

python 复制代码
import matplotlib.pyplot as plt
import pandas as pd

data = pd.read_csv("movies.csv")
data["year"] = pd.to_numeric(data["year"], errors="coerce")
data["score"] = pd.to_numeric(data["score"], errors="coerce")
data = data.dropna(subset=["year", "score"])

score_by_year = data.groupby("year")["score"].mean()

score_by_year.plot(kind="bar", color="green")

plt.title("不同年份电影平均分")
plt.xlabel("年份")
plt.ylabel("平均分")
plt.tight_layout()
plt.show()

中文乱码时,可以设置字体:

python 复制代码
plt.rcParams["font.sans-serif"] = ["SimHei"]

如果你的系统没有 SimHei,就需要换成系统里存在的中文字体。

FastAPI 把统计结果做成接口

安装:

bash 复制代码
pip install fastapi uvicorn pandas

新建 main.py

python 复制代码
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Movie(BaseModel):
    title: str
    year: int
    score: float


def load_movies() -> pd.DataFrame:
    data = pd.read_csv("movies.csv")
    data["year"] = pd.to_numeric(data["year"], errors="coerce")
    data["score"] = pd.to_numeric(data["score"], errors="coerce")
    return data.dropna(subset=["year", "score"])


@app.get("/")
def root():
    return {"message": "Movie Data API"}


@app.get("/movies/by-year")
def movies_by_year():
    data = load_movies()
    result = data.groupby("year")["title"].count()
    return {str(year): int(count) for year, count in result.items()}


@app.get("/movies/score-by-year")
def score_by_year():
    data = load_movies()
    result = data.groupby("year")["score"].mean()
    return {str(year): round(float(score), 2) for year, score in result.items()}

启动:

bash 复制代码
uvicorn main:app --reload

访问:

text 复制代码
http://127.0.0.1:8000/movies/by-year

自动接口文档:

text 复制代码
http://127.0.0.1:8000/docs

路径参数和查询参数

路径参数:

python 复制代码
@app.get("/movies/{year}")
def movies_by_one_year(year: int):
    data = load_movies()
    filtered = data[data["year"] == year]
    return filtered.to_dict(orient="records")

访问:

text 复制代码
http://127.0.0.1:8000/movies/2020

查询参数:

python 复制代码
@app.get("/movies")
def list_movies(min_score: float = 0):
    data = load_movies()
    filtered = data[data["score"] >= min_score]
    return filtered.to_dict(orient="records")

访问:

text 复制代码
http://127.0.0.1:8000/movies?min_score=8.8

FastAPI 会根据函数参数自动解析请求。

请求体和 Pydantic

如果要新增电影,可以用 Pydantic 模型描述请求体。

python 复制代码
from pydantic import BaseModel


class Movie(BaseModel):
    title: str
    year: int
    score: float


@app.post("/movies")
def create_movie(movie: Movie):
    return {
        "message": "电影已接收",
        "movie": movie.model_dump()
    }

Pydantic 会帮你校验字段类型。

如果 year 传了无法转换的内容,FastAPI 会返回清楚的校验错误。

常见问题

requests 请求一直卡住

原因:没有设置 timeout。

修复:

python 复制代码
requests.get(url, timeout=10)

XPath 取不到数据

可能原因:

  1. 页面结构和你想的不一样。
  2. 数据由 JavaScript 动态加载。
  3. XPath 路径太依赖层级。

先打印 response.text[:1000],确认 HTML 里有没有目标内容。

CSV 中文乱码

写文件时用:

python 复制代码
encoding="utf-8"

如果要给 Excel 打开,可以试:

python 复制代码
encoding="utf-8-sig"

Pandas 求平均失败

原因:数字列是字符串或包含脏数据。

修复:

python 复制代码
data["score"] = pd.to_numeric(data["score"], errors="coerce")

FastAPI 读不到 CSV

原因:运行目录不对。

修复:

pathlib 定位文件:

python 复制代码
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
MOVIE_FILE = BASE_DIR / "movies.csv"

最终项目结构

text 复制代码
movie-data-project
├── crawler.py
├── analysis.py
├── main.py
├── movies.csv
└── requirements.txt

crawler.py 负责抓取和保存 CSV。

analysis.py 负责 Pandas 分析。

main.py 负责 FastAPI 接口。

movies.csv 是数据文件。

requirements.txt 记录依赖。

练习

在现有项目上增加一个接口:

text 复制代码
/movies/top?limit=3

要求返回评分最高的前 limit 部电影。

参考代码:

python 复制代码
@app.get("/movies/top")
def top_movies(limit: int = 3):
    data = load_movies()
    top_data = data.sort_values("score", ascending=False).head(limit)
    return top_data.to_dict(orient="records")

参考资料