2023年10月

by
emanjusaka from ​
https://www.emanjusaka.top/archives/11
彼岸花开可奈何
本文欢迎分享与聚合,全文转载请留下原文地址。

Shiro
权限框架认证失败默认是重定向页面的,这对于前后端分离的项目及其不友好,可能会造成请求404的问题。现在我们自定义过滤器实现认证失败返回json数据。

拦截器就是一道道的关卡,每一道关卡都有各自的职责。

实现思路

由于
Shiro
默认的过滤器认证失败后是进行重定向操作的,所以我们考虑自定义过滤器重写它的逻辑。

  1. 设置
    Shiro

    ShiroFilterFactoryBean
    拦截请求进行认证并配置自定义的拦截器。

  2. 实现自定义的拦截器,重写认证失败后的逻辑。

实现过程

  1. 配置
    Shiro

    ShiroFilterFactoryBean
    设置拦截请求进行认证

    @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            Map<String, String> map = new LinkedHashMap<>();
            //登出
            map.put("/logout", "logout");
            // 登录
            map.put("/login","anon");
            //对所有用户认证
            map.put("/**", "authc");
            Map<String, Filter> filterMap = new HashMap<>();
            // 自定义的拦截器
            filterMap.put("authc",new ShiroLoginFilter());
            shiroFilterFactoryBean.setFilters(filterMap);
            shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
            return shiroFilterFactoryBean;
        }
    

上面配置对登录接口进行了放行,对其他接口都要进行认证,这个可以根据自己的实际需要去配置。同时还要配置我们的自定义拦截器,拦截器Map 的key要和配置的认证
authc
一致,否则会不生效。

  1. 实现自定义的拦截器,重写认证失败后的逻辑。

    package com.icms.shiro.filter;
    
    import com.alibaba.fastjson.JSON;
    import com.icms.enu.ExceptionCodeEnum;
    import com.icms.exception.CustomException;
    import com.icms.page.Result;
    import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
    import org.camunda.bpm.model.bpmn.impl.instance.From;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;
    
    /**
     * @Author emanjusaka
     * @Date 2023/10/25 14:42
     * @Version 1.0
     */
    public class ShiroLoginFilter extends FormAuthenticationFilter {
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            httpResponse.setStatus(200);
            httpResponse.setContentType("application/json;charset=utf-8");
            //解决跨域问题
            if ("OPTIONS".equals(httpRequest.getMethod())){
                httpResponse.setStatus(HttpServletResponse.SC_NO_CONTENT);;
                return true;
            }
            httpResponse.getWriter().print(JSON.toJSONString(new Result(ExceptionCodeEnum.STATUS_CODE_NO_LOGIN)));
            httpResponse.getWriter().flush();
            httpResponse.getWriter().close();
            return false;
        }
    }
    
    

    这里自定义拦截器继承
    FormAuthenticationFilter
    重写了
    onAccessDenied
    方法。在
    onAccessDenied
    方法中我们可以返回我们需要的 json 数据,也可以记录日志执行我们自己的方法。这里返回的数据是我自定义的
    Result
    类,里面包含了错误码和错误信息。

    认证失败是我们的业务逻辑的错误而不是网络请求的错误,所以我们把HTTP的状态码设置成了
    200
    ,通过我们自己定义的
    Result
    来返回实际的错误信息。

    注意
    :Shiro本身是没有解决跨域问题的,我们要自己实现解决
    Shiro
    的跨域问题。

    例如
    /user/getUserInfo
    接口,没有配置过滤,就会被拦截,这个时候无论是在Controller上还是在接口实现上配置
    @CrossOrigin
    ,都不会生效。这个时候需要做如下配置:

    //解决跨域问题
            if ("OPTIONS".equals(httpRequest.getMethod())){
                httpResponse.setStatus(HttpServletResponse.SC_NO_CONTENT);
                return true;
            }
    

    自己实现拦截器设置允许跨域也是可以的,这里使用的上述的方法。

扩展知识

Shiro中的拦截器

  1. authc拦截器:主要用于实现基于表单的身份验证,它会拦截用户登录表单提交的路径,并在拦截器工厂中配置该路径。此外,它负责创建登录认证所需的Token令牌,并触发登录认证流程。如果用户已经登录,那么将直接进入要访问的路径;如果用户未登录,则访问会被拒绝,并自动跳转到登录页面。

  2. authcBasic拦截器:主要用于实现基于HTTP基本认证的身份验证。

  3. logout拦截器:主要用于处理用户的注销请求。

  4. user拦截器:充当了整个安全管理器的入口,主要负责拦截需要安全控制的请求并进行处理。

  5. anon拦截器:这种拦截器允许不需要登录就能访问的资源,通常用于静态资源或者移动端接口。

  6. roles拦截器:主要负责用户的角色校验。

  7. perms拦截器和roles拦截器:这两个拦截器主要与授权相关,用于处理用户角色和权限相关的请求。

  8. port拦截器:它主要拦截网络请求,验证用户是否具有访问特定端口的权限。

  9. rest拦截器:用于在Web应用程序中对HTTP请求的请求方法(HTTP method)进行权限过滤和控制。它的作用是限制用户对某些HTTP请求方法的访问权限,例如GET、POST、PUT、DELETE等。通过该过滤器,您可以根据需要来控制某些请求方法的访问权限,并且可以根据不同的请求方法,对不同的用户或用户组进行特定的授权设置。

  10. ssl拦截器:主要用于处理SSL协议相关的请求。

  11. noSessionCreation拦截器:用于处理无状态会话的过滤器。

public enum DefaultFilter{
  anno(AnonymousFilter.class),
  authc(FormAuthenticationFilter.class),
  authcBasic(BasicHttpAuthenticationFilter.class),
  logout(LogoutFilter.class),
  noSessionCreation(NoSessionCreationFilter.class),
  perms(PermissionsAuthorizationFilter.class),
  port(PortFilter.class),
  rest(HttpMethodPermissionFilter.class),
  roles(RolesAuthorizationFilter.class),
  ssl(SslFilter.class),
  user(UserFilter.class);
}

与身份验证相关的拦截器

  • authc
    (FormAuthenticationFilter)

    基于表单的拦截器;如“/**=authc”,如果没有登录会跳转到相应的登录页面登录。
    主要属性:
    usernameParam:表单提交的用户名参数名(username)。
    passwordParam:表单提交的密码参数名(password)。
    rememberMeParam:表单提交的记住我参数名(rememberMe)。
    loginUrl:登录页面地址(/login.jsp)。
    successUrl:登录成功后的默认重定向地址。
    failureKeyAttribute:登录失败后错误信息存储Key(shiroLoginFailure)。

  • authcBasic
    (BasicHttpAuthenticationFilter)

    Basic HTTP身份验证拦截器,主要属性:
    applicationName:弹出登录框显示的信息(application)。

  • logout
    (LogoutFilter)

    退出拦截器,主要属性:
    redirectUrl:退出成功后重定向的地址(/)。
    示例:“/logout=logout”

  • user
    (UserFilter)

    用户拦截器,用户已经身份验证/记住我登录的都可。
    示例:“/**=user”

  • anon
    (AnnonymousFilter)

    匿名拦截器,即不需要登录即可访问,一般用于静态资源过滤或者需要在登录之前进行的请求。
    示例:“/static/**=anon”

与授权相关的拦截器

  • roles
    (RolesAuthorizationFilter)

    角色授权拦截器,验证用户是否拥有所有角色。主要属性:
    loginUrl:登录页面地址(/login.jsp)。
    unauthorizedUrl:未授权后重定向的地址。
    示例:“/admin/**=roles[admin]”

  • perms
    (PermissionsAuthorizationFilter)

    权限授权拦截器,验证用户是否拥有所有权限,属性和roles一样。
    示例:“/user/**=perms[“user:create”]”

  • port
    (PortFilter)

    端口拦截器,主要属性:
    port(80):可以通过的端口。
    示例:“/test=port[80]”,如果用户访问该页面是非80端口,将自动将端口改为80并重定向到该80端口,其他路径/参数等都一样。

  • rest
    (HttpMethodPermissionFilter)

    rest风格拦截器,自动根据请求方法构建权限字符串(GET=read,POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read,MKCOL=create)构建权限字符串。
    示例:“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)。

  • ssl
    (SslFilter)

    SSL拦截器,只有请求协议是https才能通过,否则自动跳转到https端口(443),其他和port拦截器一样。

其他拦截器

  • noSessionCreation
    (NoSessionCreationFilter)

    不创建会话拦截器,调用subject.getSession(false)不会有问题,但是如果subject.getSession(true)将抛出异常。

本文原创,才疏学浅,如有纰漏,欢迎指正。如果本文对您有所帮助,欢迎点赞,并期待您的反馈交流,共同成长。
原文地址:
https://www.emanjusaka.top/archives/11
微信公众号:emanjusaka的编程栈

一、前言

今天玩儿点儿特别的,AI大行其道的今天,还没玩过AI模型的程序员绝对不是个好厨子。我本人比较喜欢音乐,但是一直没有出道,很是遗憾。那么今天,我就使用AI模型亲手打造一个堪比真人的歌手,让她C位霸气出道。

首先,让这位女歌手露一嗓子:
点我试听

唱的还不错吧,接下来咱们来分析这位歌手是如何打造出来的。

二、技术

飞桨(PaddlePaddle)
以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心框架、基础模型库、端到端开发套件、工具组件和服务平台于一体,2016 年正式开源,是全面开源开放、技术领先、功能完备的产业级深度学习平台。

上面的介绍是不是很唬人?从百度飞桨官网贴过来的。显然,本文使用的技术是基于百度飞桨框架打造,当然了,你要让我从零开始搞AI......那我还是选择当厨子。

飞桨Hub(PaddleHub)
是基于PaddlePaddle生态下的预训练模型,旨在为开发者提供丰富的、高质量的、直接可用的预训练模型。

说白了,这个飞桨Hub是模型集中营,提供了很多已经训练好的模型,用户可以拿来直接使用,而不需要再耗费过多的精力、资源去自己训练模型。

Python
,飞桨平台基于Python语言实现,不必多说了,毕竟,Python是世界上最好的语言.java。

Anaconda
,是一个常用的python包管理程序,可以方便的管理我们的多个Python环境。比如你的A软件需要基于Python3.7,B软件需要基于Python3.8,用Anaconda可以方便的对不同的环境进行管理和切换。

三、环境搭建

1、安装Anaconda

到官网
https://www.anaconda.com/
或者
https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/
下载一个较新的版本即可。

安装过程很简单,猛点下一步即可,建议你安装到C盘之外的盘。

即便你装在了D盘,但是创建环境的时候,环境信息默认还是会被创建到C盘,所以还应该做如下操作:

编辑C:\Users\你的用户名\.condarc文件(如果没有此文件请手动创建),文件内容如下:

channels:
  - defaults
envs_dirs: [D:\anaconda3\envs]
pkgs_dirs: [D:\anaconda3\pkgs]

这样环境信息就会被保存在D盘了。

2、创建Python环境

找到刚刚安装的anaconda,打开Anaconda Prompt命令行工具,执行如下语句:

conda create --name paddle_env python=3.8 --channel https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/

以上代码表示,创建一个Python3.8的环境,环境名称为paddle_env,软件源从https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ 地址下载。

3、安装百度飞桨环境

输入如下命令,表示接下来的所有操作,我要在刚才创建的那个名为paddle_env的环境中进行:

conda activate paddle_env

输入如下命令,表示安装百度飞桨基础环境:

pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple

输入如下命令,表示安装百度飞桨Hub:

pip install paddlehub -i https://mirror.baidu.com/pypi/simple

最后,我们再新建一个环境变量HUB_HOME。因为paddlehub的模型下载安装时,会默认装在c盘。我们将HUB_HOME指到其他的磁盘即可。

四、安装模型

上面将基础环境搭建好了,接下来我们安装语音合成模型Diffsinger。

DiffSinger
,一个基于扩散概率模型的 SVS 声学模型。DiffSinger 是一个参数化的马尔科夫链,它可以根据乐谱的条件,迭代地将噪声转换为旋律谱。通过隐式优化变异约束,DiffSinger 可以被稳定地训练并产生真实的输出。

上面在说什么,我也看不太懂。但通俗的说,这个模型可以将乐谱转换为旋律谱。

首先,这个模型依赖如onnxruntime包,我们通过如下命令进行安装:

pip install onnxruntime -i https://mirror.baidu.com/pypi/simple

然后,我们安装Diffsinger模型:

hub install diffsinger==1.0.0

五、创造歌手

目前万事俱备,我们开始创造歌手。

首先我们看这个模型的调用参数是怎样的:

inputs (Dict[str, str]): 输入数据格式如下;
{
  'text': '小酒窝长睫毛AP是你最美的记号',
  'notes': 'C#4/Db4 | F#4/Gb4 | G#4/Ab4 | A#4/Bb4 F#4/Gb4 | F#4/Gb4 C#4/Db4 | C#4/Db4 | rest | C#4/Db4 | A#4/Bb4 | G#4/Ab4 | A#4/Bb4 | G#4/Ab4 | F4 | C#4/Db4',
  'notes_duration': '0.407140 | 0.376190 | 0.242180 | 0.509550 0.183420 | 0.315400 0.235020 | 0.361660 | 0.223070 | 0.377270 | 0.340550 | 0.299620 | 0.344510 | 0.283770 | 0.323390 | 0.360340',
  'input_type': 'word'
}
sample_num (int): 生成音频的数量;
save_audio (bool): 是否保存音频文件;
save_dir (str): 保存处理结果的文件目录。

其中需要以下说明:

text:就是指要唱的歌曲歌词;

notes:表示歌词对应的曲谱;

notes_duration:表示歌词中每个字要唱的时长;

接下来,我们准备以下Python代码:

import paddlehub as hub

module = hub.Module(name="diffsinger")
results = module.singing_voice_synthesis(
  inputs={
    'text': '小酒窝长睫毛AP是你最美的记号',
    'notes': 'C#4/Db4 | F#4/Gb4 | G#4/Ab4 | A#4/Bb4 F#4/Gb4 | F#4/Gb4 C#4/Db4 | C#4/Db4 | rest | C#4/Db4 | A#4/Bb4 | G#4/Ab4 | A#4/Bb4 | G#4/Ab4 | F4 | C#4/Db4',
    'notes_duration': '0.407140 | 0.376190 | 0.242180 | 0.509550 0.183420 | 0.315400 0.235020 | 0.361660 | 0.223070 | 0.377270 | 0.340550 | 0.299620 | 0.344510 | 0.283770 | 0.323390 | 0.360340',
    'input_type': 'word'
  },
  sample_num=1,
  save_audio=True,
  save_dir='./outputs'
)

我们执行以上脚本:

python test.py

最终生成了开头贴出的唱段。

上面生成的是翻唱歌曲。接下来,我们再为歌手写一首原创主打歌曲,鄙人不才作词如下哈哈:

双手敲下黑白键盘,这首音乐你是否会喜欢

作曲可难倒我了,我打算找AI来帮忙,找百度文心一言生成以下曲谱:

C#4/Db4 | F#4/Gb4 | G#4/Ab4 | A#4/Bb4 | F#4/Gb4 | F4/G4 | C#4/Db4 | C#4/Db4 | rest | C#4/Db4 | A#4/Bb4 | G#4/Ab4 | F#4/Gb4 | C#4/Db4 | G#4/Ab4 | A#4/Bb4 | G#4/Ab4 | F#4 | C#4/Db4

每个字的持续时间瞎编一下:

0.407140 | 0.376190 | 0.242180 | 0.509550 | 0.183420 | 0.315400 0.235020 | 0.361660 | 0.223070 | 0.377270 | 0.340550 | 0.299620 | 0.344510 | 0.283770 | 0.323390 | 0.360340 | 0.509550 | 0.183420 | 0.315400 0.235020 | 0.661660


重新执行Python脚本,主打歌生成了:
点我试听

六、结语

AI大时代,AI技术每个人都可以触手可及,程序员群体更是近水楼台,最起码可以起到自我娱乐的效果。

我们可以将以上技术部署到服务器,做成网站、小程序、APP等,借助AI平台的能力,实现自动生成歌词,自动生成曲谱,自动生成唱段等,实现全民娱乐。

作为一个程序员,云服务器现在应该是标配了,学生可以用来学习,菜鸟可以用来练手,老鸟玩点有趣的东西偶尔赚点小钱。你如果想买一台云服务器来玩儿,下面是直达腾讯云优惠专区的链接:

https://cloud.tencent.com/act/cps/redirect?redirect=5186&cps_key=814b8b5d55ef58acc94a1b6bf43d5a2b&from=console

BeautifulSoup库用于从HTML或XML文件中提取数据。它可以自动将复杂的HTML文档转换为树形结构,并提供简单的方法来搜索文档中的节点,使得我们可以轻松地遍历和修改HTML文档的内容。广泛用于Web爬虫和数据抽取应用程序中。

读者如果需要使用这个库,同样需要执行pip命令用以安装:

21.8.1 属性定位链接

通过
HTML
属性我们可以轻松的实现对特定页面特定元素的提取,如下代码我们首先封装两个函数,其中
get_page_attrs
函数用于一次性解析需求,函数
search_page
则用于多次对页面进行解析,这两个函数如果传入
attribute
属性则用于提取属性内的参数,而传入
text
则用于提取属性自身文本。

import requests
from bs4 import BeautifulSoup

header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98"}

# 参数1: 解析页面URL
# 参数2: 需要解析的页面定位
# 参数3: 提取标签属性
# 参数4:设置超时时间
# 参数5:设置返回类型(attribute 返回属性字段,text 返回文本字段)
def get_page_attrs(url,regx,attrs,timeout,type):
    respon_page = []
    try:
        respon = requests.get(url=url, headers=header, timeout=timeout)
        if respon.status_code == 200:
            if respon != None:
                soup = BeautifulSoup(respon.text, "html.parser")
                ret = soup.select(regx)
                for item in ret:
                    if type == "attribute":
                        respon_page.append( str(item.attrs[attrs] ))
                    if type == "text":
                        respon_page.append(str(item.get_text()))

            return respon_page
        else:
            return None
    except Exception:
        return None
    return None

# 对页面多次搜索
# 参数1: 需要解析的html文本
# 参数2: 需要解析的页面定位
# 参数3: 提取标签属性
# 参数5:设置返回类型(attribute 返回属性字段,text 返回文本字段)
def search_page(data,regx,attrs,type):
    respon_page = []
    if data != None:
        soup = BeautifulSoup(data, "html.parser")
        ret = soup.select(regx)
        for item in ret:
            if type == "attribute":
                respon_page.append( str(item.attrs[attrs] ))
            if type == "text":
                respon_page.append(str(item.get_text()))
    return respon_page

通过使用上述两个封装函数,读者就可以轻松的实现对特定网页页面元素的定位,首先我们通过
CSS
属性定位一篇文章中的图片链接,这段代码如下;

if __name__ == "__main__":
    # 通过CSS属性定位图片
    ref = get_page_attrs("https://www.cnblogs.com/LyShark/p/15914868.html",
                   "#cnblogs_post_body > p > img",
                   "src",
                   5,
                   "attribute"
                   )
    print(ref)

当上述代码运行后,即可提取出特定网址链接内,属性
#cnblogs_post_body > p > img
中图片的
src
属性,并提取出图片属性
attribute
自身参数。

接着我们继续使用该函数实现定位文章列表功能,文章列表的定位同理,此处第二个参数应修改为
href
属性,如下代码分别使用两种方式实现对文章列表的定位功能;

if __name__ == "__main__":
    # 定位文章列表,两种方式均可
    ref = get_page_attrs("https://www.cnblogs.com/lyshark",
                   "#mainContent > div > div > div.postTitle > a",
                   "href",
                   5,
                   "attribute"
                   )
    print(ref)

    ref = get_page_attrs("https://www.cnblogs.com/lyshark",
                   "div[class='day'] div[class='postCon'] div a",
                   "href",
                   5,
                   "attribute"
                   )
    print(ref)

代码运行后即可输出
lyshark
网站中主页所有的文章地址信息,输出如下图所示;

当需要定位文章内容时,我们只需要将第二个属性更改为空格,并将第四个属性修改为
text
此时则代表只提取属性内的文本。

if __name__ == "__main__":
    # 定位文章文本字段
    ref = get_page_attrs("https://www.cnblogs.com/lyshark",
                   "div[class='day'] div[class='postCon'] div[class='c_b_p_desc']",
                   "",
                   5,
                   "text"
                   )

    for index in ref:
        print(index)

运行上述代码片段,即可提取出主页中所有的文本信息,如下图所示;

如果需要在同一个页面中多次定位那么就需要使用
search_page
函数了,如下代码中我们需要在一个页面内寻找两个元素,此时就需要定位两次;

if __name__ == "__main__":
    respon = requests.get(url="https://yiyuan.9939.com/yyk_47122/", headers=header, timeout=5)

    ref = search_page(respon.text,
                      "body > div.hos_top > div > div.info > div.detail.word-break > h1 > a",
                      "",
                      "text"
                      )
    print(ref)

    ref = search_page(respon.text,
                      "body > div.hos_top > div > div.info > div.detail.word-break > div.tel > span",
                      "",
                      "text"
                      )
    print(ref)

代码运行后,即可通过依次请求,分别输出该页面中的两个元素,如下图所示;

21.8.2 查询所有标签

使用
find_all
函数,可实现从
HTML

XML
文档中查找所有符合指定标签和属性的元素,返回一个列表,该函数从用于精确过滤,可同时将该页中符合条件的数据一次性全部筛选出来。

其基本语法为:

find_all(name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)
  • name:标签名或列表,用于查找指定标签名的元素,如果为 True 或 None,则查找所有标签元素
  • attrs:字典,用于指定属性名和属性值,用于查找具有指定属性名和属性值的元素
  • recursive:布尔值,表示是否递归查找子标签,默认为 True
  • text:字符串或正则表达式,用于匹配元素的文本内容
  • limit:整数,限制返回的匹配元素的数量
  • kwargs:可变参数,用于查找指定属性名和属性值的元素

我们以输出
CVE
漏洞列表为例,通过使用
find_all
查询页面中所有的
a
标签,并返回一个列表,通过对列表元素的解析,依次输出该漏洞的序号,网址,以及所对应的编号信息。

import re
import requests
from bs4 import BeautifulSoup

header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98"}

# 查找文中 所有a标签 且类名是c_b_p_desc_readmore的 并提取出其href字段
# print(bs.find_all('a',class_='c_b_p_desc_readmore')[0]['href'])

# 提取 所有a标签 且id等于blog_nav_admin 类等于menu 并提取出其href字段
# print(bs.find_all('a',id='blog_nav_admin',class_='menu')[0]['href'])
# print(bs.find_all('a',id='blog_nav_admin',class_='menu')[0].attrs['href'])

if __name__ == "__main__":
    url = "https://cassandra.cerias.purdue.edu/CVE_changes/today.html"
    new_cve = []
    ret = requests.get(url=url, headers=header, timeout=5)
    soup = BeautifulSoup(ret.text, 'html.parser')
    for index in soup.find_all('a'):
        href = index.get('href')
        text = index.get_text()
        cve_number = re.findall("[0-9]{1,}-.*",index.get_text())
        print("序号: {:20} 地址: {} CVE-{}".format(text,href,cve_number[0]))

读者可自行运行上述代码,即可匹配出当前页面中所有的
CVE
漏洞编号等,如下图所示;

21.8.3 取字串返回列表

在BeautifulSoup4中,
stripped_strings
是一个生成器对象,用于获取
HTML
标签内所有文本内容的迭代器。它会自动去除每个文本的前后空格和换行符,只返回纯文本字符串。
stripped_strings
可以用于处理
HTML
文档中的多行文本、空格等特殊符号,也可用于将元素下面的所有字符串以列表的形式返回。

import requests
from bs4 import BeautifulSoup

header = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98"}

if __name__ == "__main__":
    ret = requests.get(url="https://www.cnblogs.com/lyshark", headers=header, timeout=3)
    text = str(ret.content.decode('utf-8'))

    bs = BeautifulSoup(text, "html.parser")
    ret = bs.select('#mainContent > div > div > div.postTitle > a > span')

    for i in ret:
        # 提取出字符串并以列表的形式返回
        string_ = list(i.stripped_strings)
        print(string_)

运行后即可获取选中元素的字符串内容,并通过
list
将其转换为列表格式,如下图所示;

通过
find_all
以及
stripped_strings
属性我们实现一个简单的抓取天气的代码,以让读者可以更好的理解该属性是如何被使用的,如下代码所示;

from bs4 import BeautifulSoup
import requests

head = {'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
ret = requests.get(url="http://www.weather.com.cn/textFC/beijing.shtml", headers=head, timeout=3)
text = str(ret.content.decode('utf-8'))

bs = BeautifulSoup(text,"html.parser")

# 定位到第一个标签上
bs.find_all('div',class_='conMidtab')[1]

# 在conMidtab里面找tr标签并从第3个标签开始保存
tr = bs.find_all('tr')[2:]

for i in tr:
    # 循环找代码中的所有td标签
    td = i.find_all('td')
    # 找所有的td标签,并找出第一个td标签
    city_td = td[0]
    # 获取目标路径下所有的子孙非标签字符串,自动去掉空字符串
    city = list(city_td.stripped_strings)[0]
    # 取出度数的标签
    temp = td[-5]
    temperature = list(temp.stripped_strings)[0]
    print('城市:{}   温度:{}'.format(city,temperature))

我们以提取北京天气为案例,当运行代码后即可取出北京市所有地区的气温数据,如下图所示;

本文作者: 王瑞
本文链接:
https://www.lyshark.com/post/ac89ee84.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

MongoDB 中的锁

前言

MongoDB 是一种常见的文档型数据库,因为其高性能、高可用、高扩展性等特点,被广泛应用于各种场景。

在多线程的访问下,可能会出现多线程同时操作一个集合的情况,进而出现数据冲突的情况,为了保证数据的一致性,MongoDB 采用了锁机制来保证数据的一致性。

下面来看看 MongoDB 中的锁机制。

MongoDB 中锁的类型

MongoDB 中使用多粒度锁定,它允许操作锁定在全局,数据库或集合级别,同时允许各个存储引擎在集合级别一下实现自己的并发控制(例如,WiredTiger 中的文档级别)。

MongoDB 中使用一个 readers-writer 锁,它允许并发多个读操作访问数据库,但是只提供唯一写操作访问。

当一个读锁存在时,其它的读操作可以继续,不会被阻塞,如果一个写锁占有这个资源的时候,其它所有的读操作和写操作都会被阻塞。也就是读读不阻塞,读写阻塞,写写阻塞。

MongoDB 中的锁首先提供了读写锁,即共享锁(Shared, S)(读锁)以及排他锁(Exclusive, X)(写锁),同时,为了解决多层级资源之间的互斥关系,提高多层级资源请求的效率,还在此基础上提供了意向锁(Intent Lock)。即锁可以划分为4种类型:

1、共享锁,读锁(S),允许多个线程同时读取一个集合,读读不互斥;

2、排他锁,写锁(X),允许一个线程写入数据,写写互斥,读写互斥;

3、意向共享锁,IS,表示意向读取;

4、意向排他锁,IX,表示意向写入;

什么是意向锁呢?

如果另一个任务企图在某表级别上应用共享或排他锁,则受由第一个任务控制的表级别意向锁的阻塞,第二个任务在锁定该表前不需要检查各个页或行锁,而只需检查表上的意向锁。

简单的讲就是意向锁是为了快速判断,表里面是否有记录被加锁。如果没有意向锁,大更新操作要判断是否有更小的操作在进行,

意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。

例如,当以写入模式(X模式)锁定集合时,相应的数据库锁和全局锁都必须以意向独占(IX)模式锁定。一个数据库可以同时以IS和IX模式进行锁定,但独占(X)锁无法与其他模式并存,共享(S)锁只能与意向共享(IS)锁并存。

MongoDB 中的锁是公平的,所有的请求都会排队获取相应的锁。但是 MongoDB 为了优化吞吐量,在执行某个请求的时候,会同时执行和它兼容的其它请求。比如一个队列一个请求队列需要的锁如下,执行IS请求的同时,会同时执行和它相容的其他S和IS请求。等这一批请求的S锁释放后,再执行X锁的请求。

IS → IS → X → X → S → IS

这种处理机制保证了在相对公平的前提下,提高了吞吐量,不会让某一类的请求长时间的等待。

对于长时间的读或者写操作,某些条件下,mongodb 会临时的 让渡 锁,以防止长时间的阻塞。

锁的让渡释放

对于大多数读取和写入操作,WiredTiger 使用乐观并发控制。WiredTiger 仅在全局、数据库和集合级别使用意向锁。当存储引擎检测到两个操作之间的冲突时,其中一个将导致写冲突,从而使 MongoDB 可以在不可见的情况下重新尝试该操作。

在某些情况下,读写操作可以释放它们持有的锁。以防止长时间的阻塞。

长时间运行的读取和写入操作,比如查询、更新和删除,在许多情况下都会释放锁。MongoDB操作也可以在影响多个文档的写入操作中,在单个文档修改之间释放锁。

对于支持文档级并发控制的存储引擎,比如 WiredTiger,在访问存储时通常不需要释放锁,因为在全局、数据库和集合级别保持的意向锁不会阻塞其他读取者和写入者。然而,操作会定期释放锁,以便:

1、避免长时间存储事务,因为这些事务可能需要在内存中保存大量数据;

2、充当中断点,以便可以终止长时间运行的操作;

3、允许需要对集合进行独占访问的操作,比如索引/集合的删除和创建。

常见操作使用的锁类型

下面列列举一下 MongoDB 中的常见操作对应的锁类型

select:库级别的意向读锁(r),表级别的意向读锁(r),文档级别的读锁(R);

update/insert:库级别的意向写锁(w),表级别的意向写锁(w),文档级别的写锁(W);

foreground 方式创建索引:库级别的写锁(W);当为一个集合创建索引时,因为是库级别的写锁,这个操作将阻塞其他的所有操作,任意基于所有数据库申请读或写锁都将等待直到前台完成索引创建操作;

background 方式创建索引:库级别的意向写锁(w),表级别的意向写锁(w);从
MongoDB 4.2
开始,在构建过程的开始和结束时,索引构建仅获取正在进行索引的集合的独占锁,以保护元数据更改。在创建索引的其它部分,使用锁的让渡行为,最大限度保证集合的读写访问。

如果定位 MongoDB 中锁操作

当查询有慢查询出现的时候,有时候会出现锁的阻塞等待,紧急情况,需要我们快速定位并且结束当前操作。

使用
db.currentOp()
就能查看当前数据库正在执行的操作。

db.currentOp()

{
    "inprog" : [
        {
            "opid" : 6222,   #进程号
            "active" : true, #是否活动状态
            "secs_running" : 3,#操作运行了多少秒
            "microsecs_running" : NumberLong(3662328),#操作持续时间(以微秒为单位)。MongoDB通过从操作开始时减去当前时间来计算这个值。
            "op" : "getmore",#操作类型,包括(insert/query/update/remove/getmore/command)
            "ns" : "local.oplog.rs",#命名空间
            "query" : {#如果op是查询操作,这里将显示查询内容;也有说这里显示具体的操作语句的
                 
            },
            "client" : "192.168.91.132:45745",#连接的客户端信息
            "desc" : "conn5",#数据库的连接信息
            "threadId" : "0x7f1370cb4700",#线程ID
            "connectionId" : 5,#数据库的连接ID
            "waitingForLock" : false,#是否等待获取锁
            "numYields" : 0,
            "lockStats" : {
                "timeLockedMicros" : {#持有的锁时间微秒
                    "r" : NumberLong(141),#整个MongoDB实例的全局读锁
                    "w" : NumberLong(0)#整个MongoDB实例的全局写锁
                },
                "timeAcquiringMicros" : {#为了获得锁,等待的微秒时间
                    "r" : NumberLong(16),#整个MongoDB实例的全局读锁
                    "w" : NumberLong(0)#整个MongoDB实例的全局写锁
                }
            }
        }
    ]
}

来看下几个主要的字段含义

  • client:发起请求的客户端;

  • opid: 操作的唯一标识;

  • secs_running:该操作已经执行的时间,单位:微妙。如果该字段的返回值很大,就需要查询请求是否合理;

  • op:操作类型。通常是query、insert、update、delete、command中的一种;

  • query/ns:这个字段能看出是对哪个集合正在执行什么操作。

当发现一个语句执行时间很久,影响到了整个数据库的运行,这时候我们 可以考虑中断这条语句的执行。

使用
db.killOp(opid)
命令终止该请求。

来个试验的栗子

对表里面一个大表创建索引,不添加 backend。

db.notifications.createIndex({userId: -1});

1、查询运行超过20S 的请求

$ db.currentOp({"active" : true, "secs_running":{ "$gt" : 20 }})

{
	"inprog" : [
		{
			"host" : "host-192-168-61-214:27017",
			"desc" : "conn50774156",
			"connectionId" : 50774156,
			"client" : "172.18.91.66:52088",
			"appName" : "Navicat",
			"clientMetadata" : {
				"application" : {
					"name" : "Navicat"
				},
				"driver" : {
					"name" : "mongoc",
					"version" : "1.16.2"
				},
				"os" : {
					"type" : "Darwin",
					"name" : "macOS",
					"version" : "20.6.0",
					"architecture" : "x86_64"
				},
				"platform" : "cfg=0x0000d6a0e9 posix=200112 stdc=206666662 CC=clang 8.0.0 (clang-800.0.42.1) CFLAGS=\"\" LDFLAGS=\"\""
			},
			"active" : true,
			"currentOpTime" : "2023-10-24T01:32:00.615+0000",
			"opid" : -1782291565,
			"lsid" : {
				"id" : UUID("fff3c45d-b6ac-4a30-b83f-5a565ba166ef"),
				"uid" : BinData(0,"EJF4gS8MLpU7cuurTHswrdjF5hInXITH3796necT7PU=")
			},
			"secs_running" : NumberLong(103),
			"microsecs_running" : NumberLong(103025729),
			"op" : "command",
			"ns" : "gleeman.$cmd",
			"command" : {
				"createIndexes" : "notifications",
				"indexes" : [
					{
						"key" : {
							"userId" : -1
						},
						"name" : "userId_-1"
					}
				],
				"$db" : "gleeman",
				"lsid" : {
					"id" : UUID("fff3c45d-b6ac-4a30-b83f-5a565ba166ef")
				},
				"$clusterTime" : {
					"clusterTime" : Timestamp(1698666666011, 1),
					"signature" : {
						"hash" : BinData(0,"iKilM1hvvIJC4hrTgu3FebYNhEw="),
						"keyId" : NumberLong("7233287468395528194")
					}
				}
			},
			"msg" : "Index Build (background) Index Build (background): 27288147/34043394 80%",
			"progress" : {
				"done" : 27288148,
				"total" : 34043394
			},
			"numYields" : 213205,
			"locks" : {
				"Global" : "w",
				"Database" : "w",
				"Collection" : "w"
			},
			"waitingForLock" : false,
			"lockStats" : {
				"Global" : {
					"acquireCount" : {
						"r" : NumberLong(213208),
						"w" : NumberLong(213208)
					}
				},
				"Database" : {
					"acquireCount" : {
						"w" : NumberLong(213209),
						"W" : NumberLong(1)
					}
				},
				"Collection" : {
					"acquireCount" : {
						"w" : NumberLong(213207)
					}
				},
				"oplog" : {
					"acquireCount" : {
						"w" : NumberLong(1)
					}
				}
			}
		}
	],
	"ok" : 1,
	"operationTime" : Timestamp(1698666666118, 1),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1698666666118, 1),
		"signature" : {
			"hash" : BinData(0,"oLzIpVSGpZ213BW4x/jY6ESKvdA="),
			"keyId" : NumberLong("7233287468395528194")
		}
	}
}

2、批量删除请求大于 20s 的请求

var ops = db.currentOp(
    {
        "active": true,
        "secs_running": {
            "$gt": 20
        }
    }
).inprog

for (i = 0; i < ops.length; i++) {
    ns = ops[i].ns;
    op = ops[i].op;
    if (ns.startsWith("system.") || ns.startsWith("local.oplog.") || ns.length === 0 || op == "none" || ns.command == "" || ns in["admin", "local", "config"]) {
        continue;
    }
    var opid = ops[i].opid;
    db.killOp(opid);
    print("Stopping op #" + opid)
}

3、kill 掉特定 client 端 ip 的请求

var clientIp="172.18.91.66";
var currOp = db.currentOp();

for (op in currOp.inprog) {
    if (clientIp == currOp.inprog[op].client.split(":")[0]) {
        db.killOp(currOp.inprog[op].opid)
    }
}

4、查询所有 wait 锁定的写操作

db.currentOp(
   {
     "waitingForLock" : true,
     $or: [
        { "op" : { "$in" : [ "insert", "update", "remove" ] } },
        { "command.findandmodify": { $exists: true } }
    ]
   }
)

5.返回索引的创建信息

db.adminCommand(
    {
      currentOp: true,
      $or: [
        { op: "command", "command.createIndexes": { $exists: true }  },
        { op: "none", "msg" : /^Index Build/ }
      ]
    }
)

总结

1、MongoDB 中使用一个 readers-writer 锁,它允许并发多个读操作访问数据库,但是只提供唯一写操作访问;

2、MongoDB 中的锁首先提供了读写锁,即共享锁(Shared, S)(读锁)以及排他锁(Exclusive, X)(写锁),同时,为了解决多层级资源之间的互斥关系,提高多层级资源请求的效率,还在此基础上提供了意向锁(Intent Lock)。即锁可以划分为4中类型:

  • 1、共享锁,读锁(S),允许多个线程同时读取一个集合,读读不互斥;

  • 2、排他锁,写锁(X),允许一个线程写入数据,写写互斥,读写互斥;

  • 3、意向共享锁,IS,表示意向读取;

  • 4、意向排他锁,IX,表示意向写入;

3、MongoDB 中支持高并发的一个重要的点就是 MongoDB 支持锁的让渡,在某些情况下,读写操作可以让渡它们持有的锁。以防止长时间的阻塞后面的操作;

参考

【mongodb锁表命令-相关文档】
https://www.volcengine.com/theme/900385-M-7-1
【mongo 中的锁】
https://www.jinmuinfo.com/community/MongoDB/docs/15-faq/03-concurrency.html
【FAQ: Concurrency】
https://www.mongodb.com/docs/manual/faq/concurrency/
【mongodb中的锁】
https://boilingfrog.github.io/2023/10/27/mongo中的锁/

企业正在享受 DevOps 实施带来的好处,但这也是有代价的。开发人员需要承担额外的责任,可能会导致他们感到疲惫不堪。因此我们可以采取一些方法来确保 DevOps 工程师的满意度。

DevOps 的支持者通常将这一概念说成是提高效率和生产力的好方法。通过加强开发人员和 IT 运维工程师之间的协作,DevOps 能让每个人都朝着共同的目标更有效地工作。在很多情况下可能都是如此。然而,有人认为
DevOps 也有一个重大缺点:负责保持 DevOps 流程运行的工程师的压力水平增加。

以下是 DevOps 可能使开发人员的工作更具挑战性的原因,以及组织可以采取哪些措施来确保他们从 DevOps 中受益而不会让工程师抓狂。

DevOps 如何为企业带来效益

首先,让我们来了解一下 DevOps 的含义及其对企业的帮助。

DevOps 的理念是开发和 ITOps 团队应该紧密合作,即开发人员和 ITOps 之间的密切合作确保两个团队能够相互支持。

DevOps 的出现是为了解决许多组织在过去几十年中面临的一个挑战,即开发人员在编写代码时往往很少或根本得不到 ITOps 团队的反馈。这导致了沟通孤岛、效率低下,在某些情况下,开发人员和 ITOps 工程师之间关系紧张。通过帮助每个人持续合作,DevOps 可以让企业避免这些陷阱。

因此,从企业的角度来看,DevOps 是一件好事,因为它有助于确保工程师尽可能地提高工作效率。它能最大限度地减少时间和精力的浪费,通常还能提高企业推出新软件的速度,这反过来又能为企业带来市场竞争优势。

DevOps 对工程师意味着什么

对于开发人员本身来说,DevOps 并不总是那么美好。DevOps 迫使工程师做两件事,这会增加他们的工作压力:

  • 承担更广泛的责任
    :DevOps 要求工程师们同时负责两类流程,而不是只负责软件开发或 IT 运营。

  • 适应节奏更快的发布周期
    :DevOps 与 CI/CD 等实践齐头并进,DevOps 团队通常需要每周至少发布一次新的应用程序更新,有时甚至需要每天发布一次。这与 "瀑布式 "软件开发战略的时代大相径庭,在 "瀑布式 "软件开发战略下,新版本即使一年发布一到两次,也是如此。

这两个因素都会使工程师的工作变得更加紧张。在采用 DevOps 的企业中,工程师被要求做得更多、更快。

当然,DevOps 为软件交付流程带来的高效率有可能使工程师在满足这些更严格要求的同时,总体压力水平低于没有 DevOps 时的水平。如果没有 DevOps,团队通常会浪费更多时间来修复代码,并在出错时相互指责,这本身就是一种压力。通过减轻这些挑战,一个实施良好的 DevOps 战略可以让工程师们在整体上更轻松一些。

但并不是所有 DevOps 战略都能得到很好的实施,DevOps 本身就存在一种风险,那就是过多的 DevOps 实践只会导致工程师压力更大,更容易倦怠
。他们可能会设法更快地发布新版本,但会牺牲工程师的工作满意度作为代价,这种成就是以高昂的代价换来的。

让 DevOps 工程师更轻松

为了避免这些问题,
选择采用 DevOps 的企业应确保设定合理的期望值
。例如,每周发布一次新版本可能会给工程师带来过大的压力,至少在一开始是这样。设定更适度的目标有助于降低 DevOps 工程师倦怠的风险。

限制 DevOps 工程师的职责
也会有所帮助。虽然 DevOps 通常鼓励 DevOps 团队中的每个人共同承担软件交付流程中每个环节的责任,但让某些工程师牵头负责某些流程(如开发、测试和部署)通常更为现实。集体责任感仍然普遍存在,但为不同类型的任务指定牵头人可以让团队成员优先处理某些任务,而不是试图掌控软件交付生命周期的每个阶段,从而减轻每位工程师的整体压力。

在某些情况下,企业首先应该
限制采用 DevOps 的范围
。并不是每个应用程序都需要持续开发;有些应用程序(如传统软件)的变化频率并不高,不值得被嵌入 CI/CD 流水线。企业在实施 DevOps 时要有战略眼光,这样才能降低将过多 DevOps 实践强加给没有能力或时间处理所有实践的工程师的风险。

总 结

在 DevOps 领域中,需要谨慎执行,因为过度的任务和压力可能会减弱其优势。虽然毫无疑问,DevOps 为企业带来了显著的优势,使开发人员能够实现更高的效率和生产力,但同时也可能给他们的团队带来负担。为了防止 burnout 的风险,请避免过度采用 DevOps 实践。

参考链接:
https://www.itprotoday.com/devops/downside-devops-stress-and-burnout-engineers