本文合并自 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,与整数同存会「谁先添加留下谁」。

容器对比表(笔记原表):

类型

可变

有序

元素要求

可哈希

空定义

list

是

是

无

否

v=[] 或 list()

tuple

否

是

无

是

v=() 或 tuple()

set

是

否

可哈希

否

v=set()

经典练习——列表去重:

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

回头看

这条路径复盘下来就是四级台阶:工具链 → 基础语法(作业驱动)→ 小项目(自己真的想用的东西)→ 真实世界(数据库/并发/爬虫)。

如果正在入门,几条真心话:

  1. 先搭笔记流水线——Markdown + 自动同步这一步,让后面每一分努力都留下复利

  2. 语法靠作业——写一遍比看十遍有效,错题比正确答案有价值

  3. 尽早做「自己想用」的东西——车牌选号器、股票查询都不是作业,是自己想要的

  4. 事故是最好的老师——被卸载脚本误删 MySQL,换来一轮系统性数据库学习

  5. 爬虫和数据库是通往真实数据的门——顺着这扇门,后来我走到了数据分析和如今的大模型应用方向(RAG)。起点,就是这 19 篇笨拙但完整的作业。