我的 Python 入门一年:19 篇作业笔记合并的学习路径
本文合并自 2022 年 5 月至 10 月的 19 篇 Python 学习笔记,按真实的学习时间线组织,代码与踩坑记录完整保留(错别字已修正,部分地方加了「回头看」注解)。原单篇笔记已归档。
第零阶段:先把写笔记的工具链跑通(2022.05.25–05.30)
学 Python 的第一件事不是写代码,而是先把「学习记录」的流水线搭起来——这一步省下的时间,在后面五个月里复利式地回来了。
学习使用 Markdown
第一篇笔记就是 Markdown 语法本身:
标题:
#一级到######六级字体:
*斜体*、**粗体**、***粗斜体***(_与__同理)
当时的目标很简单:把写好的 .md 文件上传到服务器,在博客里展示出来。
PyCharm 配置默认的 md 文件模板
按照 文件 → 设置 → 编辑器 → 文件和代码模板 → + 的路径新建一个扩展名为 md 的文件模板:
title: 第篇博客
author: Jqh
tags: []
categories: []
date: ${YEAR}-${MONTH}-${DAY} ${HOUR}:${MINUTE}:${SECOND}
---
这样 PyCharm 每次新建 md 文件自动带上 front matter。
当时的同步链路:PyCharm 本地编辑 → 群晖 Synology Drive Client 自动同步到 NAS → NAS 的 blog 目录挂载在 CentOS 服务器上 → 服务器跑一键提交脚本更新博客。
回头看:这就是我最早的「自动发布流水线」,后来演进成 Obsidian + Halo,再到现在你看到的这套。当时踩的第一个坑也很有代表性——PyCharm 新建的文件权限与 hexo 默认不符,无法直接提交,最后在一键提交脚本的 hexo g 前面加了一行授权:
chmod 664 -R /blog/source
Jupyter 初体验:jieba 词频统计
第一次用 Jupyter Notebook 跑的例子是统计《半笙食谱》里出现频率前 50 的人名:
import jieba.posseg as posseg
import pandas as pd
if __name__ == '__main__':
with open('./半笙食谱.txt','r',encoding='utf-8') as fin:
content = fin.read()
words = []
for word,flag in posseg.cut(content):
if flag == 'nr': # nr = 人名词性
words.append(word)
result = str(pd.Series(words).value_counts()[:50])
print(result[:-13:],end='')
第一个报错:ModuleNotFoundError——没装 jieba。Jupyter 里直接装:
!pip install jieba
回头看:posseg.cut() 按词性过滤是中文 NLP 的入门姿势,这个例子后来直接演变成统计《大奉打更人》人名词频的练习(bk4 那篇,同款代码换本书):
import jieba.posseg as posseg
import pandas as pd
with open('./大奉打更人.txt','r',encoding='utf-8') as fin:
content = fin.read()
words = [w for w, flag in posseg.cut(content) if flag == 'nr']
print(str(pd.Series(words).value_counts()[:20])[:-13:], end='')
用 Jupyter Notebook 执行更新 Hexo + 字符串反转四式
把 Notebook 变成博客发布工具:
!cd /blog/ && ./g.sh
顺便装了个 Jupyter 主题:
!pip install --upgrade jupyterthemes
!jt -t oceans16 -f fira -fs 12 -cellw 90% -ofs 10 -dfs 11 -T
这篇笔记的后半段是字符串反转的四种姿势,值得完整保留:
切片(最 Pythonic):
strn = "Hello world!"
s1 = strn[::-1]
print(s1) # !dlrow olleH
reversed() + join():
s2 = reversed(strn)
print(''.join(s2))
循环累加:
def reversed_string(text):
result = ''
for char in text:
result = char + result
return result
reduce:
from functools import reduce
def reversed_string_reduce(text):
return reduce(lambda a, b: b + a, text)
再附一组当时记的内置函数笔记:
list_test = ['a1','a2','a','3','a','4']
print(' '.join(reversed(list_test))) # 4 a 3 a a2 a1 反转
print(list(enumerate(list_test))) # 组合成索引序列
print(sorted(list_test)) # ['3','4','a','a','a1','a2']
随笔:一次卸载事故
2022-05-27 的随笔记录了一次事故:尝试搭建 WordPress 做博客,用网上教程一键安装 LNMP,因本地已有 MySQL 就没勾选装它的——后来嫌 WordPress 慢想舍弃,一键卸载时把本地的 MySQL 也一起卸掉了。
明天重新安装下 mysql,用于学习数据分析及 sql 语句。
回头看:这是第一次为「一键脚本」付出代价。后来在服务器运维里遇到过无数次同类问题(卸载脚本顺带走数据、覆盖式升级冲掉配置),这个教训比这一年的 Python 作业都值钱。
第一阶段:基础语法作业(2022.06.16)
跟着课程做的基础语法作业,三篇连发:列表、元组、集合/字典,最后是一篇阶段总结。
列表作业(10 题全解)
1. 列表基础操作全家桶
li = ["alex", "WuSir", "ritian", "barry", "武沛齐"]
print(len(li)) # 计算长度
li.append('seven') # 追加
li.insert(0,'Tony') # 索引0位置插入(位置是第1)
li.insert(1,'Kelly') # 索引1位置插入(位置是第2)
li[2] = '妖怪' # 修改索引2的元素(位置是第3)
data=[1,"a",3,4,"heart"]
li.extend(data) # 逐元素追加另一个列表
s = "qwert"
li.extend(s) # 字符串也是可迭代对象,逐字符追加
li.remove("barry") # 按值删除
li.pop(1) # 按索引弹出
del li[1:3] # 切片删除
2. 切片练习:li = [1, 3, 2, "a", 4, "b", 5, "c"]
l1 = li[0:3] # [1,3,2]
l2 = li[3:6] # ["a",4,"b"]
l3 = li[::2] # [1,2,4,5]
l4 = li[1:-1:2] # [3,"a","b"]
l5 = li[1::2] # [3,"a","b","c"]
l6 = [li[7:]] # ["c"]
l7 = li[5:0:-2] # ["b","a",3]
3. 嵌套列表的深层修改
lis = [2, 3, "k", ["qwe", 20, ["k1", ["tt", 3, "1"]], 89], "ab", "adv"]
lis[2] = lis[2].upper() # "k" -> "K"
lis[1] = '100' # 3 -> "100"
lis[3][2][1][0] = 101 # 深层的 "tt" -> 101
lis[3].insert(0, '火车头') # 子列表头部插入
4-5. 循环输出元素和索引(从 0 和从 1 两种)
users = ["武沛齐","景女神","肖大侠"]
# 方法一
for i in range(len(users)):
print(f'{i} {users[i]}')
# 方法二
for i in users:
print(f'{users.index(i)} {i}')
6. 商品选择小程序(while + input + 条件校验)
goods = ['汽车', '飞机', '火箭']
num_list = ['0', '1', '2']
while 1:
user_in = input('请输入序号选择商品(q/Q 退出):')
if user_in.upper() == 'Q':
print('退出!'); break
elif user_in in num_list:
print(f'您选择的商品是:{goods[int(user_in)]}')
else:
print('输入错误,请重新输入!')
7-8. range + 条件筛选
# 0~50 内被 3 整除的数
result = [i for i in range(51) if i % 3 == 0]
# 插到索引 0,得到倒序 [48,45,42...]
result = []
for i in range(51):
if i % 3 == 0:
result.insert(0, i)
9. strip + startswith 组合(注意细节)
li = ["alexC", "AbC ", "egon", " riTiAn", "WuSir", " aqc"]
result = []
for i in li:
li[li.index(i)] = i.strip()
if i.startswith('a'):
result.append(i)
# ' aqc' 以空格开头不满足题意,result 只有 ['alexC']
10. 京牌筛选
data = ["京1231", "冀8899", "京166631", "晋989"]
jing = [i for i in data if i.startswith('京')]
print(jing, len(jing))
元组作业
概念对比(当时的原话整理):
列表是可变序列,元组是不可变序列;元组访问和处理速度更快,只读场景优先用元组
两者都支持切片访问,但元组不支持切片修改
元组可以作为字典的键,列表不行
(1)是整型 1,(1,)才是单元素元组元组的元素不能被替换,但元素若是可变类型(如内嵌 list),内部仍可修改
作业选解:
布尔转换练习——哪些值为 False:
# 1 -> True "" -> False -19 -> True
# [] -> False [11,22] -> True
# (1) -> True (1,2,3) -> True () -> False
a = [1,2,3]、b = [(1),(2),(3)]、c = [(1,),(2,),(3,)] 的区别:前两个存的是整型,第三个存的全是元组。
字符串分割转元组:
text = "wupeiqi|alex|eric"
print(tuple(text.split('|')))
生成一副扑克牌(排除大小王)——双重循环 + 元组:
color_list = ["红桃", "黑桃", "方片", "梅花"]
result = []
for color in color_list:
for num in range(1, 14):
result.append((color, num))
# [("红桃",1), ("红桃",2) ...]
集合、字典、浮点型笔记
当时最长的课堂笔记,核心内容完整保留。
集合(set):无序、可变、不允许重复的容器;v1 = {} 定义的是字典,空集合必须 set()。
data = {"刘嘉玲", '关之琳', "王祖贤"}
data.add("郑裕玲") # 添加
data.discard("关之琳") # 删除
s1 = {"刘能", "赵四", "皮长山"}
s2 = {"刘科长", "冯乡长", "皮长山"}
print(s1 & s2) # 交集 {"皮长山"}
print(s1 | s2) # 并集
print(s1 - s2) # 差集:s1 有且 s2 没有
集合的元素必须可哈希(int/bool/str/tuple 可以,list/set/dict 不行);因为哈希存储,查找效率非常高。True/False 本质是 1/0,与整数同存会「谁先添加留下谁」。
容器对比表(笔记原表):
经典练习——列表去重:
v = [11,22,11,22,44455]
result = list(set(v)) # [11,22,44455]
字典(dict):无序(3.6+ 实际有序)、键不重复、键值对容器。键必须可哈希,值任意。
独有方法速记:
info = {"age":12, "status":True, "name":"武沛齐"}
info.get("email") # 键不存在返回 None(不报错,推荐用它取值)
info.get("hobby", 123) # 不存在时返回默认值 123
info.keys() / info.values() / info.items() # 「高仿列表」(py3)
info.setdefault("age", 18) # 键存在则不动,不存在才设置
info.update({"age":14}) # 批量更新/添加
info.pop("age") # 删除并返回值
info.popitem() # 移除最后一个(py3.6+)
循环三式:
for key in info: print(key) # 所有键
for v in info.values(): print(v) # 所有值
for k, v in info.items(): print(k, v) # 键值对(元组拆包)
嵌套规则:键必须可哈希(内嵌 list 的 tuple 也不行);值可以是任意类型。以及布尔值与 1/0 做键的陷阱:
data_dict = {1: 29, True: 5}
print(data_dict) # {1: 5} —— True 就是 1,后写覆盖先写
回头看:当年笔记里就标注了「项目开发建议用 get 而不是 [] 取值」,这条到今天写任何 Python 代码依然成立。
阶段总结:格式化、运算符、进制、编码
字符串格式化(两代):
# format
text = "我叫{0},今年{1}岁,真实姓名是{0}".format("武沛齐", 18)
text = "我叫{n1},今年{age}岁".format(n1="武沛齐", age=18)
# f-string(py3.6+,最便捷)
name = "喵喵"; age = 19
text = f"名字叫{name},今年{age}岁"
v = f"今年{22:#b}岁" # 进制:#b 二进制 #o 八进制 #x 十六进制
# py3.8+ 调试语法
text = f"今年{19 + 2=}岁" # 输出:今年19 + 2=21
运算符优先级:加减乘除 > 比较 > not and or;逻辑运算内部 not > and > or。绝招:加括号。
进制转换:
bin(25) # '0b11001'
oct(23) # '0o27'
hex(28) # '0x1c'
int("0b11001", base=2) # 25
编码解码:
a = '王'
ord(a) # 30340(unicode 码位)
a.encode("utf-8") # b'\xe7\x8e\x8b' 汉字 3 字节
a.encode('gbk') # b'\xcd\xf5' 汉字 2 字节
a.encode('gbk').decode('gbk') # '王'(gbk 解码必填,utf-8 可省)
核心认知(笔记十条总结的浓缩):ascii / unicode / utf-8 本质都是字符与二进制的对照关系,utf-8 是对 unicode 码位的压缩编码;一个字节 8 位。
第二阶段:拿语法做点真东西(2022.06)
随机车牌号选号器
规则:7 位车牌,首位汉字「京」;第二位地级行政区字母(不含 I/O);后 5 位数字字母混合、最多 2 个字母且不含 I/O。每批 20 个不重复,最多 3 次重选。
完整实现:
import random
First_Word = '京'
EN_Word = "ABCDEFGHJKLMNPQRSTUVWXYZ" # 无 I/O
NUM_Word = "0123456789"
NUMBER = [str(i) for i in range(1, 21)] # 选项编号
Choice_Number = 3
Car_Max_Number = 20
def Creat_Car_Number():
Surplus_Word = []
En_Number = 0
Second_Word = random.choice(EN_Word)
for i in range(5):
if En_Number < 2:
Number = str(random.sample(EN_Word + NUM_Word, 1))[2:3]
if Number in EN_Word:
En_Number += 1
Surplus_Word.append(Number)
else:
Surplus_Word.append(random.choice(NUM_Word))
return First_Word + Second_Word + '-' + ''.join(Surplus_Word)
def Choice_Car_Number():
Car_list = []
while 1:
Car_Number = Creat_Car_Number()
if len(Car_list) != Car_Max_Number and Car_Number not in Car_list:
Car_list.append(Car_Number)
elif len(Car_list) == Car_Max_Number:
return Car_list
def Show_Car_Number():
return list(enumerate(Choice_Car_Number(), 1))
def Print_Car_Number(Car_list_OBJ):
print('*' * 30)
for count, element in Car_list_OBJ:
print(f'选项编号:{count},车牌号:{element}')
print('*' * 30)
def Choice_Car_list():
return [Show_Car_Number() for _ in range(Choice_Number)]
if __name__ == '__main__':
Choice_count = 1
Result_List = Choice_Car_list()
while 1:
print('当前为第{}批车牌'.format(Choice_count))
Print_Car_Number(Result_List[Choice_count-1])
s = str(input('请输入想选择的序号:输入n换一批:'))
if s in NUMBER:
s = int(s)
sel = list(Result_List[Choice_count-1])[s-1]
print('恭喜!您选择的号码是:{},车牌号为:{}'.format(sel[0], sel[1]), end='')
break
elif s == 'n':
print('已为您重新生成一批车牌。')
Choice_count += 1
elif s == 'q':
print('退出!'); break
else:
print('请输入正确的选项编号或序号!')
执行效果:
当前为第1批车牌
******************************
选项编号:1,车牌号:京V-R3F59
选项编号:2,车牌号:京D-P5E89
选项编号:3,车牌号:京F-Z1G14
...(共20个)
******************************
请输入想选择的序号:输入n换一批:2
恭喜!您选择的号码是:2,车牌号为:京D-P5E89
踩坑记录:exit() 在 ipython 里行为异常,全部改成了 break。
快递地址分拣
把一批 ['姓名', '省市区街道地址'] 数据按省份归类输出成 JSON。数据样例(完整约 130 条):
title_str = '''[
['王*龙', '北京市海淀区苏州街大恒科技大厦南座4层'],
['庞*飞', '北京市昌平区汇德商厦四楼403'],
['顾*锐', '江苏省扬州市三垛镇工业集中区扬州市立华畜禽有限公司'],
['王*飞', '上海市徐汇区H88越虹广场B座5E'],
...
]'''
核心处理:
# 字符串解析成列表(当时还不会 ast/re,手写了状态机)
def Tran_list(one_str: str) -> list:
result_list, middle_list = [], []
count = 1
result = ''
for i in one_str:
if count % 2 == 0:
if i != "'":
result += i
else:
middle_list.append(result)
result = ''
count += 1
elif i == "'":
count += 1
elif i == ']':
if middle_list:
result_list.append(middle_list)
middle_list = []
return result_list
# 按省份归组拼接 JSON
def Splicing_Str(one_list, region_list):
result = '{\n'
for y in region_list:
scr_list = ''
for x in one_list:
if y in x[1]:
scr_list += ' ' + str(x) + ',\n'
if scr_list:
result += ' "' + y + '": [\n' + scr_list
result = result[:-2] + '\n ],\n'
return result[:-2] + '\n}'
回头看:当时手写单引号配对状态机来解析字符串,其实一行 ast.literal_eval() 就能解决——但那个状态机让我第一次真正理解了「解析」这件事。
炸金花(zjh)游戏
完整实现一副牌生成、发牌、六种牌型判定、比大小、频率统计:
from random import sample
from collections import Counter
# 生成一副完整的扑克牌(牌面 -> 点数映射)
def creat_poker_dic() -> dict:
suit = ["♠", "♥", "♦", "♣"]
num = [str(i) for i in range(2, 11)] + ["J", "Q", "K", "A"]
poker_lst = {}
for j in suit:
count = 2
for n in num:
poker_lst[f"{j}{n}"] = count
count += 1
return poker_lst
# 发牌:每人3张互不重复
def deal_card(pls: list, pks: list) -> list:
result = []
for p in pls:
pk = sample(pks, 3)
for _pk in pk:
pks.remove(_pk)
result.append({"user": p, "poker": pk})
return result
# 牌型判定与打分
def calculate(_poker_lst: dict, pk_lst: list) -> tuple:
n_lst = list(map(lambda x: _poker_lst[x], pk_lst)) # 点数映射
same_suit = len(set(pk[:1] for pk in pk_lst)) == 1 # 同花色
continuity = sorted(n_lst) == list(range(min(n_lst), max(n_lst)+1)) \
or set(n_lst) == {14, 2, 3} # 连续(含 A23 特殊顺)
check = len(set(n_lst))
sort = sorted(n_lst); middle = sort[1]
max_pk, min_pk = max(sort), min(sort)
if check == 1: return n_lst[0]+5200, "豹子"
elif check == 2:
w = [i for i in n_lst if n_lst.count(i) == 2][0]
single = [i for i in n_lst if i != w][0]
return w*2*14 + single + 1000, "对子"
elif continuity and same_suit:
return sum(n_lst)*30+4000, "同花顺"
elif same_suit: return max_pk*15 + middle*2 + min_pk + 3200, "同花"
elif continuity: return sum(n_lst)*30+2000, "顺子"
else: return max_pk*15 + middle*2 + min_pk, "单张"
def begin(_poker_lst, _players):
pokers = list(_poker_lst.keys())
poker_grp = deal_card(_players, pokers)
for p in poker_grp:
p["score"], p["type"] = calculate(_poker_lst, p["poker"])
print("玩家:{},手牌:{},牌型:{}".format(p['user'], p['poker'], p['type']))
winner = max(poker_grp, key=lambda x: x["score"])
print("赢家是:玩家{},{}最大".format(winner["user"], winner["type"]))
def start_game(_poker_lst, _players, freq=100):
type_lst = []
for i in range(freq):
grp = deal_card(_players, list(_poker_lst.keys()))
for p in grp:
_, t = calculate(_poker_lst, p["poker"])
type_lst.append(t)
c = Counter(type_lst)
total = sum(c.values())
for item in c.items():
print(f"{item[0]}频率:{item[1]/total:.2%}")
if __name__ == '__main__':
players = [f"p{i}" for i in range(1, 6)]
start_game(creat_poker_dic(), players, freq=50)
50 局统计(与概率论对得上):
单张 62.00% 对子 16.00% 同花 12.00% 顺子 8.00% 同花顺 2.00%
股票查询脚本
历时两天写完的命令行股票查询系统:代码精确查、名称模糊查(re.search)、条件筛选(当前价>50、市值>200亿、涨跌幅<20%),数据从本地 txt 读取。核心:
import re
def creat_lst(file_path):
with open(file_path, 'r+', encoding='utf-8') as f:
b = [i.strip() for i in f]
gp_lst = [b[i].split(',') for i in range(1, len(b))]
title_lst = b[0].split(',')
name_lst = [b[i].split(',')[1] for i in range(1, len(b))]
return title_lst, gp_lst, name_lst
def find(title_type, _num):
"""解析 '当前价>50' / '市值>200亿' / '涨跌幅<20%' 这类条件表达式"""
cond = query.replace(title_type, '').replace('市盈率', '').replace('TTM', '')
if cond[-1] == '%': num = float(cond[1:-1]) / 100
elif cond[-1] == '万': num = float(cond[1:-1])
elif cond[-1] == '亿': num = float(cond[1:-1]) * 10000
else: num = float(cond[1:])
show = []
for i in range(len(gp_lst)):
check_info = normalize(gp_lst[i][_num]) # 同样的单位归一化
if cond[0] == '>' and check_info > num: show.append(i)
elif cond[0] == '<' and check_info < num: show.append(i)
for i in show: show_result(i)
if __name__ == '__main__':
title_lst, gp_lst, name_lst = creat_lst('股票信息.txt')
while 1:
query = input('股票查询接口:')
if query == 'quit': break
elif query in ('h', 'help'): show_help()
elif 'SZ' in query or 'SH' in query: ... # 代码精确查
else: ... # 名称模糊 / 条件查
查询效果:
股票查询接口:股份
股票代码 股票名称 当前价 涨跌额 涨跌幅 ...
SH603348 文灿股份 18.06 +1.64 +9.99% ...
当时给自己列的掌握度清单:list 80% / dict 50% / str 90% / 异常处理 10% / 文件处理 70%。
回头看:那个 %/万/亿 的单位归一化条件解析,其实就是后来写各种 DSL 的最小原型。
100 练习(经典 20 题,上下两篇)
Day1 精选:
无重复数字的三位数(1、2、3、4 组成):
for i in range(1,5):
for j in range(1,5):
for k in range(1,5):
if i != j and j != k and i != k:
print(i, j, k) # 共 24 个
利润分段提成:
bonus = int(input('请输入利润'))
if 100000 >= bonus > 0:
print(f"奖金为{bonus*0.1}")
elif bonus > 100000:
print(f"奖金为{(bonus-100000)*0.075+10000}")
完全平方数对(+100 与 +268 都是平方数):
from math import sqrt
for i in range(100000):
if sqrt(i+100) == int(sqrt(i+100)) and sqrt(i+268) == int(sqrt(i+268)):
print(i) # 21, 261, 1581
判断某天是当年第几天(预累积月份表 + 闰年加一天)、三数排序(a.sort())、9×9 乘法表:
for i in range(1,10):
for j in range(1,10):
print(f'{i}*{j}={i*j}', end=' ')
print()
以及国际象棋棋盘((i+j)%2 控制黑白格)、金字塔楼梯等图形打印题。
Day2 精选:
斐波那契兔子问题:
a, b = 1, 1
valu = []
for i in range(3): # month = 6
valu.append(a); valu.append(b)
a = a + b; b = a + b
print(f'{valu[5]*2}只') # 16只
素数筛选(101-200):
from math import sqrt
a = []
for i in range(101, 201):
pic = 0
for j in range(2, int(sqrt(i))+1):
if i % j == 0: pic = 1
if pic == 0: a.append(i)
# [101, 103, ..., 199] 共 21 个
水仙花数:
for i in range(100, 1000):
a, b, c = int(str(i)[0]), int(str(i)[1]), int(str(i)[2])
if a**3 + b**3 + c**3 == i: print(i) # 153, 370, 371, 407
分解质因数(90 = 2×3×3×5):
num = 90
a = []
def cut(n):
for i in range(2, n+1):
if n % i == 0 and n > i: return i
elif n == i: return 0
while 1:
i = cut(num)
if i: a.append(i); num = int(num/i)
else: break
a.append(num)
完数(1000 以内:[6, 28, 496])、条件表达式判等级(A/B/C)、字符分类统计(isalpha/isspace/isdigit)、$a+aa+aaa+\cdots$ 数列求和、小球反弹(第 10 次落地共经过 199.9 米,反弹 0.098 米)等。
第三阶段:往真实世界走(2022.07–10)
MySQL 基础操作
数据库/表/行的增删改查速查(当时的完整笔记):
-- 数据库
create database day25db DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
drop database 数据库名;
use 数据库名; show tables;
-- 建表(从简单到完整约束)
create table tb5(
id int not null auto_increment primary key, -- 主键 + 自增
name varchar(16) not null,
email varchar(32) null,
age int default 3
) default charset=utf8;
-- 改表
alter table 表名 add 列名 类型;
alter table 表名 drop column 列名;
alter table 表名 modify column 列名 新类型;
alter table 表名 change 原列名 新列名 新类型;
-- 行操作
insert into tb1(name,password) values('武沛齐','123123'),('alex','123');
delete from tb1 where name="wupeiqi" and password="123";
update tb1 set age=age+1 where id=2;
update L3 set name=concat(name,"123") where id=2; -- concat 字符串拼接
select id, name as N, age from tb1 where id > 1;
-- 清空表:truncate 快但不可回滚
truncate table 表名;
回头看:一个月前刚被一键卸载脚本误删过 MySQL,这篇就是重装后的第一轮系统学习——事故直接转化成了学习动力。
并发编程:进程、守护进程、锁
同步 vs 异步——三个网址请求从串行 6 秒到并行 2 秒:
from multiprocessing import Process
import time
def get_request(url):
print('正在请求:', url); time.sleep(2); print('请求结束:', url)
if __name__ == "__main__":
start = time.time()
p_list = []
for url in ['www.1.com', 'www.2.com', 'www.3.com']:
p = Process(target=get_request, args=(url,))
p_list.append(p)
p.start()
for pp in p_list:
pp.join() # 主进程等所有子进程结束
print('总耗时:', time.time() - start) # ≈2s
进程数据隔离——子进程改全局变量,主进程看不到:
ticketNum = 10
def func(num):
global ticketNum
ticketNum -= num
# 主进程 join 后打印 ticketNum 依然是 10
# 进程之间完全独立,各有各的内存空间——数据不共享
进程创建的第二种方式(继承 Process 重写 run)与守护进程:
p.daemon = True # 必须在 start() 之前;主进程代码结束,守护进程随即终止
进程锁——经典抢票案例,三个进程同时读写 db.txt 会超卖,加锁解决:
from multiprocessing import Process, Lock
import time, json
def task(lock):
lock.acquire() # 上锁
search() # 查余票
get() # 抢票(读改写 db.txt)
lock.release() # 解锁
lock = Lock()
for i in range(3):
Process(target=task, args=(lock,)).start()
加锁的代价就是失去异步效果——当时笔记里加粗的结论,至今是并发问题的第一直觉。
Scrapy 重写 ImagesPipeline
爬虫图片管道定制的标准三步:①爬虫文件提取图片链接封装进 item;②自定义管道继承 ImagesPipeline 重写三个方法;③配置开启管道并指定 IMAGES_STORE 目录。
import scrapy
from scrapy.pipelines.images import ImagesPipeline
class mediaPileline(ImagesPipeline):
# 对图片地址发起请求,返回二进制数据
def get_media_requests(self, item, info):
yield scrapy.Request(item['src'])
# 指定保存的文件名
def file_path(self, request, response=None, info=None, *, item=None):
img_name = request.url.split('/')[-1]
print(img_name, '下载保存成功!')
return f'{img_name}.jpg'
# 把 item 传给下一个管道(没有下一个可不写)
def item_completed(self, results, item, info):
return item
回头看
这条路径复盘下来就是四级台阶:工具链 → 基础语法(作业驱动)→ 小项目(自己真的想用的东西)→ 真实世界(数据库/并发/爬虫)。
如果正在入门,几条真心话:
先搭笔记流水线——Markdown + 自动同步这一步,让后面每一分努力都留下复利
语法靠作业——写一遍比看十遍有效,错题比正确答案有价值
尽早做「自己想用」的东西——车牌选号器、股票查询都不是作业,是自己想要的
事故是最好的老师——被卸载脚本误删 MySQL,换来一轮系统性数据库学习
爬虫和数据库是通往真实数据的门——顺着这扇门,后来我走到了数据分析和如今的大模型应用方向(RAG)。起点,就是这 19 篇笨拙但完整的作业。
- 感谢你赐予我前进的力量

