Contents

Python_Note4

Contents
Beautiful landscape

Nature's Beauty

Explore the wonders of the world

参考资料

logger 包

Python 的 logging 包是一个强大的日志记录系统,是 Python 标准库的一部分。它提供了灵活的日志记录功能,可以帮助开发者跟踪程序运行状态、调试问题以及记录重要事件。

1. 基本概念

日志级别

logging 定义了 5 个标准日志级别(按严重性递增):

级别 数值 使用场景
DEBUG 10 详细信息,用于调试
INFO 20 确认程序按预期运行
WARNING 30 表示意外情况或未来可能出现的问题
ERROR 40 严重问题,程序无法执行某些功能
CRITICAL 50 严重错误,程序可能无法继续运行

主要组件

  1. Logger:记录日志的接口,应用程序通过它记录日志
  2. Handler:决定日志记录的输出位置(控制台、文件等)
  3. Filter:提供更细粒度的控制,决定哪些日志记录会被输出
  4. Formatter:指定日志记录的最终输出格式

2. 基本使用

简单示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import logging

# 配置基本日志记录
logging.basicConfig(level=logging.INFO)

# 记录不同级别的日志
logging.debug('这是一条调试信息')# 不会显示
logging.info('这是一条普通信息')
logging.warning('这是一条警告信息')
logging.error('这是一条错误信息')
logging.critical('这是一条严重错误信息')

输出格式定制

1
2
3
4
5
6
7
8
9
import logging

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)

logging.info('这是一条格式化的日志信息')

3. 高级用法

使用 Logger 对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import logging

# 创建 logger 对象
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)

# 创建控制台 handler 并设置级别
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# 创建 formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 将 formatter 添加到 handler
ch.setFormatter(formatter)

# 将 handler 添加到 logger
logger.addHandler(ch)

# 记录日志
logger.debug('调试信息')
logger.info('普通信息')

文件日志记录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import logging

logger = logging.getLogger('file_logger')
logger.setLevel(logging.INFO)

# 创建文件 handler
fh = logging.FileHandler('app.log')
fh.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)

logger.addHandler(fh)

logger.info('这条信息会被记录到文件中')

日志轮转

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from logging.handlers import RotatingFileHandler

logger = logging.getLogger('rotating_logger')
logger.setLevel(logging.INFO)

# 每个文件最大 1MB,保留 3 个备份文件
handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=3)
handler.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

logger.addHandler(handler)

for i in range(10000):
logger.info(f'这是第 {i} 条日志信息')

定时轮转日志

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from logging.handlers import TimedRotatingFileHandler

logger = logging.getLogger('timed_rotating_logger')
logger.setLevel(logging.INFO)

# 每天午夜轮转日志,保留 7 天备份
handler = TimedRotatingFileHandler('app.log', when='midnight', backupCount=7)
handler.setLevel(logging.INFO)

formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

logger.addHandler(handler)

logger.info('这条日志会根据时间轮转')

4. 最佳实践

  1. 模块化日志记录:每个模块使用自己的 logger,命名通常为 __name__
1
logger = logging.getLogger(__name__)
  1. 合理设置日志级别:生产环境通常使用 INFO 或 WARNING,开发环境使用 DEBUG

  2. 避免在热路径中记录 DEBUG 日志:先检查是否启用该级别

1
2
if logger.isEnabledFor(logging.DEBUG):
logger.debug('Message with %s', expensive_arg())
  1. 使用结构化日志:考虑使用 json 或自定义格式以便于日志分析

  2. 异常记录:使用 logger.exception()logger.error(exc_info=True) 记录异常堆栈

1
2
3
4
try:
1 / 0
except ZeroDivisionError:
logger.exception("发生了除以零的错误")

5. 配置方式

字典配置(Python 3.2+)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import logging.config

config = {
'version': 1,
'formatters': {
'detailed': {
'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
},
'file': {
'class': 'logging.FileHandler',
'filename': 'app.log',
'mode': 'w',
'formatter': 'detailed',
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file']
},
}

logging.config.dictConfig(config)

文件配置

可以保存在单独的配置文件中(如 logging.conf):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[loggers]
keys=root

[handlers]
keys=consoleHandler,fileHandler

[formatters]
keys=simpleFormatter

[logger root]
level=DEBUG
handlers=consoleHandler,fileHandler

[handler consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)

[handler fileHandler]
class=FileHandler
level=DEBUG
formatter=simpleFormatter
args=('app.log', 'a')

[formatter simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S

然后加载配置:

1
2
3
import logging.config

logging.config.fileConfig('logging.conf')

6. 高级特性

日志过滤器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyFilter(logging.Filter):
def filter(self, record):
# 只允许包含特定文字的日志通过
return 'important' in record.getMessage()

logger = logging.getLogger('filtered_logger')
logger.addFilter(MyFilter())

logger.info('这是一条普通信息')# 不会被记录
logger.info('这是一条重要信息 important')# 会被记录

上下文信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import logging
from logging import LoggerAdapter

logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)

class ContextFilter(logging.Filter):
def filter(self, record):
record.user = 'John'# 添加上下文信息
return True

logger.addFilter(ContextFilter())
formatter = logging.Formatter('%(asctime)s - %(user)s - %(levelname)s - %(message)s')
for handler in logger.handlers:
handler.setFormatter(formatter)

logger.info('系统消息')

# 或者使用 LoggerAdapter
extra = {'user': 'Alice'}
logger = LoggerAdapter(logger, extra)
logger.info('系统消息')

多进程日志记录

在多进程环境中,使用 QueueHandlerQueueListener

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import logging
import logging.handlers
import multiprocessing

def listener_process(queue):
root = logging.getLogger()
h = logging.FileHandler('multi_process.log')
f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
h.setFormatter(f)
root.addHandler(h)

while True:
try:
record = queue.get()
if record is None:
break
logger = logging.getLogger(record.name)
logger.handle(record)
except Exception:
import sys, traceback
print('Error in listener:', file=sys.stderr)
traceback.print_exc(file=sys.stderr)

def worker_process(queue):
queue_handler = logging.handlers.QueueHandler(queue)
root = logging.getLogger()
root.addHandler(queue_handler)
root.setLevel(logging.DEBUG)

logging.info('来自工作进程的消息')

if __name__ == '__main__':
queue = multiprocessing.Queue()
listener = multiprocessing.Process(target=listener_process, args=(queue,))
listener.start()

workers = []
for i in range(2):
worker = multiprocessing.Process(target=worker_process, args=(queue,))
workers.append(worker)
worker.start()

for worker in workers:
worker.join()

queue.put(None)
listener.join()

7. 总结

Python 的 logging 模块提供了强大而灵活的日志记录功能:

  • 支持多级别日志记录
  • 可配置的输出目标和格式
  • 支持模块化日志记录
  • 提供日志轮转功能
  • 支持多进程环境
  • 可通过多种方式配置

合理使用 logging 模块可以大大提高应用程序的可维护性和可调试性,是 Python 开发中不可或缺的工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""
日志处理
"""
import logging
import os
from logging.handlers import RotatingFileHandler
import colorlog  # 导入 colorlog
def setup_logging(log_file=None, log_level=logging.INFO, max_bytes=1 * 1024 * 1024, backup_count=5):
    """
    配置日志记录,包括日志轮换和控制台输出,支持彩色日志
    :param log_file: 日志文件路径,如果为None则使用默认路径
    :param log_level: 日志等级
    :param max_bytes: 单个日志文件的最大字节数,默认为1MB
    :param backup_count: 保留的日志文件备份数
    """
    if log_file is None:
        log_file = os.getenv('LOG_FILE_PATH', 'docs/log/test.log')

    # 确保日志文件夹存在
    log_dir = os.path.dirname(log_file)
    if not os.path.exists(log_dir):
        os.makedirs(log_dir)

    # 创建根记录器
    logger = logging.getLogger()
    logger.setLevel(log_level)

    # 清除已有的处理器,避免重复添加
    if logger.hasHandlers():
        logger.handlers.clear()

    # 创建文件处理器,支持日志轮换
    file_handler = RotatingFileHandler(log_file, mode='w', maxBytes=max_bytes, backupCount=backup_count)
    file_handler.setLevel(log_level)

    # 创建控制台处理器,日志输出到控制台
    console_handler = logging.StreamHandler()
    console_handler.setLevel(log_level)

    # 定义日志格式
    log_format = '%(asctime)s - %(levelname)-8s - %(message)-60s - [%(filename)-15s:%(lineno)-4d]'
    color_format = "%(log_color)s" + log_format

    # 设置文件处理器的格式
    file_formatter = logging.Formatter(log_format)
    file_handler.setFormatter(file_formatter)

    # 设置控制台处理器的彩色格式
    color_formatter = colorlog.ColoredFormatter(
        color_format,
        log_colors={
            'DEBUG': 'cyan',
            'INFO': 'green',
            'WARNING': 'yellow',
            'ERROR': 'red',
            'CRITICAL': 'bold_red',
        }
    )
    console_handler.setFormatter(color_formatter)

    # 将处理器添加到记录器中
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)

    # 添加日志分隔符
    logger.info("=" * 60)
    logger.info("新日志开始".center(60, ' '))
    logger.info("=" * 60)


def log_data(data):
    """
    记录数据的日志
    """
    logging.info(f"数据记录: {data}")


# 通用异常处理装饰器
def log_exceptions(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            logging.error(f"{func.__name__} 执行时发生错误", exc_info=True)
            raise e

    return wrapper

# 示例:调用日志设置
if __name__ == "__main__":
    setup_logging(log_file='/Users/zhangxianping/Desktop/pythonProject/SiCore_ChipTest/docs/log//test.log', log_level=logging.DEBUG)
    log_data("这是一个测试数据记录")
    logging.debug("这是一条调试信息")
    logging.warning("这是一条警告信息")
    logging.error("这是一条错误信息")
    logging.critical("这是一条严重错误信息")

获取系统监控信息

1
2
# 导入模块
import psutil

⭕ 关于 CPU

1
2
3
4
5
6
7
8
# 核心数
psutil.cpu_count(logical=False)
# 参数 logical=False 只会返回物理内核


# 使用率
psutil.cpu_percent(interval=0.5)
# 若加上参数 percpu=True 则会返回一个列表,包含各个核心的使用率

⭕ 关于内存

1
2
3
4
# 内存信息,包括:总容量、可用空间及占比等
psutil.virtual_memory()

# 可以通过点号访问具体的属性,例如 psutil.virtual_memory().total

⭕ 关于磁盘

1
2
3
4
5
6
# 磁盘分区信息,包括:总容量、可用空间及占比等
psutil.disk_partitions()


# 指定目录的磁盘信息
psuntil.disk_usage('/')

⭕ 关于网络

1
2
# 可以获取到收、发数据包的数量
。。。。。。

⭕ 关于用户

1
2
# 活动用户信息
psutil.users()

⭕ 保存到日志

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import psutil
import datetime
import unicodedata


def align_string(text, width, alignmethod):
    # 处理中英文混合字符串的对齐问题
    text_width = sum(2 if unicodedata.east_asian_width(c) in ('F', 'W') else 1 for c in text)
    if alignmethod == 'ljust':
        tmp = text.ljust(width - text_width)
    elif alignmethod == 'rjust':
        tmp = text.rjust(width - text_width)
    elif alignmethod == 'center':
        tmp = text.center(width - text_width)
    return tmp


cpu_per = psutil.cpu_percent(interval=0.5)
memory_info = psutil.virtual_memory()
disk_info = psutil.disk_usage("/")
net_info = psutil.net_io_counters()
current_time = datetime.datetime.now().strftime("%F %T")


log_template = f"""
|{align_string('监控时间', 19, 'center')}|{align_string('CPU使用率', 19, 'center')}|{align_string('内存使用率', 19, 'center')}|{align_string('硬盘使用率', 19, 'center')}|{align_string('网络收发量', 28, 'center')}|
|{align_string('', 19, 'ljust')}|{align_string(f'(共{psutil.cpu_count(logical=False):.2f}核CPU)', 19, 'center')}|{align_string(f'(总计{memory_info.total/1024/1024/1024:.2f}G内存)', 19, 'center')}|{align_string(f'(总计{disk_info.total/1024/1024/1024:.2f}G硬盘)', 19, 'center')}|{align_string('', 28, 'ljust')}|
|{'-' * 19}|{'-' * 19}|{'-' * 19}|{'-' * 19}|{'-' * 28}|
|{align_string(current_time, 19, 'ljust')}|{align_string(f'{cpu_per}%', 19, 'ljust')}|{align_string(f'{memory_info.percent}%', 19, 'ljust')}|{align_string(f'{disk_info.percent}%', 19, 'ljust')}|{align_string(f'收:{net_info.bytes_recv}/发:{net_info.bytes_sent}', 28, 'ljust')}|
|{'-' * 19}|{'-' * 19}|{'-' * 19}|{'-' * 19}|{'-' * 28}|
"""
print(log_template)

with open('log.txt', 'a') as f:
    f.write(log_template + '\n\n')

若需要持续写入日志文件,可以利用 while 循环
每隔一段时间去重新计算需要的指标即可


发送邮件

1
2
# 导入模块
import yagmail
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 先创建对象
ya_obj = yagmail.SMTP(
    user = "发件人邮箱",
    password = "授权码(非密码,在邮箱设置)",
    host = "网易邮箱服务器smtp.163.com"
)


# 配置邮件内容
content = '。。。'


# 发送邮件
ya_obj.send('收件人邮箱', '邮件主题', content)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from bs4 import BeautifulSoup
import smtplib
from email.mime.text import MIMEText


msg_from = '870407139@qq.com'    #发送方邮箱
passwd = '...'    #填入发送方邮箱的授权码
receivers = ['870407139@qq.com,503734057@qq.com']    #收件人邮箱

subject = "北大研招网更新连接"    #主题
content = "..."    #正文
msg = MIMEText(content)
msg['Subject'] = subject
msg['From'] = msg_from
msg['To'] = ','.join(receivers)

try:
    s = smtplib.SMTP_SSL("smtp.qq.com",465)                   #邮件服务器及端口号
    s.login(msg_from, passwd)
    s.sendmail(msg_from, msg['To'].split(','), msg.as_string())
    print("发送成功")
except:
    print("发送失败")
finally:
    s.quit()

⭕ 实际应用

可以结合上面的监控信息
判断当 CPU 使用率超过 80%,内存使用率超过 90% 的时候,发送邮件给指定收件人

1
2
if cpu_per>80 or memory_info.percent>90:
    # 发送邮件。。。

re 模块

⭕ match、search 方法

1
2
3
# 从字符串的【开头位置】匹配
# 可以用于【验证】输入的【内容是否满足某种格式】
re.match("正则表达式", "要验证/检测的字符串")
1
2
# 从需要检测的字符串中【搜索】满足正则的内容
re.search("正则表达式", "要搜索的字符串")

以上两个方法,如果匹配成功,返回 match object 对象
如果匹配失败,返回 None

可以通过 .group() 获取结果

⭕ sub 方法

对 search 到的字符(串)进行 替换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import re

def add(temp):
    strNum = temp.group()
    num = int(strNum) + 1
    return str(num)


# 通过传入 add 函数,对 search 到的内容进行更加【多样化的替换】
ret = re.sub(r"\d+", add, "hello python = 997")
print(ret)

⭕ split 方法

根据正则表达式对字符串进行 拆分

1
2
3
4
# 根据冒号或者空格拆分
result = re.split(":| ", "info:hello@163.com zhangsan lisi")
print(result)
# 相比于 str.split() 更加灵活

⭕ 贪婪与非贪婪

正则表达式默认为 贪婪模式
满足表达式的情况下,尽可能多获取内容

要转换成 非贪婪模式,在 +*{} 等符号的后面添加 ? 符号即可

1
2
3
result = re.match("aaa(\d+?)", "aaa123456")
# 匹配到 1 就停止了
print(result.group())

⭕ 其他

字符串前 r 的作用:让正则中的 \ 不再是转义 的特殊含义

1
2
3
4
5
# result = re.match("<([a-zA-Z0-9]+)><([a-zA-Z0-9]+)>.*</\\2></\\1>", "<html><h1>asdbj</h1></html>")
# 等价写法
result = re.match(r"<([a-zA-Z0-9]+)><([a-zA-Z0-9]+)>.*</\2></\1>", "<html><h1>asdbj</h1></html>")

print(result.group())

SQL 语句

⭕ pymysql 的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pymysql

# 建立连接对象
conn = pymysql.connect(
    host='localhost',
    port=3306,
    user='root', password='123456'
)

# 创建游标对象
cur = conn.cursor()

# 使用游标对象执行 SQL 语句
sql = """
select * from students
"""
results = cur.execute(sql)
# 提交
conn.commit()
# 仅从查询的数据中取出一条数据
oneres = cur.fetchone()
# fetchall 返回嵌套元组,每个元组是一条数据
res = cur.fetchall()

# 关闭游标对象
cur.close()

# 关闭连接对象
conn.close()

⭕ pymongo 的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import os
from pymongo import MongoClient


# 配置 mongodb 数据库
host = os.environ.get('MONGODB_HOST', '127.0.0.1')  # 本地数据库
port = os.environ.get('MONGODB_PORT', '27017')  # 数据库端口
mongo_url = 'mongodb://{}:{}'.format(host, port)
mongo_db = os.environ.get('MONGODB_DATABASE', '...')
client = MongoClient(mongo_url)
db = client[mongo_db]

#db['...'].create_index('id', unique=True)

⭕ SQL 注入

SQL 注入是一种常见的安全漏洞
主要发生在使用动态构建 SQL 查询语句时,未对用户输入进行 充分验证和过滤
导致恶意用户可以通过输入 特定的 SQL 语句 来执行非法操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sql = f"""
select * from goods where name = {input_name} order by id desc
"""
# 当 input_name = ' or 1 or ' 时,执行
# select * from goods where name = '' or 1 or '' order by id desc
# 会导致注入问题


# 防止注入的写法
sql = """
select * from goods where name = %s order by id desc
"""
result = cur.execute(sql, [input_name, ])

一些对策:
1、不要直接将用户输入的数据拼接为 SQL 查询语句
对语句进行 适当的转义或参数化处理
2、将 SQL 查询语句和参数分开处理
确保参数值不会被当作 SQL 语句的一部分执行
3、使用 ORM(Object-Relational Mapping)框架
帮助开发者避免直接操作 SQL 语句,通过对象来操作数据库
4、在数据库连接时,使用具有 最小权限的用户
避免数据库用户具有过高的权限,以减少潜在的风险

  • 下一步学习方向: Web 开发(Flask/Django)
    数据分析(Pandas)
    自动化办公(openpyxl)
  • 推荐学习资源

DrissionPage

运行模式

特性 匿名模式 非匿名模式
数据存储 关闭浏览器后自动删除历史、cookie、缓存等 正常保存历史、cookie、缓存等
会话隔离 独立且全新的会话 复用本地已有的用户数据和会话
适用场景 干净的独立环境,多账号隔离 持久化登录状态

设置匿名模式

1
2
co = ChromiumOptions()
co.incognito()
特性 无头模式 非无头模式
显示 无图形界面,后台静默运行 显示浏览器窗口,可见页面操作过程
消耗 资源占用低 渲染界面,占用较高

设置无头模式

1
2
3
co = ChromiumOptions()
co.headless()
co.set_argument('--disable-blink-features=AutomationControlled')
特性 沙盒模式 非沙盒模式
定义 浏览器运行在受限的环境中,无系统资源访问权限 浏览器直接以当前用户权限运行,无隔离机制
安全性
消耗 较高,需维护沙盒环境 较低

设置无沙盒模式

1
2
3
co = ChromiumOptions()
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')

Selenium

参考资料

简介

Selenium 支持的语言包括 C#,Java,Perl,PHP,Python 和 Ruby
它是自动化测试套件,也可以用于爬虫

Selenium 测试脚本可以使用任何支持的编程语言进行编码,并且可以直接在大多数现代 Web 浏览器中运行

安装

pip 下载 selenium
并根据自己的浏览器及版本,安装相应的驱动

使用

1
2
3
4
5
option = webdriver.ChromeOptions()
# 防止自动关闭浏览器
option.add_experimental_option('detach', True)

driver = webdriver.Chrome(chrome_options = option)

定位元素

进入浏览器的开发者工具

1、根据 id 定位
<input id='search-input' type='text' value='' placeholder='请输入'>

1
driver.find_element_by_id('search-input')

由于 id 的唯一性,我们可以直接通过 id 定位

2、根据 name 定位
<meta name='keywords' content='github,coding'>

1
driver.find_element_by_name('keywords')

3、根据 class 定位
<div class='toolbar-container'>

1
driver.find_element_by_class_name('toolbar-container')

4、根据 tag 定位

5、根据 xpath 定位

在 XML 文档中定位元素的语言

1
2
3
4
5
6
7
# 绝对路径,根据层级关系定位
driver.find_element_by_xpath('/html/body/div/div/input[1]')

# 根据元素属性定位
driver.find_element_by_xpath("//*[@id='search-input']")

# 也可以结合上述二者

6、根据 css 定位

定位较为灵活,并且速度较快

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 选择 class 为指定值的元素
driver.find_element_by_css_selector('.toolbar-container')

# 选择 id 为指定值的元素
driver.find_element_by_css_selector('#search-input')

# 选择所有 input 元素
driver.find_element_by_css_selector('input')

# 选择父元素为 div 的所有 input 元素
driver.find_element_by_css_selector('div>input')

# div 标签与 input 标签在同级,且 input 在 div 之后
driver.find_element_by_css_selector('div+input')

# 选择 type 属性值为 text 所有的元素
driver.find_element_by_css_selector("[type='text']")

7、定位文本链接
<div class='text-box'>hello world</div>

1
2
3
4
driver.find_element_by_link_text('hello world')

# partial 只需要部分文本即可定位
driver.find_element_by_partial_link_text('hello')

定位一组元素

将上述的 element 改为 elements 即可
然后通过遍历返回的元素列表,进行相应的处理

控制

浏览器控制

可以修改窗口大小
全屏显示
刷新
前进、后退
窗口切换
切换到出现的弹窗(点击某个按钮后出现)
上传下载文件

还可以通过执行 js 语句

1
2
3
4
5
6
# 打开新的标签页
js = "window.open('https://baidu.com')"
driver.execute_script(js)

# 滑动滚动条
js = "window.scrollTo(0, 500)"

可以模拟输入指定内容
清除文本内容
判断元素是否可见
获取标签属性值
获取元素尺寸、文本

鼠标控制

可以实现
鼠标左、右键单击
双击
拖动
悬停

键盘控制

常用的按键都用相应的关键词映射

元素等待

很多页面都使用 ajax 技术,页面的元素 不是同时被加载出来的
为了防止定位这些尚在加载的元素导致报错,可以设置元素等待来增加脚本的稳定性

显式等待

设置一个超时时间,每隔一段时间就检测一下元素是否存在
注意,没找到之前或者没超时之前,都不会执行后续的内容
类似于阻塞了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By


driver = webdriver.Chrome()
element = WebDriverWait(driver, 5, 0.5)\
          .until(
            EC.presence_of_element_located((By.ID, 'kw')),
            message = '超时了'
          )

隐式等待

与显式等待类似,但是超时抛出的异常不同

1
2
3
# 设置隐式等待 5 秒
driver.implicitly_wait(5)
# 一般配合 try except 捕获超时异常

cookies 操作

常使用 selenium + requests 实现 cookie 持久化
即先用 selenium 模拟登陆获取 cookie ,再通过 requests 携带 cookie 进行请求

可以添加、获取、删除 cookie 等操作

其他

有些网站的 li 标签是懒加载,只有当用户滑动至最后标签时,才会加载后边的数据
一种思路是通过循环来实现重复滚动
每一次循环都进行元素的查找和 js 滑动滚动条脚本的执行

关闭全部页面并退出 quit
关闭当前页面 close
截图 get_screenshot_as_png
获取页面的 html 源码 page_source

实战

一般验证码需要通过第三方平台实现破解

哔哩哔哩

先打开登录界面
定位用户名和密码对应的标签
输入相关数据
点击登录

之后会弹出文字验证码
获取验证码图片的方式有两种
1、截图并裁剪
2、通过 xpath 等方式获取图片的 url 地址
图片保存在本地后,将图片传给第三方平台破解
可能还需要将图片缩小,然后将返回的坐标再按比例放大
最后按序点击即可

PySide

参考资料

简介

Qt 库是 C++ 语言开发的,里面有非常强大的图形界面开发库
PySide2、PyQt5 可以让我们通过 Python 语言使用 Qt

PyQT5 和 PySide2 大部分的接口都是类似的
二者之间的互相兼容,通常只要把导入的名字从 PyQt5 换成 PySide2 就行

PyQT 是由 Riverbank Computing Ltd 开发维护的
采用了 GPL 加商业许可 两种许可证模式
由 GPL 的传染性,这就意味着如果你的代码使用了 PyQT,那么就必须开源代码
而如果不想开源你的代码,你就得 购买商业许可证
这导致 PyQT 商业不友好
于是 Nokia(QT 的开发者)打算和 Riverbank Computing 谈判,劝说 PyQT 采用 LGPL(相对来说是商业友好的)
但是 Riverbank 不同意该建议(因为损失了利益)
Nokia 便决定开发自己的版本。这就是后来的 PySide

安装

pip install pyside2 -i https://mirrors.aliyun.com/pypi/simple

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import sys
from PySide2.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton


def click():
    print("Button is clicked!")

# 每个 GUI 都必须包含一个 Qapplication
# argv 表示获取命令行参数,如果不用获取,则可以使用 [] 代替
app = QApplication(sys.argv)

# QMainWindow 类似一个容器(窗口)
# 用来包含按钮、文本、输入框等 widgets
win = QMainWindow()
# 定义窗口的尺寸,四个参数分别对应坐标 x、y 和宽高
win.setGeometry(400, 400, 400, 300)
win.setWindowTitle("PySide2 Tutorial")

# 设置文本内容
label = QLabel(win)
label.resize(200, 100)
label.setText("Hi this is PySide2")
label.move(100, 100)

# 按钮和事件
button = QPushButton(win)
button.resize(200, 100)
button.setText("Hi! Click Me")
button.move(100, 100)
# 将按钮和函数 click 进行绑定
button.clicked.connect(click)

# 显示窗口
win.show()
# 设置窗口一直运行,直到使用关闭按钮进行关闭
sys.exit(app.exec_())

常用控件

函数 用途
QLabel 用于显示文本或图像,没有提供用户交互功能
QComboBox 组合框(也就是下拉框),可以弹出可选项目列表。组合框可以是可编辑的
QCheckBox 复选框
QRadioButton 单选按钮,isChecked() 方法返回值 True 表示选中,False 表示未选中
QLineEdit 单行文本框控件,可以输入单行字符串,无法换行输入。多行可用 QTextEdit
QPushButton 切换按钮,通过点击可以在 按下和未按下 两种状态之间切换
QTableWidget 显示数据表格
QSlider 滚动条,具有简单句柄的小部件,可以来回拉动
QProgressBar 进度条,在处理冗长任务时直观地让用户知道任务正在进行中
QCalendarWidget 日历小部件
  • 对话框类的控件
    所有对话框的父类是 QDialog

    控件 描述
    QMessageBox 用于提示信息, 如警告、询问和严重出错等
    QFileDialog 用于打开文件和保存文件,可以设置过滤器限制文件后缀名
    QFontDialog 用于设置字体
    QColorDialog 用于设置颜色
  • 容器

    控件 描述
    QTabWidget 选项卡
    QDockWidgetWidget 浮动窗口,可以保持浮动状态或者在制定位置附加到主窗口
    QScrollBar 水平或者垂直的滚动条,扩大窗口的有效装载面积
    QGroupBox 组合框,具有标题和面板,面板中可以容纳各种组件

实战

制作简易的天气查询软件

  • 使用 Qt Designer 设计一个界面
    Qt Designer 的路径 D:\python\Lib\site-packages\qt5_applications\Qt\bin\designer.exe

    用到的控件有:按钮、组合框、文本标签、下拉选择框、文本编辑框
    同时定义了两个按钮 queryBtn 及 clearBtn,分别用来查询及清空天气数据

  • 按钮绑定槽函数:
    1、在 Qt Designer 右下角选择 信号/槽编辑器,点击 + 号新增
    2、分别选择 queryBtn 及 clearBtn,选择 信号 clicked(),接收者 Dialog 及槽 accept()
    【这里的槽 accept() 需要在 ui 转换成的 py 文件中进行修改,改为对应的点击函数】
    最后保存为 Weather.ui 文件

PySide 支持直接使用 .ui 文件

1
2
3
4
5
6
7
8
import sys
from PyQt5 import QtWidgets, uic


app = QtWidgets.QApplication(sys.argv)
window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()

也可以将 ui 文件转换为 py 文件的命令
pyuic5 -o weather.py .\weather.ui

  • 对话框类 Dialog
    在 MainDialog 中调用界面类 Ui_Dialog
    然后在其中中添加查询天气的业务逻辑代码,这样就做到了 界面显示和业务逻辑的分离

    新增 demo.py 文件,在 MainDialog 类中定义了两个 槽函数 queryWeather()clearText()
    与 ui 文件中定义的两个按钮(queryBtn 和 clearBtn)的触发 clicked 信号 进行绑定

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    import sys
    # weather.ui 文件转换为的 py 文件,其中包含 Ui_Dialog 类
    import Weather
    from PySide2.QtWidgets import QApplication, QDialog
    import requests
    
    
    class MainDialog(QDialog):
        def __init__(self, parent=None):
            super(QDialog, self).__init__(parent)
            # 设置 UI
            self.ui = Weather.Ui_Dialog()
            self.ui.setupUi(self)
    
        # 实现业务逻辑
        def queryWeather(self):
            cityName = self.ui.comboBox.currentText()
            cityCode = self.getCode(cityName)
            r = requests.get(
                "https://restapi.amap.com/v3/weather/weatherInfo?key=f4fd5b287b6d7d51a3c60fee24e42002&city={}".format(cityCode))
            if r.status_code == 200:
                data = r.json()['lives'][0]
                weatherMsg = '城市:{}\n天气:{}\n温度:{}\n风向:{}\n风力:{}\n湿度:{}\n发布时间:{}\n'.format(
                    data['city'],
                    data['weather'],
                    data['temperature'],
                    data['winddirection'],
                    data['windpower'],
                    data['humidity'],
                    data['reporttime'],
                )
            else:
                weatherMsg = '天气查询失败,请稍后再试!'
            self.ui.textEdit.setText(weatherMsg)
    
        def getCode(self, cityName):
            cityDict = {"北京": "110000",
                        "苏州": "320500",
                        "上海": "310000"}
            return cityDict.get(cityName, '101010100')
    
        def clearText(self):
            self.ui.textEdit.clear()
    
    if __name__ == '__main__':
        myapp = QApplication(sys.argv)
        myDlg = MainDialog()
        myDlg.show()
        sys.exit(myapp.exec_())
    
  • 将代码打包成 exe 文件
    常用的工具有:PyInstaller,py2exe,cx_Freeze,bbfreze,py2app 等

    • fbs
      要求 Python 版本小于等于 3.6
      先创建一个虚拟环境,再安装 pip install fbs PySide2==5.12.0

      创建新的 fbs 项目 fbs startproject

      启动界面进行测试 fbs run

      打包为 APP fbs freeze

      创建安装程序 fbs installer
      注意的是在 Windows 上面我们需要安装 NSIS
      需要把 NSIS 的根目录添加到 system PATH 中

物料表格匹配

首先通过 Qt Designer 设计好界面
将 ui 文件转换成 py 文件

然后编写主要文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 通过全局变量设置样式
# 三对双引号内书写 css
startbutton_qss = """
    QPushButton{
        background:orange;
        color:white;
        font-size:18px;
        border-radius: 24px;
        font-family: 微软雅黑;
    }
    QPushButton:pressed{
        background:black;
    }
"""

主要分为两个窗口,一个用于文件转换,另一个用于物料匹配
这两个类都继承于 QtWidgets.QDialog

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- `class FileConversionWindow()`
    - __init__
        初始化一些控件,绑定好点击事件和信号,设置好样式
    - callback
        用于进度条参数的回传
    - callback_done
        进度条结束的信号
    - messageDialog1
        通知对话框 `QtWidgets.QMessageBox` 实现通知信息
    - select_oldBomfile
        文件选择对话框 `QtWidgets.QFileDialog.getOpenFileName`
    - select_totalBomfile
    - run_convert
        主要的文件处理逻辑,最后将前面定义的信号 emit 出来


- `class BomCompareWindow()`
    - __init__
    - select_file
    - select_file_new
    - show_result
    - show_log
    - run_compare


- `class Ui_MainWindow(object)`

通过 QtCore.pysideSignal 设置信号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 创建 app
app = QtWidgets.QApplication(sys.argv)
# 设置主窗口
MainWindow = QtWidgets.QMainWindow()
# 初始化 UI
ui = Ui_MainWindow()
# 将 UI 设置到主窗口上
ui.setupUi(MainWindow)
# 展示主窗口
MainWindow.show()
# 当 app 执行结束动作时,系统关闭 app
sys.exit(app.exec_())

turtle

海龟绘图
实现在平铺的纸上画线

1
2
3
4
5
6
7
from turtle import *
# 注意这么导入的对象集很大,可能发生名称冲突


# 推荐如下导入方式
import turtle
# width() 就要变成 turtle.width()

⭕ 移动和绘制

标准模式下,默认是面向东,此时是 0°
逆时针旋转 90° 即北
(注:logo 模式下,默认是 0° 面向北)

作用 写法
抬笔 turtle.penup()
turtle.up()
turtle.pu()
下笔 turtle.pendown()
turtle.down()
turtle.pd()
当前画笔是否落下 turtle.isdown()
(落下则返回 true,否则返回 false
前进 n 步 turtle.forward(n)
turtle.fd(n)
(同理有 turtle.backward(n).bk(n)
向左旋转 120° turtle.left(m)
turtle.lt(m)
(同理有 turtle.right(m).rt(m)
获取海龟的 xy 坐标 turtle.position()
turtle.pos()
(仅获取 x 坐标 turtle.xcor()
仅获取 y 坐标 turtle.ycor()
设置海龟的坐标,不改变方向,画笔落下就会画线 turtle.goto(x, y)
turtle.setpos(x, y)
(单独设置横纵坐标 turtle.setx(x)turtle.sety(y)
turtle.teleport(x, y, fill_gap=False) 有类似作用,但是不会画线)
设置海龟的朝向 turtle.setheading(90) 朝向北
获取海龟的朝向 turtle.heading()
绘制半径为 120 个单位的半圆(圆心在海龟的左侧) turtle.circle(120, 180)
绘制直径为 20,颜色为蓝色的圆点 turtle.dot(20, 'blue')
(若未指定直径,则选取 max(pensize+4, 2*pensize)
在当前位置印一个海龟形状 stamp_id = turtle.stamp()
清除印记 turtle.clearstamp(stamp_id)
撤销动作 turtle.undo() 可以配合循环进行多次的撤销
获取夹角 turtle.towards(x, y)
x 为海龟对象时,y=none
获取距离 turtle.distance(x, y)
设置角度的度量单位为弧度 turtle.radians()
设置画笔线条颜色和填充颜色 turtle.color('blue', 'green') 支持 rgb 元组
单独控制线条颜色 turtle.pencolor('blue')
单独控制填充色 turtle.fillcolor('green')
控制填充的打开和关闭(二者需要同时出现) turtle.begin_fill()
turtle.end_fill()
返回填充状态,布尔值 turtle.filling()
控制线宽或者线条粗细 turtle.width(3)
turtle.pensize(3)
(若不指定参数,则返回当前的 size)
设置海龟移动的速度 turtle.speed('normal')
1~10 为最慢到快,0 为最快
获取当前画笔的配置,返回值是字典 turtle.pen()
也可以向其传入参数和值进行设置,一般是一个字典
当海龟消失在屏幕中时,需要将其送回起点 turtle.home()
清空窗口 turtle.clearscreen()
判断海龟何时回到初始点 abs(pos()) < 1
防止脚本结束时,海龟窗口被自动关闭 turtle.mainloop()
窗口就会等待被主动终止
清除绘制的内容,海龟回到原点所有设置变为默认值 turtle.reset()
删除指定海龟的绘图,但是海龟不动 turtle.clear()
使海龟不可见 turtle.hideturtle()
turtle.ht()
使海龟可见 turtle.showturtle()
turtle.st()
返回海龟是否可见,布尔类型 turtle.isvisible()
书写内容 turtle.write("Home = ", move=True, align="center")
move 为真值,画笔会移至文本的右下角
创建并返回海龟的克隆体(具有相同的位置、朝向和海龟属性) turtle.clone()
获取海龟对象自身 turtle.getturtle()
turtle.getpen()
获取海龟绘图场所的 TurtleScreen 类对象 turtle.getscreen()
设置海归外观 turtle.shape(name='turtle')
此外还有 “arrow”, “circle”, “square”, “triangle”, “classic” 等外观
为鼠标点击海龟事件绑定函数 turtle.onclick(myfunc, btn=1, add=None)
btn=1 表示绑定鼠标左键,当 myfuncnone 时,意味着 移除现有绑定
为鼠标【释放】点击海龟事件绑定函数 turtle.onrelease(myfunc, btn=1, add=None)

⭕ 面向对象

1
2
3
4
5
from turtle import Turtle

t1 = Turtle()
t2 = Turtle()
# 通过这种方式,允许屏幕同时存在多只海龟
1
2
3
# 自定义屏幕
t1.screen.title('turtle demo')
t1.screen.bgcolor('orange')

⭕ 搭配循环绘制各种几何图案

1
2
3
4
5
for steps in range(100):
    for c in ('blue', 'red', 'green'):
        color(c)
        forward(steps)
        right(30)

NumPy

⭕ 常用语句

1
import numpy as np

🔘 numpy 数组的大小形状

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
a = np.arange(12).reshape(3, 4)


a.ndim
# 返回维度数量

a.shape
# 返回二维数组,表示各个维度的大小

a.dtype.name
# 数据类型

a.size
# 数组的长宽相乘

type(a)
# 获取数据类型,numpy.ndarray

🔘 修改形状、类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
a.resize((2, 6))    # 不支持 -1
a.reshape((2, -1))
# 注意这里的 -1
# 不用去管几列,只要专注于自己想要将原矩阵修改为几行即可


# 将数组展平
a.flat    # 返回值是迭代器,可以通过list关键字转换成列表
list(a.flat)
a.ravel()    # 直接返回 numpy 数组对象
1
2
3
np.array([[1, 2], [3, 4]], dtype=complex)
# 这里在创建的时候指定了数据形式为【复数】
# 还可以指定 dtype=np.float32 等等

🔘 创建区间数据

1
2
3
4
5
6
7
8
9
np.arange(10, 30, 5)
# 从10开始取值,步长为5,直到30但不包括30
np.arange(0, 2, 0.3)
# [0,2),步长为 0.3


# 在区间 [0,2] 上生成【平均间隔】的 5 个数
np.linspace(0, 2, 5)
# array([0. , 0.5, 1. , 1.5, 2. ])

🔘 矩阵乘法

1
2
3
4
5
6
7
8
A * B
# elementwise product,相应位置的数字相乘


A.T @ B
# matrix product,矩阵相乘
A.T.dot(B)    # 同上
np.dot(A.T, B)    # 同上

🔘 其他

索引:

1
2
3
4
5
6
7
8
9
c = np.array([[[  0,  1,  2],
               [ 10, 12, 13]],

              [[100, 101, 102],
               [110, 112, 113]]])


c[1, ...]    # 等同于 c[1, :, :] or c[1]
c[..., 2]    # 等同于 c[:, :, 2]

数组的随机乱序排列:

1
2
3
nums = [1,2,3,4,5,6,7]
a = np.random.RandomState(0)
a.permutation(nums)

Pandas

Pandas 建立在 NumPy 基础上

NumPy 最适合处理 同构 数值数组数据
而 Pandas 是为处理 表格异构数据 而设计的

  • Pandas 的三个基本数据结构
    数据结构 说明
    Series 带索引的一维数组,也是特殊的字典
    DataFrame 带行列索引的二维数组,也是特殊的字典
    Index 不可变数组或有序集合

⭕ Series 对象

创建一个 Series 对象:

1
2
3
4
5
6
7
8
data = pd.Series([0.2, 0.5, 0.7, 1])


print(data)
print('----------------')
print(data.index)    #查看 Serise 对象的索引
print('----------------')
print(data.values)    #查看 Serise 对象的值
NumPy 数组 Series 对象
通过【隐式定义的整数索引】获取数组 用【显式定义的索引】与数值关联
索引不限于整数,可以是任意类型
例如字符串,也可以不连续且无顺序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
arr = [0.2, 0.5, 0.7, 1]
ind1 = ['a', 'c', 'b', 'd']
ind2 = [2, 5, 3, 7]


d1 = pd.Series(arr)
# 默认索引是从 0 开始依次标号
print('d1的索引是:', d1.index)

# 将【字符(串)】作为索引
d2 = pd.Series(arr, index=ind1)
print('d2的索引是:', d2.index)

# 将【无序整数】作为索引
d3 = pd.Series(arr, index=ind2)
print('d3的索引是:', d3.index)

# 不同的索引表示同一值
print(d1[0], d2['a'], d3[2])

🔘 通过传入字典来创建 Series 对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
pop_dict = {
    'California': 38332521,
    'Texas': 26448193,
    'New York': 19651127,
    'Florida': 19552860,
    'Illinois': 12882135
}


pop = pd.Series(pop_dict)
print(pop)
print('----------------')
# 取出 Series 对象 California 对应的值
print('California人口数为:', pop['California'])

⭕ DataFrame 对象

  • DataFrame 对象创建方式
    通过单个 Series 对象创建
    通过字典列表创建
    通过 Series 对象字典创建
    通过 NumPy 二维数组创建
    通过 NumPy 结构化数组创建
    ……

🔘 通过单个 Series 对象创建

1
2
3
4
5
6
7
8
9
import pandas as pd


# 创建一个单个 Series 对象
data = pd.Series([1, 2, 3, 4, 5])

# 使用该 Series 对象创建 DataFrame
df = pd.DataFrame(data, columns=['Column_Name'])
print(df)

🔘 通过字典列表创建

1
2
3
4
5
6
7
8
9
# 创建一个字典列表
data = [
    {'a': 1, 'b': 2},
    {'a': 5, 'b': 10, 'c': 20}
]


# 使用字典列表创建 DataFrame
df = pd.DataFrame(data)

🔘 通过 Series 对象字典创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
area_dict = {
    'Texas': 695662,
    'New York': 141297,
    'Florida': 170312,
    'Illinois': 149995,
    'California': 423967
}
area = pd.Series(area_dict)


# 两列,每列都为 Series 对象,通过字典传入
states = pd.DataFrame(
    {'pop': pop,
    'area': area}
)

🔘 通过 NumPy 二维数组创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import numpy as np

# 创建一个 NumPy 二维数组
data = np.array(
    [[1, 2, 3],
    [4, 5, 6]]
)


# 使用 NumPy 二维数组创建 DataFrame
df = pd.DataFrame(
    data,
    columns=['Column1', 'Column2', 'Column3']
)

🔘 通过 NumPy 结构化数组创建

1
2
3
4
5
6
7
8
9
# 创建一个 NumPy 结构化数组
data = np.array(
    [(1, 'a'), (2, 'b'), (3, 'c')],
    dtype=[('Column1', int), ('Column2', str)]
)


# 使用 NumPy 结构化数组创建 DataFrame
df = pd.DataFrame(data)

🔘 通过传入 data、index、columns 参数创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
df = pd.DataFrame(
    np.arange(6).reshape(3, 2),
    # 设置【行索引】分别为 a、b、c
    index = ['a', 'b', 'c'],
    # 设置【列名】分别为 foo、bar
    columns = ['foo', 'bar']
)


print(df['foo'])
# 查看 DataFrame 对象 foo 列 a 行对应的值
print(df['foo']['a'])

⭕ Index 对象

1
2
3
4
ind1 = pd.Index([2, 3, 5, 7, 11])
ind2 = pd.Index(['2', '3', '5', '7'])
print(ind1)
print(ind2)

⭕ 数据选取

假设 data 是 Series 类型

1
2
3
4
5
6
# Serise 对象索引为 1 对应的值
print(data[1])


# Serise 对象索引值为 0 到 2 的值
print(data[0:3])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 【显式索引】
# 使用的是行和列的【标签】
# 包括结束位置
df.loc['行标签', '列标签']


# 【隐式索引】
# 使用行和列的整数下标
# 索引从 0 开始
df.iloc[1, 2]
1
2
3
# 注意
df.iloc[32]    # 结果是 Series
df.iloc[[32]]    # 结果是 DataFrame

⭕ 一些代码操作

🔘 常用操作

1
2
3
4
5
6
7
8
9
# 某个 dataframe 中的值需要进行替换
# 我们可以使用 replace
# 也可以通过下面的方法
b = {
    '原本的值1': 新的值1,
    '原本的值2': 新的值2,
    ......
}
df['...'] = df['...'].map(b)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 查看字段名
df.columns
# 字段名重命名
df.rename(columns={'原来字段名':'新字段名'}, inplace=True)


# 查看行列数
df.shape


# 查看数据前 5 行
df.head()
# 查看数据后 5 行
df.tail()


# 查看df的字段的数据类型
df.dtypes
# 数据类型的转换
df['...'].astype(...)


# 数据描述统计
df.describe().T
# 其中最后五列的【分位数】等同于【np.percentile】
pd.DataFrame(
    np.percentile(
        df,
        q=[0,25,50,75,100],
        axis=0
    ).T
)

🔘 增加列

1
2
3
4
5
6
7
8
states = pd.DataFrame(
    {'pop': pop,
    'area': area}
)


# 类似字典新增键值对
states['density'] = states['pop']/states['area']

🔘 筛选数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 取出 grammer 字段中,值等于 Python 的数据
df[df['grammer']=='Python']


# 取出 popularity 字段中,值大于 3 且小于 7 的数据
df[(df['popularity'] > 3) & (df['popularity'] < 7)]


# 取出 popularity 字段为最大值的数据
df[df["popularity"] == df["popularity"].max()]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 取出 grammer 字段中,值含有 Python 的数据
result = df['grammer'].str.contains('Python')
# result 是布尔值 Series,即一列的 True 或者 False
# 可能存在缺失值,故填补以防止报错
result.fillna(value=False, inplace=True)
df[result]


# 结合 NumPy 的函数
# 筛选出薪资大于 5000 的最后 3 个数据
res = df.iloc[np.where(df['salary'] > 5000)[0][-3:], :]


# 筛选出字段 name 值出现在列表中的数据
df[df['name'].isin(['--', '*'])]
# 反向筛选 ~
df[~df['name'].isin(['--', '*'])]


# 提取某一字段在指定位置的值
df['...'].take([1, 10, 15])
df['...'].iloc[[1, 10, 15]]
# 取出第 2、11、16 行的数据

🔘 数据描述统计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 统计 grammer 字段中各类值出现的次数
df['grammer'].value_counts()


# 计算 popularity 字段的均值
df['popularity'].mean()
# 计算每一行的均值
df[["col1","col2","col3"]].mean(axis=1)


# 通过匿名函数统计字段值的长度,生成新字段
df['len_str'] = df['grammer'].map(
    lambda x:len(x)
)


# 获取某一字段的中位数
import numpy as np
np.median(df['popularity'])

🔘 关于数据处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 利用平均值填补法,对popularity字段进行缺失值填补
# .interpolate()是插值函数,默认是线性插值
# 可以实现前后值的平均值
df['popularity'].fillna(
    df['popularity'].interpolate,
    inplace=True
)


# 删除所有存在缺失值的行
data.dropna(axis=0, how='any', inplace=True)
1
2
3
4
5
6
7
8
9
# 根据 grammer 字段【去除重复值】
df.drop_duplicates(
    ['grammer'],
    inplace=True
)


# 查看某一字段中有几类值
df['...'].nunique()

🔘 Pandas 100

1
2
3
4
5
6
7
8
9
# 找局部极大值可以看作是:从侧面看一些起伏的山岭并找到所有的山顶
# 先做差分(后一个减去前一个),
# 再通过一个 sign()函数,使得:负数为 -1,正数为 1,0还是 0,
# 最后再做一次差分


tem = np.diff(np.sign(np.diff(df4["col1"])))
# 局部极大值的下标
np.where(tem ==-2)[0] + 1

🔘 其他

1
2
# 字段值转成列表
df['grammer'].tolist()
1
2
3
# 字段顺序的更换
cols = df.columns[[新的顺序对原df的字段下标进行改变]]
df = df[cols]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 添加数据
row = {
    'grammer': 'Perl',
    'popularity': 6.6,
}
df = df.append(row, ignore_index=True)


# 合并两个df
... = pd.concat([df1, df2], axis=1)    #两列左右合并
... = pd.concat([df1, df2], axis=0, ignore_index=True)    #上下合并


# 删除某字段
df.drop(['popularity'], axis=1)
# 或者
del df['popularity']
1
2
3
4
5
6
# 根据 popularity 字段进行降序排列
df.sort_values(
    'popularity',
    inplace=True,
    ascending=False
)
1
2
3
4
5
6
7
8
# apply 的应用,比循环处理效率高
def func(df):
    。。。。。。
    return df


# 分别对每一列使用 func 函数
df = df.apply(func, axis=1)
1
2
3
4
5
6
7
8
9
# 【数据分箱】
bins = [0,5000,20000,50000]
group_names = ["低","中","高"]
# 通过将salary字段分箱得到新字段categories的三种类别
dff['categories'] = pd.cut(
    dff["salary"],
    bins=bins,
    labels=group_names
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 若两字段的值都是字符串,可以直接相加
# 通过 map 或者 astype 将数据转换成字符串类型
df['test'] = df['...'].map(str) + df['...'].map(str)
df['test'] = df['...'].astype(str) + df['...'].astype(str)
# map 函数是利用函数或者 lambda 匿名函数,实现转换成 str 类型的目的
# astype 是 pandas 中的函数


# 两个数值类型的字段可以直接进行算术运算
import numpy as np
# 计算二范数
np.linalg.norm(df['...']-df['...'])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 将某一字段设置为索引
df = df.set_index('...')


# 重置行索引
df = df.reset_index()


# 修改列名
df.columns = ['...', '...', ...]
1
2
3
# 时间数据的处理【to_pydatetime】
# 原本值是 2020-03-20 11:30:18
dff.iloc[i, 0] = dff.iloc[i, 0].to_pydatetime().strftime('%m-%d')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 前后两条数据的差值,也叫做【差分】
df['...'].diff()


# 前后两条数据的变化率
df['...'].pct_change()


# 设置长度为 5 的数据滑动窗口,并计算均值
df['...'].rolling(5).mean()
# 前四个值为空


# 累计均值
data["开盘价(元)"].expanding(min_periods=1).mean()
# 比如第一行是1月4号的开盘价均值,是它本身
# 第二行就是1月4号与1月5号的开盘价均值,两个相加求和取均值
# 依此类推,最后一行就是总的开盘价的均值
# .expanding(min_periods=1).sum() 则是计算累计和
# 注意和 rolling().mean()不一样,.expanding()的窗口在不断变大


# 将数据往后移动 5 行,在时间序列数据中也即向后推迟 5 个时间点
df.shift(5)
# 反之向前移动
df.shift(-5)

# 计算布林指数,公式如下:
#     中轨线 = N 日的移动平均线
df['...'].rolling(N).mean()
#     上轨线 = 中轨线 + 两倍的标准差
df['...'].rolling(N).mean()+2*df['...'].rolling(N).mean().std()
#     下轨线 = 中轨线 - 两倍的标准差
df['...'].rolling(N).mean()-2*df['...'].rolling(N).mean().std()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 卷积
np.convolve(
    df["col2"],
    np.ones(3)/3,
    mode="valid"
)


# 假设 df['col2'] 的前三个是 0、5、10
# 三个均乘以 np.ones(3)/3 的第一个数 1/3, 得到 0、5/3、10/3
# 同理乘以第二个数同样是 1/3, 得到 0、5/3、10/3
# 同理乘以第三个数同样是 1/3, 得到 0、5/3、10/3
# 最后求和:
# 0  5/3  10/3
#    0    5/3   10/3
#         0     5/3    10/3
# -----------------------
# 0  5/3  5     5      10/3
# 取最中间那位, 即 5
# 所以上面卷积结果 temp 的第一个数为 5
# 大概就是每做一次乘积移动一下, 最后错位相加起来, 再看 mode取的是默认的 full、same 还是 valid
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 将某些字段名转换成字段值
import pandas as pd

df = pd.DataFrame({
    'Name': {0:'Jane', 1:'Tom'},
    'Course': {0:'Master', 1:'Graduate'},
    'Age': {0:27, 1:31}
})


# 将 Course 和 Age 字段的种类合并作为var列
df1 = pd.melt(
    df,
    id_vars=['Name'], value_vars=['Course', 'Age'],
    var_name='Course & Age', value_name='values'
)
df1

Melt 意思是熔化
Pivot 是转动的意思

1
2
3
4
5
6
7
8
9
df2 = pd.pivot_table(
    df1,
    index='Name',
    values='values',
    columns='Course & Age'
)
df2.columns.name = None
df2.index.name = None
df2    # 对比原数据 df

Matplotlib

⭕ 基本设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt


# 设置中文、负号正常显示
plt.rcParams['font.sans-serif'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False


# 设置画图风格
plt.style.use('classic')
# 'seaborn-whitegrid'


# 将 x 轴转换成【对数格式】
plt.semilogx(..., ...)

⭕ 编写风格

matlab 编写风格:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建一个画布
plt.figure()
# 创建两行一列的子区域
plt.subplot(2, 1, 1)
plt.plot(x, y1, '-')
plt.subplot(2, 1, 2)
plt.plot(x, y2, '--')


# 设置图像标题
plt.title(' ')
# 设置横、纵轴名称
plt.xlabel(' ')
plt.ylabel(' ')
# 添加图例
plt.legend(' ')
# 限制坐标轴范围
# 列表的参数分别为[xmin, xmax, ymin, ymax]
plt.axis( [-1, 11, -1.5, 1.5] )


# 多子图设置同一标题
fig.suptitle('...', size=20)

面向对象编写风格:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fig, ax = plt.subplots(
    3, 8, figsize=(9, 4),
    sharex='col', sharey='row',
    subplot_kw={'xticks':[], 'yticks':[]},
    gridspec_kw=dict(hspace=0.1, wspace=0.1)
)


ax[0].plot(x, y1, '-')    # 注意索引从 0 开始
ax[1].plot(x, y2, '--')

ax[0].set_title(' ')
ax[0].set_xlabel(' ')
ax[0].set_ylabel(' ')
ax[0].legend(' ')
ax[0].set_xlim()
ax[0].set_ylim()
1
2
3
4
5
6
7
8
# 嵌套的子图绘制
# 先定义一个子区域来画图
ax1 = plt.axes()
# 在其中定义一个小图
ax2 = plt.axes([0.4, 0.6, 0.2, 0.2])
x = np.linspace(0, 10, 100)
ax1.plot(x, np.sin(x))
ax2.plot(x, np.cos(x))
1
2
# 绘制不规则子图
grid = plt.GridSpec(2, 3, wspace=0.4, hspace=0.3)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 等高线绘制
# 通过 np.meshgrid(x,y) 得到网格型数据,即一个矩阵
# 再通过横纵坐标计算得到对应位置的 z 值
contours = plt.contour(
    X, Y, Z,
    3,    # 这里的 3 用来控制等高线的疏密程度,越大越密
    colors='black'
)
# 给等高线标上对应的值
plt.clabel(
    contours,
    inline=True,
    fontsize=10
)
plt.imshow(
    Z,
    extent=[0, 5, 0, 5],
    origin='lower',
    cmap=plt.cm.viridis
)
# 颜色条
cb = plt.colorbar()
cb.set_label('。。。')    # 为颜色条添加标题
1
2
3
4
5
6
# 二维直方图
plt.hist2d(
    x, y,
    bins=30,
    cmap=plt.cm.gray_r
)

⭕ seaborn 绘图

1
2
3
4
import seaborn as sns
import pandas as pd
sns.set()
sns.color_palette( 'CMRmap_r' )    #调色盘
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 绘制直方图,展示数据分布
sns.histplot(
    x=data['...'],
    shrink=0.8, bins=12, hue=data['...'],
    multiple="dodge", palette='brg'
)

# 条形图,不同类别出现的频次
sns.barplot(
    x=data['...'].value_counts().index,
    y=data["..."].value_counts(),
    saturation=0.75
)

# 散点图
sns.scatterplot(
    x=data['...'],
    y=data['...']
)

# 绘制正态分布曲线(也可以用来绘制等高线)
sns.kdeplot(data, shade=True)

# 箱线图
sns.catplot(
    data=data,
    x='...', y='...',
    kind="box",
    height=6, aspect=1.5, color="#FBC02D",
)

# 热力图
sns.heatmap(
    data=data[:, ?:?].corr(),
    annot=True,
    cbar=True,
    cmap="binary"
)

# 多子图展示变量间的【相关性】
sns.jointplot(
    "x", "y",
    data,
    kind='hex'
)

# 展示变量之间的关系
sns.pairplot(
    iris[cols],
    hue='species', height=2.5
)

常见的 marker:

https://img-blog.csdnimg.cn/img_convert/b9b1ef54333d060f73d48ee2cf70db29.png
常见的 marker


Scipy

可以用于: 插值、概率统计、曲线拟合、最优化、积分、解微分方程、处理稀疏矩阵、线性代数等等


plotly

带有交互式的可视化,详细见 “D:\jupyterproject\或许有用\Python 整理\kaggle\2022 机器学习与数据科学\比较高级的绘图方法.ipynb” 这一节


其他

logging

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import logging
import datetime


# 创建logger
logger = logging.getLogger('my_logger_1')
logger.setLevel(logging.DEBUG)  # 设置日志级别

# 创建file handler,用于写入日志文件
now = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
fh = logging.FileHandler(f'myapp_{now}.log', encoding='utf-8')
fh.setLevel(logging.DEBUG)

# 将日志输出到控制台
controlshow = logging.StreamHandler()
controlshow.setLevel(logging.DEBUG)

# 创建formatter,用于控制日志格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 为handler添加formatter
fh.setFormatter(formatter)
controlshow.setFormatter(formatter)

# 为logger添加handler
logger.addHandler(fh)
logger.addHandler(controlshow)

# 写入日志
logger.debug('这是一条debug级别的日志信息')
logger.info('这是一条info级别的日志信息')
logger.warning('这是一条warning级别的日志信息')
logger.error('这是一条error级别的日志信息')
logger.critical('这是一条critical级别的日志信息')

# 关闭文件句柄
fh.close()

文件解压

1
2
3
4
5
from zipfile import ZipFile


data_extract = ZipFile('zip文件路径', 'r')
data_extract.extratall()

时间模块

1
2
3
4
5
6
import datetime


t = datetime.datetime.now(tz=None)
# 参数 tz 用于设置时区
# 省略或者None则默认使用本机的时区
1
2
3
4
5
6
t1 = t.strftime('%Y-%m-%d %H:%M:%S')
# 输出:'2024-06-15 21:48:00'

# 逆操作
datetime.datetime.strptime(t1, '%Y-%m-%d %H:%M:%S')
# 输出:datetime.datetime(2024, 6, 15, 21, 48, 0, ???)

上述内容中的 %Y 需要区分大小写
%y 得到的内容是 24 而不是 2024

1
2
3
4
5
# 时间戳
t2 = t.timestamp()

# 逆操作
datetime.datetime.fromtimestamp(t2, tz=None)

时间加减

1
2
3
4
5
6
from datatime import timedelta


t3 = t + timedelta(hours=1)    # t 时间上再加一小时
# hours=-1 即减去一小时
# 当然还可以设置其他参数,例如 weeks、days、minutes、seconds 等等

word 辅助模块

1
2
3
4
5
6
7
8
9
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH  #设置对象居中、对齐等。
from docx.enum.text import WD_TAB_ALIGNMENT,WD_TAB_LEADER,WD_LINE_SPACING  #设置制表符等
from docx.shared import Inches   #设置图像大小
from docx.shared import Pt,Cm   #设置像素、缩进等
from docx.shared import RGBColor    #设置字体颜色
from docx.shared import Length    #设置宽度
from docx.oxml.ns import qn  #设置中文版式
import os
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 创建一个空白文档
document = Document()

# 正文格式设置
# 设置基础字体——西文
document.styles['Normal'].font.name = 'Times New Roman'
# 设置基础中文字体——宋体
document.styles['Normal']._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体')
# 设置字体颜色为黑色
document.styles['Normal'].font.color.rgb = RGBColor(0,0,0)
# 设置字体大小
document.styles['Normal'].font.size = Pt(10.5)
# 设置段落、行间距、缩进
document.styles['Normal'].paragraph_format.space_before = Pt(0)
document.styles['Normal'].paragraph_format.space_after = Pt(0)
document.styles['Normal'].paragraph_format.line_spacing = 1.5
document.styles['Normal'].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
document.styles['Normal'].paragraph_format.first_line_indent = Pt(21)
1
2
3
4
5
6
# 添加段落
document.add_paragraph().add_run('需要添加的文本内容')
document.add_paragraph().add_run('')

# 分页
document.add_page_break()

单元测试

单元测试模块 unittest 小示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import unittest


class TestAddition(unittest.TestCase):
    def setUp(self):
        print("Setting up the test")
    def tearDown(self):
        print("Tearing down the test")
    def test_twoPlusTwo(self):
        total = 2+2
        # 使用断言语句
        self.assertEqual(4, total)

if __name__ == '__main__':
    unittest.main()

单元测试的开始 setUp 和结束 tearDown 函数,每个测试的开始和结束都会运行一次
添加不同类型的“断言”语句

其他

⭕ 如何实现想要的功能?思路分析

列出需要的功能
设计框架,功能对应需要通过哪些函数实现
各个函数实现的具体步骤

⭕ 继承问题

假设类 D 继承了类 B 和类 C,而类 B 和类 C 都继承了类 A
当创建 D 的实例 d 时,会按照方法解析顺序(Method Resolution Order,MRO)来确定调用父类的顺序

在 Python 中,MRO 是通过 C3 线性化算法来确定的
在这个算法中,首先会按照 广度优先搜索 的顺序来遍历继承关系图,然后保持子类在父类之前的顺序

在这个例子中,MRO 的顺序是 D -> B -> C -> A -> object
因此,在调用 super().__init__() 时,会按照这个顺序 依次调用父类的 __init__ 方法

⭕ @property

使得类中的 方法可以作为属性 调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Pager:
    def __init__(self, current_page):
        # 用户当前请求的页码(第一页、第二页...)
        self.current_page = current_page
        # 每页默认显示 10 条数据
        self.per_items = 10

    @property
    def start(self):
        val = (self.current_page - 1) * self.per_items + 1
        return val

    @property
    def end(self):
        val = self.current_page * self.per_items
        return val


p = Pager(2)
print(p.start)  # 就是起始值,即:11
print(p.end)  # 就是结束值,即:20
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Goods:
    # python3 中默认继承 object 类
    # python3 中才有 @xxx.setter @xxx.deleter

    @property
    def price(self):
        print('@property')

    @price.setter
    def price(self, value):
        print('@price.setter')

    @price.deleter
    def price(self):
        print('@price.deleter')


obj = Goods()
obj.price
obj.price = 123
del obj.price

或者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Goods:
    def get_price(self):
        print('price of goods')

    def set_price(self, value):
        print('set')

    def del_price(self):
        print('delete')

    goods = property(
        get_price,
        set_price,
        del_price,
        'description...'
    )


obj = Goods()
obj.goods
obj.goods = '5元'
del obj.goods

⭕ 上下文管理器实现文件操作

with open('') as f: 语句就是通过定义 上下文方法 来实现文件的操作
下面我们尝试实现 MyFile()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyFile(object):
    def __init__(self, file_name, file_model):
        # 创建实例属性
        self.file_name = file_name
        self.file_model = file_model

    # 上文方法(打开资源)
    def __enter__(self):
        # 打开文件,返回文件资源
        self.file = open(self.file_name, self.file_model)
        return self.file

    # 下文方法(关闭资源)
    def __exit__(self, exc_type, exc_val, exc_tb):
        # 关闭文件资源
        self.file.close()


with MyFile('???.txt', 'r') as f:
    data = f.read()
    print(data)

⭕ 文件 I/O

  • DataFrame 写入文件

    1
    2
    
    df.to_excel('。。。.xlsx', index=False)
    df.to_csv('。。。.csv', index=False)
    
  • 路径标准化

    1
    2
    3
    4
    
    import os
    input_path = 'D:/jupyterproject/logo.jpg   '
    input_file = os.path.normpath(input_path.strip())
    # normpath 统一为双反斜杠类型的 \\
    
  • 图片的读取

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    input_file = '???.jpg'
    from PIL import Image
    import numpy as np
    
    image = Image.open(input_file) # 用PIL中的Image.open打开图像
    image = image.resize((512, 512), Image.LANCZOS)    #修改图片大小
    image = image.convert('RGB')                       #转换成RGB模式
    raw_image = np.array(image)                        #转化成numpy数组
    print(raw_image.size)
    
    image_PIL = Image.fromarray(
        np.uint8(raw_image).reshape((512, 512, 3)),
        mode="RGB",    #array转换成图片
    )
    

问题整理

Python 中写实例方法时先加 self,若后续发现无需访问实例属性,再改为静态方法(移除 self 并添加 @staticmethod)

self/this 的本质是让方法知道“当前操作的是哪个对象实例”,不同语言仅在“是否显式声明”上有差异,目的一致

实例方法、类方法、静态方法的使用场景
优先写实例方法,只要方法需要操作对象的具体数据(实例属性),就用 self 参数
若写完方法发现仅使用了类属性,仅处理类共享数据,则用 @classmethod + cls 装饰为类方法,避免硬编码类名
若方法不访问实例或者类属性,仅逻辑上归类,用 @staticmethod 装饰为静态方法

Python 通过属性/方法名前的下划线数量来标记访问权限(非强制语法,更多是“约定”):

权限类型 声明方式 示例
公有 无下划线 nameget_name()
保护 单下划线 _ _age_calc()
私有 双下划线 __ __id__secret()
  1. 公有属性/方法(无下划线)

    • 访问范围:类内部、子类、类外部均可直接访问
    • 用途:开放给外部使用的“接口”,如 user.nameuser.login()
  2. 保护属性/方法(单下划线 _

    • 约定含义:“仅供内部使用,不建议外部直接访问”(但无语法限制)。
    • 访问行为
      • 类内部/子类中可直接访问(如 self._age)。
      • 类外部可以访问(如 obj._age),但 Python 解释器(如 PyCharm)会发出警告(提醒开发者遵守约定)。
    • 用途:类内部辅助逻辑,不希望被外部随意调用(如数据校验、中间计算)。
  3. 私有属性/方法(双下划线 __

    • 语法限制:Python 会对名称进行“名字改写”(__xxx_类名__xxx),外部无法直接访问。
    • 访问行为
      • 类内部可直接访问(如 self.__id)。
      • 类外部直接访问会报错(如 obj.__id → 提示“属性不存在”)。
      • 类外部若强行访问,需用改写后的名称(如 obj._User__id),但强烈不建议(破坏封装)。
    • 用途:类内部核心逻辑,完全禁止外部直接访问(如加密密钥、敏感数据)。

为什么要区分访问权限?核心目的不是为了安全,而是为了“代码可读性”和“使用规范”

  1. 明确接口边界:告诉使用者哪些属性/方法是“安全可用”的(公有),哪些是“内部实现”(保护/私有),避免误用。例:一个类有 100 个方法,若全是公有,使用者难以判断哪些该调用、哪些是内部逻辑。
  2. 降低维护成本:保护/私有成员的修改不会影响外部代码(因为外部不依赖它们)。
  3. 避免命名冲突:私有属性通过“名字改写”,可防止子类意外覆盖父类的内部方法。
  • Java:访问权限是强制语法public/protected/private),编译时会检查,违规则报错。

  • Python:访问权限是约定为主(仅私有有语法辅助),更依赖开发者自觉遵守(“我们都是成年人了”)。

  • 优先用公有:开放给外部的功能,明确标记为公有(无下划线)。

  • 内部逻辑用保护:类内部辅助方法/属性,加单下划线(_),提示外部“勿调用”。

  • 核心机密用私有:完全禁止外部访问的敏感数据/方法,加双下划线(__),通过语法限制防止误操作。