关于Python:8. Python项目开发与代码规范

一、项目结构划分(模块、包)

基本概念:

  • 模块(module):一个 .py 文件就是一个模块。

  • 包(package):包含 __init__.py 的文件夹是包,可以组织多个模块。

1. 为什么要划分结构?

真实开发中,项目会包含:

  • 多个功能(比如爬虫、解析、存储)

  • 工具函数

  • 配置文件

  • 测试代码

  • 接口服务(比如 Flask)

 所以必须用「模块」+「包」结构来拆分,不然所有代码都堆在一个 py 文件里,后期维护会崩溃。

2. 推荐项目结构示例(中小型项目)

「一个爬虫 + 接口服务」项目举例说明:

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

3. 模块与包如何使用?

1)requirements.txt

flask
requests

2main.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"</code></pre> 
  <p>10)tests/test_crawler.py</p> 
  <pre><code class="language-python">from core.crawler import fetch_html

def test_fetch_html():
    html = fetch_html("https://example.com")
    assert "Example Domain" in html</code></pre> 
  <p>11)tests/test_parser.py</p> 
  <pre><code class="language-python">from core.parser import extract_title

def test_extract_title():
    html = "<html><head><title>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"
}

4. 如何让这些模块正确导入?

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

5. 总结

模块化是让代码结构清晰、易维护、可扩展的关键。

哪怕是小项目,也应该有像样的模块划分,才能快速扩展新功能,不至于一团乱麻。


二、单元测试(unittest、pytest)

单元测试(Unit Test)是对程序中最小的功能单元(如函数、方法)进行自动化测试的技术,确保其行为符合预期。

为什么要写单元测试?

  • 自动检查代码是否出错

  • 修改代码后迅速验证有没有“改坏”

  • 保证重构/新增功能的安全性

  • 是开发中“质量保障体系”的一部分

常见的单元测试框架

框架 特点
unittest Python 官方内置框架,语法类似 Java 的 JUnit
pytest 社区主流框架,语法更简洁,功能更强大,推荐使用

1. 使用 unittest(官方标准库)

基本写法:

# 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

2. 使用 pytest(更简洁、更现代,推荐)

基本写法:

# 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)

3. 实际项目中如何组织测试?

推荐结构如下:

my_project/
├── core/
│   └── crawler.py
├── tests/
│   ├── test_crawler.py       # 对 core/crawler.py 的测试
│   └── test_parser.py        # 对 core/parser.py 的测试

4. 测试中的模拟(mock)

有些函数你不希望真正运行,比如网络请求或数据库操作。

可以使用 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"

5. 总结:unittest vs pytest

项目 unittest pytest
语法风格 面向类 面向函数
使用复杂度 较繁琐 更简洁
插件生态 基本没有 非常丰富
推荐程度 仅限官方保守项目 推荐

三、日志记录(logging模块)

为什么需要 logging?

print() logging
仅用于调试,不能控制输出格式/级别 可设置级别、输出文件、格式、模块分离
不利于上线,产线调试难 正规、可控、可长期存储分析
无法禁用 可全局统一设置关闭或切换等级

logging 的五个日志等级(从低到高)

级别 logging 方法 说明
DEBUG logger.debug() 调试信息,最详细
INFO logger.info() 正常运行的日志
WARNING logger.warning() 警告级别,非致命问题
ERROR logger.error() 错误,导致某个功能失败
CRITICAL logger.critical() 严重错误,可能程序要挂了

1. logging 基础用法

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: 错误信息

2. 日志保存到文件

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("程序启动")

3. 日志模块封装(推荐做法)

实际开发中推荐封装一个统一日志模块,例如:

# 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("网页爬取失败")

4. 高级用法(多个模块、多个日志文件)

可以为不同模块创建不同日志:

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 - 解析失败

5. 日志目录管理建议

项目结构:

my_project/
├── logs/
│   ├── runtime.log
│   └── error.log
└── utils/
    └── logger.py

可以在 logger.py 中做自动创建 logs 目录:

import os
if not os.path.exists("logs"):
    os.makedirs("logs")

6. 总结

实战技巧

场景 logging 推荐做法
控制台调试 使用 StreamHandler
日志保存 使用 FileHandler
多模块使用 封装 get_logger()
多级别日志 使用 logger.setLevel()if logger.handlers 防止重复添加
日志格式统一 使用 logging.Formatter

替代方案和日志框架

框架 说明
loguru 更现代,语法极简,自动格式化、文件轮转
structlog 结构化日志,适合服务日志分析
logging.handlers.TimedRotatingFileHandler 实现自动按时间切割日志(比如每天一个)

logging 是程序的黑匣子,print 是调试的临时手电筒。真正开发必须用 logging。


四、Git 版本控制

Git 是一个分布式版本控制系统,可以让你:

  • 随时回退代码

  • 记录开发历史

  • 与别人协作开发

  • 在不同分支做实验而不影响主干

核心概念

名词 含义 类比
工作区(Working Directory) 你当前写代码的目录 操作文件夹
暂存区(Staging Area) 准备提交的文件 快递打包
仓库区(Repository) Git 存储历史的地方 快递仓库
提交(Commit) 一次版本快照 快递发出
分支(Branch) 独立的开发轨道 各搞各的,不冲突
合并(Merge) 把分支的改动合并回来 拉支线回主线

1. Git 安装教程(Windows)

步骤 1:下载 Git

  1. 访问 Git 官方下载页面:Git Downloads

  2. 自动检测操作系统,点击Windows图标下载 Git 安装包。

步骤 2:运行安装包

  1. 下载完成后,运行安装包。

  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"  # 或者使用你喜欢的编辑器

2. 基本操作流程(最常用的 Git 命令)

假设项目在 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  # 合并后可删分支

3. .gitignore —— 忽略不需要提交的文件

创建 .gitignore 文件,常见内容:

__pycache__/      # 忽略 Python 编译后的缓存文件
*.pyc            # 忽略所有 .pyc 文件
*.log            # 忽略所有日志文件
.env             # 忽略环境配置文件(如虚拟环境)
.idea/           # 忽略 JetBrains IDE(如 PyCharm)的配置目录
logs/            # 忽略日志文件目录

逐行解释:

  • __pycache__/
  • Python 会将源代码编译成字节码文件(.pyc 文件),并保存在 __pycache__/ 目录下。它们是缓存文件,通常不需要提交到 Git 仓库中。
  • *.pyc
  • 忽略所有以 .pyc 结尾的文件,这些是 Python 编译的字节码文件,也不需要跟踪。
  • *.log
  • 忽略所有 .log 后缀的日志文件,通常用来存储程序运行过程中的日志信息。
  • .env
  • 忽略 .env 文件,这个文件通常用来存储环境变量,诸如数据库密码、API 密钥等敏感信息,应该避免上传到 Git 仓库。
  • idea/
  • 忽略 JetBrains 系列 IDE(如 PyCharm)生成的配置文件夹,里面存储的是本地开发环境的配置信息,不需要上传。
  • logs/
  • 忽略存储日志文件的目录 logs/,这些是程序运行时生成的,不是代码的一部分。

这样运行生成的缓存/临时文件就不会污染仓库。

4. GitHub 配合使用(远程仓库)

设置远程仓库:

git remote add origin https://github.com/你的账号/你的仓库名.git

推送本地代码:

git push -u origin main

以后只要:

git push

5. 撤销与回退

操作 命令
撤销未 add 的修改 git checkout 文件名
撤销 add 的修改 git reset HEAD 文件名
回退上一个 commit git reset --soft HEAD~1(保留修改)git reset --hard HEAD~1(强制删除)

6. 图示 Git 生命周期流程

[工作区] → git add → [暂存区] → git commit → [仓库区]

分支 ← checkout / merge → 合并测试 / 主干

7. GUI 工具推荐(可视化操作更清晰)

  • GitHub Desktop

  • Sourcetree

  • VSCode 自带 Git 支持(左侧版本控制按钮)

  • tig(终端图形化 Git 日志工具)

8. 总结

  • Git 是一种非常强大的版本控制工具,适用于个人项目和团队协作。

  • 常用的 Git 命令有 git addgit commitgit pushgit mergegit branch 等。

  • 分支和合并是 Git 的核心功能,适用于功能开发、实验、bug 修复等任务。

  • 与远程仓库(如 GitHub)结合使用,可以方便地协作开发和备份代码。

掌握 Git 的基本操作,能够在团队开发中更加高效地管理代码版本,同时也能避免因冲突而导致的开发问题。

你可能感兴趣的:(代码规范)