2024年11月

《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

在Rust中,结构体(Struct)是一种自定义数据类型,它允许我们将多个相关的值组合在一起,形成一个更复杂的数据结构。结构体在Rust中被广泛应用于组织和管理数据,具有灵活性和强大的表达能力。本节将详细介绍Rust中结构体的概念、定义语法、方法以及相关特性,并提供代码示例来帮助读者更好地理解结构体的使用方法。

8.3.1 结构体的定义

Rust 中的结构体与元组都可以将若干类型不一定相同的数据捆绑在一起形成整体,但结构体的每个成员和其本身都有一个名字,这样访问它的成员的时候就不用记住下标了。元组常用于非定义的多值传递,而结构体用于规范常用的数据结构。结构体的每个成员叫作“字段”。

在Rust中,我们可以使用struct关键字定义一个结构体。结构体允许我们定义多个字段(Fields),每个字段都有自己的类型和名称。通过将字段组合在一起,可以创建自己的数据类型,以便更好地表示和操作数据。以下是一个简单的结构体定义的示例:

structPoint {
x: i32,
y: i32,
}

在上述示例中,我们定义了一个名为Point的结构体,它具有两个字段x和y,分别是i32类型的整数。再来看一个结构体定义:

structSite {
domain: String,
name: String,
nation: String,
found: u32
}

注意:如果你常用C/C++,请记住在Rust中struct语句仅用来定义,不能声明实例,结尾不需要“;” 符号,而且每个字段定义之后用“,”分隔。

8.3.2 结构体实例化

一旦定义了结构体,可以通过实例化结构体来创建具体的对象。可以通过以下两种方式实例化结构体:

1)声明式实例化

let p = Point { x: 10, y: 20 };

在上述示例中,我们使用Point结构体的定义创建了一个名为p的实例,同时指定了字段x和y 的值。

2)可变实例化

如果需要修改结构体的字段值,可以在定义结构体变量时加上mut,代码如下:

let mut p = Point { x: 10, y: 20};
p.x
= 30;
p.y
= 40;

在上述示例中,我们创建了一个可变实例p,并修改了字段x和y的值。

Rust很多地方受JavaScript的影响,在实例化结构体的时候用JSON对象的key: value语法来实现,比如:

let mysite =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
nation: String::
from("China"),
found:
2024};

如果你不了解 JSON 对象,可以不用管它,记住格式就可以了:

结构体类名 {
字段名 : 字段值,
...
}

这样的好处是不仅使程序更加直观,还不需要按照定义的顺序来输入成员的值。如果正在实例化的结构体有字段名称和现存变量名称一样,可以简化书写:

let domain = String::from("www.qq.com");
let name
= String::from("qq");
let runoob
=Site {
domain,
//等同于 domain : domain, name, //等同于 name : name, nation: String::from("China"),
traffic:
2024};

有这样一种情况:想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中一两个字段的值,可以使用结构体更新语法:

let site =Site {
domain: String::
from("www.qq.com"),
name: String::
from("qq"),
..qq
};

注意:..qq后面不可以有逗号。这种语法不允许一成不变地复制另一个结构体实例,意思就是至少重新设定一个字段的值,才能引用其他实例的值。

8.3.3 结构体的方法
在Rust中,结构体可以拥有自己的方法。方法是与结构体关联的函数,可以通过结构体实例调用。以下是一个结构体方法的示例:

structRectangle {
width: u32,
height: u32,
}

impl Rectangle {
//使用关键字impl来定义结构体的一个或多个方法 fn area(&self) -> u32 { //用关键字fn定义具体的函数 self.width *self.height
}
}

fn main() {
let rect
= Rectangle { width: 10, height: 20};
let area
=rect.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Rectangle的结构体,并为其实现一个area方法,用于计算矩形的面积。在main函数中,我们创建一个Rectangle实例rect,然后通过调用area方法计算矩形的面积并打印出来。

8.3.4 结构体的关联函数

除实例方法外,结构体还可以定义关联函数(Associated Functions)。关联函数是直接与结构体关联的函数,不需要通过结构体实例来调用。以下是一个关联函数的示例:

structCircle {
radius: f64,
}

impl Circle {
fn
new(radius: f64) ->Circle {
Circle { radius }
}

fn area(
&self) ->f64 {
std::f64::consts::PI
* self.radius *self.radius
}
}

fn main() {
let circle
= Circle::new(5.0);
let area
=circle.area();
println
!("Area: {}", area);
}

在上述示例中,我们定义一个名为Circle的结构体,并为其实现一个关联函数new,用于创建一个新的Circle实例。在main函数中,我们通过调用Circle::new关联函数创建一个Circle实例circle,然后通过调用area方法计算圆的面积并打印出来。

8.3.5 结构体的特性

Rust的结构体具有两个特性:元组结构体(Tuple Struct)和类单元结构体(Unit-Like Struct)。

元组结构体是一种特殊类型的结构体,它没有命名的字段,只有字段的类型。元组结构体使用圆括号而不是花括号来定义。比如:

struct Color(i32, i32, i32);

在上述示例中,我们定义了一个名为Color的元组结构体,它包含3个i32类型的字段。

类单元结构体是一种没有字段的结构体,类似于空元组。比如:

struct Empty;

在上述示例中,我们定义了一个名为Empty的类单元结构体。

前段时间在搬迁项目的时候,遇到一个问题,就是用sqlsugar调用oracle的存储过程的时候调用不了;

当时卡了一整天,现在有空了把这个问题记录分享一下。

先去nuget上安装一下sqlsugar的包:

再安装一个oracle的驱动:

添加一下Json包:

再去创建一下连接

再创建一个测试用的存储过程

create or replace procedure pr_test(i_name   invarchar2,
i_age
invarchar2,o_resultout sys_refcursor) asbegin

open o_result
for select * fromdual;

end pr_test;

创建一个类来接受存储过程返回的数据

    public classPeople
{
public string Dummy { get; set; }
}

单独把存储过程里面的那句sql拿出来执行,会得到下面的结果:

dual这个表是oracle提供的一个表,里面就一个X,一般可以用这个来测试数据库连接是不是正常。

调用的方式如下:

里面那个
游标
的入参必须是个空字符,我之前尝试过object,null,就是没想到过会是一个空字符。

当时也是没想到一个空字符,就把我卡了一个下午,这个坑应该是不会再踩了。

Kubernetes 中实现 MySQL 的读写分离

在 Kubernetes 中实现 MySQL 的读写分离,可以通过主从复制架构来实现。在这种架构中,MySQL 主节点(Master)负责处理所有写操作,而 MySQL 从节点(Slave)负责处理所有读操作。下面是一个详细的步骤指南:

步骤 1:创建 Kubernetes 集群

确保你有一个运行良好的 Kubernetes 集群,建议有3个以上的节点,以便更好地分配资源并实现高可用性。

步骤 2:创建 MySQL 主从复制 Docker 镜像

  1. 首先,需要构建一个支持主从复制的 MySQL 镜像,或直接使用现有支持主从复制的 MySQL 镜像。

  2. 如果要自己配置,可以从 MySQL 官方镜像开始,通过设置 my.cnf 文件来支持主从复制。

  3. 主要的配置如下:


    • 主节点配置(Master):设置 server-id,并启用二进制日志(log-bin)。

    • 从节点配置(Slave):设置不同的 server-id,并配置为从属节点。

步骤 3:创建 Kubernetes Secret 存储 MySQL 密码

为了安全性,我们可以使用 Kubernetes Secret 来存储 MySQL 密码。

apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
mysql-root-password: <base64编码的root密码>
mysql-replication-user: <base64编码的replication用户名>
mysql-replication-password: <base64编码的replication密码>

步骤 4:部署 MySQL 主节点

  1. 创建主节点的配置文件
    mysql-master-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-master
spec:
replicas: 1
selector:
  matchLabels:
    app: mysql
    role: master
template:
  metadata:
    labels:
      app: mysql
      role: master
  spec:
    containers:
    - name: mysql
      image: mysql:5.7
      env:
      - name: MYSQL_ROOT_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-root-password
      - name: MYSQL_REPLICATION_USER
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-user
      - name: MYSQL_REPLICATION_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-password
      ports:
      - containerPort: 3306
      volumeMounts:
      - name: mysql-persistent-storage
        mountPath: /var/lib/mysql
    volumes:
    - name: mysql-persistent-storage
      persistentVolumeClaim:
        claimName: mysql-pv-claim
  1. 创建 MySQL 主节点的 Service:

apiVersion: v1
kind: Service
metadata:
name: mysql-master
spec:
ports:
- port: 3306
  targetPort: 3306
selector:
  app: mysql
  role: master

步骤 5:部署 MySQL 从节点

  1. 创建从节点的配置文件
    mysql-slave-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-slave
spec:
replicas: 2
selector:
  matchLabels:
    app: mysql
    role: slave
template:
  metadata:
    labels:
      app: mysql
      role: slave
  spec:
    containers:
    - name: mysql
      image: mysql:5.7
      env:
      - name: MYSQL_ROOT_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-root-password
      - name: MYSQL_REPLICATION_USER
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-user
      - name: MYSQL_REPLICATION_PASSWORD
        valueFrom:
          secretKeyRef:
            name: mysql-secret
            key: mysql-replication-password
      - name: MYSQL_MASTER_HOST
        value: "mysql-master"
      ports:
      - containerPort: 3306
      volumeMounts:
      - name: mysql-persistent-storage
        mountPath: /var/lib/mysql
    volumes:
    - name: mysql-persistent-storage
      persistentVolumeClaim:
        claimName: mysql-pv-claim
  1. 创建 MySQL 从节点的 Service:

apiVersion: v1
kind: Service
metadata:
name: mysql-slave
spec:
ports:
- port: 3306
  targetPort: 3306
selector:
  app: mysql
  role: slave

步骤 6:设置主从复制

在从节点启动后,执行以下命令来配置主从复制:

  1. 登录主节点,创建用于复制的用户:

    CREATE USER 'replication'@'%' IDENTIFIED BY 'replication_password';
    GRANT REPLICATION SLAVE ON *.* TO 'replication'@'%';
    FLUSH PRIVILEGES;
  2. 获取主节点状态:

    SHOW MASTER STATUS;
  3. 登录到从节点,将其配置为主节点的从属节点:

    CHANGE MASTER TO
      MASTER_HOST='mysql-master',
      MASTER_USER='replication',
      MASTER_PASSWORD='replication_password',
      MASTER_LOG_FILE='<上一步中获取的 File>',
      MASTER_LOG_POS=<上一步中获取的 Position>;
    START SLAVE;
  4. 检查从节点状态以确认同步是否成功:

    SHOW SLAVE STATUS\G

步骤 7:配置读写分离

在 Kubernetes 中,可以使用一个自定义的 Service 来实现读写分离:

  1. 创建 MySQL 读写分离的 Service:

    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-read-write
    spec:
    ports:
    - port: 3306
      targetPort: 3306
    selector:
      app: mysql
      role: master
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-read-only
    spec:
    ports:
    - port: 3306
      targetPort: 3306
    selector:
      app: mysql
      role: slave
  2. 通过应用层(例如应用代码)选择访问不同的 Service 来实现读写分离:


    • 写操作:通过
      mysql-read-write
      Service 连接。

    • 读操作:通过
      mysql-read-only
      Service 连接。

步骤 8:测试读写分离

  1. 将写操作请求发送到
    mysql-read-write
    服务,验证数据是否被正确写入。

  2. 将读操作请求发送到
    mysql-read-only
    服务,确保从节点上能够读到主节点写入的数据。

步骤 9:监控与维护

可以通过 Prometheus 和 Grafana 对 MySQL 集群进行监控,关注主从复制的延迟和节点的健康状态,以便及时处理故障。

总结

主节点负责处理写操作,从节点负责处理读操作,应用可以根据需求连接到不同的 Service 来实现高效的数据库读写分离。

web 21——弱口令爆破&custom iterator

进去要求输入账号密码,账号输入
admin
,一般来说管理员用户名都会是这个,密码随便输,然后burpsuite抓包
可以看到账号密码在
Authorization
传输,形式是
账号:密码
的base64加密,把他发到
Intruder
模块

模式选
sniper
,因为要对整个账号密码字符进行加密,不能分开爆破,选中要爆破的地方

选择
custom iterator
模式,在位置1写入
admin
,分隔符写
:

位置2导入提供的字典

添加
base64
加密,取消选中Palyload Encoding编码,因为在进行base64加密的时候在最后可能存在
==
这样就会影响base64加密的结果

开始攻击,点击状态码进行筛选,找到爆破出的密码,将密码解密后为
shark63
,输入即可得到flag

web 22——子域名爆破&oneforall

OneForAll,是 shmilylty 在 Github 上开源的子域收集工具,可以实现对子域名的爆破

python oneforall.py --target ctf.show run

可以看到爆破出了很多结果,不过这题的域名失效了,不然应该会有一个
flag.ctf.show

web 23——md5爆破&burp&python

看一下代码,通过
get
方式提交一个
token
参数,要求MD5 加密结果的第 2 位、第 15 位、第 18 位字符是否相等,且这三位字符的数字之和除以第 2 位字符的值是否等于第 32 位字符的数字值

方法1——burpsuite爆破

不管他到底什么条件,直接burpsuite爆破数字0-500,发现422的时候返回长度不同,422就是满足条件的

方法2——python脚本爆破

通过遍历二字符的字符串,寻找符合条件的字符串,得到两个可用字符串
3j

ZE

import hashlib  
  
dic = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"  
for a in dic:  
    for b in dic:  
        t = str(a) + str(b)  
        md5 = hashlib.md5(t.encode('utf-8')).hexdigest()  
  
        if md5[1] == md5[14] == md5[17]:  # 确保这些位置的字符相同  
            # 确保这些字符是数字  
            if 48 <= ord(md5[1]) <= 57 and 48 <= ord(md5[14]) <= 57 and 48 <= ord(md5[17]) <= 57:  
                # 确保md5[31]也是数字,并符合数学关系  
                if 48 <= ord(md5[31]) <= 57:  
                    num1 = int(md5[1])  
                    num14 = int(md5[14])  
                    num17 = int(md5[17])  
                    num31 = int(md5[31])  
  
                    # 判断除数是否为零  
                    if num1 == 0:  
                        continue  # 跳过当前循环  
  
                    if (num1 + num14 + num17) / num1 == num31:  
                        print(t)

web 24——初探伪随机数

本题考察的是php伪随机数,通过
mt_srand(1);
播种后,再通过同一随机数算法计算出来的随机数值是固定的,因此只要看一下服务器php版本,然后在本地起一下以下代码就可以得到随机数了,各位师傅也可以尝试刷新,会发现每次给出来的值都是同一个。

<?php
mt_srand(372619038);
echo "随机数:".mt_rand();
?>

web 25——伪随机数&种子爆破

要获得flag,必须输入
token
为第二、三个随机数的和,因此必须知道
seed
是什么

传入
?r=0
就可以获得第一个随机数的负值,为
-449307572

接下来就要爆破
seed
,这里我们使用php_mt_seed工具,下载与使用方法请自行百度。可以看到爆出来很多
seed
,由于php版本不同产生的随机数会略有区别,因此需要选择与服务器php版本对应的
seed

看一眼php版本,选择1103714832,这里可能得几个都试试,博主试了后两个都没出来

写个php脚本输出需要的随机数

<?php
    mt_srand(1103714832);
    echo mt_rand()."\n";
    $result = mt_rand()+mt_rand();
    echo $result;
?>

提交
r

token
,得到flag

web 26——数据库密码爆破

本题还是弱口令爆破,就是换到了系统安装的场景,直接对密码进行数字的爆破即可,答案是7758521,爆破的量还挺大的
另外这题的代码逻辑有点问题,什么都不填点安装然后抓包就会发现flag直接在返回包里了,不过这样就没有爆破的味道了,还是建议按上面的方法爆一下

web 27——门户网站爆破

看到一个登陆界面,但是现在啥信息都没有,肯定不能直接爆破,看到下面有录取名单和学籍信息查询系统

看到这里,猜测是爆破身份证号(这里是生日被隐藏了),然后通过录取查询获得密码

抓包,这题很奇怪,火狐好像很难抓到包,要么用谷歌抓,或者用火狐一直点,总归能抓到。给对生日进行爆破,payload类型选日期,选择开始与结束的年月日,选择日期格式,y代表年,M代表月,d代表日

找到长度不同的数据包

返回信息需要Unicode解码一下,结果给出学号和密码,登陆得到flag

# 原始字符串
encoded_str = r"\u606d\u559c\u60a8\uff0c\u60a8\u5df2\u88ab\u6211\u6821\u5f55\u53d6\uff0c\u4f60\u7684\u5b66\u53f7\u4e3a02015237 \u521d\u59cb\u5bc6\u7801\u4e3a\u8eab\u4efd\u8bc1\u53f7\u7801"
# 使用 unicode_escape 解码
decoded_str = encoded_str.encode('utf-8').decode('unicode_escape')
print(decoded_str)

web 28——目录爆破

看到url是
/0/1/2.txt
,猜测是对目录中的数字进行爆破,删掉2.txt,对
0

1
爆破,用
cluster bomb
模式

设置payload set 1和2都为数字0-99

爆破,找到能访问的目录,看一眼返回包就是flag

上一篇:《人工智能模型训练中的数据之美——探索TFRecord》

序言:
自然语言处理(NLP)是人工智能中的一种技术,专注于理解基于人类语言的内容。它包含了编程技术,用于创建可以理解语言、分类内容,甚至生成和创作人类语言的新作品的模型。在接下来的几章中,我们将会探讨这些技术。此外,现在有许多利用 NLP 的服务来创建应用程序,比如聊天机器人(它们属于应用,属于Agent应用开发),但这些内容不在知识的范围之内——我们将专注于 NLP 的基础知识(实现原理),以及如何进行语言建模,使您可以训练神经网络,教导电脑去理解和分类文本。

我们将从本节开始,先了解如何将语言分解成数字,以及这些数字如何用于神经网络,所谓‘分解’其实就给用一个数字代替语言句子中的字词或者词根,因为计算机只能处理数字;人们把语言转换成数字交由电脑处理后,再重新转回语言文字就可以被人类识别并知道电脑做了什么了。

将语言编码为数字

有多种方法可以将语言编码成数字。最常见的是通过字母进行编码,就像字符串在程序中存储时的自然形式一样。不过,在内存中,您存储的不是字母本身,而是它的编码——可能是 ASCII、Unicode 值,或者其他形式。例如,考虑单词“listen”。用 ASCII 编码的话,这个单词可以被表示为数字 76、73、83、84、69 和 78。这种编码方式的好处是,您现在可以用数字来表示这个单词。但如果考虑“silent”这个词,它是“listen”的一个字母异位词。尽管这两个单词的编码数字相同,但顺序不同,这可能会让建立一个理解文本的模型变得有些困难。

一个“反义词异构词”是指一个单词的字母顺序颠倒后形成的另一个单词,且二者具有相反的含义。例如,“united”和“untied”就是一对反义词异构词,另外还有“restful”和“fluster”,“Santa”和“Satan”,“forty-five”和“over fifty”。我之前的职位名称是“Developer Evangelist”,后来改成了“Developer Advocate”——这是个好事,因为“Evangelist”就是“Evil’s Agent”(邪恶代理人)的反义词异构词!

一种更好的替代方法可能是用数字来编码整个单词,而不是逐个字母编码。在这种情况下,“silent”可以用数字x表示,“listen”可以用数字y表示,它们彼此不会重叠。

使用这种技术,考虑一个句子比如“I love my dog.”您可以将它编码为数字 [1, 2, 3, 4]。如果您想要编码“I love my cat.”,可以是 [1, 2, 3, 5]。您已经可以看出这些句子在数值上相似——[1, 2, 3, 4] 看起来很像 [1, 2, 3, 5],因此可以推测它们的含义相似。

这个过程叫做“分词”,接下来您将探索如何在代码中实现它。

分词入门

TensorFlow Keras 包含一个称为“preprocessing”的库,它提供了许多非常实用的工具来为机器学习准备数据。其中之一是“Tokenizer”,它可以将单词转化为令牌。让我们通过一个简单的示例来看它的实际操作:

import tensorflow as tf

from tensorflow import keras

from tensorflow.keras.preprocessing.text import Tokenizer

sentences = [

'Today is a sunny day',

'Today is a rainy day'

]

tokenizer = Tokenizer(num_words=100)

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

print(word_index)

在这个例子中,我们创建了一个 Tokenizer 对象,并指定了它可以分词的单词数量。这将是从词库中生成的最大令牌数。我们这里的词库非常小,只包含六个独特的单词,所以远小于所指定的一百个。

一旦我们有了一个分词器,调用 fit_on_texts 就会创建出令牌化的单词索引。打印出来会显示词库中的键/值对集合,类似于这样:

{'today': 1, 'is': 2, 'a': 3, 'day': 4, 'sunny': 5, 'rainy': 6}

这个分词器非常灵活。例如,如果我们将语料库扩展,添加另一个包含单词“today”且带有问号的句子,结果会显示它足够智能,可以将“today?”过滤成“today”:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?'

]

输出结果为:{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

这种行为是由分词器的filters参数控制的,默认情况下会移除除撇号外的所有标点符号。因此,例如,“Today is a sunny day”将根据之前的编码变成一个包含 [1, 2, 3, 4, 5] 的序列,而“Is it sunny today?”将变成 [2, 7, 4, 1]。当您已将句子中的单词分词后,下一步就是将句子转换为数字列表,其中数字是单词在词典中的键值对所对应的值。

将句子转换为序列

现在您已经了解了如何将单词分词并转化为数字,接下来的一步是将句子编码为数字序列。分词器有一个名为text_to_sequences的方法,您只需传递句子的列表,它就会返回序列的列表。例如,如果您修改之前的代码如下:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?'

]

tokenizer = Tokenizer(num_words=100)

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

sequences = tokenizer.texts_to_sequences(sentences)

print(sequences)

您将得到表示这三句话的序列。回想一下词汇索引是这样的:

{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

输出结果将如下所示:

[[1, 2, 3, 4, 5], [1, 2, 3, 6, 5], [2, 7, 4, 1]]

然后,您可以将数字替换成单词,这样句子就会变得有意义了。

现在考虑一下,当您用一组数据训练神经网络时会发生什么。通常的模式是,您有一组用于训练的数据,但您知道它无法涵盖所有的需求,只能尽量覆盖多一些。在 NLP 的情况下,您的训练数据中可能包含成千上万个单词,出现在不同的上下文中,但您不可能在所有的上下文中涵盖所有可能的单词。所以,当您向神经网络展示一些新的、之前未见过的文本,包含未见过的单词时,会发生什么呢?您猜对了——它会感到困惑,因为它完全没有那些单词的上下文,结果它的预测就会出错。

使用“词汇表外”令牌

处理这些情况的一个工具是“词汇表外”(OOV)令牌。它可以帮助您的神经网络理解包含未见过的文本的数据上下文。例如,假设您有以下的小型语料库,希望处理这样的句子:

test_data = [

'Today is a snowy day',

'Will it be rainy tomorrow?'

]

请记住,您并没有将这些输入添加到已有的文本语料库中(可以视作您的训练数据),而是考虑预训练网络如何处理这些文本。如果您使用已有的词汇和分词器来分词这些句子,如下所示:

test_sequences = tokenizer.texts_to_sequences(test_data)

print(word_index)

print(test_sequences)

输出结果如下:

{'today': 1, 'is': 2, 'a': 3, 'sunny': 4, 'day': 5, 'rainy': 6, 'it': 7}

[[1, 2, 3, 5], [7, 6]]

那么新的句子,在将令牌换回单词后,变成了“today is a day”和“it rainy”。

正如您所见,几乎完全失去了上下文和意义。这里可以用“词汇表外”令牌来帮助,您可以在分词器中指定它。只需添加一个名为 oov_token 的参数,您可以将其设置为任意字符串,但确保它不会出现在您的语料库中:

tokenizer = Tokenizer(num_words=100, oov_token="
")

tokenizer.fit_on_texts(sentences)

word_index = tokenizer.word_index

sequences = tokenizer.texts_to_sequences(sentences)

test_sequences = tokenizer.texts_to_sequences(test_data)

print(word_index)

print(test_sequences)

您会看到输出有了一些改进:

{'
': 1, 'today': 2, 'is': 3, 'a': 4, 'sunny': 5, 'day': 6, 'rainy': 7, 'it': 8}

[[2, 3, 4, 1, 6], [1, 8, 1, 7, 1]]

您的令牌列表中多了一个新的项“
”,并且您的测试句子保持了它们的长度。现在反向编码后得到的是“today is a day”和“ it rainy ”。

前者更加接近原始含义,而后者由于大部分单词不在语料库中,仍然缺乏上下文,但这算是朝正确方向迈出了一步。

理解填充(padding)

在训练神经网络时,通常需要所有数据的形状一致。回忆一下之前章节中提到的,训练图像时需要将图像格式化为相同的宽度和高度。在文本处理中也面临相似的问题——一旦您将单词分词并将句子转换为序列后,它们的长度可能会各不相同。为了使它们的大小和形状一致,可以使用填充(padding)。

为了探索填充,让我们在语料库中再添加一个更长的句子:

sentences = [

'Today is a sunny day',

'Today is a rainy day',

'Is it sunny today?',

'I really enjoyed walking in the snow today'

]

当您将它们转换为序列时,您会看到数字列表的长度不同:

[

[2, 3, 4, 5, 6],

[2, 3, 4, 7, 6],

[3, 8, 5, 2],

[9, 10, 11, 12, 13, 14, 15, 2]

]

(当您打印这些序列时,它们会显示在一行上,为了清晰起见,我在这里分成了多行。)

如果您想让这些序列的长度一致,可以使用 pad_sequences API。首先,您需要导入它:

from tensorflow.keras.preprocessing.sequence import pad_sequences

使用这个 API 非常简单。要将您的(未填充的)序列转换为填充后的集合,只需调用 pad_sequences,如下所示:

padded = pad_sequences(sequences)

print(padded)

您会得到一个格式整齐的序列集合。它们会在单独的行上,像这样:

[[ 0 0 0 2 3 4 5 6]

[ 0 0 0 2 3 4 7 6]

[ 0 0 0 0 3 8 5 2]

[ 9 10 11 12 13 14 15 2]]

这些序列被填充了 0,而 0 并不是我们单词列表中的令牌。如果您曾疑惑为什么令牌列表从 1 开始而不是 0,现在您知道原因了!

现在,您得到了一个形状一致的数组,可以用于训练。不过在此之前,让我们进一步探索这个 API,因为它提供了许多可以优化数据的选项。

首先,您可能注意到在较短的句子中,为了使它们与最长的句子形状一致,必要数量的 0 被添加到了开头。这被称为“前填充”,它是默认行为。您可以通过 padding 参数来更改它。例如,如果您希望序列在末尾填充 0,可以使用:

padded = pad_sequences(sequences, padding='post')

其输出如下:

[[ 2 3 4 5 6 0 0 0]

[ 2 3 4 7 6 0 0 0]

[ 3 8 5 2 0 0 0 0]

[ 9 10 11 12 13 14 15 2]]

现在您可以看到单词在填充序列的开头,而 0 位于末尾。

另一个默认行为是,所有句子都被填充到与最长句子相同的长度。这是一个合理的默认设置,因为这样您不会丢失任何数据。权衡之处在于您会得到大量填充。如果不想这样做,比如因为某个句子太长导致填充过多,您可以使用 maxlen 参数来指定所需的最大长度,如下所示:

padded = pad_sequences(sequences, padding='post', maxlen=6)

其输出如下:

[[ 2 3 4 5 6 0]

[ 2 3 4 7 6 0]

[ 3 8 5 2 0 0]

[11 12 13 14 15 2]]

现在您的填充序列长度一致,且填充量不多。不过,您会发现最长句子的一些单词被截断了,它们是从开头截断的。如果您不想丢失开头的单词,而是希望从句子末尾截断,可以通过 truncating 参数来覆盖默认行为,如下所示:

padded = pad_sequences(sequences, padding='post', maxlen=6, truncating='post')

结果显示最长的句子现在从末尾截断,而不是开头:

[[ 2 3 4 5 6 0]

[ 2 3 4 7 6 0]

[ 3 8 5 2 0 0]

[ 9 10 11 12 13 14]]

TensorFlow 支持使用“稀疏”(形状不同的)张量进行训练,这非常适合 NLP 的需求。使用它们比本书的内容稍微进阶一些,但在您完成接下来几章提供的 NLP 入门后,可以进一步查阅文档了解更多。

移除停用词和清理文本

在接下来的章节中,我们会看一些真实的文本数据集,并发现数据中经常有不想要的文本内容。你可能需要过滤掉一些所谓的“停用词”,这些词过于常见,不带任何实际意义,比如“the”,“and”和“but”。你也可能会遇到很多HTML标签,去除它们可以使文本更加干净。此外,其他需要过滤的内容还包括粗话、标点符号或人名。稍后我们会探索一个推文的数据集,其中经常包含用户的ID,我们也会想要去除这些内容。

虽然每个任务会因文本内容的不同而有所差异,但通常有三种主要的方法可以编程地清理文本。第一步是去除HTML标签。幸运的是,有一个名叫BeautifulSoup的库可以让这项任务变得简单。例如,如果你的句子包含HTML标签(比如
),以下代码可以将它们移除:

from bs4 import BeautifulSoup

soup = BeautifulSoup(sentence)

sentence = soup.get_text()

一种常见的去除停用词方法是创建一个停用词列表,然后预处理句子,移除其中的停用词。以下是一个简化的例子:

stopwords = ["a", "about", "above", ... "yours", "yourself", "yourselves"]

一个完整的停用词列表可以在本章的一些在线示例中找到。然后,当你遍历句子时,可以使用如下代码来移除句子中的停用词:

words = sentence.split()

filtered_sentence = ""

for word in words:

if word not in stopwords:

filtered_sentence = filtered_sentence + word + " "

sentences.append(filtered_sentence)

另一件可以考虑的事情是去除标点符号,它可能会干扰停用词的移除。上面展示的代码是寻找被空格包围的词语,因此如果停用词后紧跟一个句号或逗号,它将不会被识别出来。

Python的string库提供的翻译功能可以轻松解决这个问题。它还带有一个常量string.punctuation,其中包含了常见的标点符号列表,因此可以使用如下代码将其从单词中移除:

import string

table = str.maketrans('', '', string.punctuation)

words = sentence.split()

filtered_sentence = ""

for word in words:

word = word.translate(table)

if word not in stopwords:

filtered_sentence = filtered_sentence + word + " "

sentences.append(filtered_sentence)

在这里,每个句子在过滤停用词之前,单词中的标点符号已经被移除。因此,如果将句子拆分后得到“it;”,它会被转换为“it”,然后作为停用词被过滤掉。不过,注意当这样处理时,你可能需要更新停用词列表。通常,这些列表中会包含一些缩略词和缩写形式,比如“you’ll”。翻译器会将“you’ll”转换为“youll”,如果想要将它过滤掉,就需要在停用词列表中添加它。

遵循这三个步骤后,你将获得一组更加干净的文本数据。但当然,每个数据集都有其独特之处,你需要根据具体情况进行调整

本节总结,
本节介绍了自然语言处理(NLP)的基础概念,包括文本编码、分词、去停用词和清理文本等技术。首先,探讨了如何将语言转为数字以便于计算机处理,并通过编码方法将单词分解为数值。接着,介绍了分词工具(如Tokenizer)在文本预处理中分配和管理单词索引。还讨论了处理未见过的词汇(OOV)以减少模型误差的策略。在清理文本方面,使用BeautifulSoup库去除HTML标签,并利用停用词列表和标点符号过滤功能对数据集进一步清理。此外,为确保数据一致性,介绍了填充(padding)技术以使数据形状一致,适用于模型训练。这些步骤为文本清理和建模提供了坚实的基础,但在实际应用中应灵活调整以应对不同数据集的需求。