From 53bce3d022487f32e833d1e9af8d11d1392db8c7 Mon Sep 17 00:00:00 2001 From: RuyiLuo Date: Thu, 14 Apr 2022 15:20:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E9=97=BB=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E7=B3=BB=E7=BB=9F=E5=AE=9E=E8=B7=B5=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 50 +- docs/_sidebar.md | 22 +- .../新闻推荐系统实践/MongoDB基础.md | 1243 +++++++++ .../新闻推荐系统实践/Mysql基础.md | 2313 +++++++++++++++++ .../新闻推荐系统实践/Redis基础.md | 1238 +++++++++ .../新闻推荐系统实践/flask简介及基础.md | 549 ++++ .../新闻推荐系统实践/scrapy基础及新闻爬取实战.md | 511 ++++ .../新闻推荐系统实践/前后端交互.md | 448 ++++ .../新闻推荐系统实践/前端基础及Vue实战.md | 865 ++++++ .../新闻推荐系统实践/推荐系统流程的构建.md | 200 ++ .../新闻推荐系统实践/数据库的基本使用问答整理.md | 149 ++ .../熟悉推荐系统基本流程问答整理.md | 280 ++ .../新闻推荐系统实践/离线物料系统的构建问答整理.md | 85 + .../新闻推荐系统实践/自动化构建用户及物料画像.md | 650 +++++ 14 files changed, 8572 insertions(+), 31 deletions(-) create mode 100644 docs/推荐系统实战/新闻推荐系统实践/MongoDB基础.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/Mysql基础.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/Redis基础.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/flask简介及基础.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/scrapy基础及新闻爬取实战.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/前后端交互.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/推荐系统流程的构建.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/数据库的基本使用问答整理.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/熟悉推荐系统基本流程问答整理.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/离线物料系统的构建问答整理.md create mode 100644 docs/推荐系统实战/新闻推荐系统实践/自动化构建用户及物料画像.md diff --git a/docs/README.md b/docs/README.md index 605a43a3..902aa266 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,28 +95,34 @@ - [排序模型&模型融合](/推荐系统实战/竞赛实践/markdown/排序模型+模型融合) #### 新闻推荐系统实践 -- 新闻推荐系统流程的构建视频讲解【已完成】 -- 离线物料系统的构建 - - Mysql基础【已完成】 - - MongoDB基础【已完成】 - - Redis基础【已完成】 - - Scrapy基础及新闻爬取实战【已完成】 - - 自动化构建用户及物料画像【已完成】 -- 前后端基础及交互 - - 前端基础及Vue实战【已完成】 - - flask简介及基础【已完成】 - - 前后端交互【已完成】 -- 推荐流程的构建【已完成】 -- 召回 - - 规则类召回 - - 热度召回【完成一半,待优化】 - - 地域召回【完成一半,待优化】 - - 模型类召回 - - YoutubeDNN召回【已完成,待优化】 - - DSSM召回【已完成,待优化】 -- DeepFM排序模型【已完成,待优化】 -- 规则与重排【完成一半,待优化】 -- 任务监控与调度【完成一半,待优化】 +- **视频** + - [新闻推荐系统流程的构建视频讲解](https://datawhale.feishu.cn/minutes/obcnzns778b725r5l535j32o) +- **文档** + - **离线物料系统的构建** + - [Mysql基础](/推荐系统实战/新闻推荐系统实践/mysql基础) + - [MongoDB基础](/推荐系统实战/新闻推荐系统实践/MongoDB基础) + - [Redis基础](/推荐系统实战/新闻推荐系统实践/Redis基础) + - [Scrapy基础及新闻爬取实战](/推荐系统实战/新闻推荐系统实践/Scrapy基础及新闻爬取实战) + - [自动化构建用户及物料画像](/推荐系统实战/新闻推荐系统实践/自动化构建用户及物料画像) + - **前后端基础及交互** + - [前端基础及Vue实战](/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战) + - [flask简介及基础](/推荐系统实战/新闻推荐系统实践/flask简介及基础) + - [前后端交互](/推荐系统实战/新闻推荐系统实践/前后端交互) + - [推荐系统流程的构建](/推荐系统实战/新闻推荐系统实践/推荐系统流程的构建) + - 召回 + - 规则类召回 + - 热度召回【完成一半,待优化】 + - 地域召回【完成一半,待优化】 + - 模型类召回 + - YoutubeDNN召回【已完成,待优化】 + - DSSM召回【已完成,待优化】 + - DeepFM排序模型【已完成,待优化】 + - 规则与重排【完成一半,待优化】 + - 任务监控与调度【完成一半,待优化】 +- **当前问题汇总** + - [熟悉推荐系统基本流程问答整理](/推荐系统实战/新闻推荐系统实践/熟悉推荐系统基本流程问答整理) + - [数据库的基本使用问答整理](/推荐系统实战/新闻推荐系统实践/数据库的基本使用问答整理) + - [离线物料系统的构建问答整理](/推荐系统实战/新闻推荐系统实践/离线物料系统的构建问答整理) ### 推荐系统算法面经 - [ML与DL基础](/推荐算法面经/ML与DL基础) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index ce329994..3b9333da 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -66,16 +66,16 @@ * [排序模型&模型融合](/推荐系统实战/竞赛实践/markdown/排序模型&模型融合) * [新闻推荐系统的实践]() * [离线物料系统的构建]() - * [Mysql基础]() - * [MongoDB基础]() - * [Redis基础]() - * [Scrapy基础及新闻爬取实战]() - * [自动化构建用户及物料画像]() + * [Mysql基础](/推荐系统实战/新闻推荐系统实践/mysql基础) + * [MongoDB基础](/推荐系统实战/新闻推荐系统实践/MongoDB基础) + * [Redis基础](/推荐系统实战/新闻推荐系统实践/Redis基础) + * [Scrapy基础及新闻爬取实战](/推荐系统实战/新闻推荐系统实践/Scrapy基础及新闻爬取实战) + * [自动化构建用户及物料画像](/推荐系统实战/新闻推荐系统实践/自动化构建用户及物料画像) * [前后端基础及交互]() - * [前端基础及Vue实战]() - * [flask简介及基础]() - * [前后端交互]() - * [推荐流程的构建]() + * [前端基础及Vue实战](/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战) + * [flask简介及基础](/推荐系统实战/新闻推荐系统实践/flask简介及基础) + * [前后端交互](/推荐系统实战/新闻推荐系统实践/前后端交互) + * [推荐系统流程的构建](/推荐系统实战/新闻推荐系统实践/推荐系统流程的构建) * [召回]() - [规则类召回]() - [热度召回]() @@ -86,6 +86,10 @@ * [DeepFM排序]() * [规则与重排]() * [任务调度与监控]() + * [当前问题汇总]() + * [熟悉推荐系统基本流程问答整理](/推荐系统实战/新闻推荐系统实践/熟悉推荐系统基本流程问答整理) + * [数据库的基本使用问答整理](/推荐系统实战/新闻推荐系统实践/数据库的基本使用问答整理) + * [离线物料系统的构建问答整理](/推荐系统实战/新闻推荐系统实践/离线物料系统的构建问答整理) * [推荐系统算法面经]() * [ML与DL基础](/推荐算法面经/ML与DL基础) * [推荐模型相关](/推荐算法面经/推荐模型相关) diff --git a/docs/推荐系统实战/新闻推荐系统实践/MongoDB基础.md b/docs/推荐系统实战/新闻推荐系统实践/MongoDB基础.md new file mode 100644 index 00000000..879e3d93 --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/MongoDB基础.md @@ -0,0 +1,1243 @@ +本文属于新闻推荐实战—数据层—构建物料池之MongoDB。MongoDB数据库在该项目中会用来存储画像数据(用户画像、新闻画像),使用MongoDB存储画像的一个主要原因就是方便扩展,因为画像内容可能会随着产品的不断发展而不断的更新。作为算法工程师需要了解常用的MongoDB语法(比如增删改查,排序等),因为在实际的工作可能会从MongoDB中获取用户、新闻画像来构造相关特征。本着这个目的,本文对MongoDB常见的语法及Python操作MongoDB进行了总结,方便大家快速了解。 + +# MongoDB简介 + +MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。在高负载的情况下,添加更多的节点,可以保证服务器性能。MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案。 + +MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 + +![img](https://i.loli.net/2021/11/02/sgy5CQIfnR9cmpO.png) + +## 主要特点 + +- MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。 +- 你可以在MongoDB记录中设置任何属性的索引 (如:FirstName="Sameer",Address="8 Gandhi Road")来实现更快的排序。 +- 你可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。 +- 如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。 +- Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。 +- MongoDb 使用update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。 +- Mongodb中的Map/reduce主要是用来对数据进行批量处理和聚合操作。 +- Map和Reduce。Map函数调用emit(key,value)遍历集合中所有的记录,将key与value传给Reduce函数进行处理。 +- Map函数和Reduce函数是使用Javascript编写的,并可以通过db.runCommand或mapreduce命令来执行MapReduce操作。 +- GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件。 +- MongoDB允许在服务端执行脚本,可以用Javascript编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。 +- MongoDB支持各种编程语言:RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。 +- MongoDB安装简单 + + + +# Linux平台安装MongoDB + +MongoDB 提供了 linux 各个发行版本 64 位的安装包,你可以在官网下载安装包。 + +MongoDB 源码下载地址:https://www.mongodb.com/download-center#community + +安装前我们需要安装各个 Linux 平台依赖包。 + +**Red Hat/CentOS:** + +``` +sudo yum install libcurl openssl +``` + +**Ubuntu 18.04 LTS ("Bionic")/Debian 10 "Buster":** + +``` +sudo apt-get install libcurl4 openssl +``` + +**Ubuntu 16.04 LTS ("Xenial")/Debian 9 "Stretch":** + +``` +sudo apt-get install libcurl3 openssl +``` + +查看ubuntu的版本 + +``` +lsb_release -a +``` + +![image-20211026193919108](https://i.loli.net/2021/11/02/4Ml1tYIbLimWS2X.png) + + + +![image-20211026201305053](https://i.loli.net/2021/11/02/cHV1hAf4s52ECUw.png) + +![image-20211026201645786](https://i.loli.net/2021/11/02/Imq9ZYdxrRXiGkl.png) + + + +这里我们选择 tgz 下载,下载完安装包,并解压 **tgz**(以下演示的是 64 位 Linux上的安装) 。 + +``` +wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.4.10.tgz #下载 +tar -zxvf mongodb-linux-x86_64-ubuntu1604-4.4.10.tgz #解压 +``` + +MongoDB 的可执行文件位于 bin 目录下,所以可以将其添加到 **PATH** 路径中 + +``` +export PATH=/bin:$PATH +``` + +****为你 MongoDB 的安装路径。 + +## 创建数据库目录 + +默认情况下 MongoDB 启动后会初始化以下两个目录: + +- 数据存储目录:/var/lib/mongodb +- 日志文件目录:/var/log/mongodb + +我们在启动前可以先创建这两个目录: + +``` +sudo mkdir -p /var/lib/mongo +sudo mkdir -p /var/log/mongodb +``` + +接下来启动 Mongodb 服务: + +``` +mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork +``` + +--------------------------------------------------------- + +## MongoDB 后台管理 Shell + +如果你需要进入 mongodb 后台管理,由于已经将MongoDB可执行文件添加到PATH路径,所以可以直接执行 mongo 命令文件。 + +MongoDB Shell 是 MongoDB 自带的交互式 Javascript shell,用来对 MongoDB 进行操作和管理的交互式环境。 + +当你进入 mongoDB 后台后,它默认会链接到 test 文档(数据库): + +![image-20211027223343278](https://i.loli.net/2021/11/02/dDlZE71WqtsS2i8.png) + +# MongoDB 概念解析 + +在mongodb中基本的概念是文档、集合、数据库。下表将帮助您更容易理解Mongo中的一些概念: + +| SQL术语/概念 | MongoDB术语/概念 | 解释/说明 | +| :----------- | :--------------- | :---------------------------------- | +| database | database | 数据库 | +| table | collection | 数据库表/集合 | +| row | document | 数据记录行/文档 | +| column | field | 数据字段/域 | +| index | index | 索引 | +| table joins | | 表连接,MongoDB不支持 | +| primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 | + +## MongoDB 创建数据库 + +### 数据库 + +一个mongodb中可以建立多个数据库。 + +MongoDB的默认数据库为"db",该数据库存储在data目录中。 + +MongoDB的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。 + +**"show dbs"** 命令可以显示所有数据的列表。 + +``` +toby@recsys:~$ mongo +MongoDB shell version: 2.6.10 +connecting to: test +> show dbs +admin (empty) +local 0.078GB +``` + +执行 **"db"** 命令可以显示当前数据库对象或集合。 + +``` +toby@recsys:~$ mongo +MongoDB shell version: 2.6.10 +connecting to: test +> db +test +``` + +运行"use"命令,可以连接到一个指定的数据库。 + +``` +toby@recsys:~$ mongo +MongoDB shell version: 2.6.10 +connecting to: test +> use admin +switched to db admin +> db +admin +> +``` + +### 语法 + +MongoDB 创建数据库的语法格式如下: + +``` +use DATABASE_NAME +``` + +如果数据库不存在,则创建数据库,否则切换到指定数据库。 + +### 实例 + +以下实例我们创建了数据库 tobytest: + +``` +toby@recsys:~$ mongo +MongoDB shell version: 2.6.10 +connecting to: test +> use tobytest +switched to db tobytest +> db +tobytest +> +``` + +如果你想查看所有数据库,可以使用 **show dbs** 命令: + +``` +> show dbs +admin (empty) +local 0.078GB +> +``` + +可以看到,我们刚创建的数据库 tobytest并不在数据库的列表中, 要显示它,我们需要向 tobytest数据库插入一些数据。 + +``` +> db.tobytest.insert({"name":"Toby"}) +WriteResult({ "nInserted" : 1 }) +> show dbs +admin (empty) +local 0.078GB +tobytest 0.078GB +> +``` + +MongoDB 中默认的数据库为 test,如果你没有创建新的数据库,集合将存放在 test 数据库中。 + +> **注意:** 在 MongoDB 中,集合只有在内容插入后才会创建! 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。 + +## MongoDB 创建集合 + +MongoDB 中使用 **createCollection()** 方法来创建集合。 + +语法格式: + +``` +db.createCollection(name, options) +``` + +参数说明: + +- name: 要创建的集合名称 +- options: 可选参数, 指定有关内存大小及索引的选项 + +options 可以是如下参数: + +| 字段 | 类型 | 描述 | +| :---------- | :--- | :----------------------------------------------------------- | +| capped | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 **当该值为 true 时,必须指定 size 参数。** | +| autoIndexId | 布尔 | 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。 | +| size | 数值 | (可选)为固定集合指定一个最大值,即字节数。 **如果 capped 为 true,也需要指定该字段。** | +| max | 数值 | (可选)指定固定集合中包含文档的最大数量。 | + +在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。 + +### 实例 + +在 tobytest 数据库中创建 runoob 集合: + +``` +> use tobytest +switched to db tobytest +> db.createCollection("tobycollection") +{ "ok" : 1 } +> +``` + +如果要查看已有集合,可以使用 **show collections** 或 **show tables** 命令: + +``` +> show tables +system.indexes +tobycollection +tobytest +> +``` + +## MongoDB 删除集合 + +MongoDB 中使用 drop() 方法来删除集合。 + +**语法格式:** + +``` +db.collection.drop() +``` + +参数说明: + +- 无 + +**返回值** + +如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。 + +### 实例 + +在数据库 tobytest中,我们可以先通过 **show collections** 命令查看已存在的集合: + +``` +> use tobytest +switched to db tobytest +> show collections +system.indexes +tobycollection +tobytest +> +``` + +接着删除集合 tobycollection: + +``` +> db.tobycollection.drop() +true +> +``` + +通过 show collections 再次查看数据库 tobytest中的集合: + +``` +> show collections +system.indexes +tobytest +> +``` + +从结果中可以看出 tobycollection集合已被删除。 + +## MongoDB 插入文档 + +文档的数据结构和 JSON 基本一样。 + +所有存储在集合中的数据都是 BSON 格式。 + +BSON 是一种类似 JSON 的二进制形式的存储格式,是 Binary JSON 的简称。 + +### 插入文档 + +MongoDB 使用 insert() 或 save() 方法向集合中插入文档,语法如下: + +``` +db.COLLECTION_NAME.insert(document) +或 +db.COLLECTION_NAME.save(document) +``` + +- save():如果 _id 主键存在则更新数据,如果不存在就插入数据。该方法新版本中已废弃,可以使用 **db.collection.insertOne()** 或 **db.collection.replaceOne()** 来代替。 +- insert(): 若插入的数据主键已经存在,则会抛 **org.springframework.dao.DuplicateKeyException** 异常,提示主键重复,不保存当前数据。 + +### 实例 + +以下文档可以存储在 MongoDB 的 tobytest 数据库 的 col 集合中: + +``` +> db.col.insert({title:'Toby MongoDB', +... description:'this is MongoDB', +... tags:['mongodb','database','NoSQL'], +... likes:1 +... }) +WriteResult({ "nInserted" : 1 }) +> +``` + +以上实例中 col 是我们的集合名,如果该集合不在该数据库中, MongoDB 会自动创建该集合并插入文档。 + +查看已插入文档: + +``` +> db.col.find() +{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +> +``` + +我们也可以将数据定义为一个变量,如下所示: + +``` +> document=({title:'Toby another MongoDB', +... description:'this is another MongoDB', +... tags:['mongodb','database','NoSQL'], +... likes:2 +... }) +``` + +执行后显示结果如下: + +``` +{ + "title" : "Toby another MongoDB", + "description" : "this is another MongoDB", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 2 +} +``` + +执行插入操作: + +``` +> db.col.insert(document) +WriteResult({ "nInserted" : 1 }) +> db.col.find() +{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 } +> +``` + +## MongoDB 更新文档 + +MongoDB 使用 **update()** 和 **save()** 方法来更新集合中的文档。接下来让我们详细来看下两个函数的应用及其区别。 + +------ + +### update() 方法 + +update() 方法用于更新已存在的文档。语法格式如下: + +``` +db.collection.update( + , + , + { + upsert: , + multi: , + writeConcern: + } +) +``` + +**参数说明:** + +- **query** : update的查询条件,类似sql update查询内where后面的。 +- **update** : update的对象和一些更新的操作符(如$,$inc...)等,也可以理解为sql update查询内set后面的 +- **upsert** : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。 +- **multi** : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。 +- **writeConcern** :可选,抛出异常的级别。 + +### 实例 + +我们在集合 col 中插入如下数据: + +``` +> db.col.insert({title:'Toby MongoDB', +... description:'this is MongoDB', +... tags:['mongodb','database','NoSQL'], +... likes:1 +... }) +WriteResult({ "nInserted" : 1 }) +> +``` + +接着我们通过 update() 方法来更新标题(title): + +``` +> db.col.update({'title':'Toby MongoDB'},{$set:{'title':'MongoDB'}}) +WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) +> db.col.find().pretty() +{ + "_id" : ObjectId("617970fc286e9ff2b1250d70"), + "title" : "MongoDB", + "description" : "this is MongoDB", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 1 +} +{ + "_id" : ObjectId("61797229286e9ff2b1250d71"), + "title" : "Toby another MongoDB", + "description" : "this is another MongoDB", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 2 +} +> +``` + +可以看到标题(title)由原来的 "Toby MongoDB" 更新为了 "MongoDB"。 + +## MongoDB 删除文档 + +MongoDB remove() 函数是用来移除集合中的数据。 + +MongoDB 数据更新可以使用 update() 函数。在执行 remove() 函数前先执行 find() 命令来判断执行的条件是否正确,这是一个比较好的习惯。 + +### 语法 + +remove() 方法的基本语法格式如下所示: + +``` +db.collection.remove( + , + +) +``` + +如果你的 MongoDB 是 2.6 版本以后的,语法格式如下: + +``` +db.collection.remove( + , + { + justOne: , + writeConcern: + } +) +``` + +**参数说明:** + +- **query** :(可选)删除的文档的条件。 +- **justOne** : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。 +- **writeConcern** :(可选)抛出异常的级别。 + +### 实例 + +以下文档我们执行两次插入操作: + +``` +> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB', tags:['mongodb','database','NoSQL'], likes:1 }) +WriteResult({ "nInserted" : 1 }) +> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB', tags:['mongodb','database','NoSQL'], likes:1 }) +WriteResult({ "nInserted" : 1 }) +> +``` + +使用 find() 函数查询数据: + +``` +> db.col.find() +{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 } +{ "_id" : ObjectId("6179747d286e9ff2b1250d72"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +{ "_id" : ObjectId("61797481286e9ff2b1250d73"), "title" : "Toby MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +> +``` + +接下来我们移除 title 为 'Toby MongoDB' 的文档: + +``` +> db.col.remove({'title':'Toby MongoDB'}) +WriteResult({ "nRemoved" : 2 }) # 删除了两个 +> db.col.find() +{ "_id" : ObjectId("617970fc286e9ff2b1250d70"), "title" : "MongoDB", "description" : "this is MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 1 } +{ "_id" : ObjectId("61797229286e9ff2b1250d71"), "title" : "Toby another MongoDB", "description" : "this is another MongoDB", "tags" : [ "mongodb", "database", "NoSQL" ], "likes" : 2 } +> +``` + +如果你只想删除第一条找到的记录可以设置 justOne 为 1,如下所示: + +``` +>db.COLLECTION_NAME.remove(DELETION_CRITERIA,1) +``` + +如果你想删除所有数据,可以使用以下方式(类似常规 SQL 的 truncate 命令): + +``` +> db.col.remove({}) +WriteResult({ "nRemoved" : 2 }) +> db.col.find() +> +``` + +## MongoDB 查询文档 + +MongoDB 查询文档使用 find() 方法。 + +find() 方法以非结构化的方式来显示所有文档。 + +### 语法 + +MongoDB 查询数据的语法格式如下: + +``` +db.collection.find(query, projection) +``` + +- **query** :可选,使用查询操作符指定查询条件 +- **projection** :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。 + +如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下: + +``` +>db.col.find().pretty() +``` + +pretty() 方法以格式化的方式来显示所有文档。 + +### 实例 + +以下实例我们查询了集合 col 中的数据: + +``` +> db.col.insert({title:'Toby MongoDB', description:'this is MongoDB',by:'Toby', tags:['mongodb','database','NoSQL'], likes:100 }) +WriteResult({ "nInserted" : 1 }) +> db.col.find().pretty() +{ + "_id" : ObjectId("6179772f286e9ff2b1250d75"), + "title" : "Toby MongoDB", + "description" : "this is MongoDB", + "by" : "Toby", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 100 +} +> +``` + +除了 find() 方法之外,还有一个 findOne() 方法,它只返回一个文档。 + +### MongoDB AND 条件 + +MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件。 + +语法格式如下: + +``` +>db.col.find({key1:value1, key2:value2}).pretty() +``` + +#### 实例 + +以下实例通过 **by** 和 **title** 键来查询 **Toby** 中 **Toby MongoDB** 的数据 + +``` +> db.col.find({'by':'Toby','title':'Toby MongoDB'}).prettydb.col.find({'by':'Toby','title':'Toby MongoDB'}).pretty() +{ + "_id" : ObjectId("6179772f286e9ff2b1250d75"), + "title" : "Toby MongoDB", + "description" : "this is MongoDB", + "by" : "Toby", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 100 +} +> +``` + +以上实例中类似于 WHERE 语句:**WHERE by='Toby' AND title='Toby MongoDB'** + +------ + +### MongoDB OR 条件 + +MongoDB OR 条件语句使用了关键字 **$or**,语法格式如下: + +``` +>db.col.find( + { + $or: [ + {key1: value1}, {key2:value2} + ] + } +).pretty() +``` + +#### 实例 + +以下实例中,我们演示了查询键 **by** 值为 **Toby**或键 **title** 值为 **Toby MongoDB** 的文档。 + +``` +> db.col.find({$or:[{"by":"Toby"},{"title":"Toby MongoDB"}]}).pretty() +{ + "_id" : ObjectId("6179772f286e9ff2b1250d75"), + "title" : "Toby MongoDB", + "description" : "this is MongoDB", + "by" : "Toby", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 100 +} +> +``` + +------ + +### AND 和 OR 联合使用 + +以下实例演示了 AND 和 OR 联合使用,类似常规 SQL 语句为: **'where likes>50 AND (by = 'Toby' OR title = 'Toby MongoDB')'** + +``` +> db.col.find({"likes":{$gt:50},$or:[{"by":"Toby"},{"title":"Toby MongoDB"}]}).pretty() +{ + "_id" : ObjectId("6179772f286e9ff2b1250d75"), + "title" : "Toby MongoDB", + "description" : "this is MongoDB", + "by" : "Toby", + "tags" : [ + "mongodb", + "database", + "NoSQL" + ], + "likes" : 100 +} +> +``` + +## MongoDB 排序 + +------ + +### MongoDB sort() 方法 + +在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。 + +#### 语法 + +sort()方法基本语法如下所示: + +``` +>db.COLLECTION_NAME.find().sort({KEY:1}) +``` + +#### 实例 + +col 集合中的数据如下: + +``` +> db.col.find().pretty() +{ + "_id" : ObjectId("61797a56286e9ff2b1250d78"), + "title" : "Toby PHP", + "description" : "this is PHP", + "by" : "Toby", + "tags" : [ + "PHP", + "Language" + ], + "likes" : 100 +} +{ + "_id" : ObjectId("61797a62286e9ff2b1250d79"), + "title" : "Toby JAVA", + "description" : "this is JAVA", + "by" : "Toby", + "tags" : [ + "JAVA", + "Language" + ], + "likes" : 50 +} +{ + "_id" : ObjectId("61797a83286e9ff2b1250d7a"), + "title" : "Toby Python", + "description" : "this is Python", + "by" : "Toby", + "tags" : [ + "Python", + "Language" + ], + "likes" : 20 +} +> +``` + +以下实例演示了 col 集合中的数据按字段 likes 的降序排列: + +``` +> db.col.find({},{'title':1,_id:0}).sort({"likes":-1}) +{ "title" : "Toby PHP" } +{ "title" : "Toby JAVA" } +{ "title" : "Toby Python" } +> +``` + +# Python MongoDB + +------ + +## PyMongo + +Python 要连接 MongoDB 需要 MongoDB 驱动,这里我们使用 PyMongo 驱动来连接。 + +### pip 安装 + +pip 是一个通用的 Python 包管理工具,提供了对 Python 包的查找、下载、安装、卸载的功能。 + +安装 pymongo: + +``` +$ python3 -m pip install pymongo +``` + +### 测试 PyMongo + +接下来我们可以创建一个测试文件 demo_test_mongodb.py,代码如下: + +``` +import pymongo +``` + +执行以上代码文件,如果没有出现错误,表示安装成功。 + +## 创建数据库 + +### 创建一个数据库 + +创建数据库需要使用 MongoClient 对象,并且指定连接的 URL 地址和要创建的数据库名。 + +如下实例中,我们创建的数据库 pydb: + +#### 实例 + +```python +import pymongo +myclient=pymongo.MongoClient("mongodb://localhost:27017/") +mydb=myclient["pydb"] +``` + +> **注意:** 在 MongoDB 中,数据库只有在内容插入后才会创建! 就是说,数据库创建后要创建集合(数据表)并插入一个文档(记录),数据库才会真正创建。 + +### 判断数据库是否已存在 + +我们可以读取 MongoDB 中的所有数据库,并判断指定的数据库是否存在: + +#### 实例 + +```python +import pymongo +myclient=pymongo.MongoClient("mongodb://localhost:27017/") +mydb=myclient["pydb"] + +dblist = myclient.list_database_names() +# dblist = myclient.database_names() +if "pydb" in dblist: + print("数据库已存在!") +else: + print('数据库不存在') +``` + +> **注意:**database_names 在最新版本的 Python 中已废弃,Python3.7+ 之后的版本改为了 list_database_names()。 + + + +![image-20211030141217841](https://i.loli.net/2021/11/02/K4oZ3xvmiGXUWsQ.png) + +## 创建集合 + +MongoDB 中的集合类似 SQL 的表。 + +### 创建一个集合 + +MongoDB 使用数据库对象来创建集合,实例如下: + +#### 实例 + +```python +import pymongo +myclient=pymongo.MongoClient("mongodb://localhost:27017/") +mydb=myclient["pydb"] + +mycol=myclient["col_set"] +``` + + + +> **注意:** 在 MongoDB 中,集合只有在内容插入后才会创建! 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。 + +### 判断集合是否已存在 + +我们可以读取 MongoDB 数据库中的所有集合,并判断指定的集合是否存在: + +#### 实例 + +```python +import pymongo +myclient=pymongo.MongoClient("mongodb://localhost:27017/") +mydb=myclient["pydb"] + +mycol=myclient["col_set"] + +collist = mydb. list_collection_names() +if "col_set" in collist: # 判断 sites 集合是否存在 + print("集合已存在!") +else: + print('集合不存在') +``` + +![image-20211030141526295](https://i.loli.net/2021/11/02/K7mJARPe1dM2Yos.png) + +## Python Mongodb 插入文档 + +MongoDB 中的一个文档类似 SQL 表中的一条记录。 + +### 插入集合 + +集合中插入文档使用 **insert_one()** 方法,该方法的第一参数是字典 **name => value** 对。 + +以下实例向 **col_set** 集合中插入文档: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +mydict = { "name": "Toby", "age": "23", "url": "https://juejin.cn/user/3403743731649863" } + +x = mycol.insert_one(mydict) +print(x) +``` + +![image-20211030142137931](https://i.loli.net/2021/11/02/yY6EmCx4PfLolFQ.png) + +在命令行看一下是否插入成功 + +``` +> use pydb +switched to db pydb +> db.col_set.find() +{ "_id" : ObjectId("617ce42cbc6011eaf1529012"), "name" : "Toby", "url" : "https://juejin.cn/user/3403743731649863", "age" : "23" } +> +``` + +### 插入多个文档 + +集合中插入多个文档使用 **insert_many()** 方法,该方法的第一参数是字典列表。 + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +mylist = [ + { "name": "Tom", "age": "100", "url": "https://juejin.cn/user/3403743731649863" }, + { "name": "Mary", "age": "101", "url": "https://juejin.cn/user/3403743731649863" }, + { "name": "Timi", "age": "10", "url": "https://juejin.cn/user/3403743731649863" }, +] + +x = mycol.insert_many(mylist) + +# 输出插入的所有文档对应的 _id 值 +print(x.inserted_ids) +``` + +![image-20211030142656115](https://i.loli.net/2021/11/02/7sS9XRKqUCFnrh6.png) + +在命令行看一下是否插入成功 + +``` +> use pydb +switched to db pydb +> db.col_set.find() +{ "_id" : ObjectId("617ce42cbc6011eaf1529012"), "name" : "Toby", "url" : "https://juejin.cn/user/3403743731649863", "age" : "23" } +{ "_id" : ObjectId("617ce591826d13d898f97890"), "name" : "Tom", "url" : "https://juejin.cn/user/3403743731649863", "age" : "100" } +{ "_id" : ObjectId("617ce591826d13d898f97891"), "name" : "Mary", "url" : "https://juejin.cn/user/3403743731649863", "age" : "101" } +{ "_id" : ObjectId("617ce591826d13d898f97892"), "name" : "Timi", "url" : "https://juejin.cn/user/3403743731649863", "age" : "10" } +> +``` + +## Python Mongodb 查询文档 + +MongoDB 中使用了 find 和 find_one 方法来查询集合中的数据,它类似于 SQL 中的 SELECT 语句。 + +### 查询一条数据 + +我们可以使用 **find_one()** 方法来查询集合中的一条数据。 + +查询 **col_set** 文档中的第一条数据: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +x = mycol.find_one() + +print(x) +``` + +![image-20211030142943707](https://i.loli.net/2021/11/02/F8GOH7PIiVUyA4J.png) + +### 查询集合中所有数据 + +**find()** 方法可以查询集合中的所有数据,类似 SQL 中的 **SELECT \*** 操作。 + +以下实例查找 **col_set** 集合中的所有数据: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +for x in mycol.find(): + print(x) +``` + +![image-20211030143207556](https://i.loli.net/2021/11/02/7kQH6zy5EjChqx1.png) + +### 查询指定字段的数据 + +我们可以使用 find() 方法来查询指定字段的数据,将要返回的字段对应值设置为 1。 + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +for x in mycol.find({},{ "_id": 0, "name": 1, "age": 1 }): + print(x) +``` + +![image-20211030144042132](https://i.loli.net/2021/11/02/DbfneXgkLsFoIQJ.png) + +### 根据指定条件查询 + +我们可以在 **find()** 中设置参数来过滤数据。 + +以下实例查找 name 字段为 "Toby" 的数据: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +myquery = { "name": "Toby" } + +mydoc = mycol.find(myquery) + +for x in mydoc: + print(x) +``` + +![image-20211030144414902](https://i.loli.net/2021/11/02/nYx3mH5oZfNdLAu.png) + +### 返回指定条数记录 + +如果我们要对查询结果设置指定条数的记录可以使用 **limit()** 方法,该方法只接受一个数字参数。 + +以下实例返回 3 条文档记录: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +myresult = mycol.find().limit(3) + +# 输出结果 +for x in myresult: + print(x) +``` + +![image-20211030144609160](https://i.loli.net/2021/11/02/hpztCWj49APuIZr.png) + +## Python Mongodb 修改文档 + +我们可以在 MongoDB 中使用 **update_one()** 方法修改文档中的记录。该方法第一个参数为查询的条件,第二个参数为要修改的字段。 + +如果查找到的匹配数据多于一条,则只会修改第一条。 + +以下实例将 age字段的值 23改为 12345: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +myquery = { "age": "23" } +newvalues = { "$set": { "age": "12345" } } + +mycol.update_one(myquery, newvalues) + +# 输出修改后的 "sites" 集合 +for x in mycol.find(): + print(x) +``` + +![image-20211030144819907](https://i.loli.net/2021/11/02/Lun1miz7sFH6SJZ.png) + +## 排序 + +**sort()** 方法可以指定升序或降序排序。 + +**sort()** 方法第一个参数为要排序的字段,第二个字段指定排序规则,**1** 为升序,**-1** 为降序,默认为升序。 + +对字段 age 按升序排序: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +mydoc = mycol.find().sort("age") +for x in mydoc: + print(x) +``` + +![image-20211030145059219](https://i.loli.net/2021/11/02/QGZ6B4AsMqSei3W.png) + +对字段 age按降序排序: + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +mydoc = mycol.find().sort("alexa", -1) + +for x in mydoc: + print(x) +``` + +![image-20211030145239034](https://i.loli.net/2021/11/02/B3v5Dkh6fYoQnTj.png) + +## Python Mongodb 删除数据 + +我们可以使用 **delete_one()** 方法来删除一个文档,该方法第一个参数为查询对象,指定要删除哪些数据。 + +以下实例删除 name 字段值为 "Timi" 的文档: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +myquery = { "name": "Timi" } + +mycol.delete_one(myquery) + +# 删除后输出 +for x in mycol.find(): + print(x) +``` + +![image-20211030145408484](https://i.loli.net/2021/11/02/crw3HJN2vQzyBW6.png) + +### 删除集合中的所有文档 + +**delete_many()** 方法如果传入的是一个空的查询对象,则会删除集合中的所有文档: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +x = mycol.delete_many({}) + +print(x.deleted_count, "个文档已删除") +``` + +![image-20211030145528857](https://i.loli.net/2021/11/02/a7l5NsKAJhVBcPk.png) + +## 删除集合 + +我们可以使用 **drop()** 方法来删除一个集合。 + +以下实例删除了 col_set集合: + +#### 实例 + +```python +import pymongo + +myclient = pymongo.MongoClient("mongodb://localhost:27017/") +mydb = myclient["pydb"] +mycol = mydb["col_set"] + +mycol.drop() + +``` + +我们在终端查看一下 + +``` +> use pydb +switched to db pydb +> show tables +system.indexes +> +``` + + + +# 总结 + +本文主要介绍了MongoDB数据库的相关概念及基本操作,为了更好的了解MongoDB在新闻推荐系统中的应用,需要了解数据库的相关概念并熟练使用python操作MongoDB。 + + + +# 参考资料 + +* https://www.runoob.com/python3/python-mongodb.html + +* https://www.runoob.com/mongodb/mongodb-tutorial.html + diff --git a/docs/推荐系统实战/新闻推荐系统实践/Mysql基础.md b/docs/推荐系统实战/新闻推荐系统实践/Mysql基础.md new file mode 100644 index 00000000..6ecdf78d --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/Mysql基础.md @@ -0,0 +1,2313 @@ +本文属于新闻推荐实战—数据层—构建物料池之MySQL。MySQL数据库在该项目中会用来存储结构化的数据(用户、新闻特征),作为算法工程师需要了解常用的MySQL语法(比如增删改查,排序等),因为在实际的工作经常会用来统计相关数据或者抽取相关特征。本着这个目的,本文对MySQL常见的语法及Python操作MySQL进行了总结,方便大家快速了解。 + +# 前言 MySQL简介 + + MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。 + + MySQL在过去由于性能高、成本低、可靠性好,已经成为最流行的开源数据库,因此被广泛地应用在Internet上的中小型网站中。随着MySQL的不断成熟,它也逐渐用于更多大规模网站和应用,比如维基百科、Google和Facebook等网站。非常流行的开源软件组合LAMP中的“M”指的就是MySQL。 + +[百度百科]: https://baike.baidu.com/item/mySQL/471251 +[维基百科]: https://zh.wikipedia.org/wiki/MySQL + + + +# 一、 Ubuntu下安装MySQL + +安装教程是在`Ubuntu20.04`下进行的,安装的MySQL版本为`8.0.27`。 + +## 1.1 安装 + +```bash +sudo apt install mysql-server mysql-client +``` + +在输入密码后,再输入`yes`即可开始安装。 + +安装完成后,通过运行命令`mysql -V`查看版本号: + +```bash +lyons@ubuntu:~$ mysql -V +mysql Ver 8.0.27-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu)) +``` + +验证MySQL服务正在运行,命令行下输入: + +```bash +sudo service mysql status +``` + +如果正在运行,则会显示: + +```bash +● mysql.service - MySQL Community Server + Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled) + Active: active (running) since Wed 2021-10-27 10:27:59 CST; 9h ago + Main PID: 6179 (mysqld) + Status: "Server is operational" + Tasks: 39 (limit: 4599) + Memory: 348.9M + CGroup: /system.slice/mysql.service + └─6179 /usr/sbin/mysqld + +10月 27 10:27:59 ubuntu systemd[1]: Starting MySQL Community Server... +10月 27 10:27:59 ubuntu systemd[1]: Started MySQL Community Server. +``` + + + +## 1.2 配置MySQL的安全性 + +1. 首先,运行命令`mysql_secure_installation`: + + ```bash + sudo mysql_secure_installation + ``` + +2. `VALIDATE PASSWORD COMPONENT` + + 设置验证密码插件。它被用来测试`MySQL`用户的密码强度,并且提高安全性。如果想设置验证密码插件,请输入`y`: + + ```bash + Connecting to MySQL using a blank password. + + VALIDATE PASSWORD COMPONENT can be used to test passwords + and improve security. It checks the strength of password + and allows the users to set only those passwords which are + secure enough. Would you like to setup VALIDATE PASSWORD component? + + Press y|Y for Yes, any other key for No: y + ``` + + 接下来,将进行密码验证等级设置,根据数字设置对应等级,这里设置为0: + + ```bash + There are three levels of password validation policy: + + LOW Length >= 8 + MEDIUM Length >= 8, numeric, mixed case, and special characters + STRONG Length >= 8, numeric, mixed case, special characters and dictionary file + + Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 0 + ``` + +3. 设置密码 + + 为MySQL root用户设置密码,设置过程中密码不会显示。如果设置了验证密码插件,将会显示密码的强度。 + + ``` + Please set the password for root here. + New password: + + Re-enter new password: + + Estimated strength of the password: 25 + Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y + ``` + +4. 移除匿名用户 + + 默认情况下,MySQL安装有一个匿名用户,允许任何人登录MySQL,而不必为他们创建用户帐户。输入`y`进行删除: + + ``` + By default, a MySQL installation has an anonymous user, + allowing anyone to log into MySQL without having to have + a user account created for them. This is intended only for + testing, and to make the installation go a bit smoother. + You should remove them before moving into a production + environment. + + Remove anonymous users? (Press y|Y for Yes, any other key for No) : y + Success. + ``` + +5. 禁止远程root用户登录 + + 输入`y`后按`enter`,将会禁止`root`用户登录。 + + ``` + Normally, root should only be allowed to connect from + 'localhost'. This ensures that someone cannot guess at + the root password from the network. + + Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y + Success. + ``` + +6. 删除测试库 + + 输入`y`后按`enter`,将会删除测试库。 + + ``` + By default, MySQL comes with a database named 'test' that + anyone can access. This is also intended only for testing, + and should be removed before moving into a production + environment. + + + Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y + - Dropping test database... + Success. + ``` + +7. 重新加载特权表 + + 输入`y`后按`enter`,将会重新加载特权表。 + + ``` + - Removing privileges on test database... + Success. + + Reloading the privilege tables will ensure that all changes + made so far will take effect immediately. + + Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y + Success. + + All done! + ``` + + 至此,配置完成。 + + + +## 1.3 以root用户登录 + +在MySQL 8.0上,root 用户默认通过`auth_socket`插件授权。`auth_socket`插件通过 Unix socket 文件来验证所有连接到`localhost`的用户。 + +这意味着你不能通过提供密码,验证为 root。此时,输入`mysql -uroot -p`可能会被拒绝访问: + +```bash +lyons@ubuntu:~$ mysql -uroot -p +mysql: [Warning] Using a password on the command line interface can be insecure. +ERROR 1698 (28000): Access denied for user 'root'@'localhost' +``` + +若要以 root 用户身份登录 MySQL服务器,输入`sudo mysql`,如下: + +```bash +# 登录密码为linux系统用户的root密码 +lyons@ubuntu:~$ sudo mysql +[sudo] lyons 的密码: +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 55 +Server version: 8.0.27-0ubuntu0.20.04.1 (Ubuntu) + +Copyright (c) 2000, 2021, Oracle and/or its affiliates. + +Oracle is a registered trademark of Oracle Corporation and/or its +affiliates. Other names may be trademarks of their respective +owners. + +Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + +mysql> +``` + +退出MySQL,请输入`exit`命令: + +```mysql +mysql> exit +Bye +lyons@ubuntu:~$ +``` + +如果你想以 root 身份登录 MySQL 服务器,便于使用其他的程序。可以将验证方法从`auth_socket`修改成`mysql_native_password`。 + ++ **方式1** + +你可以通过运行下面的命令实现: + +```bash +-- 语法中的'你的密码’指的是你自己设置的登录密码,可设置为字母数字组合。 +ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; +FLUSH PRIVILEGES; +``` + +示例: + +```mysql +-- 在mysql下,将密码设置为'mysql123' +mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysql123'; +Query OK, 0 rows affected (0.00 sec) + +-- 刷新系统权限 +mysql> FLUSH PRIVILEGES; +Query OK, 0 rows affected (0.01 sec) + +mysql> exit +Bye + +-- 现在便可以通过mysql -uroot -p登录 +-- 登录密码为前面设置的'mysql123' +lyons@ubuntu:~$ mysql -uroot -p +Enter password: +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 57 +Server version: 8.0.27-0ubuntu0.20.04.1 (Ubuntu) + +Copyright (c) 2000, 2021, Oracle and/or its affiliates. + +Oracle is a registered trademark of Oracle Corporation and/or its +affiliates. Other names may be trademarks of their respective +owners. + +Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + +mysql> +mysql> exit +Bye + +-- 同时,命令sudo mysql会被拒绝访问 +lyons@ubuntu:~$ sudo mysql +ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO) +``` + +当然,若要再次修改回`sudo mysql`的方式来登录`root`用户,方法类似: + +```mysql +ALTER USER 'root'@'localhost' IDENTIFIED WITH auth_socket BY '你的密码'; + +FLUSH PRIVILEGES; +``` + ++ **方式2** + + 推荐的选项,就是创建一个新的独立管理用户,拥有所有数据库的访问权限: + +```mysql +# 创建用户 +CREATE USER '用户名'@'localhost' identified by '你的密码' + +# 赋予admin用户全部的权限,你也可以只授予部分权限 +GRANT ALL PRIVILEGES ON *.* TO '用户名'@'localhost'; +``` + +​ 示例: + +```mysql +# 创建名为admin的用户,密码为mysql123 +mysql> create user 'admin'@'localhost' identified by 'mysql123'; +Query OK, 0 rows affected (0.01 sec) + +# 将访问所有database以及表的权利授权用户admin +#with gran option表示该用户可给其它用户赋予权限,但不可能超过该用户已有的权限 +mysql> grant all privileges on *.* to 'admin'@'localhost' with grant option; +Query OK, 0 rows affected (0.00 sec) + +mysql> FLUSH PRIVILEGES; +Query OK, 0 rows affected (0.00 sec) + +# 查看已有的用户 +mysql> select user, host from mysql.user; ++------------------+-----------+ +| user | host | ++------------------+-----------+ +| admin | localhost | +| debian-sys-maint | localhost | +| mysql.infoschema | localhost | +| mysql.session | localhost | +| mysql.sys | localhost | +| root | localhost | ++------------------+-----------+ +6 rows in set (0.00 sec) + +# 退出root用户登录 +mysql> exit +Bye + +# 登录admin用户,输入密码mysql123即可登录成功 +lyons@ubuntu:~$ mysql -uadmin -p +Enter password: +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 16 +Server version: 8.0.27-0ubuntu0.20.04.1 (Ubuntu) + +Copyright (c) 2000, 2021, Oracle and/or its affiliates. + +Oracle is a registered trademark of Oracle Corporation and/or its +affiliates. Other names may be trademarks of their respective +owners. + +Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + +mysql> +``` + +说明:`'admin'@'localhost'`中,`localhost`指本地才可连接,可以将其换成`%`指任意`ip`都能连接,也可以指定`ip`连接。 + + + +## 1.4 修改密码 + +将用户`admin`的登录密码修改为`mysql321`: + +```mysql +ALTER USER 'admin'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysql321'; +``` + + + +## 1.5 撤销用户授权 + +```mysql +# 查看用户的权限 +show grants for 'admin'@'localhost'; + +# 撤销用户的权限 +# 用户有什么权限就撤销什么 +revoke all privileges on *.* from 'admin'@'localhost'; +``` + + + +## 1.6 删除用户 + +```MYSQL +drop user 'admin'@'localhost'; +``` + + + +> 注:MySQL 8.0版本和5.0部分命令有所改掉,上述语法都是在8.0版本下运行通过的;请务必检查自己的MySQL版本号。 + + + +# 二、MySQL预备知识 + +在正式学习MySQL之前,我们先来了解一下SQL语句的书写规范以及命名规则等。 + +## 2.1 SQL书写规范 + +在写SQL语句时,要求按照如下规范进行: + ++ SQL 语句要以分号(;)结尾 + ++ SQL 不区分关键字的大小写 ,这对于表名和列名同样适用。 + ++ 插入到表中的数据是区分大小写的。例如,数据Computer、COMPUTER 或computer,三者是不一样的。 + ++ 常数的书写方式是固定的,在SQL 语句中直接书写的字符串、日期或者数字等称为常数。常数的书写方式如下所示。 + + + SQL 语句中含有字符串的时候,需要像'abc'这样,使用单引号(')将字符串括起来,用来标识这是一个字符串。 + + SQL 语句中含有日期的时候,同样需要使用单引号将其括起来。日期的格式有很多种('26 Jan 2010' 或者'10/01/26' 等)。 + + 在SQL 语句中书写数字的时候,不需要使用任何符号标识,直接写成1000 这样的数字即可。 + ++ 单词之间需要用半角空格或者换行来分隔。 + ++ SQL中的注释主要采用`--`和`/* ... */`的方式,第二种方式可以换行。在MySQL下,还可以通过`#`来进行注释。 + + + +## 2.2 命名规则 + ++ 在数据库中,只能使用半角英文字母、数字、下划线(_)作为数据库、表和列的名称 。 ++ 名称必须以半角英文字母作为开头。 ++ 名称不能重复,同一个数据库下不能有2张相同的表。 + + + +## 2.3. 数据类型 + +MySQL 支持所有标准 SQL 数值数据类型,包括: + +### (1)数值类型 + +数值包含的类型如下: + ++ 整型数据:`TINYINT`、`INTEGER`、`SMALLINT`、`MEDIUMINT`、`DECIMAL` 、`NUMERIC` 和`BIGINT`。 + ++ 浮点型数据:`DECIMAL`、`FLOAT`、`REAL` 和 `DOUBLE PRECISION`)。 + +其中,关键字`INT`是`INTEGER`的同义词,关键字`DEC`是`DECIMAL`的同义词。 + +不同关键字的主要区别就是表示的范围或精度不一样。具体如下表: + +| 类型 | 大小 | 范围(有符号) | 范围(无符号) | 用途 | +| :----------: | :--------------------------------------: | :----------------------------------------------------------- | :----------------------------------------------------------- | :-------------- | +| TINYINT | 1 Bytes | (-128,127) | (0,255) | 小整数值 | +| SMALLINT | 2 Bytes | (-32 768,32 767) | (0,65 535) | 大整数值 | +| MEDIUMINT | 3 Bytes | (-8 388 608,8 388 607) | (0,16 777 215) | 大整数值 | +| INT或INTEGER | 4 Bytes | (-2 147 483 648,2 147 483 647) | (0,4 294 967 295) | 大整数值 | +| BIGINT | 8 Bytes | (-9,223,372,036,854,775,808,9 223 372 036 854 775 807) | (0,18 446 744 073 709 551 615) | 极大整数值 | +| FLOAT | 4 Bytes | (-3.402 823 466 E+38,-1.175 494 351 E-38),0,(1.175 494 351 E-38,3.402 823 466 351 E+38) | 0,(1.175 494 351 E-38,3.402 823 466 E+38) | 单精度 浮点数值 | +| DOUBLE | 8 Bytes | (-1.797 693 134 862 315 7 E+308,-2.225 073 858 507 201 4 E-308),0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 0,(2.225 073 858 507 201 4 E-308,1.797 693 134 862 315 7 E+308) | 双精度 浮点数值 | +| DECIMAL | 对DECIMAL(M,D) ,如果M>D,为M+2否则为D+2 | 依赖于M和D的值 | 依赖于M和D的值 | 小数值 | + +### (2)日期和时间类型 + +表示时间值的日期和时间类型为`DATETIME`、`DATE`、`TIMESTAMP`、`TIME`和`YEAR`。具体如下表: + +| 类型 | 大小 ( bytes) | 范围 | 格式 | 用途 | +| :-------- | :------------ | :----------------------------------------------------------- | :------------------ | :----------------------- | +| DATE | 3 | 1000-01-01/9999-12-31 | YYYY-MM-DD | 日期值 | +| TIME | 3 | '-838:59:59'/'838:59:59' | HH:MM:SS | 时间值或持续时间 | +| YEAR | 1 | 1901/2155 | YYYY | 年份值 | +| DATETIME | 8 | 1000-01-01 00:00:00/9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 | +| TIMESTAMP | 4 | 1970-01-01 00:00:00/2038结束时间是第 2147483647 秒,北京时间 2038-1-19 11:14:07,格林尼治时间 2038年1月19日 凌晨 03:14:07 | YYYYMMDD HHMMSS | 混合日期和时间值,时间戳 | + +### (3)字符串类型 + +字符串类型指`CHAR`、`VARCHAR`、`BINARY`、`VARBINARY`、`BLOB`、`TEXT`、`ENUM`和`SET`。具体如下表: + +| 类型 | 大小 | 用途 | +| :--------- | :-------------------- | :------------------------------ | +| CHAR | 0-255 bytes | 定长字符串 | +| VARCHAR | 0-65535 bytes | 变长字符串 | +| TINYBLOB | 0-255 bytes | 不超过 255 个字符的二进制字符串 | +| TINYTEXT | 0-255 bytes | 短文本字符串 | +| BLOB | 0-65 535 bytes | 二进制形式的长文本数据 | +| TEXT | 0-65 535 bytes | 长文本数据 | +| MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 | +| MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 | +| LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 | +| LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 | + ++ `char`声明的是定长字符串。若实际中字符串长度不足,则会在末尾使用空格进行填充至声明的长度。 + ++ `varchar`声明的是可变长字符串。存储过程中,只会按照字符串的实际长度来存储,但会多占用一位来存放实际字节的长度。 + + + +# 三、数据库的基本操作 + +首先,我们来学习在MySQL下如何操作数据库。 + +## 3.1 数据库的创建 + +通过`CREATE`命令,可以创建指定名称的数据库,语法结构如下: + +```mysql +CREATE DATABASE [IF NOT EXISTS] <数据库名称>; +``` + +MySQL 的数据存储区将以目录方式表示 MySQL 数据库,因此数据库名称必须符合操作系统的文件夹命名规则,不能以数字开头,尽量要有实际意义。 + +MySQL下不运行存在两个相同名字的数据库,否则会报错。如果使用`IF NOT EXISTS`(可选项),可以避免此类错误。 + +示例: + +```mysql +-- 创建名为shop的数据库。 +CREATE DATABASE shop; +``` + + + +## 3.2 数据库的查看 + +1. 查看所有存在的数据库 + +```MYSQL +SHOW DATABASES [LIKE '数据库名'];; +``` + +`LIKE`从句是可选项,用于匹配指定的数据库名称。`LIKE` 从句可以部分匹配,也可以完全匹配。 + +示例: + +```mysql +SHOW DATABASES; + +-- 结果如下: ++--------------------+ +| Database | ++--------------------+ +| information_schema | +| mysql | +| performance_schema | +| shop | +| sys | ++--------------------+ +5 rows in set (0.01 sec) +``` + +```mysql +-- %表示任意0个或多个字符,可匹配任意类型和长度的字符。 +SHOW DATABASES LIKE 'S%'; + +-- 结果如下 ++---------------+ +| Database (S%) | ++---------------+ +| shop | +| sys | ++---------------+ +2 rows in set (0.00 sec) +``` + +2. 查看创建的数据库 + +```mysql +SHOW CREATE DATABASE <数据库名>; +``` + +示例: + +```mysql +SHOW CREATE DATABASE shop; + +-- 或者 +SHOW CREATE DATABASE shop \G + +-- 结果如下 +*************************** 1. row *************************** + Database: shop +Create Database: CREATE DATABASE `shop` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */ +1 row in set (0.00 sec) +``` + +`CHARACTER SET utf8mb4`表示编码字符集为`utf8mb4`。 + + + +## 3.3 选择数据库 + +在操作数据库前,必须指定所要操作的数据库。通过`USE`命令,可以切换到对应的数据库下。 + +```mysql +USE <数据库名> +``` + +示例: + +```mysql +-- 切换到数据库shop下。 +USE shop; + +-- 结果如下 +Database changed +``` + + + +## 3.4 删除数据库 + +通过`DROP`命令,可以将相应数据库进行删除。 + +```mysql +DROP DATABASE [IF EXISTS] <数据库名> +``` + +其中,`IF EXISTS`为可选性,用于防止数据库不存在时报错。 + +示例: + +```mysql +DROP DATABASE shop; + +SHOW DATABASES; +``` + +考虑到后面表的操作都是shop数据库下,在实验完`DROP`删除数据库命令后,请从新创建数据库shop并通过`USE`命令切换到该数据库下。 + + + +# 四、表的基本操作 + +表相当于文件,表中的一条记录就相当于文件的一行内容,不同的是,表中的一条记录有对应的标题,称为表的字段。 + +## 4.1 表的创建 + +创建表的语法结构如下: + +```mysql +CREATE TABLE <表名> (<字段1> <数据类型> <该列所需约束>, + <字段2> <数据类型> <该列所需约束>, + <字段3> <数据类型> <该列所需约束>, + <字段4> <数据类型> <该列所需约束>, + . + . + . + <该表的约束1>, <该表的约束2>,……); +``` + +示例: + +```mysql +-- 创建一个名为Product的表 +CREATE TABLE Product( + product_id CHAR(4) NOT NULL, + product_name VARCHAR(100) NOT NULL, + product_type VARCHAR(32) NOT NULL, + sale_price INT, + purchase_price INT, + regist_date DATE, + PRIMARY KEY (product_id) +); +``` + +在第二章中,我们介绍过不同的数据类型: + ++ `CHAR`为定长字符,这里`CHAR`旁边括号里的数字表示该字段最长为多少字符,少于该数字将会使用空格进行填充。 + ++ `VARCHAR`表示变长字符,括号里的数字表示该字段最长为多少字符,存储时只会按照字符的实际长度来存储,但会使用额外的1-2字节来存储值长度。 + + + +简单介绍一下该语句中出现的约束条件,约束条件在后面会详细介绍: + ++ `PRIMARY KEY`:主键,表示该字段对应的内容唯一且不能为空。 ++ `NOT NULL`:在 `NULL` 之前加上了表示否定的` NOT`,表示该字段不能输入空白。 + +通过`SHOW TABLES`命令来查看当前数据库下的所有的表名: + +```mysql +SHOW TABLES; + +-- 结果如下 ++----------------+ +| Tables_in_shop | ++----------------+ +| Product | ++----------------+ +1 rows in set (0.00 sec) +``` + +通过`DESC <表名>`来查看表的结构: + +```mysql +DESC Product; + +-- 结果如下 ++----------------+--------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------------+--------------+------+-----+---------+-------+ +| product_id | char(4) | NO | PRI | NULL | | +| product_name | varchar(100) | NO | | NULL | | +| product_type | varchar(32) | NO | | NULL | | +| sale_price | int | YES | | NULL | | +| purchase_price | int | YES | | NULL | | +| regist_date | date | YES | | NULL | | ++----------------+--------------+------+-----+---------+-------+ +6 rows in set (0.00 sec) +``` + + + +## 4.2 表的删除 + +删除表的语法结构如下: + +```mysql +DROP TABLE <表名>; + +-- 例如:DROP TABLE Product; +``` + +说明:通过`DROP`删除的表示无法恢复的,在删除表的时候请谨慎。 + + + +## 4.3 表的更新 + +通过`ALTER TABLE`语句,我们可以对表字段进行不同的操作,下面通过示例来具体学习用法。 + +示例: + +1. 创建一张名为Student的表 + +```mysql +CREATE TABLE Student( + id INT PRIMARY KEY, + name CHAR(15) +); +``` + + + +```mysql +DESC student; + +-- 结果如下 ++-------+----------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++-------+----------+------+-----+---------+-------+ +| id | int | NO | PRI | NULL | | +| name | char(15) | YES | | NULL | | ++-------+----------+------+-----+---------+-------+ +2 rows in set (0.00 sec) +``` + +2. 更改表名 + + 通过`RENAME`命令,将表名从Student => Students。 + +```mysql +ALTER TABLE Student RENAME Students; +``` + +3. 插入新的字段 + + 通过`ADD`命令,新增字段sex和age。 + +```mysql +-- 不同的字段通过逗号分开 +ALTER TABLE Students ADD sex CHAR(1), ADD age INT; +``` + +​ 其它插入技巧: + +```mysql +-- 通过FIRST在表首插入字段stu_num +ALTER TABLE Students ADD stu_num INT FIRST; + +-- 指定在字段sex后插入字段height +ALTER TABLE Students ADD height INT AFTER sex; +``` + +```mysql +DESC Students; + +-- 结果如下 ++---------+----------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++---------+----------+------+-----+---------+-------+ +| stu_num | int | YES | | NULL | | +| id | int | NO | PRI | NULL | | +| name | char(15) | YES | | NULL | | +| sex | char(1) | YES | | NULL | | +| height | int | YES | | NULL | | +| age | int | YES | | NULL | | ++---------+----------+------+-----+---------+-------+ +6 rows in set (0.00 sec) +``` + +4. 字段的删除 + + 通过`DROP`命令,可以对不在需要的字段进行删除。 + +```mysql +-- 删除字段stu_num +ALTER TABLE Students DROP stu_num; +``` + +5. 字段的修改 + + 通过`MODIFY`修改字段的数据类型。 + +```mysql +-- 修改字段age的数据类型 +ALTER TABLE Students MODIFY age CHAR(3); +``` + +​ 通过`CHANGE`命令,修改字段名或类型 + +```mysql +-- 修改字段name为stu_name,不修改数据类型 +ALTER TABLE Students CHANGE name stu_name CHAR(15); + +-- 修改字段sex为stu_sex,数据类型修改为int +ALTER TABLE Students CHANGE sex stu_sex INT; +``` + +```mysql +DESC Students; + +-- 结果如下 ++----------+----------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------+----------+------+-----+---------+-------+ +| id | int | NO | PRI | NULL | | +| stu_name | char(20) | YES | | NULL | | +| stu_sex | int | YES | | NULL | | +| height | int | YES | | NULL | | +| age | char(3) | YES | | NULL | | ++----------+----------+------+-----+---------+-------+ +5 rows in set (0.00 sec) +``` + + + +## 4.4 表的查询 + +通过`SELECT`语句,可以从表中取出所要查看的字段的内容: + +```mysql +SELECT <字段名>, …… + FROM <表名>; +``` + +如要直接查询表的全部字段: + +```mysql +SELECT * + FROM <表名>; +``` + +其中,**星号(*)**代表全部字段的意思。 + +示例: + +1. 建表并插入数据 + + 在MySQL中,我们通过`INSERT`语句往表中插入数据,该语句在后面会详细介绍,该小节的重点是学会使用`SELECT`。 + +```mysql +-- 向Product表中插入数据 +INSERT INTO Product VALUES + ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20'), + ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11'), + ('0003', '运动T恤', '衣服', 4000, 2800, NULL), + ('0004', '菜刀', '厨房用具', 3000, 2800, '2009-09-20'), + ('0005', '高压锅', '厨房用具', 6800, 5000, '2009-01-15'), + ('0006', '叉子', '厨房用具', 500, NULL, '2009-09-20'), + ('0007', '擦菜板', '厨房用具', 880, 790, '2008-04-28'), + ('0008', '圆珠笔', '办公用品', 100, NULL,'2009-11-11') + ; +``` + +2. 查看表的内容 + +```mysql +-- 查看表的全部内容 +SELECT * + FROM Product; + +-- 结果如下 ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | T恤衫 | 衣服 | 1000 | 500 | 2009-09-20 | +| 0002 | 打孔器 | 办公用品 | 500 | 320 | 2009-09-11 | +| 0003 | 运动T恤 | 衣服 | 4000 | 2800 | NULL | +| 0004 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-09-20 | +| 0005 | 高压锅 | 厨房用具 | 6800 | 5000 | 2009-01-15 | +| 0006 | 叉子 | 厨房用具 | 500 | NULL | 2009-09-20 | +| 0007 | 擦菜板 | 厨房用具 | 880 | 790 | 2008-04-28 | +| 0008 | 圆珠笔 | 办公用品 | 100 | NULL | 2009-11-11 | ++------------+--------------+--------------+------------+----------------+-------------+ +8 rows in set (0.00 sec) +``` + +```mysql +-- 查看部分字段包含的内容 +SELECT + product_id, + product_name, + sale_price + FROM Product; + +-- 结果如下 ++------------+--------------+------------+ +| product_id | product_name | sale_price | ++------------+--------------+------------+ +| 0001 | T恤衫 | 1000 | +| 0002 | 打孔器 | 500 | +| 0003 | 运动T恤 | 4000 | +| 0004 | 菜刀 | 3000 | +| 0005 | 高压锅 | 6800 | +| 0006 | 叉子 | 500 | +| 0007 | 擦菜板 | 880 | +| 0008 | 圆珠笔 | 100 | ++------------+--------------+------------+ +8 rows in set (0.00 sec) +``` + +3. 对查看的字段从新命名 + + 通过`AS`语句对展示的字段另起别名,这不会修改表内字段的名字。 + +```mysql +SELECT + product_id AS ID, + product_type AS TYPE + FROM Product; + +-- 结果如下 ++------+--------------+ +| ID | TYPE | ++------+--------------+ +| 0001 | 衣服 | +| 0002 | 办公用品 | +| 0003 | 衣服 | +| 0004 | 厨房用具 | +| 0005 | 厨房用具 | +| 0006 | 厨房用具 | +| 0007 | 厨房用具 | +| 0008 | 办公用品 | ++------+--------------+ +8 rows in set (0.00 sec) +``` + +​ 设定汉语别名时需要使用双引号(")括起来,英文字符则不需要。 + +```Mysql +SELECT + product_id AS "产品编号", + product_type AS "产品类型" + FROM Product; +``` + +4. 常数的查询 + + `SELECT`子句中,除了可以写字段外,还可以写常数。 + +```mysql +SELECT + '商品' AS string, + '2009-05-24' AS date, + product_id, + product_name + FROM Product; + +-- 结果如下 ++--------+------------+------------+--------------+ +| string | date | product_id | product_name | ++--------+------------+------------+--------------+ +| 商品 | 2009-05-24 | 0001 | T恤衫 | +| 商品 | 2009-05-24 | 0002 | 打孔器 | +| 商品 | 2009-05-24 | 0003 | 运动T恤 | +| 商品 | 2009-05-24 | 0004 | 菜刀 | +| 商品 | 2009-05-24 | 0005 | 高压锅 | +| 商品 | 2009-05-24 | 0006 | 叉子 | +| 商品 | 2009-05-24 | 0007 | 擦菜板 | +| 商品 | 2009-05-24 | 0008 | 圆珠笔 | ++--------+------------+------------+--------------+ +8 rows in set (0.00 sec) +``` + +5. 删除重复行 + + 在`SELECT`语句中使用`DISTINCT`可以去除重复行。 + +```mysql +SELECT + DISTINCT regist_date + FROM Product; + +-- 结果如下 ++-------------+ +| regist_date | ++-------------+ +| 2009-09-20 | +| 2009-09-11 | +| NULL | +| 2009-01-15 | +| 2008-04-28 | +| 2009-11-11 | ++-------------+ +6 rows in set (0.01 sec) +``` + +​ 在使用`DISTINCT` 时,`NULL `也被视为一类数据。`NULL `存在于多行中时,会被合并为一条`NULL `数据。 + +​ 还可以通过组合使用,来去除列组合重复的数据。`DISTINCT `关键字只能用在第一个列名之前。 + +```mysql +SELECT + DISTINCT product_type, regist_date + FROM Product; + +-- 结果如下,列出了所有的组合 ++--------------+-------------+ +| product_type | regist_date | ++--------------+-------------+ +| 衣服 | 2009-09-20 | +| 办公用品 | 2009-09-11 | +| 衣服 | NULL | +| 厨房用具 | 2009-09-20 | +| 厨房用具 | 2009-01-15 | +| 厨房用具 | 2008-04-28 | +| 办公用品 | 2009-11-11 | ++--------------+-------------+ +7 rows in set (0.00 sec) +``` + +6. 指定查询条件 + + 首先通过`WHERE` 子句查询出符合指定条件的记录,然后再选取出` SELECT `语句指定的列,语法结构如下: + +```mysql +SELECT <字段名>, …… + FROM <表名> + WHERE <条件表达式>; +``` + +​ 示例: + +```mysql +SELECT product_name + FROM Product + WHERE product_type = '衣服'; + +-- 结果如下 ++--------------+ +| product_name | ++--------------+ +| T恤衫 | +| 运动T恤 | ++--------------+ +2 rows in set (0.01 sec) +``` + +注意,`WHERE`子句要紧跟在`FROM`子句之后。 + + + +## 4.5 表的复制 + +表的复制可以将表结构与表中的数据全部复制,或者只复制表的结构。 + +```mysql +-- 将整个表复制过来 +CREATE TABLE Product_COPY1 + SELECT * FROM Product; + +SELECT * FROM Product_COPY1; + +-- 结果如下 ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | T恤衫 | 衣服 | 1000 | 500 | 2009-09-20 | +| 0002 | 打孔器 | 办公用品 | 500 | 320 | 2009-09-11 | +| 0003 | 运动T恤 | 衣服 | 4000 | 2800 | NULL | +| 0004 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-09-20 | +| 0005 | 高压锅 | 厨房用具 | 6800 | 5000 | 2009-01-15 | +| 0006 | 叉子 | 厨房用具 | 500 | NULL | 2009-09-20 | +| 0007 | 擦菜板 | 厨房用具 | 880 | 790 | 2008-04-28 | +| 0008 | 圆珠笔 | 办公用品 | 100 | NULL | 2009-11-11 | ++------------+--------------+--------------+------------+----------------+-------------+ +8 rows in set (0.00 sec) +``` + +```mysql +-- 通过LIKE复制表结构 +CREATE TABLE Product_COPY2 + LIKe Product; + +SELECT * FROM Product_COPY2; + +-- 结果如下 +Empty set (0.00 sec) -- 表为空的 + +DESC Product_COPY2; + +-- 结果如下 +-- 表结构已复制过来 ++----------------+--------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------------+--------------+------+-----+---------+-------+ +| product_id | char(4) | NO | PRI | NULL | | +| product_name | varchar(100) | NO | | NULL | | +| product_type | varchar(32) | NO | | NULL | | +| sale_price | int | YES | | 0 | | +| purchase_price | int | YES | | NULL | | +| regist_date | date | YES | | NULL | | ++----------------+--------------+------+-----+---------+-------+ +6 rows in set (0.01 sec) +``` + + + +# 五、运算符 + +## 5.1 算术运算符 + +我们可以在`SELECT`语句中使用计算表达式: + +```mysql +SELECT + product_name, + sale_price, + sale_price * 2 AS "sale_price_x2" + FROM Product; + +-- 结果如下 ++--------------+------------+---------------+ +| product_name | sale_price | sale_price_x2 | ++--------------+------------+---------------+ +| T恤衫 | 1000 | 2000 | +| 打孔器 | 500 | 1000 | +| 运动T恤 | 4000 | 8000 | +| 菜刀 | 3000 | 6000 | +| 高压锅 | 6800 | 13600 | +| 叉子 | 500 | 1000 | +| 擦菜板 | 880 | 1760 | +| 圆珠笔 | 100 | 200 | ++--------------+------------+---------------+ +8 rows in set (0.00 sec) +``` + + ++ 四则运算所使用的运算符(+, -, *, /)称为算术运算符。 + ++ 在运算表达式中,也可以使用(),括号中的运算表达式优先级会得到提升。 + ++ **NULL**的计算结果,仍然还是**NULL**。 + + + +## 5.2 比较运算符 + +在 `WHERE` 子句中通过使用比较运算符可以组合出各种各样的条件表达式。 + +```mysql +SELECT product_name, product_type + FROM Product + WHERE sale_price = 500; +``` + +常见比较运算符如下表: + +| 运算符 | 含义 | +| ------ | -------- | +| = | 相等 | +| <> | 不相等 | +| \>= | 大于等于 | +| \> | 大于 | +| <= | 小于等于 | +| < | 小于 | + ++ 不能对**NULL**使用任何比较运算符,只能通过`IS NULL`语句来判断: + +```mysql +SELECT + product_name, + purchase_price + FROM Product + WHERE purchase_price IS NULL; +``` + +​ 希望选取不是 NULL 的记录时,需要使用`IS NOT NULL`运算符。 + ++ 对字符串使用比较符 + +​ MySQL中字符串的排序与数字不同,典型的规则就是按照字典顺序进行比较,也就是像姓名那样,按照条目在字典中出现的顺序来进行排序。例如: + +```mysql +'1' < '10' < '11' < '2' < '222' < '3' +``` + + + +## 5.3 逻辑运算符 + +1. 使用`NOT`否认某一条件: + +```mysql +SELECT + product_name, + product_type, + sale_price + FROM Product + WHERE NOT sale_price >= 1000; +``` + +2. `AND`运算符和`OR`运算符 + +```mysql +SELECT product_type, sale_price + FROM Product + WHERE product_type = '厨房用具' + AND sale_price >= 3000; + +-- 结果如下 ++--------------+------------+ +| product_type | sale_price | ++--------------+------------+ +| 厨房用具 | 3000 | +| 厨房用具 | 6800 | ++--------------+------------+ +2 rows in set (0.00 sec) +``` + +```mysql +SELECT product_type, sale_price + FROM Product + WHERE product_type = '厨房用具' + OR sale_price >= 3000; + +-- 结果如下 ++--------------+------------+ +| product_type | sale_price | ++--------------+------------+ +| 衣服 | 4000 | +| 厨房用具 | 3000 | +| 厨房用具 | 6800 | +| 厨房用具 | 500 | +| 厨房用具 | 880 | ++--------------+------------+ +5 rows in set (0.00 sec) +``` + +3. 逻辑运算符和真值 + ++ 符**NOT**、**AND** 和 **OR** 称为逻辑运算符; ++ 真值就是值为**真(TRUE)**或**假 (FALSE)**; + ++ 在查询**NULL**时,SQL中存在第三种真值,**不确定(UNKNOWN)**,**NULL**和任何值做逻辑运算结果都是不确定; ++ 考虑 **NULL** 时的条件判断也会变得异常复杂,因此尽量给字段加上**NOT NULL**的约束。 + + + +# 六、分组查询 + +## 6.1 聚合函数 + +通过 SQL 对数据进行某种操作或计算时需要使用函数。 + ++ `COUNT`:计算表中的记录数(行数) + ++ `SUM`: 计算表中数值列中数据的合计值 + ++ `AVG`: 计算表中数值列中数据的平均值 + ++ `MAX`: 求出表中任意列中数据的最大值 + ++ `MIN`: 求出表中任意列中数据的最小值 + +示例: + +```mysql +-- 计算全部数据的行数 +SELECT COUNT(*) FROM Product; + +-- 结果如下 ++----------+ +| COUNT(*) | ++----------+ +| 8 | ++----------+ +1 row in set (0.00 sec) +``` + +**注意点1**:除了`COUNT`可以将`*`作为参数,其它的函数均不可以。 + +```mysql +-- 计算最高的销售价格 +SELECT MAX(sale_price) FROM Product; + +-- 结果如下 ++-----------------+ +| MAX(sale_price) | ++-----------------+ +| 680000 | ++-----------------+ +1 row in set (0.00 sec) +``` + +注意点2:当将字段名作为参数传递给函数时,只会计算不包含`NULL`的行。 + +示例: + +```mysql +-- purchase_price字段是包含NULL值的 +SELECT purchase_price FROM Product; + +-- 结果如下 ++----------------+ +| purchase_price | ++----------------+ +| 500 | +| 320 | +| 2800 | +| 700 | +| 1250 | +| NULL | +| 198 | +| NULL | ++----------------+ +8 rows in set (0.00 sec) +``` + +以*为参数传递给`COUNT`函数 + +```mysql +SELECT COUNT(*) FROM Product; + +-- 结果如下 ++----------+ +| COUNT(*) | ++----------+ +| 8 | ++----------+ +1 row in set (0.00 sec) +``` + +以purchase_price为参数传递给`COUNT`函数 + +```mysql +SELECT COUNT(purchase_price) FROM Product; + +-- 结果如下 ++-----------------------+ +| COUNT(purchase_price) | ++-----------------------+ +| 6 | ++-----------------------+ +1 row in set (0.00 sec) +``` + +可以看到结果并不一样,函数忽略了值为**NULL**的行。 + +`SUM`,`AVG`函数时也一样,计算时会直接忽略,并不会当做0来处理!特别注意`AVG`函数,计算时分母也不会算上`NULL`行。 + +**注意点3**:`MAX/MIN`函数几乎适用于所有数据类型的列,包括字符和日期。`SUM/AVG`函数只适用于数值类型的列。 + +**注意点4**:在聚合函数删除重复值 + +```mysql +SELECT COUNT(DISTINCT product_type) + FROM Product; + +-- 结果如下 ++------------------------------+ +| COUNT(DISTINCT product_type) | ++------------------------------+ +| 3 | ++------------------------------+ +1 row in set (0.01 sec) +``` + +`DISTINCT`必须写在括号中。这是因为必须要在计算行数之前删除 product_type 字段中的重复数据。 + + + +## 6.2 对表分组 + +如果对Python的Pandas熟悉,那么大家应该很了解`groupby`函数,可以根据指定的列名,对表进行分组。在MySQL中,也存在同样作用的函数,即`GROUP BY`。 + +语法结构如下: + +```mysql +SELECT <列名1>, <列名2>, <列名3>, …… + FROM <表名> + GROUP BY <列名1>, <列名2>, <列名3>, ……; +``` + +示例: + +```mysql +SELECT product_type, COUNT(*) + FROM Product + GROUP BY product_type; + +-- 结果如下 ++--------------+----------+ +| product_type | COUNT(*) | ++--------------+----------+ +| 衣服 | 2 | +| 办公用品 | 2 | +| 厨房用具 | 4 | ++--------------+----------+ +3 rows in set (0.01 sec) +``` + +1. 在该语句中,我们首先通过`GROUP BY`函数对指定的字段product_type进行分组。分组时,product_type字段中具有相同值的行会汇聚到同一组。 + +2. 最后通过`COUNT`函数,统计不同分组的包含的行数。 + +简单来理解: + ++ 例如做操时,老师将不同身高的同学进行分组,相同身高的同学会被分到同一组,分组后我们又统计了每个小组的学生数。 + ++ 将这里的同学可以理解为表中的一行数据,身高理解为表的某一字段。 ++ 分组操作就是`GROUP BY`,`GROUP BY`后面接的字段等价于按照身高分组,统计学生数就等价于在`SELECT`后用了`COUNT(*)`函数。 + +注意:`GROUP BY `子句的位置一定要写在`FROM` 语句之后(如果有 `WHERE` 子句的话需要写在 `WHERE` 子句之后) + +``` +1. SELECT → 2. FROM → 3. WHERE → 4. GROUP BY +``` + +当被聚合的键中,包含`NULL`时,在结果中会以“不确定”行(空行)的形式表现出来,也就是字段中为`NULL`的数据会被聚合为一组。 + +## 6.3 使用WHERE语句 + +在对表进行分组之前,也可以是先使用`WHERE`对表进行条件过滤,然后再进行分组处理。语法结构如下: + +```mysql +SELECT <列名1>, <列名2>, <列名3>, …… + FROM <表名> + WHERE + GROUP BY <列名1>, <列名2>, <列名3>, ……; +``` + +示例: + +```mysql +-- WHERE语句先将表中类型为衣服的行筛选出来 +-- 然后再按照purchase_price来进行分组 +SELECT purchase_price, COUNT(*) + FROM Product + WHERE product_type = '衣服' + GROUP BY purchase_price; + +-- 结果如下 ++----------------+----------+ +| purchase_price | COUNT(*) | ++----------------+----------+ +| 500 | 1 | +| 2800 | 1 | ++----------------+----------+ +2 rows in set (0.01 sec) +``` + +该语法实际的执行顺序为: + +``` +FROM → WHERE → GROUP BY → SELECT +``` + ++ 使用`GROUP BY`子句时,`SELECT`子句中不能出现聚合键之外的字段名。即,若`GROUP BY`选中purchase_price字段进行分组,则在`SELECT`语句中只能选中purchase_price字段,其它字段如product_id等均不行。 ++ `WHERE`语句中,不可以使用聚合函数。`WHERE`子句只能指定记录(行)的条件,而不能用来指定组的条件。即`WHERE MAX(purchase_price) > 1000`这样的语句是非法的。 + + + +## 6.4 为聚合结果指定条件 + +前面提到了`WHERE`语句中不能使用聚合函数,但是实际操作时需要通过聚合函数来进行过滤怎么办呢?这就要用到`HAVING`语句了。语法结构如下: + +```mysql +SELECT <列名1>, <列名2>, <列名3>, …… + FROM <表名> + GROUP BY <列名1>, <列名2>, <列名3>, …… +HAVING <分组结果对应的条件> +``` + +在`HAVING`的子句中能够使用的 3 种要素如下所示: + +● 常数 + +● 聚合函数 + +● `GROUP BY`子句中指定的字段名(即聚合键) + +示例: + +```mysql +-- 不使用HAVING语句 +SELECT product_type, AVG(sale_price) + FROM Product + GROUP BY product_type; + +-- 结果如下 ++--------------+-----------------+ +| product_type | AVG(sale_price) | ++--------------+-----------------+ +| 衣服 | 2500.0000 | +| 办公用品 | 300.0000 | +| 厨房用具 | 279500.0000 | ++--------------+-----------------+ +3 rows in set (0.00 sec) +``` + +```mysql +-- 使用HAVING语句 +-- 通过HAVING语句将销售平均价格大于等于2500的组给保留了 +SELECT product_type, AVG(sale_price) + FROM Product + GROUP BY product_type +HAVING AVG(sale_price) >= 2500; + +-- 结果如下 ++--------------+-----------------+ +| product_type | AVG(sale_price) | ++--------------+-----------------+ +| 衣服 | 2500.0000 | +| 厨房用具 | 279500.0000 | ++--------------+-----------------+ +2 rows in set (0.00 sec) +``` + +可以看到使用`HAVING`语句后,输出的结果有所变化。大致流程如下: + ++ 首先,`FROM`语句会选中表Product; ++ 然后,`GROUP BY`语句会选中字段product_type进行分组; ++ 之后,通过`HAVING`语句将销售平均价格大于等于2500的组保留下来; ++ 最后,通过`SELECT`语句将保留下的组的产品类型和平均价格显示出来; + + + +如果是对**表的行**进行条件指定,`WHERE`和`HAVING`都可以生效。 + +```mysql +-- 下面两条语句执行结果一致 +SELECT product_type, COUNT(*) + FROM Product + GROUP BY product_type + HAVING product_type = '衣服'; + +SELECT product_type, COUNT(*) + FROM Product + WHERE product_type = '衣服' + GROUP BY product_type; + +-- 结果如下 ++--------------+----------+ +| product_type | COUNT(*) | ++--------------+----------+ +| 衣服 | 2 | ++--------------+----------+ +1 row in set (0.01 sec) +``` + +但是,一般而言如果是对表的行进行条件指定,最好还是使用`WHERE`语句,因为`WHERE`的执行速度更快。 + + + +## 6.5 对表的查询结果进行排序 + +如果希望对表的查询结果根据某指定的字段进行排序,可以使用`ORDER BY`语句。语法结构如下: + +```mysql +SELECT <列名1>, <列名2>, <列名3>, …… + FROM <表名> + ORDER BY <排序基准列1>, <排序基准列2>, …… +``` + +示例: + +```mysql +SELECT product_id, product_name, sale_price, purchase_price + FROM Product; + +-- 结果如下 ++------------+--------------+------------+----------------+ +| product_id | product_name | sale_price | purchase_price | ++------------+--------------+------------+----------------+ +| 0001 | T恤衫 | 1000 | 500 | +| 0002 | 打孔器 | 500 | 320 | +| 0003 | 运动T恤 | 4000 | 2800 | +| 0004 | 菜刀 | 300000 | 700 | +| 0005 | 高压锅 | 680000 | 1250 | +| 0006 | 叉子 | 50000 | NULL | +| 0007 | 擦菜板 | 88000 | 198 | +| 0008 | 圆珠笔 | 100 | NULL | ++------------+--------------+------------+----------------+ +8 rows in set (0.01 sec) +``` + +```mysql +-- 根据字段sale_price的值进行排序 +SELECT product_id, product_name, sale_price, purchase_price + FROM Product +ORDER BY sale_price; + +-- 结果如下 ++------------+--------------+------------+----------------+ +| product_id | product_name | sale_price | purchase_price | ++------------+--------------+------------+----------------+ +| 0008 | 圆珠笔 | 100 | NULL | +| 0002 | 打孔器 | 500 | 320 | +| 0001 | T恤衫 | 1000 | 500 | +| 0003 | 运动T恤 | 4000 | 2800 | +| 0006 | 叉子 | 50000 | NULL | +| 0007 | 擦菜板 | 88000 | 198 | +| 0004 | 菜刀 | 300000 | 700 | +| 0005 | 高压锅 | 680000 | 1250 | ++------------+--------------+------------+----------------+ +8 rows in set (0.00 sec) +``` + +可以看到`ORDER BY`默认是按照升序的方式进行排序的,正式的书写方式应该是在字段后加上关键字`ASC`,即`ORDER BY sale_price ASC`。 + +如果我们希望按照降序的方式,可以通过`DESC`关键词进行指定。 + +```mysql +SELECT product_id, product_name, sale_price, purchase_price + FROM Product +ORDER BY sale_price DESC; + +-- 结果如下 ++------------+--------------+------------+----------------+ +| product_id | product_name | sale_price | purchase_price | ++------------+--------------+------------+----------------+ +| 0005 | 高压锅 | 680000 | 1250 | +| 0004 | 菜刀 | 300000 | 700 | +| 0007 | 擦菜板 | 88000 | 198 | +| 0006 | 叉子 | 50000 | NULL | +| 0003 | 运动T恤 | 4000 | 2800 | +| 0001 | T恤衫 | 1000 | 500 | +| 0002 | 打孔器 | 500 | 320 | +| 0008 | 圆珠笔 | 100 | NULL | ++------------+--------------+------------+----------------+ +8 rows in set (0.00 sec) +``` + +前面展示了指定一个字段来对表进行排序,实际上我们可以指定多个字段来进行排序。 + +示例: + +```mysql +SELECT regist_date, product_id, sale_price, purchase_price + FROM Product +ORDER BY regist_date, product_id; + +-- 结果如下 ++-------------+------------+------------+----------------+ +| regist_date | product_id | sale_price | purchase_price | ++-------------+------------+------------+----------------+ +| 2009-10-10 | 0002 | 500 | 320 | +| 2009-10-10 | 0003 | 4000 | 2800 | +| 2009-10-10 | 0004 | 300000 | 700 | +| 2009-10-10 | 0005 | 680000 | 1250 | +| 2009-10-10 | 0006 | 50000 | NULL | +| 2009-10-10 | 0007 | 88000 | 198 | +| 2009-10-10 | 0008 | 100 | NULL | +| 2021-10-30 | 0001 | 1000 | 500 | ++-------------+------------+------------+----------------+ +``` + +可以看到先按照`regist_date`的大小进行排序,在字段`regist_date`中具有相同的值的行,接着会按照`product_id`进行排序。 + +使用含有 NULL 的列作为排序键时,NULL 会在结果的开头或末尾汇总显示。 + +在`ORDER BY`子句中可以使用`SELECT`子句中定义的别名。 + +```mysql +-- 将product_id命名为ID,然后按照ID进行排序 +SELECT product_id as ID, product_name, sale_price, purchase_price + FROM Product +ORDER BY ID; + +-- 结果如下 ++------+--------------+------------+----------------+ +| ID | product_name | sale_price | purchase_price | ++------+--------------+------------+----------------+ +| 0001 | T恤衫 | 1000 | 500 | +| 0002 | 打孔器 | 500 | 320 | +| 0003 | 运动T恤 | 4000 | 2800 | +| 0004 | 菜刀 | 300000 | 700 | +| 0005 | 高压锅 | 680000 | 1250 | +| 0006 | 叉子 | 50000 | NULL | +| 0007 | 擦菜板 | 88000 | 198 | +| 0008 | 圆珠笔 | 100 | NULL | ++------+--------------+------------+----------------+ +8 rows in set (0.00 sec) +``` + +为什么`ORDER BY`中可以使用`SELECT`定义的别名呢? + +这是因为在MySQL中,`ORDER BY `的执行次序在`SELECT`之后。 + + + +# 七、数据的插入及更新 + +## 7.1 数据的插入 + +通过命令`INSERT`,可以向表中插入数据: + +```mysql +-- 往表中插入一行数据 +INSERT INTO <表名> (字段1, 字段2, 字段3, ……) VALUES (值1, 值2, 值3, ……); + +-- 往表中插入多行数据 +INSERT INTO <表名> (字段1, 字段2, 字段3, ……) VALUES + (值1, 值2, 值3, ……), + (值1, 值2, 值3, ……), + ... + ; +``` + +示例: + +1. 创建表并插入数据 + +```mysql +-- 创建表 +CREATE TABLE ProductIns +(product_id CHAR(4) NOT NULL, + product_name VARCHAR(100) NOT NULL, + product_type VARCHAR(32) NOT NULL, + sale_price INTEGER DEFAULT 0, -- DEFAULT 0:表示将字段sale_price的默认值设为0 + purchase_price INT , + regist_date DATE , + PRIMARY KEY (product_id)); + +-- 通过单行方式插入 +INSERT INTO + ProductIns(product_id, product_name, product_type, sale_price, purchase_price, regist_date) + VALUES ('0001', '打孔器', '办公用品', 500, 320, '2009-09-11'); + +-- 当对表插入全字段时,可以省略表后的字段清单 +INSERT INTO ProductIns VALUES('0002', '高压锅', '厨房用具', 6800, 5000, '2009-01-15'); + +-- 通过多行方式插入 +INSERT INTO ProductIns VALUES + ('0003', '菜刀', '厨房用具', 3000, 2800, '2009-09-20'), + ('0004', '订书机', '办公用品', 100, 50, '2009-09-11'), + ('0005', '裙子', '衣服', 4100, 3200, '2009-01-23'), + ('0006', '运动T恤', '衣服', 4000, 2800, NULL), + ('0007', '牙刷', '日用品', 20, 10, '2010-03-22'); +``` + +```mysql +SELECT * FROM ProductIns; + +-- 结果如下 ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | 打孔器 | 办公用品 | 500 | 320 | 2009-09-11 | +| 0002 | 高压锅 | 厨房用具 | 6800 | 5000 | 2009-01-15 | +| 0003 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-09-20 | +| 0004 | 订书机 | 办公用品 | 100 | 50 | 2009-09-11 | +| 0005 | 裙子 | 衣服 | 4100 | 3200 | 2009-01-23 | +| 0006 | 运动T恤 | 衣服 | 4000 | 2800 | NULL | +| 0007 | 牙刷 | 日用品 | 20 | 10 | 2010-03-22 | ++------------+--------------+--------------+------------+----------------+-------------+ +7 rows in set (0.00 sec) +``` + +2. 插入NULL + + `INSERT `语句中想给某一列赋予**NULL**值时,可以直接在` VALUES`子句的值清单中写入**NULL**。 + +```mysql +INSERT INTO ProductIns VALUES ('0008', '叉子', '厨房用具', 500, NULL, '2009-09-20'); +``` + +3. 插入默认值 + + 在前面我们创建表时,字段sale_price包含了一条约束条件,默认为0。我们在插入数据时,可以直接用`DEFAULT`对该字段赋值。前提是,该字段被指定了默认值。 + +```mysql +-- 通过显式方法设定默认值 +INSERT INTO + ProductIns (product_id, product_name, product_type, sale_price, purchase_price, regist_date) + VALUES ('0009', '擦菜板', '厨房用具', DEFAULT, 790, '2009-04-28'); + +-- 通过隐式方法插入默认值 +INSERT INTO + ProductIns (product_id, product_name, product_type, purchase_price, regist_date) + VALUES ('0010', '擦菜板', '厨房用具', 790, '2009-04-28'); +``` + + + +## 7.2 数据的删除 + +通过`DROP TABLE`或者`DELETE`语句,可以对表进行删除,但二者存在一定的区别。 + ++ `DROP TABLE` 语句可以将表完全删除。 ++ `DELETE` 语句会留下表结构,而删除表中的全部数据。 + +无论通过哪种方式删除,数据都是难以恢复的。 + +1. 通过`DROP`进行删除 + + 语法结构为: + +```mysql +DROP <表名>; +``` + +2. 通过`DELETE`进行删除 + + 语法结构如下,记得要加`FROM`: + +```mysql +DELETE FROM <表名>; +``` + +​ 同时,也可以通过`WHERE`语句来指定删除的条件: + +```mysql +DELETE FROM <表名> + WHERE <条件>; +``` + +​ 需要注意的是,`DELETE`语句的删除对象并不是表或者列,而是记录(行)。 + +示例: + +```mysql +SELECT * FROM Product; + +-- 结果如下 +mysql> SELECT * FROM Product; ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | T恤衫 | 衣服 | 1000 | 500 | 2009-09-20 | +| 0002 | 打孔器 | 办公用品 | 500 | 320 | 2009-09-11 | +| 0003 | 运动T恤 | 衣服 | 4000 | 2800 | NULL | +| 0004 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-09-20 | +| 0005 | 高压锅 | 厨房用具 | 6800 | 5000 | 2009-01-15 | +| 0006 | 叉子 | 厨房用具 | 500 | NULL | 2009-09-20 | +| 0007 | 擦菜板 | 厨房用具 | 880 | 790 | 2008-04-28 | +| 0008 | 圆珠笔 | 办公用品 | 100 | NULL | 2009-11-11 | ++------------+--------------+--------------+------------+----------------+-------------+ +8 rows in set (0.00 sec) + +-- 删除销售价格大于等于4000的行 +DELETE FROM Product + WHERE sale_price >= 4000; + +-- 结果如下 +mysql> SELECT * FROM Product; ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | T恤衫 | 衣服 | 1000 | 500 | 2009-09-20 | +| 0002 | 打孔器 | 办公用品 | 500 | 320 | 2009-09-11 | +| 0004 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-09-20 | +| 0006 | 叉子 | 厨房用具 | 500 | NULL | 2009-09-20 | +| 0007 | 擦菜板 | 厨房用具 | 880 | 790 | 2008-04-28 | +| 0008 | 圆珠笔 | 办公用品 | 100 | NULL | 2009-11-11 | ++------------+--------------+--------------+------------+----------------+-------------+ +6 rows in set (0.00 sec) +``` + +3. 通过`TRUNCATE`进行删除 + + 在MySQL中,还存在一种删除表的方式,就是利用`TRUNCATE`语句。它的功能和`DROP`类似,但是不能通过`WHERE`指定条件,优点是速度比`DROP`快得多。 + +```mysql +TRUNCATE Product; + +-- 结果如下 +mysql> SELECT * FROM Product; +Empty set (0.00 sec) + +mysql> DESC Product; ++----------------+--------------+------+-----+---------+-------+ +| Field | Type | Null | Key | Default | Extra | ++----------------+--------------+------+-----+---------+-------+ +| product_id | char(4) | NO | PRI | NULL | | +| product_name | varchar(100) | NO | | NULL | | +| product_type | varchar(32) | NO | | NULL | | +| sale_price | int | YES | | NULL | | +| purchase_price | int | YES | | NULL | | +| regist_date | date | YES | | NULL | | ++----------------+--------------+------+-----+---------+-------+ +6 rows in set (0.00 sec) +``` + + + +## 7.3 数据的更新 + +当我们使用`INSERT`语句插入错误的数据后,若我们不想删除后从新插入,那就要使用到`UPDATE`语句。 + +1. 基本用法 + + `UPDATE`的语法结构如下: + +```mysql +UPDATE <表名> + SET <字段名> = <表达式>; +``` + +​ 示例: + +```mysql +-- 由于前面演示删除语句时,表Product的内容已清空 +-- 所以,这里从新进行数据插入 +INSERT INTO Product VALUES + ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20'), + ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11'), + ('0003', '运动T恤', '衣服', 4000, 2800, NULL), + ('0004', '菜刀', '厨房用具', 3000, 2800, '2009-09-20'), + ('0005', '高压锅', '厨房用具', 6800, 5000, '2009-01-15'), + ('0006', '叉子', '厨房用具', 500, NULL, '2009-09-20'), + ('0007', '擦菜板', '厨房用具', 880, 790, '2008-04-28'), + ('0008', '圆珠笔', '办公用品', 100, NULL,'2009-11-11') + ; + +-- 修改表中所有行regist_date的值 +UPDATE Product + SET regist_date = '2009-10-10'; + +-- 结果如下 +mysql> SELECT * FROM Product; ++------------+--------------+--------------+------------+----------------+-------------+ +| product_id | product_name | product_type | sale_price | purchase_price | regist_date | ++------------+--------------+--------------+------------+----------------+-------------+ +| 0001 | T恤衫 | 衣服 | 1000 | 500 | 2009-10-10 | +| 0002 | 打孔器 | 办公用品 | 500 | 320 | 2009-10-10 | +| 0003 | 运动T恤 | 衣服 | 4000 | 2800 | 2009-10-10 | +| 0004 | 菜刀 | 厨房用具 | 3000 | 2800 | 2009-10-10 | +| 0005 | 高压锅 | 厨房用具 | 6800 | 5000 | 2009-10-10 | +| 0006 | 叉子 | 厨房用具 | 500 | NULL | 2009-10-10 | +| 0007 | 擦菜板 | 厨房用具 | 880 | 790 | 2009-10-10 | +| 0008 | 圆珠笔 | 办公用品 | 100 | NULL | 2009-10-10 | ++------------+--------------+--------------+------------+----------------+-------------+ +8 rows in set (0.00 sec) +``` + +2. 指定条件 + +```mysql +UPDATE <表名> + SET <列名> = <表达式> + WHERE <条件>; +``` + +​ 示例: + +```mysql +UPDATE Product + SET regist_date = '2021-10-30' + WHERE product_id = '0001'; +``` + +​ 注意,你也可是使用**NULL**对表进行更新,不过更新的字段必须满足没有**主键**和**NOT NULL**的约束条件。 + +3. 多列更新 + + 多列更新只需要用逗号(,)连接更改的字段即可。 + +```mysql +UPDATE Product + SET + sale_price = sale_price * 10, + purchase_price = purchase_price / 2 + WHERE product_type = '厨房用具'; +``` + + + +# 八、Pymysql的使用 + +在正式介绍`pymysql`的用法之前,我们先思考一件事,我们希望借助`pymysql`完成什么事情? + +之前,我们在命令行下,通过输入SQL语句来完成对数据库和表的增删改查。那么,我们也希望能够在Python下能够完成同样的操作,并且能够返回相应的反馈。具体任务包括: + +1. 登陆并连接到MySQL下的用户; +2. 切换到相应的数据库下; +3. 完成对表的增删改查; + +接下来的内容将围绕这3部分来介绍。 + + + +## 8.1 安装pymysql + +通过`pip`,我们可以完成对`pymysql`的安装: + +```bash +python3 -m pip install PyMySQL +``` + + + +## 8.2 连接数据库 + +如果希望在Python中操作MySQL数据库,那么首先就要登陆到MySQL下的用户。 + +我们通过创建库pymysql下的类`connect`的一个实例来登陆到数据库。 + +示例: + +```python +import pymysql + +# 这里登陆到我之前创建的admin账户 +db = pymysql.connect( + host='localhost', + user='admin', + password='mysql123', + database='shop', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor +) +``` + +参数解释: + ++ `host`:数据库服务器地址,默认`localhost`; ++ `user`:所要登陆的用户名; ++ `password`:用户的登录密码; ++ `database`:所要连接的数据库库名; ++ `charset`:使用的字符类型; ++ `cursorclass`:定义游标使用的类型,通过指定游标使用的类型,在返回输出的结果时将按照指定的类型进行返回。例如,这里设置为字典游标。 + + + +## 8.3 创建游标 + +关于游标,可以理解为在命令行中的光标。在命令行中,我们是在光标处键入语句的。这里游标的起到类似作用。 + +```python +# 创建游标 +cursor = db.cursor() +``` + +实际上,除了在初始化`connect`的实例时指定游标类型,我们在初始化游标时也可以指定游标类型,默认为元组类型。 + +```python +cursor = db.cursor(cursor=pymysql.cursors.DictCursor) +``` + +`cursors`共支持四类游标: + ++ `Cursor`: 默认,元组类型 + ++ `DictCursor`: 字典类型 + ++ `SSCursor`: 无缓冲元组类型 + ++ `SSDictCursor`: 无缓冲字典类型 + + + +## 8.4 类方法 + +初始化完类`connect`和`cursor`的实例后,我们先来了解一下这两个类下包含的方法。了解这些方法有利于我们后面在python下操作mysql: + ++ `connect`下的类方法: + + `close()`:在完成操作后,需要关闭与数据库之间的连接; + + `commit()`:如果执行语句中发生了数据更改,需要提交更改到稳定的存储器; + + `cursor(cursor=None)`:创建一个游标,前面我们在初始化`connect`类是指定了游标类型,通过`cursor`初始化游标时,也可以进行游标类型指定; + + `rollback()`:事务回滚; + ++ `pymysql.cursors`下的类方法: + + `close()`:结束时,关闭游标; + + `execute()`:通过游标执行语句; + + `executemany()`:通过游标执行多条语句; + + `fetchone()`:获取单条数据; + + `fetchmany(size=None)`:获取size条数据; + + `fetchall()`:获取多条数据; + + `scroll(value, mode)`:数据的查询操作都是基于游标,可以通过`scroll`控制游标的位置。 + + `mode=absolute`:绝对位置移动,控制游标位置到上一次查询的第`value`条数据,最小值为`0`; + + `mode=relative`:相对位置移动,基于当前位置,跳过`value`条数据; + +更详细的资料,可参考官方的API或者Github: + +- [pymysql github](https://github.com/PyMySQL/PyMySQL) + +- [pymysql document](https://pymysql.readthedocs.io/en/latest/modules/index.html#) + + + +## 8.5 实战 + ++ 示例1: + +  在这个示例中,我们将做两件事情:创建表和插入数据。 + +```python +import pymysql + +# 以admin身份连接到数据库shop +connection = pymysql.connect( + host='localhost', + user='admin', + password='mysql123', + database='shop', + charset='utf8mb4', +) + +# 创建游标 +cursor = connection.cursor(cursor=pymysql.cursors.DictCursor) + +# 1. 创建了一个表 +sql = """ +CREATE TABLE Employee( + id INT PRIMARY KEY, + name CHAR(15) NOT NULL + ) + """ + +# 提交执行 +cursor.execute(sql) + +# 2. 往表中插入数据 +sql = "INSERT INTO Employee (id, name) VALUES (%s, %s)" +values = [(1, 'XiaoBai'), + (2, 'XiaoHei'), + (3, 'XiaoHong'), + (4, 'XiaoMei'), + (5, 'XiaoLi')] + +try: + # 通过executemany可以插入多条数据 + cursor.executemany(sql, values) + # 提交事务 + connection.commit() +except: + connection.rollback() + + +# 3. 关闭光标及连接 +cursor.close() +connection.close() +``` + ++ 示例2 + + 在示例1的基础上,我们继续执行查询工作。 + +```python +import pymysql + +# 以admin身份连接到数据库shop +connection = pymysql.connect( + host='localhost', + user='admin', + password='mysql123', + database='shop', + charset='utf8mb4', +) + +with connection: + # 创建游标 + cursor = connection.cursor(cursor=pymysql.cursors.DictCursor) + + # 1. 通过fetchone只查询一条 + cursor.execute("SHOW CREATE TABLE Employee") + result = cursor.fetchone() + print(f'查询结果1: \n{result}') + + # 2. 通过fetchmany查询size条 + cursor.execute("DESC Employee") + result = cursor.fetchmany(size=2) + print(f'查询结果2: \n{result}') + + # 3. 通过fetchall查询所有 + cursor.execute("SELECT * FROM Employee") + result = cursor.fetchall() + print(f'查询结果3: \n{result}') + + # 4. 通过scroll回滚到第0条进行查询 + cursor.scroll(0, mode='absolute') + result = cursor.fetchone() + print(f'查询结果4: \n{result}') + + # 5. 通过scroll跳过2条进行查询 + cursor.scroll(2, mode='relative') + result = cursor.fetchone() + print(f'查询结果5: \n{result}') + + cursor.close() +``` + +​ 控制台打印结果如下: + +```bash +查询结果1: +{'Table': 'Employee', 'Create Table': 'CREATE TABLE `Employee` (\n `id` int NOT NULL,\n `name` char(15) NOT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci'} +查询结果2: +[{'Field': 'id', 'Type': 'int', 'Null': 'NO', 'Key': 'PRI', 'Default': None, 'Extra': ''}, {'Field': 'name', 'Type': 'char(15)', 'Null': 'NO', 'Key': '', 'Default': None, 'Extra': ''}] +查询结果3: +[{'id': 1, 'name': 'XiaoBai'}, {'id': 2, 'name': 'XiaoHei'}, {'id': 3, 'name': 'XiaoHong'}, {'id': 4, 'name': 'XiaoMei'}, {'id': 5, 'name': 'XiaoLi'}] +查询结果4: +{'id': 1, 'name': 'XiaoBai'} +查询结果5: +{'id': 4, 'name': 'XiaoMei'} +``` + ++ 示例3: + + ​ 该示例将演示SQL注入的问题。先建立一个表并插入数据: + +```python +import pymysql + +# 以admin身份连接到数据库shop +connection = pymysql.connect( + host='localhost', + user='admin', + password='mysql123', + database='shop', + charset='utf8mb4', +) + +# 创建游标 +cursor = connection.cursor(cursor=pymysql.cursors.DictCursor) + +sql = """ + CREATE TABLE UserInfo( + id INT PRIMARY KEY, + name VARCHAR(15), + password CHAR(15) NOT NULL + ) + """ + +cursor.execute(sql) + +sql = "INSERT INTO UserInfo (id, name, password) VALUES (%s, %s, %s)" +values = [(1, 'XiaoBai', '123'), + (2, 'XiaoHei', '234'), + (3, 'XiaoHong', '567'), + (4, 'XiaoMei', '321'), + (5, 'XiaoLi', '789')] + +cursor.executemany(sql, values) +connection.commit() +``` + +​ 再写一个程序,根据输入判定登陆是否成功: + +```python +import pymysql + +# 以admin身份连接到数据库shop +connection = pymysql.connect( + host='localhost', + user='admin', + password='mysql123', + database='shop', + charset='utf8mb4', +) + +# 创建游标 +cursor = connection.cursor(cursor=pymysql.cursors.DictCursor) + +while True: + user = input("输入用户:").strip() + password = input("输入密码:").strip() + sql = "select name, password from UserInfo where name='%s' and password='%s' " % (user, password) + + cursor.execute(sql) + # 打印用户和密码 + result=cursor.fetchone() + print(result) + + if result: + print("成功登陆\n") + else: + print("登陆失败\n") +``` + +​ 在控制台下,我们进行了三组用户和密码的验证: + +```python +输入用户:XiaoBai +输入密码:123 +{'name': 'XiaoBai', 'password': '123'} +成功登陆 + +输入用户:XiaoBai +输入密码:321 +None +登陆失败 + +输入用户:XiaoBai' -- dsd +输入密码:321 +{'name': 'XiaoBai', 'password': '123'} +成功登陆 +``` + +​ 可以看出,第1组和第2组验证正常,但是第3组出现了异常,输入错误的密码却可以正确登陆。 + +​ 这是因为在MySQL中`--`的含义是注释,如果通过字符串进行拼接: + +```mysql +select name, password from UserInfo where name='XiaoBai' -- dsd' and password='321' +``` + +​ 实际等价于: + +```mysql +sselect name, password from UserInfo where name='XiaoBai' +``` + +​ 解决办法:通过`execute`或者`executemany`来进行拼接。将语句: + +```python +sql = "select name, password from UserInfo where name='%s' and password='%s' " % (user, password) +cursor.execute(sql) +``` + +​ 改为: + +```python +sql = "select name, password from UserInfo where name=%s and password=%s" +cursor.execute(sql, (user, password)) +``` + diff --git a/docs/推荐系统实战/新闻推荐系统实践/Redis基础.md b/docs/推荐系统实战/新闻推荐系统实践/Redis基础.md new file mode 100644 index 00000000..2d79c1fb --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/Redis基础.md @@ -0,0 +1,1238 @@ +## ***Redis* 基础** + +### 简介: + +Redis(**Re**mote **Di**ctionary **S**erver ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库。由于是内存数据库,读写非常高速,可达10w/s的评率,所以一般应用于数据变化快、实时通讯、缓存等。但内存数据库通常要考虑机器的内存大小。Redis 是完全开源免费的,遵守 BSD 协议,是一个灵活的高性能 key-value 数据结构存储,可以用来作为数据库、缓存和消息队列。相比于其他的 key-value 缓存产品有以下三个特点: + +- Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载到内存使用。 +- Redis 不仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。 +- Redis 支持主从复制,即 master-slave 模式的数据备份。 + +### 安装: + +本项目是基于Ubuntu环境进行开发,因此接下来都以Ubuntu的环境为基础,对于其他开发环境,大家可以参考相关的[资料](https://www.redis.com.cn/redis-installation.html)进行学习。 + +**安装Redis服务器:** + +```shell +sudo apt-get install redis-server +``` + +下载完成的结果 + +![image-20211030164414594](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20211030164414594.png) + +**启动Redis服务:** + +一般来说,当安装完成后,Redis服务器会自动启动,可以通过以下命令检查是否启动成功。(ps:如果Active显示为 active(running) 状态:表示redis已在运行,启动成功) + +```shell +service redis-server status +``` + +![image-20211030164432589](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20211030164432589.png) + +检查当前进程,查看redis是否启动。(ps: 可以看到redis服务正在监听6379端口) + +```shell +ps -aux|grep redis-server +``` + +![image-20211030164448713](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20211030164448713.png) + +或者进入redis客户端,与服务器进行通信,当输入ping命令,如果返回 PONG 表示Redis已成功安装。 + +```shell +redis-cli +``` + +![image-20211030164455928](http://ryluo.oss-cn-chengdu.aliyuncs.com/图片image-20211030164455928.png) + +上面的127.0.0.1 是redis服务器的 IP 地址,6379 是 Redis 服务器运行的端口。 + + + +### 命令: + +下面简单介绍一些常用的Redis命令: + +**1、基本操作命令:** + +- **启动Redis** + + ```shell + redis-server [--daemonize yes][--port 6379] + ``` + + 可以通过带参数方式来启动,如果参数过多,可以使用/etc/redis/redis.conf下面的配置文件来启动Redis。 + + ```shell + redis-server /etc/redis/redis.conf + ``` + +- **连接Redis** + + ```shell + redis-cli [-h host -p port -a password] + ``` + + 其中上面参数默认的是redis-server的默认地址和端口号,password可以在服务启动时采用参数的方式或者配置文件方式都可进行设置。因此可以通过redis-cli,可以连上我们服务器端的redis服务。 + +- **停止Redis** + + 停止Redis有两种方法,一种是通过 redis-cli 停止,另一种是通过杀掉redis服务进程 + + ```shell + > redis-cli shutdown + + > kill redis-pid + ``` + +- **切换库指令** + + redis.conf配置中默认16个库,下标从0~15。进入客服端默认选中第0个库,可以通过select命令进行切换,index表示库的小标。 + + ```shell + 127.0.0.1:6379> SELECT index + ``` + +- **删除当前库的数据** + + 删除当前选择的数据库中的所有数据,这个命令永远不会出现失败。 + + ```shell + 127.0.0.1:6379[1]> FLUSHDB + ``` + +- **删除所有库的数据** + + 删除所有数据库里面的数据,注意是所有数据库,这个命令永远不会出现失败。 + + ```shell + 127.0.0.1:6379[1]> FLUSHALL + ``` + +- **查看key的数量** + + 查看当前选择的库中key的数量 + + ```shell + 127.0.0.1:6379> DBSIZE + ``` + + 测试以上命令 + + ```shell + neu@neu:~$ redis-server --daemonize yes --port 6378 --requirepass 123456 + 28518:C 26 Oct 20:52:56.389 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo + 28518:C 26 Oct 20:52:56.389 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=28518, just started + 28518:C 26 Oct 20:52:56.389 # Configuration loaded + neu@neu:~$ redis-cli -p 6378 -a 123456 + 127.0.0.1:6378> set name jiangyou # 在第0个数据库插入一个值 + OK + 127.0.0.1:6378> select 1 # 选择第1个数据库 + OK + 127.0.0.1:6378[1]> set age 26 # 在第1个数据库插入一个值 + OK + 127.0.0.1:6378[1]> DBSIZE # 第1个数据库当前的key值数量 + (integer) 1 + 127.0.0.1:6378[1]> FLUSHDB # 删除第1个数据库的所有值 + OK + 127.0.0.1:6378[1]> DBSIZE # 删除后数据库中没有值 + (integer) 0 + 127.0.0.1:6378[1]> SELECT 0 # 切换到第0个数据库 + OK + 127.0.0.1:6378> DBSIZE # 第1个数据库的值存在,因此FLUSHDB只删除第1个数据库的所有值 + (integer) 1 + 127.0.0.1:6378> SELECT 1 + OK + 127.0.0.1:6378[1]> FLUSHALL # 切换到第1个数据库,使用FLUSHALL删除所有数据库的值 + OK + 127.0.0.1:6378[1]> SELECT 0 + OK + 127.0.0.1:6378> DBSIZE # 切换到第0个数据库,发现所有的值已被删除 + (integer) 0 + ``` + + + +**2、Key的操作命令:** + +​ 该部分指令主要是为了对数据库中的key进行增删改查等一些列操作,下面主要介绍几个常用的命令。 + +- **查找符合模板的Key** + + ```shell + KEYS pattern + ``` + + 该指令查找数据库所有符合pattern的key,其中pattern可以为?、* 、[abc...]、[a-d]等方式。其中?代表一个任意一个字符,*代表任意0个或多个字符,[abc...]代表只能是[]中的值,[a-d]代表a到d范围内总的值。 + + ```shell + 127.0.0.1:6378> keys * + 1) "age" + 2) "school" + 3) "home" + 4) "name" + 127.0.0.1:6378> keys *e + 1) "age" + 2) "home" + 3) "name" + 127.0.0.1:6378> keys a?e + 1) "age" + 127.0.0.1:6378> keys [a-n][ao]me + 1) "home" + 2) "name" + 127.0.0.1:6378> keys [a-n][a]me + 1) "name" + ``` + +- **查找存在key的数量** + + ```shell + EXISTS key or [key…] + ``` + + 该指令为了查找一个或多个key,返回存在key值的数量。 + + ```shell + 127.0.0.1:6378> exists name + (integer) 1 + 127.0.0.1:6378> exists name home id + (integer) 2 + ``` + +- **设置过期时间** + + ```shell + EXPIRE key seconds + ``` + + expire 设置 key 的过期时间,时间过期后,key 会被自动删除,设置成功返回1,key不存在返回0。 + + ```shell + TTL key + ``` + + ttl 命令以秒为单位返回key的剩余过期时间,如果key不存在返回 -2 key 存在但没有关联超时时间则返回 -1 。 + + ```shell + 127.0.0.1:6378> expire name 30 + (integer) 1 + 127.0.0.1:6378> ttl name + (integer) 26 + 127.0.0.1:6378> ttl name + (integer) -2 + 127.0.0.1:6378> ttl age + (integer) -1 + 127.0.0.1:6378> ttl id + (integer) -2 + ``` + +- **Key所属类型** + + ```shell + TYPE key + ``` + + type命令以字符串的形式返回存储在 `key` 中的值的类型,可返回的类型有:`string`, `list`, `set`, `zset`,`hash` 和 `stream`,如果key值不存在返回`none`。 + + ```shell + 127.0.0.1:6378> set key1 "value" + OK + 127.0.0.1:6378> lpush key2 "value" + (integer) 1 + 127.0.0.1:6378> SADD key3 "value" + (integer) 1 + 127.0.0.1:6378> type key1 + string + 127.0.0.1:6378> type key2 + list + 127.0.0.1:6378> type key3 + set + 127.0.0.1:6378> type key + none + ``` + +- **删除Key** + + ```shell + DEL key or [key…] + ``` + + del命令删除指定的key,不存在的key忽略,返回0,如果key存在,返回删除的key的个数。 + + ```shell + 127.0.0.1:6378> del key + (integer) 0 + 127.0.0.1:6378> del key1 key2 + (integer) 2 + ``` + +**3、字符串类型—string命令:** + +​ 字符串是Redis中最常见的数据类型,它能够存储任何形式的字符串,其中包括二进制格式,JSON格式,序列化格式等数据。而string相关的命令则是用于管理redis字符串值,下面介绍一些常见命令。 + +**基础命令** + +- **SET** + + set命令将key是定为指定的字符串,如果key存在,则会覆盖原来的值。 + + ```shell + SET key value [EX seconds] [PX milliseconds] [NX|XX] + ``` + + 其中set可以为设定的值设置过期时间,EX表示秒数,PX表示毫秒。参数NX表示只有键key不存在的时候才会设置key的值,XX表示只有键key存在的时候才会设置key的值。 + +- **GET** + + get命令返回与键 `key` 相关联的字符串值。 + + ```shell + GET key + ``` + + 如果key不存在,返回nil,如果key的值是非字符串类型,那么返回一个错误。 + +- **APPEND** + + append命令将指定的key追加值。如果key存在,并且是字符串,则会将value追加到key原值的末尾,如果key值是非字符串则会报错,当key不存在时候,改命令类似于set,简单将key设定为value。 + + ```shell + APPEND KEY_NAME NEW_VALUE + ``` + +- **INCR** + + incr 命令将 key 中储存的数字值增一。如果key不存在,key值会被初始化为0,在进行incr操作。如果字符串类型的值不能表示为数字,则会报错。 + + ```shell + INCR KEY_NAME + ``` + +- **DECR** + + decr命令将 key 中储存的数字值减一,和incr命令相似。 + + ```shell + DECR KEY_NAME + ``` + + + +**常用命令** + +- **STRLEN** + + Strlen 命令将获取指定 key 所储存的字符串值的长度,如果key存储的不是字符串类型或不存在时,返回错误。 + + ```shell + STRLEN KEY_NAME + ``` + +- **SETRANG** + + Setrange命令是将从偏移量 `offset` 开始, 用 `value` 参数覆盖键 `key` 储存的字符串值。 + + ```shell + SETRANGE key offset value + ``` + + 不存在的键 `key` 当作空白字符串处理,如果键 `key` 原来储存的字符串长度比偏移量小,那么原字符和偏移量之间的空白将用零字节("\x00" )进行填充。 + +- **GETRANG** + + Getrange命令返回存储在 key 中的字符串的子串,由 `start` 和 `end` 偏移决定(都包括在内)。负数偏移提供相对字符串结尾的偏移。并且该命令会通过将结果范围限制为字符串的实际长度来处理超出范围的请求。 + + ```shell + GETRANGE key start end + ``` + + 当key不存在返回空字符串。 + +- **MSET** + + 命令设置多个 `key` 的值为各自对应的 value。如果key存在,则会用新值替换旧值,如果key不存在,会重新创建,该命令总是返回“OK”,因为 MSET不会失败。 + + ```shell + MSET key value [key value ...] + ``` + +- **MGET** + + 命令返回所有(一个或多个)给定 key 的值,值的类型是字符串。 如果给定的 key 里面有某个 key 不存在或者值不是字符串,那么这个 key 返回特殊值 `nil` 。 + + ```shell + MGET key [key ...] + ``` + +测试以上命令 + +```shell +127.0.0.1:6379> set name jiang XX # XX表示只有键key存在的时候才会设置key的值 +(nil) +127.0.0.1:6379> set name jiang NX # NX表示只有键key不存在的时候才会设置key的值 +OK +127.0.0.1:6379> get name # 返回与键 `key` 相关联的字符串值。 +"jiangyou" +127.0.0.1:6379> get age # 键key不存在的时候返回nil +(nil) +127.0.0.1:6379> APPEND name # 将value追加到key原值的末尾,返回值的总长度 +(integer) 14 +127.0.0.1:6379> get name +"jiangyou" +127.0.0.1:6379> set age 24 EX 30 # 设置age 的值,并设置了过期时间 EX表示秒 +OK +127.0.0.1:6379> incr age # 在age上进行增 1 +(integer) 25 +127.0.0.1:6379> get age +"25" +127.0.0.1:6379> decr age # 在age上进行减 1 +(integer) 24 +127.0.0.1:6379> get age +"24" +127.0.0.1:6379> incr name # 由于name值不能表示数字,无法增1 +(error) ERR value is not an integer or out of range +127.0.0.1:6379> STRLEN name # name对应的string的长度 +(integer) 8 +127.0.0.1:6379> SETRANGE name 10 hahaha # 从偏移量为10 的位置开始加入hahaha +(integer) 16 +127.0.0.1:6379> get name # 不足的用\x00 补充 +"jiangyou\x00\x00hahaha" +127.0.0.1:6379> GETRANGE name 0 -1 # 获取name的值,改方式类似于python的数组查找 +"jiangyou\x00\x00hahaha" +127.0.0.1:6379> MSET age 26 home liaoning # 为多个key赋值 +OK +127.0.0.1:6379> MGET age home addr # 查找多个key对应的值,不存在的key返回nil。 +1) "26" +2) "liaoning" +3) (nil) +``` + + + +**4、列表—list命令:** + +**基本命令** + +- **LPUSH** + + Lpush 将一个或多个值插入到列表`key` 的头部。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。如果 key 对应的值不是 list 类型,那么会返回一个错误。可以使用一个命令把多个元素 push 进入列表。 + + ```shell + LPUSH key value [value ...] + ``` + + + +- **RPUSH** + + Rpush 将向存储在 key 中的列表的尾部插入所有指定的值。如果 key 不存在,那么会创建一个空的列表然后再进行 push 操作。 当 key 保存的不是列表,那么会返回一个错误。 + + ```shell + RPUSH key value [value ...] + ``` + + + +- **LRANGE** + + Lrange将返回列表中指定区间内的元素(闭区间),区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。如果start大于最大小标,那么叫返回空列表。 + + ```shell + LRANGE key start end + ``` + +- **LINDEX** + + Lindex 将返回列表 key 里索引 index 位置存储的元素。 index 下标是从 0 开始索引的,所以 0 是表示第一个元素, 1 表示第二个元素,并以此类推。 负数索引用于指定从列表尾部开始索引的元素,在这种方法下,-1 表示最后一个元素,-2 表示倒数第二个元素,并以此往前推。当 key 值不是列表的时候,会返回错误。 + + ```shell + LINDEX key index + ``` + +- **LLEN** + + Llen 将用于返回存储在 `key` 中的列表长度。 如果 `key` 不存在,则 `key` 被解释为一个空列表,返回 `0` 。 如果 `key` 不是列表类型,返回一个错误。 + + ```shell + LLEN key + ``` + + + +**常用命令** + +- **LREM** + + Lrem将用于从列表 key 中删除前 count 个值等于 `element` 的元素。 这个 count 参数通过下面几种方式影响这个操作,如果count > 0, 从头到尾删除值为 value 的元素;如果count < 0,将从尾到头删除值为 value 的元素;如果 count = 0 将移除所有值为 value 的元素 + + ```shell + LREM key count value + ``` + + + +- **LSET** + + Lset 将用于设置列表 key 中 index 位置的元素值为 `element`。 + + ```shell + LSET key index value + ``` + +- **LINSERT** + + Linsert 将用于把 `element` 插入到列表 `key` 的前面或后面。当 `key` 不存在时,这个list会被看作是空list,什么都不执行;当 `key` 存在,值不是列表类型时,返回错误。 + + ```shell + LINSERT key BEFORE|AFTER pivot value + ``` + +测试上面命令: + +```shell +127.0.0.1:6379> RPUSH myarrs 1 1 1 1 2 2 # 从list的右边开始往myarrs里面添加值 +(integer) 6 +127.0.0.1:6379> LRANGE myarrs 0 -1 # 返回myarrs的List中所有值 +1) "1" +2) "1" +3) "1" +4) "1" +5) "2" +6) "2" +127.0.0.1:6379> LPUSH myarrs 0 0 -1 # 从list的左边开始往myarrs里面添加值 +(integer) 9 +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "0" +4) "1" +5) "1" +6) "1" +7) "1" +8) "2" +9) "2" +127.0.0.1:6379> LINDEX myarrs -2 # 根据索引返回List中的值 +"2" +127.0.0.1:6379> LLEN myarrs # 返回List中元素个数 +(integer) 9 +127.0.0.1:6379> LREM myarrs 2 1 # 删除myarrs中的1 count为2 所以从头往尾删除两个1 +(integer) 2 +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "0" +4) "1" +5) "1" +6) "2" +7) "2" +127.0.0.1:6379> LREM myarrs -1 1 # 删除myarrs中的1 count为-1 所以从尾往头删除1个1 +(integer) 1 +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "0" +4) "1" +5) "2" +6) "2" +127.0.0.1:6379> LREM myarrs 0 2 # 删除myarrs中的2 count为0 删除所有等于2的元素 +(integer) 2 +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "0" +4) "1" +127.0.0.1:6379> LSET myarrs 2 5 # 根据索引设置myarrs的值,将索引为2 的位置赋值为5 +OK +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "5" +4) "1" +127.0.0.1:6379> LINSERT myarrs before 5 4 # 在第一个值为5的位置的前面插入一个4 +(integer) 5 +127.0.0.1:6379> LRANGE myarrs 0 -1 +1) "-1" +2) "0" +3) "4" +4) "5" +5) "1" +``` + + + +**5、哈希类型—hash命令:** + +hash类似于java中的HashMap,在Reids中做了更多的优化。此外hash是一个sytring类型的field和value的映射表,特别适合用于存储对象。例如我们可以借用hash数据结构来存储用户信息,商品信息等。 + +**基本命令** + +- HSET + + Hset 命令用于为存储在 `key` 中的哈希表的 `field` 字段赋值 `value` 。如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。如果字段(`field`)已经存在于哈希表中,旧值将被覆盖。 + + ```shell + HSET key field value + ``` + +- HGET + + Hget 命令用于返回哈希表中指定字段 `field` 的值。如果给定的字段或 key 不存在时,返回 nil 。 + + ```shell + HGET key field + ``` + +- HMSET + + Hmset 命令用于同时将多个 field-value (字段-值)对设置到哈希表中。此命令会覆盖哈希表中已存在的字段,如果哈希表不存在,会创建一个空哈希表,并执行 HMSET 操作。 + + ```shell + HMSET key field value [field value ...] + ``` + +- HGETALL + + Hgetall 命令用于返回存储在 `key` 中的哈希表中所有的域和值。返回值以列表形式返回哈希表的字段及字段值,若 key 不存在,返回空列表。 + + ```shell + HGETALL key + ``` + +- HDEL + + Hdel 命令用于删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。 如果 `key` 不存在,会被当作空哈希表处理并返回 `0` 。 + + ```shell + HDEL key field [field ...] + ``` + +**常用命令** + +- HEXISTS + + Hexists 命令用于查看哈希表的指定字段`field` 是否存在。如果表含有给定字段`field`会返回1,否则返回0。 + + ```shell + HEXISTS key field + ``` + +- HKEYS + + Hkeys返回存储在 `key` 中哈希表的所有域。当 key 不存在时,返回空表。 + + ```shell + HKEYS key + ``` + +- HVALS + + Hvals 命令返回哈希表所有域(field)的值。当 key 不存在时,返回空表。 + + ```shell + HVALS key + ``` + +测试以上命令 + +```shell +127.0.0.1:6379> HSET userinfo name jiangyou # 创建新的hash表,并存入对象userinfo的name属性 +(integer) 1 # 返回赋值成功域的个数 +127.0.0.1:6379> HSET userinfo age 26 home liaoming school neu # 设置userinfo对象的多个域的值 +(integer) 3 # 返回赋值成功域的个数 +127.0.0.1:6379> HKEYS userinfo # 查看userinfo的所有域的名 +1) "name" +2) "age" +3) "home" +4) "school" +127.0.0.1:6379> HKEYS users # 当key不存在时,返回空 +(empty list or set) +127.0.0.1:6379> HVALS userinfo # 返回key值的所有域的值 +1) "jiangyou" +2) "26" +3) "liaoming" +4) "neu" +127.0.0.1:6379> HEXISTS userinfo name # 查看哈希表的指定字段`name` 该字段存在,返回1 +(integer) 1 +127.0.0.1:6379> HEXISTS userinfo addr # 查看哈希表的指定字段`addr` 该字段存在,返回0 +(integer) 0 +127.0.0.1:6379> HGETALL userinfo # 查看哈希表中存储在 `key` 中的所有的域和值 +1) "name" +2) "jiangyou" +3) "age" +4) "26" +5) "home" +6) "liaoming" +7) "school" +8) "neu" +127.0.0.1:6379> HGETALL users # `key` 不存在,会被当作空哈希表处理并返回。 +(empty list or set) +127.0.0.1:6379> HDEL userinfo school home # 删除哈希表 key 中的一个或多个指定域,返回的为成功删除的域的个数。 +(integer) 2 +127.0.0.1:6379> HGETALL userinfo +1) "name" +2) "jiangyou" +3) "age" +4) "26" +``` + + + +**6、集合类型—set命令:** + +**基本命令** + +- **SADD** + + Sadd 将命令将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略。假如集合 key 不存在,则创建一个只包含被添加的元素作为成员的集合。当集合 key 不是集合类型时,返回一个错误。 + + ```shell + SADD key member [member ...] + ``` + +- **SMEMBERS** + + Smembers 将返回存储在 `key` 中的集合的所有的成员。 不存在的集合被视为空集合。 + + ```shell + SMEMBERS key + ``` + +- **SISMEMBER** + + Sismember 将用于判断元素 `member` 是否集合 `key` 的成员。如果成员元素是集合的成员,返回 1 ;如果成员元素不是集合的成员,或 `key` 不存在,返回0。 + + ```shell + SISMEMBER key member + ``` + +- **SCARD** + + Scard 将返回集合中元素的数量。 + + ```shell + SCARD key + ``` + +- **SREM** + + Srem将在集合中删除指定的元素。如果指定的元素不是集合成员则被忽略。如果集合 `key` 不存在则被视为一个空的集合,该命令返回0。如果key的类型不是一个集合,则返回错误。 + + ```shell + SCARD key member [member ...] + ``` + + +**常用命令** + +- **SRANDMEMBER** + + Srandmember 将仅使用`key` 参数,那么随机返回集合`key` 中的一个随机元素。如果count是整数且小于元素的个数,返回含有 count 个不同的元素的数组,如果count是个整数且大于集合中元素的个数时,返回整个集合的所有元素,当count是负数,则会返回一个包含count的绝对值的个数元素的数组,如果count的绝对值大于元素的个数,则返回的结果集里会出现一个元素出现多次的情况。 + + ```shell + SRANDMEMBER key [count] + ``` + +- **SPOP** + + Spop 将从集合 `key`中删除并返回一个或多个随机元素。这个命令和 SRANDMEMBER相似, SRANDMEMBER 只返回随机成员但是不删除这些返回的成员。 + + ```shell + SPOP key [count] + ``` + + +测试以上命令 + +```shell +127.0.0.1:6379> SADD name zhangsan lisi wangwu # 赋值key为name的set集合,返回赋值成功的个数 +(integer) 3 +127.0.0.1:6379> SMEMBERS name # 查看存储在name中的集合的所有的成员。 +1) "zhangsan" +2) "lisi" +3) "wangwu" +127.0.0.1:6379> SISMEMBER name zhangsan # 判断元素 zhangsan 是否集合 name 的成员,如果是 返回1 +(integer) 1 +127.0.0.1:6379> SISMEMBER name xuliu # 判断元素 xuliu 是否集合 name 的成员,如果不是 返回0 +(integer) 0 +127.0.0.1:6379> SCARD name +(integer) 3 +127.0.0.1:6379> SREM name zhangsan xuliu # 删除 name 的成员,如果存在直接删除,否则忽略。返回删除成功的元素个数 +(integer) 1 +127.0.0.1:6379> SMEMBERS name +1) "lisi" +2) "wangwu" +127.0.0.1:6379> SRANDMEMBER name 5 # 随机返回集合name中的一个随机元素,count为5 大于集合个数,返回整个集合元素 +1) "lisi" +2) "wangwu" +127.0.0.1:6379> SRANDMEMBER name 1 # 随机返回集合name中的一个随机元素,count为1 随机返回集合中任意一个元素 +1) "wangwu" +127.0.0.1:6379> SRANDMEMBER name -5 # 随机返回集合name中的一个随机元素,count为-5 返回的结果集里会出现一个元素出现多次 +1) "wangwu" +2) "lisi" +3) "lisi" +4) "lisi" +5) "wangwu" +127.0.0.1:6379> SPOP name 0 # 随机删除并返回集合name中的一个或多个随机元素,count为0 返回的结果集里不会出现任何元素 +(empty array) +127.0.0.1:6379> SPOP name 1 # 随机删除并返回集合name中的一个或多个随机元素,count为1 返回的结果集里会出现一个元素出现多次 +1) "lisi" +127.0.0.1:6379> SPOP name -5 # 随机删除并返回集合name中的一个或多个随机元素,count 不能为负数。 +(error) ERR value is out of range, must be positive +``` + + + +**7、有序集合类型—sortedset命令:** + +**基本命令** + +- **ZADD** + + Zadd 将一个或多个 `member` 元素及其 `score` 值加入到有序集 `key` 当中。如果某个 `member` 已经是有序集的成员,那么更新这个 `member` 的 `score` 值,并通过重新插入这个 `member` 元素,来保证该 `member` 在正确的位置上。如果有序集合 `key` 不存在,则创建一个空的有序集并执行 ZADD操作。当 `key` 存在但不是有序集类型时,返回一个错误。`score` 值可以是整数值或双精度浮点数,`score` 可为正也可以为负。 + + ```shell + ZADD key [NX|XX] [CH] [INCR] score member [score member ...] + ``` + + - **XX**: 仅更新存在的成员,不添加新成员。 + - **NX**: 不更新存在的成员。只添加新成员。 + - **LT**: 更新新的分值比当前分值小的成员,不存在则新增。 + - **GT**: 更新新的分值比当前分值大的成员,不存在则新增。 + - **CH**: 返回变更成员的数量。变更的成员是指 **新增成员** 和 **score值更新**的成员,命令指明的和之前score值相同的成员不计在内。 注意: 在通常情况下,ZADD返回值只计算新添加成员的数量。 + - **INCR**: [ZADD](https://www.redis.com.cn/commands/zadd.html) 使用该参数与 [ZINCRBY](https://www.redis.com.cn/commands/zincrby.html) 功能一样。一次只能操作一个score-element对。 + + 举例子: + + ```shell + ``` + + + +- **ZRANG** + + Zrange将返回有序集中,指定区间内(闭区间)的成员,其中成员的按分数值递增(从小到大)来排序,具有相同分数值的成员按字典序(lexicographical order )来排列。如果你需要成员按值递减(从大到小)来排列,可以使用 `ZREVRANGE`命令。下标参数 `start` 和 `stop` 都以 `0` 为底,也就是说,以 `0` 表示有序集第一个成员,以 `1` 表示有序集第二个成员,以此类推。其中 start和stop参数的细节同 `ZRANG`命令。 + + ```shell + ZRANGE key start stop [WITHSCORES] + ``` + + + +- **ZREVRANGE** + + Zervrange 将返回有序集`key`中,指定区间内的成员。其中成员的位置按score值递减(从高到低)来排列。具有相同score值的成员按字典序的反序排列。 除了成员排序相反外,`ZREVRANGE`命令的其他方面和`ZRANGE`命令一样。 + + ```shell + ZREVRANGE key start stop [WITHSCORES] + ``` + + + +- **ZREM** + + Zrem 将从有序集合`key`中删除指定的成员`member`。如果`member`不存在则被忽略。当key存在,但是不是有序集合类型时,返回类型错误。返回的是从有序集合中删除的成员个数,不包括不存在的成员。 + + ```shell + ZREM key member [member ...] + ``` + + + +- **ZCARD** + + Zcard 将返回有序集的成员个数。 当 `key` 不存在时,返回 `0` 。 + + ```shell + ZCARD key + ``` + + + +**常用命令** + +- **ZRANGEBYSCORE** + + 该指令将返回有序集 `key` 中,所有 `score` 值介于 `min` 和 `max` 之间(包括等于 `min` 或 `max` )的成员。有序集成员按 `score` 值递增(从小到大)次序排列。具有相同 `score` 值的成员按字典序来排列(该属性是有序集提供的,不需要额外的计算)。可选的 `LIMIT` 参数指定返回结果的数量及区间(就像SQL中的 `SELECT LIMIT offset, count` ),注意当 `offset` 很大时,定位 `offset` 的操作可能需要遍历整个有序集,此过程最坏复杂度为 O(N) 时间。可选的 `WITHSCORES` 参数决定结果集是单单返回有序集的成员,还是将有序集成员及其 `score` 值一起返回。 + + ```shell + ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] + ``` + + + +- **ZREVRANGEBYSCORE** + + 该指令将返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。具有相同分数值的成员按字典序来排列(该属性是有序集提供的,不需要额外的计算)。默认情况下,区间的取值使用闭区间 (小于等于或大于等于),你也可以通过给参数前增加 ( 符号来使用可选的开区间 (小于或大于)。可选的LIMIT参数指定返回结果的数量及区间(类似SQL中SELECT LIMIT offset, count)。注意,如果offset太大,定位offset就可能遍历整个有序集合,这会增加O(N)的复杂度。可选参数WITHSCORES会返回元素和其分数,而不只是元素。 + + ```shell + ZREVRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] + ``` + + + +- **ZCOUNT** + + Zcount 将返回有序集 `key` 中, `score` 值在 `min` 和 `max` 之间(默认包括 `score` 值等于 `min` 或 `max` )的成员的数量。 + + ```shell + ZCOUNT key min max + ``` + + + +## Python调用Redis + +在Python中,目前可以通过一个redis模块来实现操控Redis,下面我们简单的介绍一下关于使用redis模块。 + +### 安装Redis模块 + +如果是在Windows 系统,安装 redis 模块可以使用以下命令: + +```shell +python -m pip install redis +``` + +如果是 Linux 系统,需要执行以下命令来安装: + +```shell +sudo pip3 install redis +``` + +如果是使用Anaconda管理环境,也可以使用以下命令安装: + +```shell +conda install redis +``` + + + +### Python连接Redis + +Redis模块提供了两种连接的模式:直连模式和连接词模式。 + +**直连模式** + +直连模式的方式简单方便,适合少量长期连接的场景。其中host参数是ip地址,如果Redis服务存在于本地,可以使用127.0.0.1,或者换成Redis服务所在的ip地址。db表示当前选择的库,其参数值可以是 0-15;如果设置连接数据库的密码,那么就需要使用password进行验证。 + +```python +import redis + +r = redis.Redis(host='127.0.0.1',port=6379,db=0,password='') +r.set('name':'jiangyou') +print(r.get('name')) +``` + +**连接池模式** + +连接池模式是使用 connection pool(连接池)来管理 redis server 的所有连接,每个Redis实例会维护自己的连接池来管理管理对一个 redis server 所有的连接,避免每次建立,释放连接的开销。 + +```python +import redis + +pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,password="",decode_responses=True, max_connections=10) +r1 = redis.Redis(connection_pool=pool) # 第一个客户端访问 +r2 = redis.Redis(connection_pool=pool) # 第二个客户端访问 +``` + +上面的参数,decode_responses=True 可以使得redis取出的结果改成字符串,其默认的是字节, max_connections参数可以设置最大连接数量,这样当有新的客户端请求连接时,只需要去连接池获取即可,这样就可以把一个连接共享给多个客服端,减少每次连接所消耗的时间以及资源。 + + + +**基本操作** + +在Redis模块中,提供了**Redis**和**StrictRedis**来支持Redis访问和操作。其中 **StrictRedis** 使用python基于Redis协议实现了所有官方的Redis操作命令,也就是说其实对于python操作redis的API接口和上面提到的Redis官方接口一样。因此下面我们就简单介绍一些常用的方法。 + +1. **String操作** + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,password="",decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + r.set('name','jiang') + r.append("name","you") # 在redis name对应的值后面追加内容 + + r.mset({'age':'26','home':'liaoning'}) + print(r.mget('name','age','home')) + print("name 长度:%d"%r.strlen('name')) #查看ame对应值的长度 + + r.incrby('age',5) #数值操作 将age对应的值 加5 + print(r.get('age')) + r.decrby('age',5) #数值操作 将age对应的值 减5 + print(r.get('age')) + r.incrbyfloat('age',5.2) #将age对应的值 加5.2 + print(r.get('age')) + r.incrbyfloat('age',-10.5) #将age对应的值 减10.5 + print(r.get('age')) + + r.setrange('name',5,'hahaha') # 修改字符串内容,从指定字符串索引开始向后替换。 + print(r.getrange('name',0,6)) # 获取子序列(根据字节获取,非字符),闭区间 + + r.delete('name') #删除key + ``` + + 运行结果 + + ```python + ['jiangyou', '26', 'liaoning'] + name 长度:8 + 31 + 26 + 31.2 + 20.7 + jiangha + ``` + +2. **Hash操作** + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,password="",decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + r.hset('user1','name','zhangsan') # user1对应的hash中设置一个键值对(不存在,则创建;否则,修改) + r.hset('user1','age','22') # user1对应的hash中设置一个键值对(不存在,则创建;否则,修改) + r.hincrbyfloat('user1','age',0.5) # 自增user1对应的hash中的指定key的值,不存在则创建key=amount + print(r.hmget('user1','name','age')) # 在user1对应的hash中获取多个key的值 + + # 一次性设置多个field和value + user_dict = { + 'password':'123', + 'gender':'M', + 'home':'辽宁' + } + r.hmset('user1',user_dict) # 在user1对应的hash中批量设置键值对 + + print("user1中存在键值对的个数:%d "%r.hlen('user1')) # 获取所有数据,字典类型 + print("user1中存在键值对的具体信息:%s"%r.hgetall('user1')) # 获取所有数据,字典类型 + print(r.hkeys("user1")) # 获取所有fields字段 + print(r.hvals("user1")) # 获取所有fields字段的values值 + + if r.hexists("user1","home"): # 检查user1对应的hash是否存在当前传入的home + r.hdel("user1",'home') # 将user1对应的hash中指定key的键值对删除 + print("已删除该键!!!") + else: + print("不存在该键!!!") + ``` + + 运行结果 + + ```python + ['zhangsan', '22.5'] + user1中存在键值对的个数:5 + user1中存在键值对的具体信息:{'name': 'zhangsan', 'age': '22.5', 'password': '123', 'gender': 'M', 'home': '辽宁'} + ['name', 'age', 'password', 'gender', 'home'] + ['zhangsan', '22.5', '123', 'M', '辽宁'] + 已删除该键!! + ``` + + + +3. **List操作** + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,password="",decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + r.lpush('database','sql','mysql','redis') # 在database对应的list中添加元素,每个新的元素都添加到列表的最左边 + print(r.lrange('database',0,-1)) + + r.linsert('database','before','mysql','mongodb') # 在database对应的列表的某一个值前或后插入一个新值,其含义为在第三个参数的前(before)或后(after) 插入参数四 + + print(r.lrange('database',0,-1)) # 在database对应的列表分片获取数据 + + print("database中元素个数:%d"%r.llen('database')) # database对应的list元素的个数 + + print("database中第2个元素:%s"%r.lindex('database',2)) #在database对应的列表中根据索引获取列表元素 + + r.lset('database', 0, 'redisdb') # 对database对应的list中的某一个索引位置重新赋值 + print(r.lrange('database',0,-1)) + + print(r.rpop('database')) # 在database对应的列表的右侧获取第一个元素并在列表中移除,返回值则是第一个元素 + + print(r.ltrim('database',0,1)) # 在database对应的列表中移除没有在start-end索引之间的值 + + while True: + result = r.brpop('database',1) # 从一个列表的右侧移除一个元素并将其添加到另一个列表的左侧 [如果列表中为空时,则返回None] + if result: + print(result) + else: + break + r.delete('database') + ``` + + 运行结果 + + ```python + ['redis', 'mysql', 'sql'] + ['redis', 'mongodb', 'mysql', 'sql'] + database中元素个数:4 + database中第2个元素:mysql + ['redisdb', 'mongodb', 'mysql', 'sql'] + sql + True + ('database', 'mongodb') + ('database', 'redisdb') + ``` + + + +4. **Set操作** + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,password="",decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + + r.sadd("name","zhangsan") # 给name对应的集合中添加元素 + r.sadd("name","zhangsan","lisi","wangwu") + + + print(r.smembers('name')) # 获取name对应的集合的所有成员 + + print(r.scard("name") ) # 获取name对应的集合中的元素个数 + + print(r.sismember('name','zhangsan')) # 检查value是否是name对应的集合内的元素,返回值为True或False + + print(r.spop('name')) # 随机删除并返回指定集合的一个元素 + print(r.smembers('name')) + + # srem(name, value) + print(r.srem("name", "zhangsan")) # 删除集合中的某个元素 + print(r.smembers('name')) + + r.sadd("name","a","b") + r.sadd("name1","b","c") + r.sadd("name2","b","c","d") + + print(r.sinter("name","name1","name2")) # 获取多个name对应集合的交集 + + print(r.sunion("name","name1","name2")) # 获取多个name对应的集合的并集 + + print(r.sdiff("name","name1","name2")) # 在第一个name对应的集合中且不在其他name对应的集合的元素集合 + + r.flushall() + ``` + + 运行结果 + + ```python + {'zhangsan', 'lisi', 'wangwu'} + 3 + True + lisi + {'zhangsan', 'wangwu'} + 1 + {'wangwu'} + {'b'} + {'d', 'c', 'b', 'wangwu', 'a'} + {'wangwu', 'a'} + ``` + + + +5. **SortedSet操作** + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + mapping = { + 'zhangsan':85, + 'lisi':92, + 'wangwu':76 + } + r.zadd('C++',mapping,nx=True) # 在C++对应的有序集合中添加元素 + print(r.zrange('C++',0,-1,withscores=True)) # 获取C++对应的有序集合的所有元素 + + print(r.zcard("C++")) # 获取C++对应的有序集合元素的数量 + print(r.zcount('C++',min=0,max=90)) # 获取C++对应的有序集合中分数 在 [min,max] 之间的个数 + + r.zincrby(name='C++',value='lisi',amount=3) # 增加C++对应的有序集合的lisi对应的分数 + print(r.zrange('C++',0,-1,desc=False,withscores=True)) # 按照索引范围获取C++对应的有序集合的元素,排序规则,默认按照分数从小到大排序 + print(r.zrevrange('C++',0,-1,withscores=True)) # 按照索引范围获取C++对应的有序集合的元素,排序规则,默认按照分数从大到小排序 + + print(r.zrangebyscore('C++',70,90)) # 按照分数范围获取C++对应的有序集合的元素,排序规则,默认按照分数从小到大排序 + print(r.zrevrangebyscore('C++',90,70)) # 按照分数范围获取C++对应的有序集合的元素,排序规则,默认按照分数从大到小排序 + + print(r.zrank('C++','lisi')) # Zrank 返回有序集中指定成员的排名,有序集成员按分数值递增(从小到大)顺序排列。 + print(r.zrevrank('C++','lisi')) # Zrevrank 返回有序集中指定成员的排名,有序集成员按分数值递增(从大到小)顺序排列。 + + mapping = { + 'xuliu':74, + 'lisi':82, + 'wangwu':87 + } + r.zadd('python',mapping,nx=True) + r.zinterstore('sum_score_i',['C++','python'],aggregate='sum') # 获取两个有序集合的交集,如果遇到相同值不同分数,则按照aggregate进行操作 + print(r.zrange('sum_score_i',0,-1,withscores=True)) + print(r.zunionstore('sum_score_u',['C++','python'],'min')) # 获取两个有序集合的并集,如果遇到相同值不同分数,则按照aggregate进行操作 + print(r.zrange('sum_score_u',0,-1,withscores=True)) + + r.zrem('C++', 'zhangsan') # 删除C++对应的有序集合中值是zhangsan的成员 + print(r.zrange('C++',0,-1,withscores=True)) + + r.zremrangebyscore('C++', min=80, max=100) # 删除C++对应的有序集合中值是zhangsan的成员 + print(r.zrange('C++',0,-1,withscores=True)) + + r.zremrangebyrank('python', min=1, max=3) # 根据排行范围删除 + print(r.zrange('python',0,-1,withscores=True)) + ``` + + 运行结果 + + ```python + [('wangwu', 76.0), ('zhangsan', 85.0), ('lisi', 92.0)] + 3 + 2 + [('wangwu', 76.0), ('zhangsan', 85.0), ('lisi', 95.0)] + [('lisi', 95.0), ('zhangsan', 85.0), ('wangwu', 76.0)] + ['wangwu', 'zhangsan'] + ['zhangsan', 'wangwu'] + 2 + 0 + [('wangwu', 163.0), ('lisi', 177.0)] + 4 + [('xuliu', 74.0), ('wangwu', 76.0), ('lisi', 82.0), ('zhangsan', 85.0)] + [('wangwu', 76.0), ('lisi', 95.0)] + [('wangwu', 76.0)] + [('xuliu', 74.0)] + ``` + +6. **管道操作** + + Redis 模块默认在执行每次请求都会向连接池请求创建连接和断开申请操作,如果想要在一次请求中指定多个命令,则可以使用pipline实现一次请求指定多个命令,并且默认情况下一次pipline 是原子性操作(即为一次操作)。 + + ```python + import redis + + pool = redis.ConnectionPool(host="127.0.0.1",port=6379,db=0,decode_responses=True,max_connections=10) + r = redis.StrictRedis(connection_pool=pool) + + pipe = r.pipeline(transaction=True) + + pipe.set('name', 'jiangyou') + pipe.set('age', 'age') + pipe.execute() + + print(r.mget("name","age")) + ``` + + 运行结果 + + ```python + ['jiangyou', 'age'] + ``` + + diff --git a/docs/推荐系统实战/新闻推荐系统实践/flask简介及基础.md b/docs/推荐系统实战/新闻推荐系统实践/flask简介及基础.md new file mode 100644 index 00000000..c742ced8 --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/flask简介及基础.md @@ -0,0 +1,549 @@ +本文属于新闻推荐实战—前后端交互—后端构建之Flask。Flask作为该项目中会用来作为系统的后台框架,作为一个算法工程师需要了解一些关于开发的知识,因为在实际的工作中经常调试线上的代码来调用策略或模型。本文将对Flask以及一些基本的使用进行了简单的介绍,方便大家快速理解项目中的相关内容。 + +# Flask简介 + + Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。 + + Flask是目前十分流行的web框架,采用Python编程语言来实现相关功能。Flask框架的主要特征是核心构成比较简单,但具有很强的扩展性和兼容性,程序员可以使用Python语言快速实现一个网站或Web服务。一般情况下,它不会指定数据库和模板引擎等对象,用户可以根据需要自己选择各种数据库。 + + +[百度百科]: https://baike.baidu.com/item/Flask/1241509 +[维基百科]: https://zh.wikipedia.org/zh-hans/Flask + + + +# 一、 准备工作 + +在学习Flask之前,已经假设你对python已经有了一定的基础,并且对于计算机知识有了一定的掌握。 + +## 1.1 环境配置 + +为了保持全局环境的干净,指定不同的依赖版本,我们可以利用virtualenv来构建虚拟的环境,类似于anaconda。 + +```bash +pip install virtualenv +``` + +通过上述指令安装virtualenv,之后将在文件夹中创建新的虚拟环境。 + +```bash +mkdir newproj +cd newproj +virtualenv venv +``` + +要在Linux激活相应的环境。 + +```bash +venv/bin/activate +``` + +接下来就可以在这个环境中安装 Flask,当然如果你也可以选择使用下述指令直接在全局环境中安装Flask。 + +```bash +pip install Flask +``` + +## 1.2 测试安装 + +为了测试装的Flask是否能正常使用,可以在编译器中输入一下代码: + +```python +from flask import Flask +app = Flask(__name__) + +@app.route('/') +def hello_world(): + return 'Hello World' + +if __name__ == '__main__': + app.run() +``` + +运行上述代码,在浏览器中打开**localhost: 5000**,将显示**Hello World**`消息。 + +```python +python Hello.py +``` + +上述代码中,Flask将(__name__)作为参数,即Flask在当前模块运行,route()函数是一个装饰器,将请求的url映射到对应的函数上。上述代码将'/'与hello_world()函数进行绑定,因此在请求localhost:5000时,网页显示 Hello World 结果。 + +程序的启动是用过Flask类的run()方法在本地启动服务器应用程序。 + +```python +app.run(host, port, debug, options) +``` + +其中参数是可选的。 + +| 序号 | 参数与描述 | +| ---- | ------------------------------------------------------------ | +| 1 | **host** 要监听的主机名。 默认为127.0.0.1(localhost)。设置为“0.0.0.0”以使服务器在外部可用 | +| 2 | **port** 默认值为5000 | +| 3 | **debug** 默认为false。 如果设置为true,则提供调试信息 | +| 4 | **options** 要转发到底层的Werkzeug服务器。 | + +# 二、主要内容 + +## 2.1 路由 + +在Flask中,路由是指用户请求的*URL*与*视图函数*之间的映射。Flask通过利用路由表将URL映射到对应的视图函数,根据视图函数的执行结果返回给WSGI服务器。路由表的内容是由开发者进行填充,主要有一下两个方式。 + +- **route装饰器**:使用Flask应用实例的*route*装饰器将一个URL规则绑定到 一个视图函数上。 + + ```python + @app.route('/test') + def test(): + return 'this is response of test function.' + ``` + + 通过装饰器的方式,Flask框架会将URL规则/test 绑定到视图函数 test()上。 + +- **add_url_rule()** :该方法直接会在路由表中注册映射关系。其实*route*装饰器内部也是通过调用add_url_rule()方法实现的路由注册。 + + ```python + def test(): + return 'this is response of test function.' + app.add_url_rule('/test',view_func=test) + ``` + +### 2.1.1 指定HTTP方法 + +默认情况下,Flask的路由支持HTTP的*GET*请求,如果需要视图函数支持HTTP的其他方法,可以通过*methods*关键字参数进行设置。关键字参数*methods*的类型为*list*,可以同时指定多种HTTP方法。 + +```python +@app.route('/user', methods = ['POST', 'GET']) +def get_users(): + if request.method == 'GET': + return ... # 返回用户列表 + else: + return ... # 创建新用户 +``` + + + +### 2.1.2 匹配动态URL + +动态URL用于当需要将*同一类URL*映射到同一个视图函数处理,比如,使用同一个视图函数 来显示不同用户的个人信息。那么可以将URL中的可变部分*使用一对小括号*<>声明为变量, 并为视图函数声明同名的参数: + +```python +@app.route('/user/') +def get_userInfo(uname): + return '%s\'s Informations' % uname +``` + +除了上述方式来设置参数,还可以在URL参数前添加转换器来转换参数类型: + +```python +@app.route('/user/') +def get_userInfo(uname): + return '%s\'s Informations' % uname +``` + +使用该方法时,请求的参数必须是属于int类型,否则将会出现404错误。目前支持的参数类型转换器有: + +| 类型转换器 | 作用 | +| :--------- | :------------------- | +| 缺省 | 字符型,但不能有斜杠 | +| int: | 整型 | +| float: | 浮点型 | +| path: | 字符型,可有斜杠 | + + + +### 2.1.3 匹配动态URL + +为了满足一个视图函数可以解决多个问题,因此每个视图函数可以配置多个路由规则。 + +```python +@app.route('/user') +@app.route('/user/') +@app.route('/user/') +def get_userInfo(uname=None): + if uname: + return '%s\'s Informations' % uname + else: + return 'this is all informations of users' +``` + + + +### 2.1.4 URL构建方法 + +在很多时候,在一个实用的视图中需要指向其他视图的连接,为了防止路径出现问题,我们可以让Flask框架帮我们计算链接URL。简单地给url_for()函数传入一个访问点,它返回将是一个可靠的URL地址: + +```python +@app.route('/') +def hello(): + return 'Hello world!' + +@app.route('/user/') +def get_userInfo(uname=None): + if uname: return '%s\'s Informations' % uname + else: return 'this is all informations of users' +@app.route('/test') +def test_url_for(): + print(url_for('hello')) # 输出:/ +``` + +添加URL变量 : 如果指定访问点对应的视图函数接收参数,那么关键字参数将生成对应的参数URL。下面的 示例将生成 /user/zhangsan: + +```python +@app.route('/') +def hello(): + return 'Hello world!' + +@app.route('/user/') +def get_userInfo(uname=None): + if uname: + return '%s\'s Informations' % uname + else: + return 'this is all informations of users' + +@app.route('/test') +def test_url_for(): + print(url_for('get_userInfo', uname='zhangsan')) # 输出:/user/zhangsan + print(url_for('test_url_for', num=2)) # 输出:/test?num=2 +``` + + + +## 2.2 请求,响应及会话 + +对于一个完整的HTTP请求,包括了来自客户端的请求对象(Request),服务器端的响应对象(Respose)和会话对象(Session)等。在Flask框架中,当然也具有这些对象,这些对象不仅可以在请求函数中使用,同时也可以在模板中使用。那我们来简单看看这些对象具体怎么使用。 + +### 2.2.1 请求对象 request + +在Flask包中,可以直接引入request对象,其中包含**Form**,**args** ,**Cookies** ,**files** 等属性。**Form** 是一个字典对象,包含表单当中所有参数及其值的键和值对;**args** 是解析查询字符串的内容,它是问号(?)之后的URL的一部分,当使用get请求时,通过URL传递参数时可以通过**args**属性获取;**Cookies** 是用来保存Cookie名称和值的字典对象;**files** 属性和上传文件有关的数据。我们以一个登陆的例子看看如何搭配使用这些属性 + +```python +from flask import request, session, make_response + +@app.route('/login', methods=['POST', 'GET']) +def login(): + if request.method == 'POST': + if request.form['username'] == 'admin': + session['username'] = request.form['username'] + response = make_response('Admin login successfully!') + response.set_cookie('login_time', time.strftime('%Y-%m-%d %H:%M:%S')) + return 'Admin login successfully!' + else: + return 'No such user!' + elif request.method == 'GET': + if request.args.get("username") == 'admin': + session['username'] = request.form['username'] + return 'Admin login successfully!' + else: + return 'No such user!' + +app.secret_key = '123456' +``` + +上述代码中,可以根据method属性判断当前请求的类型,通过form属性可以获取表单信息,并通过session来存储用户登陆信息。特别提醒,使用session时一定要设置一个密钥`app.secret_key`,并且密钥要尽量复杂。 + +我们可以使用make_response的方法就是用来构建`response`对象的第二个参数代表响应状态码,缺省就是”200”。`response`对象的详细使用可参阅Flask的[官方API文档](http://flask.pocoo.org/docs/0.10/api/#response-objects)。通过创建的`response`对象可以使用`response.set_cookie()`函数,来设置Cookie项,之后这个项值会被保存在浏览器中,等下次请求时可以从request对象中获取到cookies对象。 + +由于现在前后端的交互会采用json的数据格式进行传输,因此当前端请求的数据是json类型的时候,可以使用get_data()方法来获取。 + +```python +from flask import Flask, jsonify, request +@app.route('/login', methods=["POST"]) +def login(): + request_str = request.get_data() + request_dict = json.loads(request_str) +``` + +获取json数据之后,可以使用flask中的jsonify对象来处理json类型数据。 + + + +### 2.2.2 响应对象 response + +如果视图函数想向前端返回数据,必须是`Response`的对象, 主要讲返回数据的几种方式: + +**视图函数 return 多个值** + +```python +@app.route("/user_one") +def user_one(): + return "userInfo.html", "200 Ok", {"name": "zhangsan"; "age":"20"} +``` + +当return多个值的时候,第一个是字符串,也是网页的内容;"200 Ok"表示状态码及解析;{"name": "zhangsan"; "age":"20"} 表示请求头。其中前面两个值是必须要的并且顺序不能改变,请求头不是必须要的,这样Flask会自动将返回值转换成一个相应的Response对象。如果仅返回一个字符串,则返回的Response对象会将该字符串作为body,状态码置为200。 + +**使用Response()构造Response对象** + +可以使用Response()手动构造一个Response对象,配置其参数后返回该对象。 + +```python +from flask import Response + +@app.route("/user_one") +def user_one(): + response = Response("user_one") + response.status_code = 200 + response.status = "200 ok" + response.data = {"name": "zhangsan"; "age":"20"} + return response +``` + +**使用make_response函数构造Response对象** + +`make_response` 函数可以传递三个参数 第一个是一个字符串,第二个传状态码,第三个传请求头。 + +```python +@app.route("/user_one") +def user_one(): + response = make_response('user_one', 200, {"name": "zhangsan"; "age":"20"}) + return response +``` + +由于现在前后端交互往往采用的是json的数据格式,因此可以将数据通过 jsonify 函数将其转化成json格式,再通过response对象发送给前端。 + +```python +@app.route('/hot_list', methods=["GET"]) +def hot_list(): + if request.method == "GET": + user_id = request.args.get('user_id') + page_id = request.args.get('page_id') + if user_id is None or page_id is None: + return make_response(jsonify({"code": 2000, "msg": "user_id or page_id is none!"}), 200) +``` + + + +## 2.3 重定向与错误处理 + +### 2.3.1重定向 + +当一个请求过来后可能还需要再请求另一个视图函数才能达到目的,那么就可以调用`redirect(location, code=302, Response=None)`函数指定重定向页面。 + +```python +from flask import Flask, redirect, url_for + +app = Flask(__name__) + +@app.route("/demo") +def demo(): + url = url_for("demo2") # 路由反转,根据视图函数名获取路由地址 + return redirect(url) + +@app.route("/demo2") +def demo2(): + return "this is demo2 page" + +@app.route("/") +def index(): + # 使用方法:redirect(location, code=302, Response=None) + return redirect("/demo", 301) +``` + +#### 常用重定向状态码 + +| 状态码 | 说明 | +| ------ | ----------------------------- | +| 300 | Multiple Choice,让用户选择 | +| 301 | Moved Permanently,永久重定向 | +| 302 | Found,临时重定向 | +| 303 | See Other,查看其它位置 | +| 304 | Not Modified,资源未发生变化 | +| 305 | Use Proxy,需要通过代理访问 | + + + +### 2.3.2错误处理 + +当请求或服务器出现错误的时候,我们希望遇到特定错误代码时重写错误页面,可以使用 **errorhandler()** 装饰器: + +```python +from flask import render_template + +@app.errorhandler(404) +def page_not_found(error): + return render_template('page_not_found.html'), 404 +``` + +当遇到404错误时,会调用page_not_found()函数,返回元组数据,第一个元素是”page_not_found.html”的模板页,第二个元素代表错误代码,返回值会自动转成 response 对象。 + +## 2.4 SQLAlchemy + +SQLAlchemy 是一个功能强大的Python ORM 工具包,为应用程序开发人员提供了SQL的全部功能和ORM操作。其中ORM (Object Relation Mapping)指的是将对象参数映射到底层RDBMS表结构的技术,ORM API提供了执行CRUD操作的方法,不需要程序员编写原始SQL语句。 + +### 2.4.1安装 + +通过下面指令可以进行安装: + +```shell +pip install SQLalchemy +``` + +在连接数据库时,我们使用pymysql框架进行连接,因此还需要使用下面指令下载pymysql框架: + +```shell +pip install pymysql +``` + + + +### 2.4.2 创建连接 + +```python +from sqlalchemy import create_engine + +def mysql_db(host='127.0.0.1',dbname='3306'): + engine = create_engine("mysql+pymysql://root:123456@{}:49168/{}?charset=utf8".format(host,dbname)) + print(engine) # Engine(mysql+pymysql://root:***@127.0.0.1:49168/3306?charset=utf8) +``` + +通过create_engine函数已经创建了Engine,在Engine内部实际上会创建一个Pool(连接池)和Dialect(方言),并且可以发现此时Engine并不会建立连接,只会等到执行到具体的语句时才会连接到数据库。上述代码默认本地已经存在并开启mysql服务。 + +对于 create_engine 函数可以有以下参数 + +```javascript +create_engine("mysql://user:password@hostname/dbname?charset=utf8", + echo=True, + pool_size=8, + pool_recycle=60*30) +``` + +第一个参数是和框架表明连接数据库所需的信息,"数据库+数据库连接框架://用户名:密码@IP地址:端口号/数据库名称?连接参数";echo是设置当前ORM语句是否转化为SQL打印;pool_size是用来设置连接池大小,默认值为5;pool_recycle设置连接失效的时间,超过时间连接池会自动断开。 + +### 2.4.3 **创建数据库表类** + +由于SQLAlchemy 是对象关系映射,在操作数据库表时需要通过操作对象实现,因此就需要创建一个数据库表类。 + +```python +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = 'UserInfo' + index = Column(Integer(), primary_key=True) + user_id = Column(Integer(), unique=True) + username = Column(String(30)) + passwd = Column(String(500)) + + def __init__(self,index, user_id, username, passwd): + self.index = index + self.user_id = user_id + self.username = username + self.passwd = passwd +``` + +通过declarative_base()函数,可以将python类和数据库表进行关联映射,并通过 \__tablename\__ 属性将数据库模型类和表进行管理。其中Column() 表示数据表中的列,Integer()和String()表示数据库的数据类型。 + +### 2.4.4 **操作数据库** + +创建完连接之后,我们需要借助sqlalchemy中的session来创建程序与数据库之间的会话。换句话来说,需要通过session才能利用程序对数据库进行CURD。这里我们可以通过 sessionmaker() 函数来创建会话。 + +```python +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +Base = declarative_base() + +def mysql_db(host='127.0.0.1',dbname='test'): + engine = create_engine("mysql+pymysql://root:123456@{}:49168/{}?charset=utf8mb4".format(host,dbname)) + + session = sessionmaker(bind=engine) + Base.metadata.create_all(engine) + return engine, session() +``` + +session常用的方法如下: + +- flush:预提交,提交到数据库文件,还未写入数据库文件中 +- commit:提交了一个事务 +- rollback:回滚 +- close:关闭session连接 + +**增加数据** + +增加一个用户: + +```python +engine, session = mysql_db() +user = User("100","zhangsan","11111") +session.add(user) +session.commit() +``` + +注意一点,session.add()不会直接提交到数据库,而是在 commit 时才会提交到数据库。add操作会把user加入当前session维护的持久空间(可以从session.dirty看到)中。 + +也可以通过add_all() 进行批量提交。 + +```python +engine, session = mysql_db() +user1 = User("101","lisi","11111") +user2 = User("102","wangwu","22222") +session.add_all([user1,user2]) +session.commit() +``` + +**查询数据** + +```python +engine, session = mysql_db() +users = session.query(User).filter_by(passwd='11111').all() + +for item in users: + print(item.username,item.passwd) +``` + +通过上面代码可以查询获取数据,通过 **session.query()** 我们查询返回了一个Query对象,此时没有去数据库查询,只有等到.count() .first() .all() 具体函数时才会去数据库执行。还可以使用 **filter()** 方法查询,与 **filter_by()** 的区别如下: + +| | | +| :----------------------------------------- | :--------------------------- | +| filter | filter_by | +| 支持所有比较运算符,相等比较用比较用== | 只能使用"=","!="和"><" | +| 过滤用类名.属性名 | 过滤用属性名 | +| 不支持组合查询,只能连续调用filter变相实现 | 参数是**kwargs,支持组合查询 | +| 支持and,or和in等 | | + +**修改数据** + +通过 query 中的 update() 方法: + +```python +session.query(User).filter_by(username="zhangsan").update({'passwd': "123456"}) +``` + +或者 + +```python +users = session.query(User).filter_by(username="zhangsan").first() +users.username = "zhangsan-test" +session.add(users) +session.commit() +``` + +**删除数据** + +通过 query 中的 delete() 方法: + +```python +session.query(User).filter(User.username == "zhangsan-test").delete() +session.commit() +``` + +或者 通过 session.delete() 方法 + +```python +users = session.query(User).filter(User.username == "lisi").first() +if users: + session.delete(users) + session.commit() +``` + + + +### 参考资料 + +1. [Flask教程](https://www.w3cschool.cn/flask/flask_sqlalchemy.html) + +2. [[SQLAlchemy 1.4 Documentation](https://www.osgeo.cn/sqlalchemy/index.html) + + diff --git a/docs/推荐系统实战/新闻推荐系统实践/scrapy基础及新闻爬取实战.md b/docs/推荐系统实战/新闻推荐系统实践/scrapy基础及新闻爬取实战.md new file mode 100644 index 00000000..8a74c546 --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/scrapy基础及新闻爬取实战.md @@ -0,0 +1,511 @@ +本文属于新闻推荐实战-数据层-构建物料池之scrapy爬虫框架基础。对于开源的推荐系统来说数据的不断获取是非常重要的,scrapy是一个非常易用且强大的爬虫框架,有固定的文件结构、类和方法,在实际使用过程中我们只需要按照要求实现相应的类方法,就可以完成我们的爬虫任务。文中给出了新闻推荐系统中新闻爬取的实战代码,希望读者可以快速掌握scrapy的基本使用方法,并能够举一反三。 + +## Scrapy基础及新闻爬取实战 + +### python环境的安装 + +python 环境,使用miniconda搭建,安装miniconda的参考链接:https://blog.csdn.net/pdcfighting/article/details/111503057。 + +在安装完miniconda之后,创建一个新闻推荐的虚拟环境,我这边将其命名为news_rec_py3,**这个环境将会在整个新闻推荐项目中使用。** + +```C++ +conda create -n news_rec_py3 python==3.8 +``` + +### Scrapy的简介与安装 + +Scrapy 是一种快速的高级 web crawling 和 web scraping 框架,**用于对网站内容进行爬取,并从其页面提取结构化数据**。 + +Ubuntu下安装Scrapy,需要先安装依赖Linux依赖 + +```C++ +sudo apt-get install python3 python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev +``` + +在新闻推荐系统虚拟conda环境中安装scrapy + +```C++ +pip install scrapy +``` + +#### scrapy项目结构 + +默认情况下,所有scrapy项目的项目结构都是相似的,在指定目录对应的命令行中输入如下命令,就会在当前目录创建一个scrapy项目 + +``` +scrapy startproject myproject +``` + +项目的目录结构如下: + +```C++ +myproject/ + scrapy.cfg + + myproject/ + __init__.py + items.py + middlewares.py + pipelines.py + settings.py + spiders/ + __init__.py +``` + +- scrapy.cfg: 项目配置文件 +- myproject/ : 项目python模块, 代码将从这里导入 +- **myproject/ items.py: 项目items文件,** +- **myproject/ pipelines.py: 项目管道文件,将爬取的数据进行持久化存储** +- myproject/ settings.py: 项目配置文件,可以配置数据库等 +- **myproject/ spiders/: 放置spider的目录,爬虫的具体逻辑就是在这里实现的(具体逻辑写在spider.py文件中),可以使用命令行创建spider,也可以直接在这个文件夹中创建spider相关的py文件** +- myproject/ middlewares:中间件,请求和响应都将经过他,可以配置请求头、代理、cookie、会话维持等 + +#### spider + +**spider是定义一个特定站点(或一组站点)如何被抓取的类,包括如何执行抓取(即跟踪链接)以及如何从页面中提取结构化数据(即抓取项)。换言之,spider是为特定站点(或者在某些情况下,一组站点)定义爬行和解析页面的自定义行为的地方。** + +爬行器是自己定义的类,Scrapy使用它从一个网站(或一组网站)中抓取信息。它们必须继承 `Spider` 并定义要做出的初始请求,可选的是如何跟随页面中的链接,以及如何解析下载的页面内容以提取数据。 + +对于spider来说,抓取周期是这样的: + +1. 首先生成对第一个URL进行爬网的初始请求,然后指定一个回调函数,该函数使用从这些请求下载的响应进行调用。要执行的第一个请求是通过调用 `start_requests()` 方法,该方法(默认情况下)生成 `Request` 中指定的URL的 `start_urls` 以及 `parse` 方法作为请求的回调函数。 +2. 在回调函数中,解析响应(网页)并返回 [item objects](https://www.osgeo.cn/scrapy/topics/items.html#topics-items) , `Request` 对象,或这些对象的可迭代。这些请求还将包含一个回调(可能相同),然后由Scrapy下载,然后由指定的回调处理它们的响应。 +3. 在回调函数中,解析页面内容,通常使用 [选择器](https://www.osgeo.cn/scrapy/topics/selectors.html#topics-selectors) (但您也可以使用beautifulsoup、lxml或任何您喜欢的机制)并使用解析的数据生成项。 +4. 最后,从spider返回的项目通常被持久化到数据库(在某些 [Item Pipeline](https://www.osgeo.cn/scrapy/topics/item-pipeline.html#topics-item-pipeline) )或者使用 [Feed 导出](https://www.osgeo.cn/scrapy/topics/feed-exports.html#topics-feed-exports) . + +**下面是官网给出的Demo:** + +```python +import scrapy + +class QuotesSpider(scrapy.Spider): + name = "quotes" # 表示一个spider 它在一个项目中必须是唯一的,即不能为不同的spider设置相同的名称。 + + # 必须返回请求的可迭代(您可以返回请求列表或编写生成器函数),spider将从该请求开始爬行。后续请求将从这些初始请求中相继生成。 + def start_requests(self): + urls = [ + 'http://quotes.toscrape.com/page/1/', + 'http://quotes.toscrape.com/page/2/', + ] + for url in urls: + yield scrapy.Request(url=url, callback=self.parse) # 注意,这里callback调用了下面定义的parse方法 + + # 将被调用以处理为每个请求下载的响应的方法。Response参数是 TextResponse 它保存页面内容,并具有进一步有用的方法来处理它。 + def parse(self, response): + # 下面是直接从response中获取内容,为了更方便的爬取内容,后面会介绍使用selenium来模拟人用浏览器,并且使用对应的方法来提取我们想要爬取的内容 + page = response.url.split("/")[-2] + filename = f'quotes-{page}.html' + with open(filename, 'wb') as f: + f.write(response.body) + self.log(f'Saved file {filename}') +``` + +#### Xpath + +**XPath 是一门在 XML 文档中查找信息的语言,XPath 可用来在 XML 文档中对元素和属性进行遍历。在爬虫的时候使用xpath来选择我们想要爬取的内容是非常方便的**,这里就提一下xpath中需要掌握的内容,参考资料中的内容更加的详细(建议花一个小时看看)。 + +要了解xpath, 需要先了解一下HTML(是用来描述网页的一种语言), 这个的细节就不详细展开 + +**划重点:** + +1. **xpath路径表达式:**XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。节点是通过沿着路径 (path) 或者步 (steps) 来选取的。 + +2. **了解如何使用xpath语法选取我们想要的内容,所以需要熟悉xpath的基本语法** + +#### scrapy爬取新闻内容实战 + +在介绍这个项目之前先说一下这个项目的基本逻辑。 + +**环境准备:** + +1. 首先Ubuntu系统里面需要安装好MongoDB数据库,这个可以参考开源项目MongoDB基础 +2. python环境中安装好了scrapy, pymongo包 + +**项目逻辑:** + +1. 每天定时从新浪新闻网站上爬取新闻数据存储到mongodb数据库中,并且需要监控每天爬取新闻的状态(比如某天爬取的数据特别少可能是哪里出了问题,需要进行排查) +2. 每天爬取新闻的时候只爬取当天日期的新闻,主要是为了防止相同的新闻重复爬取(当然这个也不能完全避免爬取重复的新闻,爬取新闻之后需要有一些单独的去重的逻辑) +3. 爬虫项目中实现三个核心文件,分别是sina.py(spider),items.py(抽取数据的规范化及字段的定义),pipelines.py(数据写入数据库) + +因为新闻爬取项目和新闻推荐系统是放在一起的,为了方便提前学习,下面直接给出项目的目录结构以及重要文件中的代码实现,最终的项目将会和新闻推荐系统一起开源出来 + +image-20211103214124327 + +1. **创建一个scrapy项目:** + +```shell +scrapy startproject sinanews +``` + +2. **实现items.py逻辑** + +```python +# Define here the models for your scraped items +# +# See documentation in: +# https://docs.scrapy.org/en/latest/topics/items.html + +import scrapy +from scrapy import Item, Field + +# 定义新闻数据的字段 +class SinanewsItem(scrapy.Item): + """数据格式化,数据不同字段的定义 + """ + title = Field() # 新闻标题 + ctime = Field() # 新闻发布时间 + url = Field() # 新闻原始url + raw_key_words = Field() # 新闻关键词(爬取的关键词) + content = Field() # 新闻的具体内容 + cate = Field() # 新闻类别 +``` + +3. **实现sina.py (spider)逻辑** + + 这里需要注意的一点,这里在爬取新闻的时候选择的是一个比较简洁的展示网站进行爬取的,相比直接去最新的新浪新闻观光爬取新闻简单很多,简洁的网站大概的链接:https://news.sina.com.cn/roll/#pageid=153&lid=2509&k=&num=50&page=1 + +image-20211103213354334 + +```python +# -*- coding: utf-8 -*- +import re +import json +import random +import scrapy +from scrapy import Request +from ..items import SinanewsItem +from datetime import datetime + + +class SinaSpider(scrapy.Spider): + # spider的名字 + name = 'sina_spider' + + def __init__(self, pages=None): + super(SinaSpider).__init__() + + self.total_pages = int(pages) + # base_url 对应的是新浪新闻的简洁版页面,方便爬虫,并且不同类别的新闻也很好区分 + self.base_url = 'https://feed.mix.sina.com.cn/api/roll/get?pageid=153&lid={}&k=&num=50&page={}&r={}' + # lid和分类映射字典 + self.cate_dict = { + "2510": "国内", + "2511": "国际", + "2669": "社会", + "2512": "体育", + "2513": "娱乐", + "2514": "军事", + "2515": "科技", + "2516": "财经", + "2517": "股市", + "2518": "美股" + } + + def start_requests(self): + """返回一个Request迭代器 + """ + # 遍历所有类型的新闻 + for cate_id in self.cate_dict.keys(): + for page in range(1, self.total_pages + 1): + lid = cate_id + # 这里就是一个随机数,具体含义不是很清楚 + r = random.random() + # cb_kwargs 是用来向解析函数parse中传递参数的 + yield Request(self.base_url.format(lid, page, r), callback=self.parse, cb_kwargs={"cate_id": lid}) + + def parse(self, response, cate_id): + """解析网页内容,并提取网页中需要的内容 + """ + json_result = json.loads(response.text) # 将请求回来的页面解析成json + # 提取json中我们想要的字段 + # json使用get方法比直接通过字典的形式获取数据更方便,因为不需要处理异常 + data_list = json_result.get('result').get('data') + for data in data_list: + item = SinanewsItem() + + item['cate'] = self.cate_dict[cate_id] + item['title'] = data.get('title') + item['url'] = data.get('url') + item['raw_key_words'] = data.get('keywords') + + # ctime = datetime.fromtimestamp(int(data.get('ctime'))) + # ctime = datetime.strftime(ctime, '%Y-%m-%d %H:%M') + + # 保留的是一个时间戳 + item['ctime'] = data.get('ctime') + + # meta参数传入的是一个字典,在下一层可以将当前层的item进行复制 + yield Request(url=item['url'], callback=self.parse_content, meta={'item': item}) + + def parse_content(self, response): + """解析文章内容 + """ + item = response.meta['item'] + content = ''.join(response.xpath('//*[@id="artibody" or @id="article"]//p/text()').extract()) + content = re.sub(r'\u3000', '', content) + content = re.sub(r'[ \xa0?]+', ' ', content) + content = re.sub(r'\s*\n\s*', '\n', content) + content = re.sub(r'\s*(\s)', r'\1', content) + content = ''.join([x.strip() for x in content]) + item['content'] = content + yield item +``` + +4. **数据持久化实现,piplines.py** + + 这里需要注意的就是实现SinanewsPipeline类的时候,里面很多方法都是固定的,不是随便写的,不同的方法又不同的功能,这个可以参考scrapy官方文档。 + +```python +# Define your item pipelines here +# +# Don't forget to add your pipeline to the ITEM_PIPELINES setting +# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html +# useful for handling different item types with a single interface +import time +import datetime +import pymongo +from pymongo.errors import DuplicateKeyError +from sinanews.items import SinanewsItem +from itemadapter import ItemAdapter + + +# 新闻item持久化 +class SinanewsPipeline: + """数据持久化:将数据存放到mongodb中 + """ + def __init__(self, host, port, db_name, collection_name): + self.host = host + self.port = port + self.db_name = db_name + self.collection_name = collection_name + + @classmethod + def from_crawler(cls, crawler): + """自带的方法,这个方法可以重新返回一个新的pipline对象,并且可以调用配置文件中的参数 + """ + return cls( + host = crawler.settings.get("MONGO_HOST"), + port = crawler.settings.get("MONGO_PORT"), + db_name = crawler.settings.get("DB_NAME"), + # mongodb中数据的集合按照日期存储 + collection_name = crawler.settings.get("COLLECTION_NAME") + \ + "_" + time.strftime("%Y%m%d", time.localtime()) + ) + + def open_spider(self, spider): + """开始爬虫的操作,主要就是链接数据库及对应的集合 + """ + self.client = pymongo.MongoClient(self.host, self.port) + self.db = self.client[self.db_name] + self.collection = self.db[self.collection_name] + + def close_spider(self, spider): + """关闭爬虫操作的时候,需要将数据库断开 + """ + self.client.close() + + def process_item(self, item, spider): + """处理每一条数据,注意这里需要将item返回 + 注意:判断新闻是否是今天的,每天只保存当天产出的新闻,这样可以增量的添加新的新闻数据源 + """ + if isinstance(item, SinanewsItem): + try: + # TODO 物料去重逻辑,根据title进行去重,先读取物料池中的所有物料的title然后进行去重 + + cur_time = int(item['ctime']) + str_today = str(datetime.date.today()) + min_time = int(time.mktime(time.strptime(str_today + " 00:00:00", '%Y-%m-%d %H:%M:%S'))) + max_time = int(time.mktime(time.strptime(str_today + " 23:59:59", '%Y-%m-%d %H:%M:%S'))) + if cur_time > min_time and cur_time <= max_time: + self.collection.insert(dict(item)) + except DuplicateKeyError: + """ + 说明有重复 + """ + pass + return item +``` + +5. 配置文件,settings.py + +```python +# Scrapy settings for sinanews project +# +# For simplicity, this file contains only settings considered important or +# commonly used. You can find more settings consulting the documentation: +# +# https://docs.scrapy.org/en/latest/topics/settings.html +# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html +# https://docs.scrapy.org/en/latest/topics/spider-middleware.html + +from typing import Collection + +BOT_NAME = 'sinanews' + +SPIDER_MODULES = ['sinanews.spiders'] +NEWSPIDER_MODULE = 'sinanews.spiders' + + +# Crawl responsibly by identifying yourself (and your website) on the user-agent +#USER_AGENT = 'sinanews (+http://www.yourdomain.com)' + +# Obey robots.txt rules +ROBOTSTXT_OBEY = True + +# Configure maximum concurrent requests performed by Scrapy (default: 16) +#CONCURRENT_REQUESTS = 32 + +# Configure a delay for requests for the same website (default: 0) +# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay +# See also autothrottle settings and docs +# DOWNLOAD_DELAY = 3 +# The download delay setting will honor only one of: +#CONCURRENT_REQUESTS_PER_DOMAIN = 16 +#CONCURRENT_REQUESTS_PER_IP = 16 + +# Disable cookies (enabled by default) +#COOKIES_ENABLED = False + +# Disable Telnet Console (enabled by default) +#TELNETCONSOLE_ENABLED = False + +# Override the default request headers: +#DEFAULT_REQUEST_HEADERS = { +# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', +# 'Accept-Language': 'en', +#} + +# Enable or disable spider middlewares +# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html +#SPIDER_MIDDLEWARES = { +# 'sinanews.middlewares.SinanewsSpiderMiddleware': 543, +#} + +# Enable or disable downloader middlewares +# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html +#DOWNLOADER_MIDDLEWARES = { +# 'sinanews.middlewares.SinanewsDownloaderMiddleware': 543, +#} + +# Enable or disable extensions +# See https://docs.scrapy.org/en/latest/topics/extensions.html +#EXTENSIONS = { +# 'scrapy.extensions.telnet.TelnetConsole': None, +#} + +# Configure item pipelines +# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html +# 如果需要使用itempipline来存储item的话需要将这段注释打开 +ITEM_PIPELINES = { + 'sinanews.pipelines.SinanewsPipeline': 300, +} + +# Enable and configure the AutoThrottle extension (disabled by default) +# See https://docs.scrapy.org/en/latest/topics/autothrottle.html +#AUTOTHROTTLE_ENABLED = True +# The initial download delay +#AUTOTHROTTLE_START_DELAY = 5 +# The maximum download delay to be set in case of high latencies +#AUTOTHROTTLE_MAX_DELAY = 60 +# The average number of requests Scrapy should be sending in parallel to +# each remote server +#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 +# Enable showing throttling stats for every response received: +#AUTOTHROTTLE_DEBUG = False + +# Enable and configure HTTP caching (disabled by default) +# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings +#HTTPCACHE_ENABLED = True +#HTTPCACHE_EXPIRATION_SECS = 0 +#HTTPCACHE_DIR = 'httpcache' +#HTTPCACHE_IGNORE_HTTP_CODES = [] +#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage' + +MONGO_HOST = "127.0.0.1" +MONGO_PORT = 27017 +DB_NAME = "SinaNews" +COLLECTION_NAME = "news" +``` + +6. 监控脚本,monitor_news.py + +```python +# -*- coding: utf-8 -*- +import sys, time +import pymongo +import scrapy +from sinanews.settings import MONGO_HOST, MONGO_PORT, DB_NAME, COLLECTION_NAME + +if __name__ == "__main__": + news_num = int(sys.argv[1]) + time_str = time.strftime("%Y%m%d", time.localtime()) + + # 实际的collection_name + collection_name = COLLECTION_NAME + "_" + time_str + + # 链接数据库 + client = pymongo.MongoClient(MONGO_HOST, MONGO_PORT) + db = client[DB_NAME] + collection = db[collection_name] + + # 查找当前集合中所有文档的数量 + cur_news_num = collection.count() + + print(cur_news_num) + if cur_news_num < news_num: + print("the news nums of {}_{} collection is less then {}".\ + format(COLLECTION_NAME, time_str, news_num)) +``` + +7. 运行脚本,run_scrapy_sina.sh + +```python +# -*- coding: utf-8 -*- +""" +新闻爬取及监控脚本 +""" + +# 设置python环境 +python="/home/recsys/miniconda3/envs/news_rec_py3/bin/python" + +# 新浪新闻网站爬取的页面数量 +page="1" +min_news_num="1000" # 每天爬取的新闻数量少于500认为是异常 + +# 爬取数据 +scrapy crawl sina_spider -a pages=${page} +if [ $? -eq 0 ]; then + echo "scrapy crawl sina_spider --pages ${page} success." +else + echo "scrapy crawl sina_spider --pages ${page} fail." +fi + +# 检查今天爬取的数据是否少于min_news_num篇文章,这里也可以配置邮件报警 +python monitor_news.py ${min_news_num} +if [ $? -eq 0 ]; then + echo "run python monitor_news.py success." +else + echo "run python monitor_news.py fail." +fi +``` + +8. 运行项目命令 + +``` +sh run_scrapy_sina.sh +``` + +最终查看数据库中的数据: + +image-20211103214611171 + +### 参考资料 + +1. [MongoDB基础](https://github.com/datawhalechina/fun-rec/blob/master/docs/%E7%AC%AC%E4%BA%8C%E7%AB%A0%20%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%AE%9E%E6%88%98/2.2%E6%96%B0%E9%97%BB%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F%E5%AE%9E%E6%88%98/docs/MongoDB%E5%9F%BA%E7%A1%80.md) +2. [Scrapy框架新手入门教程](https://blog.csdn.net/sxf1061700625/article/details/106866547/) +3. [scrapy中文文档](https://www.osgeo.cn/scrapy/index.html) +4. [Xpath教程](https://www.w3school.com.cn/xpath/index.asp) +5. https://github.com/Ingram7/NewsinaSpider + +6. https://www.cnblogs.com/zlslch/p/6931838.html + diff --git a/docs/推荐系统实战/新闻推荐系统实践/前后端交互.md b/docs/推荐系统实战/新闻推荐系统实践/前后端交互.md new file mode 100644 index 00000000..e251e651 --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/前后端交互.md @@ -0,0 +1,448 @@ +本文属于新闻推荐实战—前后端基础及交互—前后端交互部分。在前两节,我们分别简单的介绍了与本项目相关的前后的基础知识,目的是为了让大家更加细致的了解整个系统的前后端交互细节,以及更全面的了解一个推荐系统所需的组成部分。本文将从前后端的交互逻辑出发,更加全面的为大家讲解系统的每个细节,了解一个简单的推荐系统内的内部组成。 + + + +### 项目样式展现 + +下面主要展现的是项目的整体部分,主要分为推荐页,热门页以及新闻详情页。 + +image-20211203154557244image-20211203155028564image-20211203155058020 + + + +## 后端目录结构 + +``` +news_rec_sys/ + conf/ + dao_config.py + controller/ + dao/ + materials/ + news_scrapy/ + user_proccess/ + material_proccess + recpocess/ + recall/ + rank/ + online.py + offline.py + scheduler/ + server.py +``` + +- **conf/dao_config.py: 候选整体配置文件** +- **controller/ : 项目中用于操作数据库的接口** +- **dao/ : 项目的实体类,对应数据库表** +- **materials/: 项目的物料部分,主要用户爬取物料以及处理用户画像和新闻画像** +- **recpocess/: 项目的推荐模块,主要包含召回和排序,以及一些线上服务和线下处理部分** +- scheduler: 项目的定时任务的脚本部分, +- server.py: 项目后端的入口部分,主要包含项目整体的后端接口部分。 + +在该项目中,前端主要使用的是Vue框架+mint-ui,后端主要使用的是Flask+Mysql+Mongodb+Redis来完成的,并且前后端采用分离的方式,通过json格式进行数据传递。其中该项目后端的主要逻辑在在server.py中,其中主要包含用户注册和登录,推荐列表,热门列表,获取新闻详情页以及用户的行为等功能。接下来将主要按照这几部分详细的介绍一下前后端如何进行交互。 + + + +### 1、用户注册登录 +为了能够对用户进行千人千面的推荐,因此需要每个使用该系统的人都需要明确先进行注册登入,为每个用户生成唯一的用户id,根据用户的历史行为,实现对用户进行个性化推荐的效果。 + +​ **注册部分:** + +```python +def register(): + """用户注册""" + request_str = request.get_data() + request_dict = json.loads(request_str) + + user = RegisterUser() + user.username = request_dict["username"] + user.passwd = request_dict["passwd"] + + # 查询当前用户名是否已经被用过了 + result = UserAction().user_is_exist(user, "register") + if result != 0: + return jsonify({"code": 500, "mgs": "this username is exists"}) + + user.userid = snowflake.client.get_guid() # 雪花算法生成唯一的用户id + + user.age = request_dict["age"] + user.gender = request_dict["gender"] + user.city = request_dict["city"] + + save_res = UserAction().save_user(user) # 将注册用户信息加入mysql + if not save_res: + return jsonify({"code": 500, "mgs": "register fail."}) + + return jsonify({"code": 200, "msg": "register success."}) +``` + +可以看到,上面的注册部分主要是记录一些用户的一些基础属性,并将用户的注册信息写入msyql表当中。值得注意的是,为了防止并发问题导致用户id出现冲突,这里采用了Twitter的雪花算法来为每个用户生成一个唯一的id。 + +​ **登录部分:** + +```python +@app.route('/recsys/login', methods=["POST"]) +def login(): + """用户登录 + """ + request_str = request.get_data() + request_dict = json.loads(request_str) + + user = RegisterUser() + user.username = request_dict["username"] + user.passwd = request_dict["passwd"] + + # 查询数据库中的用户名或者密码是否存在 + try: + result = UserAction().user_is_exist(user, "login") + # print(result,"login") + if result == 1: + return jsonify({"code": 200, "msg": "login success"}) + elif result == 2: + # 密码错误 + return jsonify({"code": 500, "msg": "passwd is error"}) + else: + return jsonify({"code": 500, "msg": "this username is not exist!"}) + except Exception as e: + return jsonify({"code": 500, "mgs": "login fail."}) +``` + +用户登陆部分,前端通过将输入的账号密码通过POST请求传给 /recsys/login,通过UserAction().user_is_exist()方法查询数据库中的用户名或者密码是否存在,其中1表示账号密码正确,2表示密码错误,0表示用户不存在。 + + + +### 2、推荐页列表 + +在项目样式展现的部分中,第一张图就是推荐页列表的样式,通过瀑布流的方式将新闻内容进行展现。 + +```python +@app.route('/recsys/rec_list', methods=["GET"]) +def rec_list(): + """推荐页""" + user_name = request.args.get('user_id') + page_id = request.args.get('page_id') + + # 查询用户的id + user_id = UserAction().get_user_id_by_name(user_name) + if not user_id: + return False + + if user_id is None or page_id is None: + return jsonify({"code": 2000, "msg": "user_id or page_id is none!"}) + try: + # 获取推荐列表新闻信息 + rec_news_list = recsys_server.get_rec_list(user_id, page_id) + if len(rec_news_list) == 0: + return jsonify({"code": 500, "msg": "rec_list data is empty."}) + return jsonify({"code": 200, "msg": "request rec_list success.", "data": rec_news_list, "user_id": user_id}) + except Exception as e: + print(str(e)) + return jsonify({"code": 500, "msg": "redis fail."}) +``` + +该部分的主要逻辑是前端通过请求 "/recsys/rec_list" 接口,后端通过前端传递过来的用户姓名,从数据库中获取用户id,再根据用户id去推荐服务(recsys_server)中获取到推荐列表。 + + + +#### 2.1、获取用户推荐列表 + +我们知道用户的推荐列表是通过推荐服务的 get_rec_list(user_id, page_id) 接口获取到的。其中需要两个参数: + +- user_id:通过用户id,我们可以去redis中查找已经给用户构建好的新闻列表,将新闻信息返回给前端。 +- page_id:通过page id定位到目前已经给用户推荐到列表的位置,然后在从该位置之后去新的新闻内容。 + +```python + def get_rec_list(self, user_id, page_id): + """给定页面的展示范围进行展示 user_id 后面做个性化推荐的时候需要用到""" + # 根据page id计算需要获取redis中哪些范围的news_id, 假设每一页展示10个新闻 + s = (int(page_id) - 1) * 10 + e = s + 9 + + # 返回的是一个news_id列表 + news_id_list = self.reclist_redis_db.zrange("rec_list", start=s, end=e) + + # 根据news_id获取新闻的具体内容,并返回一个列表,列表中的元素是按照顺序展示的新闻信息字典 + news_info_list = [] + news_expose_list = [] + for news_id in news_id_list: + news_info_dict = self._get_news_simple(news_id) + news_info_list.append(news_info_dict) + news_expose_list.append(news_info_dict["news_id"]) # 记录在用户曝光表上[user_exposure] + + self._save_user_exposure(user_id,news_expose_list) # 曝光落表 + return news_info_list +``` + +这里的逻辑,主要是先根据page id,计算从redis中推荐列表取的范围。在得到新闻id列表之后,通过_get_news_simple() 方法从mysql何redis中获取新闻列表所需的展现内容。 + +为了提高用户体验,这里考虑将已经在推荐列表中给用户曝光过的新闻,当天内不会再通过热门页对用户进行曝光。因此这里需要利用_save_user_exposure()方法来将已经曝光过的新闻存储到redis中,这样在热门推荐中,针对用户的曝光会对热门推荐的内容进行过滤。 + +返回的数据格式如下: + +```json +"data": [ + { + "news_id": "4bfb8aab-bcd8-4c74-b7fd-92b28ca5df69", + "cate": "国内", + "read_num": 0, + "likes": 0, + "collections": 0, + "ctime": "2021-11-30 12:07", + "title": "北京市政协十三届五次会议将于2022年1月5日召开" + }, + ... + { + "news_id": "4ded60ac-aa2f-408b-af4d-09ca0c58b50a", + "cate": "国内", + "read_num": 6, + "likes": 1, + "collections": 0, + "ctime": "2021-11-30 10:44", + "title": "江西万载县委原书记胡全顺获刑十一年六个月" + }] +``` + + + +### 3、热门推荐页 +热门推荐页部分,前端通过请求'/recsys/hot_list'接口,通过传递用户姓名和当前页号来获取热门新闻列表。主要的逻辑和获取推荐页相同,区别在于热门新闻信息主要是通过推荐服务(recsys_server)中的get_hot_list()方法来获取到热门新闻推荐列表。 + +```python +@app.route('/recsys/hot_list', methods=["GET"]) +def hot_list(): + """热门页面""" + if request.method == "GET": + user_name = request.args.get('user_id') + page_id = request.args.get('page_id') + + if user_name is None or page_id is None: + return jsonify({"code": 2000, "msg": "user_name or page_id is none!"}) + + # 查询用户的id + user_id = UserAction().get_user_id_by_name(user_name) + if not user_id: + return False + + try: + # # 获取热门列表新闻信息 + rec_news_list = recsys_server.get_hot_list(user_id) + + if len(rec_news_list) == 0: + return jsonify({"code": 200, "msg": "request redis data fail."}) + # rec_news_list = recsys_server.get_hot_list(user_id, page_id) + return jsonify({"code": 200, "msg": "request hot_list success.", "data": rec_news_list, "user_id": user_id}) + except Exception as e: + print(str(e)) + return jsonify({"code": 2000, "msg": "request hot_list fail."}) +``` + +可以看到这里其实在后端逻辑上和推荐列表部分相似,主要在于get_hot_list()和get_rec_list()的区别;而热门推荐部分内在的细节内容,将会在后面详细介绍,这里不再赘述。 + + + +### 4、 新闻详情页 + +在项目样式展现的部分中,第三附图就是新闻详情页的样式。该部分主要包含一些新闻的详细信息,其中还有两个按钮,用于收集用户的显性反馈,用户可以根据自己对该文章的喜好程度进行喜欢和收藏的反馈内容。 + +```python +@app.route('/recsys/news_detail', methods=["GET"]) +def news_detail(): + """一篇文章的详细信息""" + user_name = request.args.get('user_name') + news_id = request.args.get('news_id') + + user_id = UserAction().get_user_id_by_name(user_name) + + # if news_id is None or user_id is None: + if news_id is None or user_name is None: + return jsonify({"code": 2000, "msg": "news_id is none or user_name is none!"}) + try: + news_detail = recsys_server.get_news_detail(news_id) + + if UserAction().get_likes_counts_by_user(user_id,news_id) > 0: + news_detail["likes"] = True + else: + news_detail["likes"] = False + + if UserAction().get_coll_counts_by_user(user_id,news_id) > 0: + news_detail["collections"] = True + else: + news_detail["collections"] = False + # print("test",news_detail) + return jsonify({"code": 0, "msg": "request news_detail success.", "data": news_detail}) + except Exception as e: + print(str(e)) + return jsonify({"code": 2000, "msg": "error"}) +``` + +上面就是详情页的后端逻辑,通过用户名字从mysql中获取用户id信息。防止用户id或者 page id出现空值的情况,需要进行判断。紧接着通过recsys_server服务的get_news_detail()方法,根据新闻的id进行获取内容。 + +如果用户对该新闻之前点击过喜欢或收藏,再次点击该新闻应该在喜欢或收藏按钮应该是点亮状态,因此还需要根据mysql中再次查询用户与该新闻是否存在记录,并将结果返回给前端,将其进行点亮展示。这里采用两个字段likes和collections,通过True,False来判断用户对该文章之前是否点击过喜欢或收藏。 + +返回的数据格式如下: + +```json +{ + "code": 0, + "data": { + "news_id": "4ded60ac-aa2f-408b-af4d-09ca0c58b50a", + "cate": "军事", + "title": "运-20加油机首次现身台海上空 堪称“战力倍增器”", + "content": "原标题:视频丨运-20加油机首次现身台海上空,堪称“战力倍增器”据台湾“中央社”报道,台防务部门晚间发布最新动态,11月28日白天解放军空军有27架次多型战机出现在了台湾所谓“西南空域”。首度被台媒披露现身台海的运油-20,是以国产运-20大型远程运输机为平台改装的空中加油机。据媒体测算,运油-20加油机装载燃油超过100吨,能大幅提升战机的空中续航能力,堪称“战力倍增器”。", + "collections": true, + "read_num": 6, + "likes": true, + "ctime": "2021-11-30 10:44", + "url": "https://news.sina.com.cn/c/2021-11-30/doc-ikyakumx1093113.shtml" + }, + "msg": "request news_detail success." +} +``` + + + +### 5、用户的行为 + +在该系统中,用户在看新闻时主要会留下三种用户行为:一是阅读,即用户在点击一篇新闻的详细页时,用户产生的行为;二是喜欢,在新闻详情页下面会存在喜欢按钮,用户可以通过点击按钮触发系统记录该行为;三是收藏,和喜欢行为同理,需要通过用户主动的方式来触发。 + +因此在用户点进一篇新闻的详情页时候,前端会发送一个请求,并给后端传递一个json格式数据: + +```json +{ + "user_name":"wang", + "news_id":"0a745412-db48-4e37-bf13-9a5b56028f7e", + "action_time":1638532127190, + "action_type":"read" +} +``` + +在点击喜欢或收藏按钮的时候同样会产生一个请求,并发送json数据: + +```json +//点击喜欢 +{ + "user_name":"wang", + "news_id":"0a745412-db48-4e37-bf13-9a5b56028f7e", + "action_time":1638532127190, + "action_type":"like:ture" +} + +//点击收藏 +{ + "user_name":"wang", + "news_id":"0a745412-db48-4e37-bf13-9a5b56028f7e", + "action_time":1638532127190, + "action_type":"collections:true" +} +``` + +通过前端的传递的数据,后端对应的接口可以通过传递的参数对用户行为进行记录: + +```python +@app.route('/recsys/action', methods=["POST"]) +def actions(): + """用户的行为:阅读,点赞,收藏""" + request_str = request.get_data() + request_dict = json.loads(request_str) + + username = request_dict.get('user_name') + newsid = request_dict.get('news_id') + actiontype = request_dict.get("action_type") + actiontime = request_dict.get("action_time") + + userid = UserAction().get_user_id_by_name(username) # 获取用户 id + if not userid: + return jsonify({"code": 2000, "msg": "user not register"}) + + action_type_list = actiontype.split(":") + + if len(action_type_list) == 2: + _action_type = action_type_list[0] + if action_type_list[1] == "false": # 如果这个参数为false的话, 表示数据库中存在记录 需要删除数据 + if _action_type=="likes": + UserAction().del_likes_by_user(userid,newsid) # 删除用户喜欢记录 + elif _action_type=="collections": + UserAction().del_coll_by_user(userid,newsid) # 删除用户收藏记录 + else: + if _action_type=="likes": # 如果这个参数为true的话, 表示数据库中不存在记录 需要添加数据 + userlikes = UserLikes() + userlikes.new(userid,username,newsid) + UserAction().save_one_action(userlikes) # 记录用户喜欢记录 + elif _action_type=="collections": + usercollections = UserCollections() + usercollections.new(userid,username,newsid) + UserAction().save_one_action(usercollections) # 记录用户收藏记录 + + try: + # 落日志 + logitem = LogItem() + logitem.new(userid,newsid,action_type_list[0]) + LogController().save_one_log(logitem) + + # 更新redis中的展示数据 新闻侧 + recsys_server.update_news_dynamic_info(news_id=newsid,action_type=action_type_list) + return jsonify({"code": 200, "msg": "action success"}) + + except Exception as e: + print(str(e)) + return jsonify({"code": 2000, "msg": "action error"}) +``` + +上述代码中主要存在三部分内容: + + + +**用户行为记录:** + +在前端传递过来的数据中存在一个字段 "action_type":"like:ture" 或 "action_type":"like:false"(收藏行为类似),对于action_type参数,其值会是一个组合字符串,冒号前面表示用户的具体行为,冒号后面表示用户当前的行为是点击喜欢还是取消喜欢(例如用户误触导致,用户再次点击则会取消)。 + +通过**true**和**false**我们不仅可以知道当前用户是点击还是取消,其实还可以知道在数据库中是否存在该用户对该新闻的行为记录。原因是当传递来的是**false**时,表明**like**的状态是从**true**变为**false**,因此数据库中肯定会存在该记录,如果是**true**,表明like的状态是从**false**变为**true**,表明此时数据库中不存在该用户对该新闻的行为记录。通过这样的方式,我们可以比较简单的对数据库进行操作,记录用户的行为。 + + + +**用户行为落日志:** + +在企业中,任何系统都会有日志的存在,其中最主要的作用是,日志相当于一个监控器,可以随时监测系统是否出现故障,通过日志可以及时定位系统中可能存在的问题。但是我们说的日志还有所区别,我们这里所说的日志主要是记录的一些线上信息,通过日志的方式进行记录,类似于我们这个系统,用户线上存在的行为,对于我们来说是十分具有意义的,我们需要通过分析这样的用户行为来更好的了解用户兴趣,从而进行更加个性化的推荐。因此我们可以借助日志的方式来记录有意义的用户数据,通过日志数据去分析数据,构建模型,这对于一个算法工程师来说是十分重要的内容。 + +当然在我们这个新闻推荐系统中,我们这么做的原因有一下几点: + +- 通过这样的方式让大家体会到日志的意义,我们可以直接通过日志获取一些线上有意义的用户数据。 +- 通过日志数据,可以帮助我们更新用户画像中的一些动态特征。 +- 在后面构建模型时,我们也能获取到用户的一些点击率,收藏率的建模,为后面的工作提供数据基础。 + +上诉代码中,我们通过 LogController() 的 save_one_log() 方法对数据进行了存储到了mysql中。 + + + +**新闻动态数据更新** + +由于我们在展现时会显示该新闻的阅读人数、喜欢人数和收藏人数,因此用户的行为实际上会改变新闻这三个属性。因此我们需要更新redis中新闻的这些动态的数据。 + +主要是通过推荐服务里面的 update_news_dynamic_info()方法进行更新。 + +```python +def update_news_dynamic_info(self, news_id,action_type): + """更新新闻展示的详细信息""" + news_dynamic_info_str = self.dynamic_news_info_redis_db.get("dynamic_news_detail:" + news_id) + news_dynamic_info_str = news_dynamic_info_str.replace("'", '"' ) # 将单引号都替换成双引号 + news_dynamic_info_dict = json.loads(news_dynamic_info_str) + + if len(action_type) == 2: + if action_type[1] == "true": + news_dynamic_info_dict[action_type[0]] +=1 + elif action_type[1] == "false": + news_dynamic_info_dict[action_type[0]] -=1 + else: + news_dynamic_info_dict["read_num"] +=1 + + news_dynamic_info_str = json.dumps(news_dynamic_info_dict) + + news_dynamic_info_str = news_dynamic_info_str.replace('"', "'" ) + res = self.dynamic_news_info_redis_db.set("dynamic_news_detail:" + news_id, news_dynamic_info_str) + return res +``` + +上述代码主要是新闻动态特征更新的部分,主要是获取redis中的信息,根据前端传递过来的行为来更新对应新闻属性的值。更改完之后,将结果重新存储到redis中。 + + + diff --git a/docs/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战.md b/docs/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战.md new file mode 100644 index 00000000..e2d46ae0 --- /dev/null +++ b/docs/推荐系统实战/新闻推荐系统实践/前端基础及Vue实战.md @@ -0,0 +1,865 @@ +## 1.Web 前端 + + Web 前端网页主要由文字、图像和超链接等元素构成。当然,除了这些元素,网页中还可以包含音频、视频以及 Flash 等。 + +### 1.1 什么是 Web + + Web(World Wide Web)即全球广域网,也称为万维网,它是一种基于超文本和 HTTP 的、全球性的、动态交互的、跨平台的分布式图形信息系统。是建立在 Internet 上的一种网络服务,为浏览者在 Internet 上查找和浏览信息提供了图形化的、易于访问的直观界面,其中的文档及超级链接将 Internet 上的信息节点组织成一个互为关联的网状结构。 + + Web 前端主要是通 `HTML`,`CSS`,`JS`,`ajax`,`DOM` 等前端技术,实现网站在客服端的正确显示及交互功能。 + +### 1.2 Web 标准构成 + +主要包括结构(Structure)、表现(Presentation)和行为(Behavior)三个方面。 + +- **结构标准**:结构用于对网页元素进行整理和分类,对于网页来说最重要的一部分 。通过对语义的分析,可以对其划分结构。具有了结构的内容,将更容易阅读 +- **表现标准**:表现用于设置网页元素的版式、颜色、大小等外观样式,主要指的是 CSS 。为了让网页能展现出灵活多样的显示效果 + +- **行为标准**:行为是指网页模型的定义及交互的编写 。使用户对网页进行操作,网页可以做出响应性的变化 + +总的来说, + +- Web 标准有三层结构,分别是结构(`HTML`)、表现(`CSS`)和行为(`JS`) +- 结构类似人的身体, 表现类似人的着装, 行为类似人的行为动作 + +- 理想状态下,他们三层都是独立的, 放到不同的文件里面 + +### 1.2.1 HTML + +- `HTML` 指的是超文本标记语言 (**H**yper **T**ext **M**arkup **L**anguage)是用来描述网页的一种语言。 +- `HTML` 不是一种编程语言,而是一种标记语言 (markup language) +- 标记语言是一套标记标签 (markup tag) + +##### 1.2.1.1 超文本的含义 + +- **超越文本限制**:可以加入图片、声音、动画、多媒体等内容 +- **超级链接文本**:可以从一个文件跳转到另一个文件,与世界各地主机的文件连接 + +##### 1.2.1.2 语法骨架格式 + +```html + + + + + 我的第一个页面 + + +

一个一级标题

+

一个段落。

+ + +``` + +- `` 声明为 HTML5 文档 +- `` 元素是 HTML 页面的根元素 +- `` 元素包含了文档的元(meta)数据 + - `` 定义网页编码格式 + - `` 元素描述了文档的标题 +- `<body>` 元素包含了可见的页面内容 + + - `<h1>` 元素定义一个标题 + - `<p>` 元素定义一个段落 + +![html](https://files.mdnice.com/user/28784/2c141964-f366-494d-a7cc-c2abcd21161b.png) + +#### 1.2.2 CSS + +- `CSS`(**C**ascading **S**tyle **S**heets) ,通常称为 `CSS` 样式表或层叠样式表(级联样式表) +- `CSS` 主要用于设置 `HTML` 页面中的文本内容(字体、大小、对齐方式等)、图片的外形(宽高、边框样式、边距等)以及版面的布局和外观显示样式。 +- `CSS` 以 `HTML` 为基础,提供了丰富的功能,如字体、颜色、背景的控制及整体排版等,而且还可以针对不同的浏览器设置不同的样式。 + +##### 1.2.2.1 CSS 规则 + +![CSS](https://files.mdnice.com/user/28784/87d28c2f-e9f9-47bd-a3e5-2659f2008b2c.png) + +- **选择器**:需要改变样式的 HTML 元素 + +- **声明**:由一个属性和一个值组成。声明之间用分号结束 + + - 属性:希望设置的样式属性。每个属性有一个值。属性和值用冒号分开 + +##### 1.2.2.2 语法格式 + +```html +<标签名 style="属性1:属性值1; 属性2:属性值2; 属性3:属性值3;"> 内容 </标签名> +``` + +例如: + +```css +<style> + /*选择器{属性:值;}*/ + p { + color:#06C; + font-size:14px; + } + /*文字的颜色是 蓝色*/ + h4 { + color:#900; + } + h1 { + color:#090; + font-size:16px; + } + body { + background:url(bg2.jpg); + } +</style> +``` + +#### 1.2.3 JS + +- `JS` (**J**ava**S**cript)是 Web 的编程语言,是一种基于对象和事件驱动并具有相对安全性的客户端脚本语言。同时也是一种广泛用于客户端 Web 开发的脚本语言,常常用来给 `HTML` 网页添加动态效果,从而实现人机交互的网页 +- 脚本语言不需要编译,在运行过程中由 `js` 解释器逐行来进行解释并执行 + +##### 1.2.3.1 JS 的组成 + +![JS](https://files.mdnice.com/user/28784/259c84c2-7570-4ab0-b192-eefe14463a47.png) + +- **ECMAScript**: 是由 ECMA 国际( 原欧洲计算机制造商协会)进行标准化的一门编程语言,这种语言在万维网上应用广泛,它往往被称为 JavaScript 或 JScript,但实际上后两者是 ECMAScript 语言的实现和扩展 +- **DOM**:文档对象模型(DocumentObject Model,简称 DOM),是 W3C 组织推荐的处理可扩展标记语言的标准编程接口。通过 DOM 提供的接口可以对页面上的各种元素进行操作(大小、位置、颜色等) +- **BOM**:浏览器对象模型(Browser Object Model,简称 BOM) 是指浏览器对象模型,它提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。通过 BOM 可以操作浏览器窗口,比如弹出框、控制浏览器跳转、获取分辨率等 + +##### 1.2.3.2 书写位置 + +**1.行内式** + +```html +<input type="button" value="点我试试" onclick="alert('Hello World')" /> +``` + +- 可以将单行或少量 `JS` 代码写在 `HTML` 标签的事件属性中(以 `on` 开头的属性),如:`onclick` +- 可读性差, 在 `HTML` 中编写 `JS` 大量代码时,不方便阅读 +- 引号易错,引号多层嵌套匹配时,非常容易弄混 + +**2.内嵌式** + +```html +<script> + alert('Hello World~!'); +</script> +``` + +- 可以将多行`JS`代码写到 `script` 标签中 + +**3.外部 JS 文件** + +```html +<script src="myScript.js"></script> +``` + +```javascript +//myScript.js文件内容 +function myFunction() +{ + document.getElementById("demo").innerHTML="我的第一个 JavaScript 函数"; +} +``` + +- 利于`HTML`页面代码结构化,把大段 `JS`代码独立到 `HTML` 页面之外,既美观,也方便文件级别的复用 +- 引用外部 `JS`文件的 `script` 标签中间不可以写代码 +- 适合于`JS` 代码量比较大的情况 + +--- + +## 2. Vue 简介 + + `Vue` 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,`Vue` 被设计为可以自底向上逐层应用。`Vue` 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,`Vue` 也完全能够为复杂的单页应用提供驱动。 + +### 2.1 安装 + +#### 2.1.1 通过< script>标签引入 + +直接下载并用 `<script>` 标签引入,`Vue` 会被注册为一个全局变量。 + +- **开发版本**:https://cn.vuejs.org/js/vue.js + +- **生产版本**:https://cn.vuejs.org/js/vue.min.js + +#### 2.1.2 通过 CDN 安装 + +- **制作原型或学习**: + +```html +<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> +``` + +- **用于生产环境**: + +```html +<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script> +``` + +- **使用原生 ES Modules**: + +```html +<script type="module"> + import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js' +</script> +``` + +#### 2.1.3 通过 npm 安装 + + 在用 `Vue` 构建大型应用时推荐使用 `npm` 安装。`npm` 能很好地和诸如 `webpack`模块打包器配合使用。同时 `Vue` 也提供配套工具来开发单文件组件。 + +1.npm 版本需要大于 3.0,如果低于此版本需要升级它: + +```shell +# 查看版本 +npm -v + +#升级 npm +npm install npm -g +``` + +2.安装 Vue + +```shell +# 使用npm安装 +npm install vue +``` + +### 2.2 创建一个 Vue 实例 + +每个 `Vue` 应用都需要通过实例化 `Vue` 来实现。 + +#### 2.2.1 语法格式 + +```javascript +var vm = new Vue({ + // 选项 +}) +``` + +例如: + +```html +<div id="example"> + <h1>title : {{title}}</h1> + <h1>url : {{url}}</h1> +</div> +<script> + var vm = new Vue({ + el: '#example', + data: { + title: "一个Vue实例", + url: "https://cn.vuejs.org/", + }, + methods: { + details: function() { + return this.title + " ------"; + } + } + }) +</script> +``` + +在 `Vue` 构造器中有一个`el` 参数,它是 `DOM` 元素中的 `id`。在上面实例中 `id` 为 `example`,这表示接下来的改动全部在以上指定的 `div` 内,`div` 外部不受影响。 + +#### 2.2.2 定义数据对象 + +在上述`Vue`实例中: + +- `data` :定义属性,实例中有 2 个属性分别为:`title`、`url`。 + +- `methods` :定义的函数,可以通过 `return` 来返回函数值。 + +- `{{ }}` :输出对象属性和函数返回值。 + + 当一个 `Vue` 实例被创建时,它向 `Vue` 的响应式系统中加入了其 `data` 对象中能找到的所有的属性。当这些属性的值发生改变时,`html` 视图将也会产生相应的变化。 + +### 2.3 Vue 的生命周期 + + 每个 `Vue` 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 `DOM` 并在数据变化时更新 `DOM` 等。同时在这个过程中也会运行一些叫做**生命周期钩子**的函数,这给了用户在不同阶段添加自己的代码的机会。 + + 下图是一个 `Vue` 实例的生命周期: +![Vue生命周期](https://files.mdnice.com/user/28784/96265b5a-a5f0-4b13-83ad-742d9167f4cd.png) + +#### 2.3.1 beforeCreate + + 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。 + + 此时组件的选项对象还未创建,`el` 和 `data` 并未初始化,因此无法访问`methods`, `data`, `computed`等上的方法和数据。 + +#### 2.3.2 created + + 在实例创建完成后被立即调用。 + + 实例已完成对选项的处理,以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 `$el` 目前尚不可用。 + + 在这一步中可以调用`methods`中的方法,改变`data`中的数据,并且修改可以通过 `Vue` 的响应式绑定体现在页面上,获取`computed`中的计算属性等等,通常我们可以在这里对实例进行预处理。但需要注意的是,这个周期中是没有什么方法来对实例化过程进行拦截的,因此假如有某些数据必须获取才允许进入页面的话,并不适合在这个方法发请求,建议在组件路由钩子`beforeRouteEnter`中完成。 + +#### 2.3.3 beforeMount + + 在挂载开始之前被调用:相关的 `render` 函数首次被调用(虚拟 `DOM`)。 + + 实例已完成以下的配置: 编译模板,把`data`里面的数据和模板生成`HTML`,完成了`el`和`data` 初始化,但此时还没有挂在`HTML`到页面上。 + +#### 2.3.4 mounted + + 实例被挂载后调用,这时 `el` 被新创建的 `vm.$el` 替换了。 + + 模板中的`HTML`渲染到`HTML`页面中,此时一般可以做一些`ajax`操作,`mounted`只会执行一次。 + + 但`mounted` 不会保证所有的子组件也都被挂载完成。如果希望等到整个视图都渲染完毕再执行某些操作,可以在 `mounted` 内部使用 `vm.$nextTick`: + +```javascript +mounted: function () { + this.$nextTick(function () { + // 仅在整个视图都被渲染之后才会运行的代码 + }) +} +//生命周期钩子的 this 上下文指向调用它的 Vue 实例。 +``` + +#### 2.3.5 beforeUpdate + + 在数据发生改变后,`DOM` 被更新之前被调用。 + + 适合在现有 `DOM` 将要被更新之前访问它,比如移除手动添加的事件监听器。可以在该钩子中进一步地更改状态,不会触发附加地重渲染过程. + +#### 2.3.6 updated + + 在数据更改导致的虚拟 `DOM` 重新渲染和更新完毕之后被调用。 + + 当这个钩子被调用时,组件 `DOM` 已经更新,所以可以执行依赖于`DOM`的操作,然后在大多是情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。 + + 但`updated` 不会保证所有的子组件也都被重新渲染完毕。如果希望等到整个视图都渲染完毕,可以在 `updated` 里使用 `vm.$nextTick`: + +```javascript +updated: function () { + this.$nextTick(function () { + // 仅在整个视图都被重新渲染之后才会运行的代码 + }) +} +``` + +#### 2.3.7 beforeDestroy + + 实例销毁之前调用。在这一步,实例仍然完全可用。 + + 这一步还可以用`this`来获取实例,一般用来做一些重置的操作,比如清除掉组件中的定时器和监听的`DOM`事件。 + +#### 2.3.8 destroyed + + 实例销毁后调用。该钩子被调用后,对应 `Vue` 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。 + +## 3 创建 Vue 项目 + + `Vue CLI` 是一个基于 `Vue.js` 进行快速开发的完整系统,提供: + +- 通过 `@vue/cli` 实现的交互式的项目脚手架 +- 通过 `@vue/cli` + `@vue/cli-service-global` 实现的零配置原型开发 +- 一个运行时依赖 (`@vue/cli-service`),该依赖 + - 可升级 + - 基于 `webpack` 构建,并带有合理的默认配置 + - 可以通过项目内的配置文件进行配置 + - 可以通过插件进行扩展 +- 一个丰富的官方插件集合,集成了前端生态中最好的工具 +- 一套完全图形化的创建和管理 `Vue.js` 项目的用户界面 + + `Vue CLI` 致力于将 `Vue` 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接。与此同时,它也为每个工具提供了调整配置的灵活性 + +### 3.1 安装 vue CLI + + Vue CLI 的包名称由 `vue-cli` 改成了 `@vue/cli`。 如果已经全局安装了旧版本的 `vue-cli` (1.x 或 2.x),需要先通过 `npm uninstall vue-cli -g`卸载它。 + +1.安装新的包 + +```shell +npm install -g @vue/cli +``` + +2.检查其版本是否正确 + +```shell +vue --version +``` + +3.升级包 + +```shell +npm update -g @vue/cli +``` + +### 3.2 创建 Vue 项目 + +#### 3.2.1 通过 vue create 创建 + +```shell +vue create hello-world +``` + +- **默认配置**:包含了基本的 Babel + ESLint 设置。适合快速创建一个新项目的原型。 + +![cli-new-project](https://files.mdnice.com/user/28784/e7fe4d8a-f923-4ed8-a638-740546a155af.png) + +- **手动选择特性**:选取需要的特性。适合面向生产的项目。 + +![cli-select-features](https://files.mdnice.com/user/28784/1ff16084-17d5-4d10-9084-cf99d9aa2fe2.png) + +```shell +# 进入项目具体路径 +cd hello-world + +# 下载依赖 +npm install + +# 启动运行项目 +npm run serve + +# 项目打包 +npm run build +``` + +#### 3.2.2 使用图形化界面创建 + +```shell +vue ui +``` + + 打开一个浏览器窗口,并以图形化界面将你引导至项目创建的流程。 +![ui-new-project](https://files.mdnice.com/user/28784/d6ad9e95-fb2a-4999-ba07-bb2d04ce17e9.png) + +#### 3.2.3 使用 2.x 模板 (旧版本)创建 + +```shell +# 全局安装一个桥接工具 +npm install -g @vue/cli-init + +# `vue init` 的运行效果将会跟 `vue-cli@2.x` 相同 +vue init webpack vue_map_test +``` + +![init-vue](https://files.mdnice.com/user/28784/8534940b-6667-4c12-b6c1-689399b47c15.png) + +```shell +# 进入项目具体路径 +cd vue_map_test + +# 下载依赖 +npm install + +# 启动运行项目,默认为8080端口 +npm run dev + +# 项目打包 +npm run build +``` + +### 3.3 Vue 项目目录 + +``` +├── v-proj +| ├── node_modules // 当前项目所有依赖,一般不可以移植给其他电脑环境 +| ├── public +| | ├── favicon.ico // 标签图标 +| | └── index.html // 当前项目唯一的页面 +| ├── src +| | ├── assets // 静态资源img、css、js +| | ├── components // 小组件 +| | ├── App.vue // 根组件 +| | ├── main.js // 全局脚本文件(项目的入口) +| | └── router.js // 路由脚本文件 +| ├── README.md +└ └── package.json //配置文件,使用npm install安装 +``` + +#### 3.3.1 public + + 可以理解为入口目录。 + +##### **favicon.ico** + + 用于作为缩略的网站标志,它显示位于浏览器的地址栏或者在标签上,用于显示网站的 logo。 + +##### **index.html** + + 首页入口文件,可以添加一些 `meta` 信息或统计代码。 + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <title>Title + + +
+ +
+ + +``` + +#### 3.3.2 src + + 是整个项目的主文件夹 ,代码大部分都在这里完成。 + +##### **assets** + + 放置一些资源文件。比如`js` 、`css`、`image`等。 + +![assets](https://files.mdnice.com/user/28784/211ea341-58ef-4bc4-a9e0-baf390ec791a.png) + +##### **components** + + 放置组件文件。一个`Vue`项目就是由一个个的组件拼装起来的。 + +例如: + +```html + + + + + +``` + +- `