基本概念:
模块(module):一个 .py
文件就是一个模块。
包(package):包含 __init__.py
的文件夹是包,可以组织多个模块。
真实开发中,项目会包含:
多个功能(比如爬虫、解析、存储)
工具函数
配置文件
测试代码
接口服务(比如 Flask)
所以必须用「模块」+「包」结构来拆分,不然所有代码都堆在一个 py 文件里,后期维护会崩溃。
「一个爬虫 + 接口服务」项目举例说明:
my_spider_project/
├── main.py # 程序入口
├── config/
│ └── settings.py # 配置项
├── core/
│ ├── crawler.py # 爬虫逻辑
│ ├── parser.py # 解析逻辑
│ └── saver.py # 存储逻辑
├── api/
│ └── server.py # Flask API 服务
├── utils/
│ ├── tools.py # 工具函数
│ └── logger.py # 日志封装
├── tests/
│ ├── test_crawler.py
│ └── test_parser.py
├── requirements.txt
└── README.md
1)requirements.txt
flask
requests
2
)main.py
(项目入口)
from api.server import app
if __name__ == "__main__":
app.run(port=5000, debug=True)
3
)config/settings.py
# 配置项
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
4
)core/crawler.py
import requests
from config.settings import HEADERS
def fetch_html(url):
try:
response = requests.get(url, headers=HEADERS, timeout=10)
response.raise_for_status()
return response.text
except Exception as e:
return f"[ERROR] {e}"
5
)core/parser.py
from bs4 import BeautifulSoup
def extract_title(html):
try:
# "html.parser": 指定解析器,这里用的是 Python 内置的解析器,
# 也可以用更强大的解析器如 lxml。
# 这一步相当于把 HTML 转换成一个可以像 DOM 一样操作的对象结构。
soup = BeautifulSoup(html, "html.parser")
return soup.title.string if soup.title else "No Title Found"
except Exception as e:
return f"[ERROR] {e}"
6
)core/saver.py
def save_to_file(data, path="output.txt"):
try:
with open(path, "w", encoding="utf-8") as f:
f.write(data)
return True
except Exception as e:
return False
7
)utils/logger.py
import logging
def get_logger(name="my_logger"):
# 从 logging 模块中获取一个命名日志记录器对象(Logger 对象)。
logger = logging.getLogger(name)
# 如果这个 logger 还没有绑定处理器(handler),就配置它, 可以避免重复绑定多个处理器。
if not logger.handlers:
# 创建一个“流处理器”,默认会把日志输出到终端控制台(即 stdout)。
handler = logging.StreamHandler()
# 创建一个“格式器”,规定日志的输出格式。
formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
# 把上面的格式器绑定到处理器上。
handler.setFormatter(formatter)
# 把处理器绑定到 logger 上,这样 logger 才能通过这个 handler 输出日志。
logger.addHandler(handler)
# 设置日志等级为 DEBUG,
# 只要日志等级是 DEBUG、INFO、WARNING、ERROR、CRITICAL,都会被输出。
logger.setLevel(logging.DEBUG)
return logger
8
)utils/tools.py
def clean_text(text):
return text.strip().replace("\n", "").replace("\r", "")
9
)api/server.py
from flask import Flask, request, jsonify
# fetch_html: 你自定义的爬虫模块中的函数,用于抓取网页 HTML。
from core.crawler import fetch_html
# extract_title: 你写的解析模块中的函数,用于提取网页 。
from core.parser import extract_title
# get_logger: 封装的日志工具函数,用于生成日志记录器。
from utils.logger import get_logger
# 创建一个 logger,可以记录请求、错误等信息。
logger = get_logger()
# 创建一个 Flask Web 应用,__name__ 是当前模块名。
app = Flask(__name__)
def success(data=None):
return jsonify({"code": 0, "msg": "success", "data": data})
def fail(msg="error", code=1):
return jsonify({"code": code, "msg": msg})
# 绑定路由 /fetch,只允许 GET 请求。
@app.route("/fetch", methods=["GET"])
def fetch():
# 从客户端发来的 HTTP GET 请求中,提取名为 "url" 的查询参数。
url = request.args.get("url")
if not url:
return fail("Missing url parameter")
logger.info(f"Fetching URL: {url}")
html = fetch_html(url)
if html.startswith("[ERROR]"):
return fail(html)
title = extract_title(html)
return success({"title": title})
@app.route("/", methods=["GET"])
def index():
return "Welcome to MySpider API! Try /fetch?url=https://example.com"
10)tests/test_crawler.py
from core.crawler import fetch_html
def test_fetch_html():
html = fetch_html("https://example.com")
assert "Example Domain" in html
11)tests/test_parser.py
from core.parser import extract_title
def test_extract_title():
html = "Test Page "
assert extract_title(html) == "Test Page"
12)README.md
# My Spider Project
这是一个基于 Flask 的爬虫接口服务项目。
## 快速开始
```bash
pip install -r requirements.txt
python main.py
访问接口:
curl "http://localhost:5000/fetch?url=https://example.com"
返回示例:
{
"code": 0,
"data": {
"title": "Example Domain"
},
"msg": "success"
}
Python 把 同一个目录 或 带 __init__.py
的文件夹 当作包。要正确导入模块,注意以下几点:
每个包都要有 __init__.py
(Python 3.3+ 可省略,但建议保留)
例如:
core/
├── __init__.py
utils/
├── __init__.py
即便是空文件也可以,作用是:告诉解释器“这是个包”。
使用相对路径导入
在包内使用模块时这样导入:
# 在 core/parser.py 中要用 core/crawler.py:
from .crawler import fetch_html
在主程序中调用模块时用绝对导入:
from core.crawler import fetch_html
模块化是让代码结构清晰、易维护、可扩展的关键。
哪怕是小项目,也应该有像样的模块划分,才能快速扩展新功能,不至于一团乱麻。
单元测试(Unit Test)是对程序中最小的功能单元(如函数、方法)进行自动化测试的技术,确保其行为符合预期。
为什么要写单元测试?
自动检查代码是否出错
修改代码后迅速验证有没有“改坏”
保证重构/新增功能的安全性
是开发中“质量保障体系”的一部分
常见的单元测试框架
框架 | 特点 |
---|---|
unittest |
Python 官方内置框架,语法类似 Java 的 JUnit |
pytest |
社区主流框架,语法更简洁,功能更强大,推荐使用 |
基本写法:
# utils/tools.py
def add(a, b):
return a + b
# tests/test_math_utils.py
import unittest
from utils.tools import add
class TestMathUtils(unittest.TestCase):
def test_add(self):
self.assertEqual(add(1, 2), 3)
self.assertEqual(add(-1, 2), 1)
self.assertNotEqual(add(2, 2), 5)
if __name__ == '__main__':
unittest.main()
断言方法(常用)
方法 | 示例 |
---|---|
assertEqual(a, b) |
断言 a == b |
assertNotEqual(a, b) |
断言 a != b |
assertTrue(x) |
x 为 True |
assertFalse(x) |
x 为 False |
assertIn(a, b) |
a in b |
assertIsNone(x) |
x 为 None |
基本写法:
# tests/test_tools.py
from utils.tools import add
def test_add():
assert add(1, 2) == 3
assert add(-1, 2) == 1
不需要类、不需要继承、不需要 main 函数
用 assert 断言即可
运行 pytest
# 安装 pytest
pip install pytest
# 运行 tests/ 下所有 test_*.py
pytest tests/
# 运行某个测试文件
pytest tests/test_tools.py
# 运行时显示更详细信息
pytest -v
高级用法示例
参数化测试
import pytest
from utils.tools import add
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, -2, -3),
])
def test_add_param(a, b, expected):
assert add(a, b) == expected
异常捕获测试
import pytest
def divide(x, y):
return x / y
def test_divide_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
推荐结构如下:
my_project/
├── core/
│ └── crawler.py
├── tests/
│ ├── test_crawler.py # 对 core/crawler.py 的测试
│ └── test_parser.py # 对 core/parser.py 的测试
有些函数你不希望真正运行,比如网络请求或数据库操作。
可以使用 unittest.mock
来模拟:
# tests/test_crawler.py
from unittest.mock import patch
from core.crawler import fetch_html
@patch('core.crawler.requests.get')
def test_fetch_html(mock_get):
mock_get.return_value.text = "ok"
html = fetch_html("https://test.com")
assert "ok" in html
unittest.mock.patch
是什么?
@patch('core.crawler.requests.get')
这行是最关键的装饰器。
它会把 core.crawler
模块里用到的 requests.get
替换成一个模拟的对象(mock 对象)。
你可以自己控制这个模拟对象返回什么内容,而不真的访问网络。
测试函数解释
def test_fetch_html(mock_get):
mock_get.return_value.text = "ok"
mock_get
是你模拟的 requests.get
你设定它的返回值 .text
是 "ok"
所以当 fetch_html("https://test.com")
执行时:
它其实调用的是你设定好的 mock_get()
所以会返回 "ok"
项目 | unittest |
pytest |
---|---|---|
语法风格 | 面向类 | 面向函数 |
使用复杂度 | 较繁琐 | 更简洁 |
插件生态 | 基本没有 | 非常丰富 |
推荐程度 | 仅限官方保守项目 | 推荐 |
为什么需要 logging?
print() |
logging |
---|---|
仅用于调试,不能控制输出格式/级别 | 可设置级别、输出文件、格式、模块分离 |
不利于上线,产线调试难 | 正规、可控、可长期存储分析 |
无法禁用 | 可全局统一设置关闭或切换等级 |
logging 的五个日志等级(从低到高)
级别 | logging 方法 | 说明 |
---|---|---|
DEBUG | logger.debug() |
调试信息,最详细 |
INFO | logger.info() |
正常运行的日志 |
WARNING | logger.warning() |
警告级别,非致命问题 |
ERROR | logger.error() |
错误,导致某个功能失败 |
CRITICAL | logger.critical() |
严重错误,可能程序要挂了 |
import logging
logging.basicConfig(
level=logging.INFO, # 日志等级
format='[%(asctime)s] %(levelname)s: %(message)s', # 输出格式
)
logging.debug("调试信息")
logging.info("正常信息")
logging.warning("警告信息")
logging.error("错误信息")
logging.critical("严重错误")
输出示例:
[2025-05-04 12:00:00,123] INFO: 正常信息
[2025-05-04 12:00:00,124] ERROR: 错误信息
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
filename='my_log.log', # 日志保存到文件
filemode='a', # 'w' 表示覆盖,'a' 表示追加
encoding='utf-8'
)
logging.info("程序启动")
实际开发中推荐封装一个统一日志模块,例如:
# utils/logger.py
import logging
def get_logger(name="default"):
logger = logging.getLogger(name)
if not logger.handlers:
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(message)s')
# 输出到控制台
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
# 输出到文件
fh = logging.FileHandler("logs/runtime.log", encoding='utf-8')
fh.setFormatter(formatter)
logger.addHandler(fh)
return logger
使用:
# main.py
from utils.logger import get_logger
logger = get_logger("spider")
logger.info("开始爬取网页")
logger.error("网页爬取失败")
可以为不同模块创建不同日志:
logger1 = get_logger("parser") # parser.log
logger2 = get_logger("saver") # saver.log
推荐统一管理并记录模块名:
formatter = logging.Formatter('[%(asctime)s] %(name)s %(levelname)s - %(message)s')
输出示例:
[2025-05-04 14:00:00] spider INFO - 页面开始抓取
[2025-05-04 14:00:01] parser ERROR - 解析失败
项目结构:
my_project/
├── logs/
│ ├── runtime.log
│ └── error.log
└── utils/
└── logger.py
可以在 logger.py 中做自动创建 logs 目录:
import os
if not os.path.exists("logs"):
os.makedirs("logs")
实战技巧
场景 | logging 推荐做法 |
---|---|
控制台调试 | 使用 StreamHandler |
日志保存 | 使用 FileHandler |
多模块使用 | 封装 get_logger() |
多级别日志 | 使用 logger.setLevel() 和 if logger.handlers 防止重复添加 |
日志格式统一 | 使用 logging.Formatter |
替代方案和日志框架
框架 | 说明 |
---|---|
loguru |
更现代,语法极简,自动格式化、文件轮转 |
structlog |
结构化日志,适合服务日志分析 |
logging.handlers.TimedRotatingFileHandler |
实现自动按时间切割日志(比如每天一个) |
logging 是程序的黑匣子,print 是调试的临时手电筒。真正开发必须用 logging。
Git 是一个分布式版本控制系统,可以让你:
随时回退代码
记录开发历史
与别人协作开发
在不同分支做实验而不影响主干
核心概念
名词 | 含义 | 类比 |
---|---|---|
工作区(Working Directory) | 你当前写代码的目录 | 操作文件夹 |
暂存区(Staging Area) | 准备提交的文件 | 快递打包 |
仓库区(Repository) | Git 存储历史的地方 | 快递仓库 |
提交(Commit) | 一次版本快照 | 快递发出 |
分支(Branch) | 独立的开发轨道 | 各搞各的,不冲突 |
合并(Merge) | 把分支的改动合并回来 | 拉支线回主线 |
步骤 1:下载 Git
访问 Git 官方下载页面:Git Downloads
自动检测操作系统,点击Windows图标下载 Git 安装包。
步骤 2:运行安装包
下载完成后,运行安装包。
安装过程中,你可以选择以下选项(大部分情况下默认即可):
选择安装路径:默认路径通常是 C:\Program Files\Git
。
选择编辑器:可以选择默认的编辑器(通常是 Vim),或者选择你常用的编辑器(比如 VSCode)。
PATH 环境变量设置:选择 “Git from the command line and also from 3rd-party software” 选项,这样可以在命令行中直接使用 Git 命令。
其他选项:建议保持默认选项,点击“Next”即可。
步骤 3:完成安装
点击“Install”开始安装,完成后点击“Finish”结束安装。
步骤 4:验证安装
安装完成后,打开 Git Bash(Git 安装时会自动创建一个快捷方式),或者使用命令行(cmd)输入以下命令来验证是否安装成功:
git --version
如果你看到类似以下输出,就说明 Git 安装成功了:
git version 2.x.x
Git 配置
安装完成后,首先设置 Git 的基本信息(用户名和邮箱),这是后续提交代码时的重要信息。
在终端中输入以下命令:
git config --global user.name "你的名字"
git config --global user.email "你的邮箱"
可以用以下命令查看当前的配置信息:
git config --list
配置 Git 编辑器,指定提交信息时使用的编辑器:
git config --global core.editor "vim" # 或者使用你喜欢的编辑器
假设项目在 my_project/
文件夹中。
1)初始化项目
cd my_project/
git init # 初始化 Git 仓库
会生成一个 .git
目录,这是 Git 的隐藏数据库。
2)查看当前状态
git status
会告诉你有哪些文件被修改但没提交。
3)添加文件到暂存区
git add main.py
# 或添加所有更改
git add .
4)提交到仓库
git commit -m "初始化 main.py,写了爬虫入口"
-m
后面是对这次提交的说明,写得清楚点对你有用。
5)查看提交历史
git log
按 q
退出。
6)创建新分支(开发/实验用)
git branch test
切换分支:
git checkout test
也可以一步到位:
git checkout -b test
7)合并分支
回到主分支:
git checkout main
git merge test # 合并 test 分支的内容
8)查看分支图
git log --oneline --graph --all
可视化查看分支变化。
9)删除分支
git branch -d test # 合并后可删分支
创建 .gitignore
文件,常见内容:
__pycache__/ # 忽略 Python 编译后的缓存文件
*.pyc # 忽略所有 .pyc 文件
*.log # 忽略所有日志文件
.env # 忽略环境配置文件(如虚拟环境)
.idea/ # 忽略 JetBrains IDE(如 PyCharm)的配置目录
logs/ # 忽略日志文件目录
逐行解释:
__pycache__/
:__pycache__/
目录下。它们是缓存文件,通常不需要提交到 Git 仓库中。*.pyc
:.pyc
结尾的文件,这些是 Python 编译的字节码文件,也不需要跟踪。*.log
:.log
后缀的日志文件,通常用来存储程序运行过程中的日志信息。.env
:.env
文件,这个文件通常用来存储环境变量,诸如数据库密码、API 密钥等敏感信息,应该避免上传到 Git 仓库。idea/
:logs/
:logs/
,这些是程序运行时生成的,不是代码的一部分。这样运行生成的缓存/临时文件就不会污染仓库。
设置远程仓库:
git remote add origin https://github.com/你的账号/你的仓库名.git
推送本地代码:
git push -u origin main
以后只要:
git push
操作 | 命令 |
---|---|
撤销未 add 的修改 | git checkout 文件名 |
撤销 add 的修改 | git reset HEAD 文件名 |
回退上一个 commit | git reset --soft HEAD~1 (保留修改)git reset --hard HEAD~1 (强制删除) |
[工作区] → git add → [暂存区] → git commit → [仓库区]
分支 ← checkout / merge → 合并测试 / 主干
GitHub Desktop
Sourcetree
VSCode 自带 Git 支持(左侧版本控制按钮)
tig
(终端图形化 Git 日志工具)
Git 是一种非常强大的版本控制工具,适用于个人项目和团队协作。
常用的 Git 命令有 git add
、git commit
、git push
、git merge
、git branch
等。
分支和合并是 Git 的核心功能,适用于功能开发、实验、bug 修复等任务。
与远程仓库(如 GitHub)结合使用,可以方便地协作开发和备份代码。
掌握 Git 的基本操作,能够在团队开发中更加高效地管理代码版本,同时也能避免因冲突而导致的开发问题。