跳转至

博客

所有权 1 所有者、移动、克隆

一、栈和堆

在程序运行时可供使用的内存包含 栈区堆区,对应着两种不同的数据结构 ,他们的特性不同,存储的数据也不同。

栈区 中存储的数据必须是 占用已知且固定的大小 的,而 堆区 中存储的数据是 编译时大小未知或可能变化 的。

使用 OhMyPosh 美化 PowerShell

官网:Home | Oh My Posh

Github:JanDeDobbeleer/oh-my-posh: The most customisable and low-latency cross platform/shell prompt renderer (github.com)

image-20230621210715832

「可定制性最强、延迟最低的跨平台 / Shell 提示符渲染器」

懒得写介绍了,简单来说,就是把你的命令行 / shell 变好看。

安装

1. Oh My Posh 本体

可以通过 Scoop / Winget 来安装,当然也可以通过一行 Powershell 命令下载官方的安装脚本并执行手动安装,这里选择使用 Scoop 安装:

PowerShell
scoop install https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/oh-my-posh.json

上面的命令会安装两样东西:

  • oh-my-posh.exe 可执行文件
  • themes 最新的 Oh My Posh 主题

可以在 POSH_THEMES_PATH 环境变量对应的文件夹中找到主题,

2. 字体

Nerd Fonts 官网:Nerd Fonts - Iconic font aggregator, glyphs/icons collection, & fonts patcher

Github:ryanoasis/nerd-fonts: Iconic font aggregator, collection, & patcher. 3,600+ icons, 50+ patched fonts: Hack, Source Code Pro, more. Glyph collections: Font Awesome, Material Design Icons, Octicons, & more (github.com)

image-20230621211655960

Oh My Posh 显示图标需要 Nerd Fonts 字体,所以这一步我们需要安装一款 Nerd Fonts 字体并设置终端使用它。Nerd Fonts 字体并不是一个具体的字体,而是一系列字体。其中的每一个字体都可以看作是 某一个流行字体 + 一系列图标字体(如上图)。同时,Nerd Fonts 中大部分经过修改后的字体会拥有一个新的名字,但是和原本的名字很像,比如很知名的 SourceCodePro,在 Nerd Fonts 中经过“改造后”就被称为 SauceCodePro Nerd Font,很有趣。

你可以选择手动从官网下载并安装,不过现在 Oh My Posh 命令可以直接安装:

Text Only
sudo oh-my-posh font install

这里 sudo 是通过 scoop 安装的,用处就是以管理员权限执行该命令。如果没有 sudo 就在有管理员权限的终端下执行命令。

当然也可以制动 --user 来不用管理员权限,仅安装到当前用户下,不过对于一些应用可能会有一些副作用,字体还是安装到整个系统而非某个用户比较好。

不过我这里依旧选择使用 scoop,主要是方便更新之类的:

Text Only
sudo scoop install -g nerd-fonts/JetBrainsMono-NF-Mono

然后在 Windows Terminal 中设置使用它:

image-20230621212703076

3. 修改 prompt

针对不同的 shell 有不同的方式,这里只写 powershell 的方式,详情参考官方文档:Change your prompt | Oh My Posh 如果不知道使用的是哪个 shell 可以运行:

PowerShell
oh-my-posh get shell

编辑 PowerShell 的 profile 脚本(你可以使用任意的编辑器,这里我就用 neovim 了):

PowerShell
nvim $PROFILE

添加下面一行:

PowerShell
oh-my-posh init pwsh | Invoke-Expression

然后通过下面的命令来重新加载 profile:

PowerShell
. $PROFILE

可以看到效果已经生效了:

image-20230621214422522

二、配置

如果不指定配置,Oh My Posh 会使用其默认配置,若想指定配置则需要修改 profile 中的初始化命令添加 --config 选项:

可以添加两种 --config 选项:

  • 到一个本地配置文件的路径
PowerShell
oh-my-posh init pwsh --config 'C:/Users/Posh/jandedobbeleer.omp.json' | Invoke-Expression
  • 到一个远端配置文件的 URL
PowerShell
oh-my-posh init pwsh --config 'https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/jandedobbeleer.omp.json' | Invoke-Expression

我们首先先使用一些 Oh My Posh 中的 themes 中带有的配置:

使用 Get-PoshThemes 可以在 powershell 中渲染出每一个主题的样子。

比如我们切换使用 catppuccin 主题:

catppuccin

PowerShell
oh-my-posh init pwsh --config "$env:POSH_THEMES_PATH/catppuccin.omp.json" | Invoke-Expression

有关更详细的自定义,参阅文档:General | Oh My Posh

动态规划

一、引子1 —— 斐波那契数列

1. 递归

斐波那契数列的定义如下:

\[ f_n = \begin{cases} 1, & n = 1, 2\\ f_{n-1} + f_{n-2}, &n > 2 \end{cases} \]

根据这个我们不难写出 递归 方式求解斐波那契数列的代码:

\[ \begin{array}{l} \text{Fibonacci-Recursive}(n)\\ \begin{array}{ll} 1 & \textbf{if } n < 2 \\ 2 & \qquad\textbf{return } 1 \\ 3 & \textbf{else} \\ 4 & \qquad\textbf{return } \text{Fibonacci-Recursive}(n-1) + \text{Fibonacci-Recursive}(n-2) \end{array} \end{array} \]

然而通过简单的实验,可以发现这种方式效率极低:

image-20230604165338430

其时间复杂度 \(T(n) = T(n-1) + T(n-2)\),可以推导出为 \(O(2^n)\) 的。造成如此的效率低下的原因就是 进行了太多次重复的运算。如果我们将递归求解的过程化成一棵树,我们可以发现其中有太多太多重复的部分:

image-20230604165544487

2. 递推

另一种方法是以递推的方式进行求解:

\[ \begin{array}{l} \text{Fibonacci-Iteration}(n)\\ \begin{array}{ll} 1 & F_1 \leftarrow 1, F_2 \leftarrow 1\\ 2 & \textbf{for } i \leftarrow 3 \textbf{ to } n \\ 3 & \qquad F_i \leftarrow F_{i-1} + F_{i-2}\\ 4 & \textbf{return } F_n \end{array} \end{array} \]

如此其时间复杂度是线性的,能够解决上述递归方式重复计算子问题导致效率低下的问题。

3. 记忆化搜索

另外还有一种方式,通过对递归方式加一点修改也可以解决这个问题。那就是 记忆化搜索,将搜索过程中的结果保存下来,并在之后的搜索过程中加以利用:

\[ \begin{array}{l} \text{Fibonacci-Recursive-Memorized}(n)\\ \begin{array}{ll} 1 & \textbf{if } F_i = NIL\\ 2 & \qquad\textbf{if } n < 2 \\ 3 & \qquad\qquad F_i \leftarrow 1 \\ 4 & \qquad\textbf{else} \\ 5 & \qquad\qquad F_i \leftarrow \text{Fibonacci}(n-1) + \text{Fibonacci}(n-2) \\ 6 & \textbf{return } F_i \end{array} \end{array} \]

二、引子2 —— 找零问题

1. 递归(暴力搜索)

目标:给定面值分别为1元、5元、10元的硬币(每种都有足够多枚),请设计一个算法,可以使用最少数量的硬币向客 户支付给定金额。

  • 此时,贪婪策略(即收银员算法)是有效的

如果还有面值为7元的硬币,那么贪婪算法可能就无法得到最优解了

  • 例如凑出19元,贪婪算法需要使用4枚硬币(10+7+1+1),但事实上只需要3枚硬币(7+7+5)

对于给定的硬币组,如何找到凑成给定金额 n 的最小硬币数?

每一次选择必定要从 1、5、7、10 中选一个,于是可以据此将选择分为四种情形:

  • 给一枚面值为 1 元的硬币,此时仍需给顾客 n-1 元
  • 给一枚面值为 5 元的硬币,此时仍需给顾客 n-5 元
  • 给一枚面值为 7 元的硬币,此时仍需给顾客 n-7 元
  • 给一枚面值为 10 元的硬币,此时仍需给顾客 n-10 元

而为了保证最终的金额最小,上面的 n-1、n-5、n-7、n-10 要以最优方式拼凑出来。

分别计算以上情形后选择一个最佳方案即可。

\[ \begin{array}{l} \text{Find-Recursive}(n)\\ \begin{array}{ll} 1 & \textbf{if } n = 0 \textbf{ then return } 0\\ 2 & \textbf{if } n < 0 \textbf{ then return } \infty\\ 3 & \textbf{return } \min\{\\ & \qquad \text{Find-Recursive}(n-1) + 1,\\ & \qquad \text{Find-Recursive}(n-5) + 1,\\ & \qquad \text{Find-Recursive}(n-7) + 1,\\ & \qquad \text{Find-Recursive}(n-10) + 1\\ & \} \end{array} \end{array} \]

但是。。这不就是暴力搜索么。

假如将问题一般化,对于目标数值 \(n\)\(m\) 种面值分别为 \(d_1, d_2, \cdots, d_m\) 的硬币,当 \(n\)\(m\) 更时,这种算法的效率将极其低下,原因依旧是太多的 重复计算

我们能不能依旧按照 一 中的思路,使用递推来降低时间复杂度呢?

2. 递推(“动态规划”)

可以发现,我们要想求解 \(n\) 元的问题,就要先求解 \(n-d_1, n-d_2, \cdots, n-d_m\) 元的问题,以此类推直到求解 \(d_1\) 元的问题,此时只有一个选项,就是选它。

那么我们可以换一种思路,从 \(d_1\) 元的问题开始,利用已知的信息逐步推演得到 \(d_1 + d_1, d_1 + d_2, \cdots d_1 + d_m\) 元的问题的答案,以此类推利用已知的信息推演出所有最优情况,最终确定 \(n\) 元的问题的答案:

\[ \begin{array}{l} \text{Find-Iteration}(n)\\ \begin{array}{ll} 1 & F_0 \leftarrow 0\\ 2 & \textbf{for } i \leftarrow 1 \textbf{ to } n\\ 3 & \qquad\textbf{for } j \leftarrow 1 \textbf{ to } m\\ 4 & \qquad\qquad\textbf{if } i - d_j > 0\\ 5 & \qquad\qquad\qquad F_i \leftarrow \min(F_i, F_{i-d_j} + 1)\\ 6 & \textbf{return } F_n \end{array} \end{array} \]

三、动态规划(咕)

四、最长上升子序列(咕)

五、0-1 背包问题(咕)

六、最长公共子序列(咕)

七、最短公共超序列(咕)

八、序列对齐(咕)

九、矩阵链乘积

1. 矩阵乘法

首先先回顾一下矩阵的乘法:

\[ \begin{pmatrix} \colorbox{#aaffff}{$a_{11}$} & \colorbox{#aaffff}{$a_{12}$}\\ a_{21} & a_{22}\\ a_{31} & a_{32}\\ \end{pmatrix} \times \begin{pmatrix} \colorbox{#aaffff}{$b_{11}$} & b_{12} & b_{13}\\ \colorbox{#aaffff}{$b_{21}$} & b_{22} & b_{23}\\ \end{pmatrix} = \begin{pmatrix} \colorbox{#aaffff}{$a_{11}b_{11} + a_{12}b_{21}$} & a_{11}b_{12} + a_{12}b_{22} & a_{11}b_{13} + a_{12}b_{23}\\ a_{21}b_{11} + a_{22}b_{21} & a_{21}b_{12} + a_{22}b_{22} & a_{21}b_{13} + a_{22}b_{23}\\ a_{31}b_{11} + a_{32}b_{21} & a_{31}b_{12} + a_{32}b_{22} & a_{31}b_{13} + a_{32}b_{23}\\ \end{pmatrix} \]

可以写作:

\[ c_{i,j} = \sum_{k = 1}^q a_{i,k}b_{k,j} \]

很容易可以写出其伪代码:

\[ \begin{array}{l} \text{Matrix-Multiply}(A_{p \times q}, B_{q \times r})\\ \textbf{Notes. }A, B, C \text{ are matrix, and } a_{i,j}, b_{i, j}, c_{i, j} \text{ are the elements in the matrix}\\ \begin{array}{ll} 1 & \textbf{for } i \leftarrow 1 \textbf{ to } p\\ 2 & \qquad\textbf{for } j \leftarrow 1 \textbf{ to } r\\ 3 & \qquad\qquad c_{i, j} \leftarrow 0\\ 4 & \qquad\qquad\textbf{for } k \leftarrow 1 \textbf{ to } q\\ 5 & \qquad\qquad\qquad c_{i, j} \leftarrow c_{i,j} + a_{i, k} \times b_{k, j}\\ 6 & \textbf{return } C_{p \times r} \end{array} \end{array} \]

2. 矩阵链乘积

当对多个矩阵进行链式乘积时,以何种顺序进行运算就成为了一个影响性能的至关重要的问题。

例如我们有这样三个矩阵:\(A_{10 \times 100}, B_{100 \times 5}, C_{5 \times 50}\)

两种截然不同的方式将导致完全不同的元素乘法次数:

\((AB)C = D_{10 \times 5} \cdot C_{5 \times 50}\):共 \(10 \cdot 100 \cdot 5 + 10 \cdot 5 \cdot 50 = 7,500\) 次元素乘法

\(A(BC) = A_{10 \times 100} \cdot E_{100 \times 50}\):共 \(10 \cdot 100 \cdot 50 + 100 \cdot 5 \cdot 50 = 75,000\) 次元素乘法

矩阵链乘积问题如下:

对于给定矩阵序列 \(A_1, A_2, \cdots, A_n\),其中 \(A_i\) 的阶数为 \(P_{i-1} \times P_{i}\),试确定矩阵相乘的次序使得元素乘法总次数最少。

每一次乘法相当于将整个矩阵序列划分为两部分,将相邻处的两个矩阵“合并”: $$ A_{i..j} = A_{i..k} \times A_{k+1..j} $$ 若令 \(F_{i,j}\) 表示 \(A_{i..j}\) 中元素乘法的次数,那么可以知道对于任意的 \(k\) 有: $$ F_{i,j} = F_{i,k} + P_{i-1} \cdot P_{k} \cdot P_{j} + F_{k+1,j} $$ 那么对于某一个 \(i, j\),只要得出对于任意的 \(i \leq k < j\) 的元素乘法次数,再求最小值即可。同时也要使 $F_{i, k} $ 和 \(F_{k+1, j}\) 最小。

现在我们直接列出其递推式:

\[ F_{i, j} = \begin{cases} 0, &i = j\\ \min_{i \leq k < j} \{ F_{i, k} + P_{i-1}P_kP_j + F_{k+1, j}\}, & i < j \end{cases} \]

伪代码如下:

\[ \begin{array}{l} \text{Matrix-Chain-Order}\\ \textbf{Input. } P_0, P_1, \cdots, P_n\\ \textbf{Output. } \text{ cost table } M \text{ and divide table } S\\ \textbf{Notes. } \text{m, s are the elements of M and S}\\ \begin{array}{ll} 1 & \textbf{for } i \leftarrow 1 \textbf{ to } n\\ 2 & \qquad m_{i, i} \leftarrow 0, s_{i, i} \leftarrow 0\\ 3 & \textbf{for } l \leftarrow 2 \textbf{ to } n\\ 4 & \qquad\textbf{for } i \leftarrow 1 \textbf{ to } n - l + 1\\ 5 & \qquad\qquad j \leftarrow i + l - 1\\ 6 & \qquad\qquad m_{i, j} \leftarrow \infty\\ 7 & \qquad\qquad\textbf{for } k \leftarrow i \textbf{ to } j-1\\ 8 & \qquad\qquad\qquad q \leftarrow m_{i, k} + P_{i-1} \times P_k \times P_j + m_{k+1, j}\\ 9 & \qquad\qquad\qquad \textbf{if } q < m_{i, j}\\ 10 & \qquad\qquad\qquad\qquad m_{i, j} \leftarrow q\\ 11 & \qquad\qquad\qquad\qquad s_{i, j} \leftarrow k\\ 12 & \textbf{return } M, S \end{array} \end{array} \]

十、跳棋棋盘(咕)

PostgreSQL Trigger

Trigger 其实就是一个数据库在特定操作后自动执行的一种函数。

它可以被连接到表和视图。

在表上,触发器 可以被定义为在任何 INSERTUPDATEDELETE 操作 之前之后 执行,且可以分为 对每条语句执行一次 还是 对每一个被修改的行执行一次

对于 UPDATE 操作,还可以指定 特定的行被更新后 执行。

视图 上,触发器 可以被定义为 代替 任何 INSERT, UPDATE, DELETE 操作执行。

触发器可以分为 语句级 的和 行级 的:

  • 语句级

BEFORE 触发器中无法访问语句产生的改动

AFTER 触发器中可以访问语句产生的所有改动

BEFORE 触发器无法访问语句所产生的改动(因为语句还未执行),而 AFTER 触发器可以访问语句所产生的所有改动。

语句

PostgreSQL SQL Dialect
1
2
3
4
5
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER /*name*/ { BEFORE | AFTER | INSTEAD OF } { /*event*/ [ OR ... ] }
    ON /*table_name*/
    [ FOR [ EACH ] { ROW | STATEMENT } ]
    [ WHEN ( /*condition*/ ) ]
    EXECUTE { FUNCTION | PROCEDURE } /*function_name*/ ( /*arguments*/ )

其中 event 可以是 INSERTUPDATE [ OF /*column_name*/ [, ...] ]DELETE

数据变更触发器

data change trigger 是一个满足以下条件的函数:

  • 无参数

  • 返回类型为 trigger

在这样的触发器中有一些特殊的变量可以访问:

  • NEW

类型为 RECORD,在 行级 触发器中。

其值为 INSERT/UPDATE 操作的 的行。

  • OLD

类型为 RECORD,在 行级 触发器中。

其值为 INSERT/UPDATE 操作的 的行。

人工智能基础项目 1.环境配置

首先需要了解的东西

假设你了解基本的 Python 语言知识,并理解 Python 包的管理方式。

以下内容需要进行了解:

  1. Jupyter Notebook

Jupyter Notebook 顾名思义,是一个笔记应用,它编辑的笔记后缀为 .ipynb,笔记的内容以块为单位组织,块分为 Markdown 内容块和 Python 代码块(其实也支持一些其他数据科学常用的语言),其中的每一个块都可以执行,Markdown 内容快执行的效果即进行渲染,代码块执行的效果即将运行的输出以及最后一条语句的值显示出来。

但说他是个应用,其实不完全是,因为它其实是一个 Web 服务,分为前后端。前端网页负责提供笔记的界面并与后端沟通,而后端 Jupyter 内核负责与 Python 解析器交流。

可以达到下面的效果:

image-20230531102957369

  1. Anaconda、conda、pip、venv/virtual env

Anaconda 是一个 Python 发行版本,其中包含 Python 本体、conda 以及 180+ 个和数据科学有关的 Python 包。他还有另一个精简版本叫做 Miniconda,可以理解为不带有那 180+ 个 Python 包,使得用户可以选择只安装自己需要的包,减小了占用的空间大小。

pip 是 python 用于管理依赖的工具,比如我要安装一个名为 matplotlib 的包,我只需执行 pip install matplotlib

venvvirtual env 都是用于创建独立的 Python 环境的工具,前者是 Python3 标准库自带的,而后者是一个单独的 Python 包。有时我们写的不同项目会用到不同版本的 Python,甚至会用到不同版本的同一个包,此时独立的环境就极为重要。

conda 是一个用于管理依赖和环境的工具,可以理解为对 pipvenv 的组合,使用起来更加便捷。

一、安装

DataSpell

前往 Jetbrains 官网:JetBrains: 软件开发者和团队的必备工具

image-20230531102328219

安装完成后打开会有这样一个界面:

image-20230531104353736

这里要求你对默认的环境进行配置,其中:

  • Type 为环境的类型,这里选择使用 Conda(接下来会说怎么安装 Conda)

image-20230531104426715

  • Conda Executable 为 Conda 可执行文件的路径
  • Conda environment 为使用的 Conda 环境,填完 Conda Executable 后应当会自动显示一个默认的环境。

Conda

可以选择去官网下载安装,但是这里我选择使用 Scoop 安装。

有关 Scoop 见 aoike - 告别繁琐安装界面,使用Scoop管理Windows软件 (azurice.github.io)

image-20230531104811410

Text Only
scoop install extras/miniconda3

然后在刚才 DataSpell 的 Conda Executable 中选择这个文件:

image-20230531105154954

Launch DataSpell!

二、基本使用

进去后,就是经典的 Jetbrains IDE 的布局。

但是它并不是按照 项目(Project)来组织的而是按照 工作区(Workspace)来组织的:

image-20230531105329818

每一个工作区中可以包含多个目录,目录可以通过按钮来进行添加。也就是说它是连接到各个位置的目录,而非将一切放到一个目录下。

比如我这里添加了一个路径为 F:\Dev\AI 的目录:

image-20230531105625426

你可以为每一个目录选择不同的解释器:

image-20230531105649721

不过目前我们不用管他。

创建一个 Jupyter Notebook 文件:

image-20230531105720278

创建完毕后打开,它会提示你没有安装 Jupyter,点击安装,安装完成后等待 Updating skeletons 完成,即可:

image-20230531105746209

image-20230531110226463

至于为什么用 DataSpell 而不直接用 Jupyter 呢?因为 DataSpell 有代码补全:

image-20230531110354194

这些个按钮自己理解一下,这个代码块的运行效果如下:

image-20230531110459955

三、更多例子

Python
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme(style="dark")

# Simulate data from a bivariate Gaussian
n = 10000
mean = [0, 0]
cov = [(2, .4), (.4, .2)]
rng = np.random.RandomState(0)
x, y = rng.multivariate_normal(mean, cov, n).T

# Draw a combo histogram and scatterplot with density contours
f, ax = plt.subplots(figsize=(6, 6))
sns.scatterplot(x=x, y=y, s=5, color=".15")
sns.histplot(x=x, y=y, bins=50, pthresh=.1, cmap="mako")
sns.kdeplot(x=x, y=y, levels=5, color="w", linewidths=1)

提示没有安装库可直接将鼠标移到红线上后点击浮动框中的 Install,或者手动执行:

Text Only
conda install numpy seaborn matplotlib

image-20230531110846059

项目要求

要求:参考三个选题(手写体数字识别、人脸识别、猫狗识别),可以结合大创的项目。不可以选择百度 AIStudio 上原封不动的项目,必须有所修改,或者修改模型结构,或者修改数据集。否则,20 分的项目,最多得 10 分。

项目内容

手写体数字识别属于典型的图像多分类问题

实践平台:百度AI实训平台-AI Studio

实践流程

  • 准备数据

  • 配置网络

  • 定义网络
  • 定义损失函数
  • 定义优化算法
  • 训练网络
  • 模型评估
  • 模型预测

实验要求

  1. 按实践流程调通程序

  2. 采用MNIST数据集,原数据集中只有黑底白字,在原数据集中增加白底黑字的数字图像,即:使得系统也能识别白底黑字的数字图像。

  3. 撰写实验报告

  4. 将调通的程序发布到百度AI实训平台-AI Studio上,将实验报告提交到指定的助教邮箱。

评分

实验报告评分标准(10分):每组组员的分数相同

  1. 实验最终准确率(占2分):按准确率高低的排序给分。

  2. 实验尝试的模型(占3分):使用2个及以上经典卷积网络(如VGG等)的,得3分,使用1个经典CNN的得2分,未使用经典CNN的得1分。

  3. 实验调参(占2分):对学习率、优化器、模型的深度和宽度(隐层中神经元的数目)、激活函数等,进行过10次及以上尝试者,得2分;只进行少于10次尝试者得1—1.5分。

  4. 实验数据分析(占3分):以图表的形式展示实验结果,在此基础上进行深入讨论者,得3分;讨论内容肤浅者,得1--2分。

2. 项目展示与答辩评分标准(10分)

总分 最好的 accuracy(或 Precision 和 Recall) 演示功能正确 超时 阐述清晰 回答问题正确
10 2 3 2 2 1

其中,超时不包括提问环节,超时和最好的 accuracy(或 Precision 和 Recall)全组统一。

  1. 每组答辩时长 15 分钟,不需要制作 PPT,每人陈述 3 分钟,即自述 12 分钟。组员分工介绍以下内容:

  2. 项目思路

  3. 数据集规模,训练集、(验证集)、测试集的样本分配

  4. 模型的网络结构:几层卷积层、几个卷积、卷积大小、激活函数、损失函数、池化层、全连接层、输出层几个神经元

  5. 超参个数、如何设置的(学习率等)
  6. 训练模型的过程、训练时长
  7. 是否修改了网络结构,怎么修改的
  8. 尝试了几个模型,对比结果的结论是什么
  9. 展示运行结果

  10. 回答问题 3 分钟(覆盖课堂讲授的理论和实践内容),每人回答一个问题。

  11. 每组组员的得分不同,取决于个人在“阐述清晰”、“回答问题正确”、“演示功能正确”环节的表现。

    • 必须弄清楚 CNN 术语含义,尤其在分组答辩时,要用对术语,否则我的问题,你可能答不到点子上,也会浪费时间。

    • 若连老师所提的问题都听不懂,或弄错术语的含义,直接扣分。

3.专题研究报告提交要求

  1. 将上述专题研究报告的word文档以邮件附件的形式提交给助教(期末公布邮箱 )。
  2. 专题研究报告命名为:AI-组号-题目;邮件主题必须为:AI报告-组号-题目,否则无效。截止日期为6月17日12点。
  3. 提交此报告,不加分,但迟交或不交,扣分。
  4. 迟交1天(晚24小时内)扣2分;
  5. 迟交2天(晚48小时内)扣5分;
  6. 迟交3天(晚72小时内)扣10分;
  7. 3天后(超过72小时)视同未提交,即专题研究环节计0分。

Python 爬虫入门

通过 Python 获取网页数据的方法一般分为两大类:

  • 基于 HTML 正则匹配

  • 基于 API 请求

前者很简单,其实就是字符串匹配,不过缺点就是较为繁琐复杂;后者也很简单,就是伪装浏览器发请求即可。

下面简单讲一下。

零、安装Python

假设你已经安装好 Python。

如何安装可以百度。不过我这里推荐一个工具,叫做 Scoop。它是一个 Windows 下的包管理器,可以在终端通过简单的一行命令完成一些软件包的安装、升级、卸载等,而且可以免掉图形化的安装界面,不必下一步下一步的点,此外也用了特殊的方法来管理环境变量,不必再配置为环境变量发愁。具体可以见 aoike - 告别繁琐安装界面,使用Scoop管理Windows软件 (azurice.github.io)

一、基于 HTML 正则匹配

1. 有关 HTML

众所周知,每一个网页都是一个 .html 文件,一个标准的 HTML 文档的结构大概长这样:

HTML
1
2
3
4
5
6
7
8
<html>
    <head>
        ...
    </head>
    <body>
        ...
    </body>
</html>

其中尖括号扩起来的一个个东西叫做 标签,标签成对出现,如 <sometag></sometag>,当然如果某些标签中不包含任何内容,也会写作 <sometag />

标签可以携带一些属性,比如 <script type="text/javascript"></script>,它有一个值为 "text/javascript"type 属性。

介绍一个重要的网站,上面包含一切 web 技术的文档:MDN Web Docs (mozilla.org)

image-20230526225418855

在一般的浏览器中按 F12 选择 元素 一栏,便可以看到网页整个的 HTML 代码。

也可以通过在目标元素处右键 -> 检查,来快速定位到其对应的 HTML 代码位置。

image-20230526225745841

2. 引入

比如对于这个页面:https://space.bilibili.com/46452693

image-20230526225556972

我想爬取他的关注、粉丝、获赞等信息。

通过 F12 我们发现,这部分对应的代码是这样的:

HTML
<div class="n-statistics">
    <a href="/46452693/fans/follow" class="n-data n-gz" title="1,596">
        <p class="n-data-k">关注数</p>
        <p id="n-gz" class="n-data-v space-attention">1596</p>
    </a>
    <a href="/46452693/fans/fans" class="n-data n-fs" title="80">
        <p class="n-data-k">粉丝数</p>
        <p id="n-fs" class="n-data-v space-fans">80</p>
    </a>
    <div title="视频、动态、专栏累计获赞319" class="n-data n-bf">
        <p class="n-data-k">获赞数</p>
        <p id="n-bf" class="n-data-v">319</p>
    </div>
    <div title="截止昨天,播放数总计为4,271" class="n-data n-bf">
        <p class="n-data-k">播放数</p>
        <p id="n-bf" class="n-data-v">4271</p>
    </div>
    <div title="截止昨天,阅读数总计为275" class="n-data n-bf">
        <p class="n-data-k">阅读数</p>
        <p id="n-bf" class="n-data-v">275</p>
    </div>
</div>

这一部分内容位于一个 classn-statisticsdiv 块中,也就是说只要我们在整篇 html 中找到这一部分,就可以从中分离出我们想要的数据。

但是怎么找?简单的字符串匹配么?

3. 正则表达式

正则表达式可以用于描述一组字符串。

比如 <div class="n-statistics">.*</div> 即可匹配上面的内容。

再进一步,<p id="n-.*>(.*)</p> 即可匹配出五个数据。

详细内容可以再查一查。

可以看看这个:Python 正则表达式 | 菜鸟教程 (runoob.com)

4. 码

简单搓了段码:

Python
import re
import requests

AZURICE = 46452693

def url(id):
    return f'https://space.bilibili.com/{id}'


def get_data(id):
    page = requests.get(url(id)).text
    res = re.findall(r'<div class="n-statistics">.*</div>', page, flags=re.S)

    block = res[0]

    res = re.findall(r'<p id="n-.*>(.*)</p>', block)
    return [int(e) for e in res]


if __name__ == "__main__":
    data = get_data(AZURICE)
    print(data)

但是,直接这样运行并不能得到想要的结果,如果将 page 打印一下会发现只有如下的内容:

HTML
1
2
3
4
5
6
7
<!DOCTYPE html><html><head><title>验证码_哔哩哔哩</title><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,viewport-fit=cover"><meta name="spm_prefix" content="333.1291"><script type="text/javascript" src="//www.bilibili.com/gentleman/polyfill.js?features=Promise%2CObject.assign%2CString.prototype.includes%2CNumber.isNaN"></script>
    <script>
    window._riskdata_ = {
      'v_voucher': 'voucher_bad3755a-dcf8-4544-91d4-87d4f5b09c07'
    }
    </script>
    <script type="text/javascript" src="//s1.hdslb.com/bfs/seed/log/report/log-reporter.js"></script><link href="//s1.hdslb.com/bfs/static/jinkela/risk-captcha/css/risk-captcha.0.4e3ed2119997a8315e1c9a96a1e93f5569d9fb5a.css" rel="stylesheet"></head><body><div id="biliMainHeader"></div><div id="risk-captcha-app"></div><script src="//s1.hdslb.com/bfs/seed/jinkela/risk-captcha-sdk/CaptchaLoader.js"></script><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/risk-captcha/1.risk-captcha.4e3ed2119997a8315e1c9a96a1e93f5569d9fb5a.js"></script><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/risk-captcha/risk-captcha.4e3ed2119997a8315e1c9a96a1e93f5569d9fb5a.js"></script></body></html>

首先,大多数网站都有反爬机制,一种常见的反爬机制就是通过请求的 headers 中的 User-Agent 来判断是否是一个真正的浏览器发送的请求。那么绕过这个机制也很简单,我们将一个真正的浏览器的 headers 中的 User-Agent 设置给 python:

Diff
import re
import requests

+ HEADERS = {
+     "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50"
+ }

AZURICE = 46452693

def url(id):
    return f'https://space.bilibili.com/{id}'


def get_data(id):
-     page = requests.get(url(id)).text
+     page = requests.get(url(id), headers=HEADERS).text
    res = re.findall(r'<div class="n-statistics">.*</div>', page, flags=re.S)

    block = res[0]

    res = re.findall(r'<p id="n-.*>(.*)</p>', block)
    return [int(e) for e in res]


if __name__ == "__main__":
    data = get_data(AZURICE)
    print(data)

现在,确实发现获取到的内容发现了改变,但是依旧不是一个完整的网页:

HTML
<!DOCTYPE html><html><head><meta name="spm_prefix" content="333.999"><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="renderer" content="webkit|ie-comp|ie-stand"><meta name="referrer" content="no-referrer-when-downgrade"><meta name="applicable-device" content="pc"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><script type="text/javascript" src="//s1.hdslb.com/bfs/seed/jinkela/short/config/biliconfig.js"></script><script type="text/javascript">var ua=window.navigator.userAgent,agents=["Android","iPhone","SymbianOS","Windows Phone","iPod"],pathname=/\d+/.exec(window.location.pathname),getCookie=function(e){return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*"+encodeURIComponent(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1"))||null},DedeUserID=getCookie("DedeUserID"),mid=pathname?+pathname[0]:null===DedeUserID?0:+DedeUserID;if(mid<1)window.location.href="https://passport.bilibili.com/login?gourl=https://space.bilibili.com";else{window._bili_space_mid=mid,window._bili_space_mymid=null===DedeUserID?0:+DedeUserID;for(var prefix=/^\/v/.test(pathname)?"/v":"",i=0;i<agents.length;i++)if(-1<ua.indexOf(agents[i])&&!/\sVR\s/g.test(ua)){window.location.href="https://m.bilibili.com/space/"+mid;break}}</script><script type="text/javascript">function getIEVersion(){var e=99;if("Microsoft Internet Explorer"==navigator.appName){var t=navigator.userAgent;null!=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})").exec(t)&&(e=parseFloat(RegExp.$1))}return e}getIEVersion()<11&&(window.location.href="https://www.bilibili.com/blackboard/activity-I7btnS22Z.html")</script><link rel="prefetch" as="script" href="//s1.hdslb.com/bfs/static/player/main/video.js?v=2023525"><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/long/js/sentry/sentry-5.2.1.min.js"></script><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/long/js/sentry/sentry.vue.js"></script><link rel="stylesheet" href="//at.alicdn.com/t/font_438759_d66lkuno6c9.css"><script id="abtest" type="text/javascript">window.abtest={"in_new_ab":true,"ab_version":{},"ab_split_num":{}}</script></body><link href="//s1.hdslb.com/bfs/static/jinkela/space/css/space.9.89c88a9b06d39e34331a447c5eb1e139e95fd3b2.css" rel="stylesheet"><link href="//s1.hdslb.com/bfs/static/jinkela/space/css/space.8.89c88a9b06d39e34331a447c5eb1e139e95fd3b2.css" rel="stylesheet"><title>Azur冰弦的个人空间-Azur冰弦个人主页-哔哩哔哩视频</title><meta name="keywords" content="Azur冰弦的个人空间,Azur冰弦个人主页"/><meta name="description" content="哔哩哔哩Azur冰弦的个人空间,提供Azur冰弦分享的视频、音频、文章、动态、收藏等内容,关注Azur冰弦账 号,第一时间了解UP注动态。这个人不是很懒于是写了一点话。"/><meta name="referrer" content="no-referrer-when-downgrade"><link rel="apple-touch-icon" href="//i0.hdslb.com/bfs/face/ec1b401b2a4caeff3c0de8536294008431ceaec7.jpg"></head><body><div id="biliMainHeader" token-support="true" disable-sticky style="height:56px"></div><div id="space-app"></div><script type="text/javascript">//日志上报
    window.spaceReport = {}
    window.reportConfig = {
      sample: 1,
      scrollTracker: true,
      msgObjects: 'spaceReport'
    }
    var reportScript = document.createElement('script')
    reportScript.src = '//s1.hdslb.com/bfs/seed/log/report/log-reporter.js'
    document.getElementsByTagName('body')[0].appendChild(reportScript)
    reportScript.onerror = function () {
      console.warn('log-reporter.js加载失败,放弃上报')
      var noop = function () { }
      window.reportObserver = {
        sendPV: noop,
        forceCommit: noop
      }
    }

    // webp支持
    function webSupportCheck() {
      const img = new Image()
      img.onload = function () {
        window.supportWebP = (img.width > 0) && (img.height > 0)
      }
      img.onerror = function () {
        window.supportWebP = false
      }
      img.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'
    }
    webSupportCheck()</script><script src="//s1.hdslb.com/bfs/seed/laputa-entry-header/bili-entry-header.umd.js"></script><script>var el=document.getElementById("biliMainHeader"),header=new BiliEntryHeader({config:{headerType:"mini",disableSticky:!0,disableChannelEntry:!1,forceVersion:3,tokenSupport:!0}});header.init(el)</script><script src="//s1.hdslb.com/bfs/static/jinkela/long/js/jquery/jquery1.7.2.min.js"></script><div style="display:none"><a href="https://www.bilibili.com/v/game/match/">赛事库</a> <a href="https://www.bilibili.com/cheese/">课堂</a> <a href="https://www.bilibili.com/festival/2021bnj">2021拜年纪</a></div><script type="text/javascript" src="//s1.hdslb.com/bfs/seed/jinkela/short/auto-append-spmid.js"></script><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/space/9.space.89c88a9b06d39e34331a447c5eb1e139e95fd3b2.js"></script><script type="text/javascript" src="//s1.hdslb.com/bfs/static/jinkela/space/space.89c88a9b06d39e34331a447c5eb1e139e95fd3b2.js"></script></body></html>

这就是另一种反爬机制,一些数据是动态加载或延迟加载的,并不会直接出现在网页上,要想获取完全加载完毕的网页,可能需要借助 Selenium 库(这个一会会提到)。

不过这里为了演示,就直接手动将页面的部分内容赋给了 page

Diff
import re
import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50"
}

AZURICE = 46452693

def url(id):
    return f'https://space.bilibili.com/{id}'


def get_data(id):
-     page = requests.get(url(id), headers=HEADERS).text
+     page = '''
+         一大堆一大堆一大堆东西。。。。。
+     ...
+     asdasdjasdjaksdasldsajd
+     Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
+     <div class="n-statistics">
+     <a href="/46452693/fans/follow" class="n-data n-gz" title="1,596">
+         <p class="n-data-k">关注数</p>
+         <p id="n-gz" class="n-data-v space-attention">1596</p>
+     </a>
+     <a href="/46452693/fans/fans" class="n-data n-fs" title="80">
+         <p class="n-data-k">粉丝数</p>
+         <p id="n-fs" class="n-data-v space-fans">80</p>
+     </a>
+     <div title="视频、动态、专栏累计获赞319" class="n-data n-bf">
+         <p class="n-data-k">获赞数</p>
+         <p id="n-bf" class="n-data-v">319</p>
+     </div>
+     <div title="截止昨天,播放数总计为4,271" class="n-data n-bf">
+         <p class="n-data-k">播放数</p>
+         <p id="n-bf" class="n-data-v">4271</p>
+     </div>
+     <div title="截止昨天,阅读数总计为275" class="n-data n-bf">
+         <p class="n-data-k">阅读数</p>
+         <p id="n-bf" class="n-data-v">275</p>
+     </div>
+ 
+     '''
    res = re.findall(r'<div class="n-statistics">.*</div>', page, flags=re.S)

    block = res[0]

    res = re.findall(r'<p id="n-.*>(.*)</p>', block)
    return [int(e) for e in res]


if __name__ == "__main__":
    data = get_data(AZURICE)
    print(data)

现在就可以得到输出:

Text Only
[1596, 80, 319, 4271, 275]

5. Selenium

刚才提到有很多反爬机制会使得直接对网页的获取并不能得到我们实际在浏览器看到的网页,这时候就需要借助 Selenium 库。

Selenium 是一个浏览器自动化库,可以通过浏览器对网页各个元素进行访问以及操作。具体可以查一查或啃一啃官方文档。

这里给一个我爬取文泉书局电子书的例子:

https://wqbook.wqxuetang.com/read/pdf?bid=2135236

上面这个网页中就包含我想要爬取的 pdf,这个网站做了很多层反爬,一年前我爬大学物理教材的时候它的反爬还没这么厉害(),下面简单讲一下。

首先,第一层反爬,在网页按 F12 无法调出开发者工具。

这是因为网页代码屏蔽了相关的按键。

解决办法:先打开其他网页,调出开发者工具,再修改地址栏回到这个网页即可。

然后我们可以发现,pdf的内容都被显示在 <img> 标签中,而且还是 base64 编码的,这意味着我们直接获取字符串进行解码即可得到图片数据:

image-20230527001646983

但是。。等一下。这并不是真实的页面,这是一张清晰度极低的预览图,而页面真正的图片被纵向切分成了 6 份:

image-20230527001809857

这就意味着,我们需要分辨出这六分的顺序,分别解码对应的图片,再进行拼接才能得到一张完整的页面。

这就是第二层反爬。

第三层反爬,很显然,不用试,这里的数据也是动态加载的,无法直接通过 get 网页地址来获取完整的网页,这就需要我们使用 Selenium 操纵浏览器模拟人的行为一页一页翻页。

第四层,这个输入页码的地方。

image-20230527002104678

它只有在被点击后才会显示出要输入页码的元素:

image-20230527002137350

而且还被折叠在 div 块中,想要将其展开看一看里面的标签长什么样又会由于这个点击使得这个输入页码的地方隐藏。


不过,还是被我爬了(

码如下

Python
import base64
import os
import re
from time import sleep
from random import random, randint

import requests
from pyquery import PyQuery as pq
from loguru import logger
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from tqdm import tqdm

from seleniumwire import webdriver

# import chromedriver_binary  # chrome 76.x

from selenium.webdriver.chrome.options import Options

UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17'  # noqa


def get_chrome_driver(timeout=120, headless=True):
    '''
    start a Selenium Chrome driver

    timeout=120; headless=True
    '''
    chrome_options0 = Options()
    chrome_options0.add_argument(f'user-agent={UA}')
    chrome_options0.add_argument("--headless")

    chrome_options1 = Options()
    chrome_options1.add_argument(f'user-agent={UA}')

    driver_ = ''
    try:
        # driver_ = webdriver.PhantomJS(exe, desired_capabilities=dcap)
        if headless:
            driver_ = webdriver.Chrome(
                service=Service(executable_path='./chromedriver.exe'),
                options=chrome_options0,
            )
        else:
            driver_ = webdriver.Chrome(
                service=Service(executable_path='./chromedriver.exe'),
                options=chrome_options1,
            )
        driver_.set_page_load_timeout(timeout + 2)
    except Exception as exc:
        logger.warning(f"webdriver.Chrome Exception: {exc}")

    return driver_


SAVE_PATH = './算法/'
BID = 2135236
url = 'https://wqbook.wqxuetang.com'
book_url = f'{url}/read/pdf?bid={BID}'
# 大学物理(第三版)上: 3221081
# 大学物理(第三版)下: 3224900
# 算法 2135236


def save_webp_from_site():
    # to rid of the browser, set headless to True
    driver = get_chrome_driver(headless=False)
    assert driver, 'Get chrome driver failed.'

    intercepted_imgs = {}

    def response_interceptor(request, response):
        t = response.headers['Content-Type']
        if t and 'image/webp' in t:
            intercepted_imgs[request.url] = response.body

    driver.response_interceptor = response_interceptor

    driver.get(book_url)

    logger.info("Waiting for login")
    sleep(10)
    logger.info("Starting")
    # class_name = 'page-head-right'  # full screen
    # driver.find_element_by_class_name(class_name).click()

    class_name = 'page-head-tol'
    doc = pq(driver.page_source)
    # Page count
    tol = doc(f'.{class_name}').text()
    total = tol.split('/')
    assert len(total) == 2, ' need to finetune '
    total = total[1].strip()

    try:
        tot_page = int(total)
    except Exception as exc:
        logger.error(exc)
        raise SystemError(' Something is wrong, need fine tune')


    logger.info('Saving webp images...')
    # tot_page = 1
    for page in tqdm(range(1, tot_page + 1)):

        # Goto page
        driver.find_element(By.CLASS_NAME, 'page-head-tol').click()
        driver.find_element(By.CLASS_NAME, 'el-input').find_element(By.TAG_NAME, 'input').send_keys(f'{page}\n')

        sleep_ = 4  # + randint(25, 45) + random()
        logger.info(' Sleeping %.2f s' % sleep_)
        sleep(sleep_)

        imgs = driver.find_element(By.ID, f'pageImgBox{page}').find_elements(By.TAG_NAME, 'img')

        picList = []

        for img in imgs:
            url = img.get_attribute('src')

            # print(len(url))
            if url in intercepted_imgs:
                res = re.match('.*left: (.*)px', img.get_attribute('style'))
                left = res.group(1)

                filename = f'{page:03d}-{left}.webp'
                with open(f'webp/{filename}', "wb") as f:
                    # b64_data = pic.split(';base64,')[1]
                    # data = base64.b64decode(b64_data)
                    f.write(intercepted_imgs[url])
            else:
                logger.error('Not intercepted')
    driver.quit()


from PIL import Image

def convert():
    SRC_PATH = './webp'
    DEST_PATH = './jpg'
    for f in tqdm(os.scandir(SRC_PATH)):
        if f.is_file():
            im = Image.open(f'{SRC_PATH}/{f.name}')
            if im.mode == "RGBA":
                im.load()  # required for png.split()
                background = Image.new("RGB", im.size, (255, 255, 255))
                background.paste(im, mask=im.split()[3])
            save_name = f.name.replace('webp', 'jpg')
            im.save(f'{DEST_PATH}/{save_name}', 'JPEG')



from PIL import Image


def concat():
    SRC_PATH = './jpg'
    DEST_PATH = './jpg-concat'

    path_list = os.listdir(SRC_PATH)

    pre_data = []

    for i in path_list:
        res = re.match(r'.*(...)-(.*).jpg', i)
        pre_data.append((int(res.group(1)), float(res.group(2)), i))

    pre_data.sort()
    # print(pre_data)

    data = []
    for i in range(0, len(pre_data), 6):
        data.append([pre_data[i+j][2] for j in range(6)])

    print(data)

    for page in tqdm(data):
        images = [Image.open(f'{SRC_PATH}/{page[i]}') for i in range(6)]
        # image = image.resize((200, 200))
        # images.append(image)
        h = images[0].height
        w = 0
        for image in images:
            w += image.width

        new_image = Image.new('RGB', (w, h), 'white')

        acc = 0
        for i in range(6):
            new_image.paste(images[i], (acc, 0))
            acc += images[i].width

        # 将最终图像保存到磁盘上
        new_image.save(f'{DEST_PATH}/{page[0][:3]}.jpg')

if __name__ == '__main__':
    # save_webp_from_site()
    # convert()
    concat()

6. 总结

因此这种方式一般为下策,十分繁琐且复杂。

二、基于 API 请求

还是 B 站的那几个数据,既然它是动态加载的那么它一定会向服务端发送网络请求来获取数据,只要我模拟浏览器,向相同的 URL,用相同的参数发送请求,不久也可以得到相同的数据了么。

在网络这一栏中我们可以寻找一下数据的请求:

image-20230527002524494

于是我们很快的就找到了(这里也是有一些技巧,比如一般是属于 Fetch/XHR 类型的,选上它可以排除掉大部分请求):

image-20230527002654812

可以发现是这样的一个请求:

image-20230527002802911

于是事情变得简单了起来:

Python
import requests
import json

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50",
}

res = requests.get('https://api.bilibili.com/x/relation/stat?vmid=46452693', headers=headers)

json_data = res.content
data = json.loads(json_data)
print(data)

得到:

Text Only
{'code': 0, 'message': '0', 'ttl': 1, 'data': {'mid': 46452693, 'following': 1596, 'whisper': 0, 'black': 0, 'follower': 80}}

但是这里只有关注数和粉丝数,这是因为其他在另一个接口中:

image-20230527003617804

但是如果我们直接请求:

Diff
import requests
import json

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50",
}

- res = requests.get('https://api.bilibili.com/x/relation/stat?vmid=46452693', headers=headers)
+ res = requests.get('https://api.bilibili.com/x/space/upstat?mid=46452693', headers=headers)

json_data = res.content
data = json.loads(json_data)
print(data)

得到的会是空数据:

Text Only
{'code': 0, 'message': '0', 'ttl': 1, 'data': {}}

这是没有登陆导致的,很多接口会设计为对登录与否返回不同的数据,或者只有登录才能访问。

可以通过在浏览器登陆后将浏览器的 cookie 设置给 python 来做到伪装登录:

image-20230527003353978

Diff
import requests
import json

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.50",
+     "cookie": "buvid3=DCD7DCA7-946A-873B-F086-BBD113D7B55B71803infoc; b_nut=1684650871; i-wanna-go-back=-1; _uuid=564E41097-B827-95A6-B426-AAB746EAEF6470634infoc; FEED_LIVE_VERSION=V8; nostalgia_conf=-1; buvid4=41861768-A85B-36DA-8BA5-DF158A877ED373582-023010820-aN5fltImCgRQWCnsP2i7D%2FJIwXV6ACOcIEqJxVQq467iPa2cgehVRg%3D%3D; CURRENT_FNVAL=4048; rpdid=|(k|~u|k)mkY0J'uY)RYl|)mk; fingerprint=5677c4085fd61a28872087c5575d0f58; buvid_fp_plain=undefined; b_ut=5; header_theme_version=CLOSE; bp_video_offset_46452693=800047030483288200; PVID=2; SESSDATA=4b7f4832%2C1700670810%2C3c842%2A52; bili_jct=62d33772c95b5dd980daf902d3e9bd48; DedeUserID=46452693; DedeUserID__ckMd5=254848859dbd9bdd; buvid_fp=1645a9fad4c3183b3f729cb6147fd8bf; sid=ef1mejq7; home_feed_column=4; browser_resolution=893-989; b_lsid=1033CB7410_18858E871D9"
}

res = requests.get('https://api.bilibili.com/x/space/upstat?mid=46452693', headers=headers)

json_data = res.content
data = json.loads(json_data)
print(data)

现在就好了:

Text Only
{'code': 0, 'message': '0', 'ttl': 1, 'data': {'archive': {'view': 4271}, 'article': {'view': 275}, 'likes': 319}}

三、总结

大概就是这两大类方法,写得比较简陋,可以简单看看。

如果有问题可以随时讨论。

-SQL解析

注释

Text Only
%option noyywrap nodefault yylineno case-insensitive

%{
void yyerror(char *s, ...);
%}

%s COMMENT

%%
    /* comments */
"--"[ \t].*      ;
"/*"             { old_state = YY_START; BEGIN COMMENT; }
<COMMENT>"*/"    { BEGIN old_state; }
<COMMENT>.|\n    ;
<COMMENT><<EOF>> { yyerror("lexer: unclosed comment"); }

    /* everything else */
[ \t\n]          ;
.                { yyerror("lexer: mystery character: '%c'", *yytext); }
%%

实训II —— SQL解析 Draft

使用 flex 生成 C++ 词法分析器

主要两种方法:

  • 简单地直接用 C++ 编译器代替 C 编译器
  • 也可以使用 -+ 选项(或者添加 %option c++),这样就会生成 lex.yy.cc 而非 lex.yy.c

使用第二种方法:

.l 文件中的 defination 部分添加

Text Only
%option c++

即可。

生成的 lex.yy.cc 文件中会引入一个 FlexLexer.h 头文件,其中定义了一个 FlexLexer 类,这是一个抽象的基类,定义了一般的扫描器类接口,在该类中提供了以下成员函数: