前面学的知识,如果只停留在单个语法点,很容易忘。
真正能把 Python 学扎实的方式,是做一个完整流程。
这篇文章把三个项目能力串起来:
- 网络机器人获取网页数据。
- Pandas 读取 CSV 并做统计分析。
- 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 取不到数据
可能原因:
- 页面结构和你想的不一样。
- 数据由 JavaScript 动态加载。
- 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")
参考资料
- Requests Quickstart:https://requests.readthedocs.io/en/latest/user/quickstart/
- lxml.html 文档:https://lxml.de/lxmlhtml.html
- Python CSV 文档:https://docs.python.org/3/library/csv.html
- Pandas Getting Started:https://pandas.pydata.org/docs/getting_started/
- Pandas GroupBy:https://pandas.pydata.org/docs/user_guide/groupby.html
- Matplotlib Tutorials:https://matplotlib.org/stable/tutorials/index.html
- FastAPI First Steps:https://fastapi.tiangolo.com/tutorial/first-steps/