2023年3月

前言

我08年毕业,大学跟着老师培训班学习的C#,那时(2003-2010)它很是时髦,毕业后也就从事了winform窗体应用程序开发。慢慢的web网站兴起,就转到aps.net开发,再到后来就上了另一艘大船(java),前端app混合开发。近三年从事web站点运维,从linux基础+docker,到数据库db,到中间件,以及各类自动化工具,趋于SRE可靠性站点工程师工作。

在四线城市“管理+业务+技术”前进的道路上,有身边的,更多的来自网络的良师益友,使我认知的半径越来越大,从蒙蒙菜鸟不断成长,在此,由衷的说声谢谢!

言归正传,今天我就把自己收藏 、不断更改的java技术一张图分享给出来,希望能对IT新人有所帮助。

java生态下后端开发都有哪些技术栈?

1. 来源

技术大咖-小傅哥2021年01月08日发表了《java后端已有哪些技术栈?》,文中介绍了java开发技术架构(经过千人问卷调查整理编制),使我受益匪浅,特此感谢!

2. 变化

结合我的所见、所识、所用,对其进行了修改,尽可能的展示出大家都在用的主流技术,欢迎大家提出修改意见,填补其他的技术。

每个技术点都是一个不小的领域,不再一一介绍,若有不了解的,大家自行度娘下。为了不带偏新人,我提下我个人的看法,希望能成为你的建议。

3. 建议

  • 积极学习,在变化中,自律自强,有所作为;

  • 逐步形成自己体系化的知识库、实践库,能减少新知识的学习成本奥;

  • 学用是智慧的取舍抉择,特别是公司业务用什么时,不要跟风、追新,而要综合指标分析确定。

软件工程始终是一项协同的工作,全栈开发想想就好,因此要根据自己的成长阶段、职业规划,选自己要学什么,要用什么,与实际工作不匹配的短板尽快的补起来,并在技能和业务统一认知上齐步发展。

4.联系,交流、共同成长

今天分享的是“学什么,用什么”,下几篇(平时工作非常的忙,在下周末抽个空吧)分享下《如何学》,《如何选》,《如何请教》

最后,既然是运维工程师,就先给大家分享个有用的,适用于企业内部 CentOS7 系列服务器初始化、符合等保测评的系统安全加固的shell脚本

# @Author: liyanjing,@E-mail: 284223249@qq.com, @wechat: Sd-LiYanJing
# @CreateTime: 2022-10-18 10:30 , @Last ModifiedTime: 2022-12-03 13:50
# @Github: https://github.com/919927181/linux-initialization.git

前言

工厂设计模式可能是最常用的设计模式之一,我想大家在自己的项目中都用到过。可能你会不屑一顾,但这篇文章不仅仅是关于工厂模式的基本知识,更是讨论如何在运行时动态选择不同的方法进行执行,你们可以看看是不是和你们项目中用的一样?

欢迎关注个人公众号【JAVA旭阳】交流沟通

小菜鸟的问题

直接上例子说明,设计一个日志记录的功能,但是支持记录到不同的地方,例如:

  • 内存中
  • 磁盘上的文件
  • 数据库
  • 百度网盘等远程存储服务

面对这么一个需求,你会怎么做呢?我们先来看看小菜鸟的做法吧。

  1. 小菜鸟创建了一个
    Logger
class Logger {
    public void log(String message, String loggerMedium) {}
}
  1. 小菜鸟想都不想,直接一通
    if else
class Logger {
    public void log(String message, String loggerMedium) {
        if (loggerMedium.equals("MEMORY")) {
            logInMemory(message);
        } else if (loggerMedium.equals("FILE")) {
            logOnFile(message);
        } else if (loggerMedium.equals("DB")) {
            logToDB(message);
        } else if (loggerMedium.equals("REMOTE_SERVICE")) {
            logToRemote(message);
        }
    }

    private void logInMemory(String message) {
        // Implementation
    }

    private void logOnFile(String message) {
        // Implementation
    }

    private void logToDB(String message) {
        // Implementation
    }

    private void logToRemote(String message) {
        // Implementation
    }
}

现在突然说要增加一种存储介质
FLASH_DRIVE
,就要改了这个类?不拍改错吗?也不符合“开闭原则”,而且随着存储介质变多,类也会变的很大,小菜鸟懵逼了,不知道怎么办?

有没有更好的方法呢?

这时候小菜鸟去找你帮忙,你一顿操作,改成了下面这样:

class InMemoryLog {
    public void logToMemory(String message) {
        // Implementation
    }
}

class FileLog {
    public void logToFile(String message) {
        //Implementation
    }
}

class DBLog {
    public void logToDB(String message) {
        // Implementation
    }
}

class RemoteServiceLog {
    public void logToService(String message) {
        // Implementation
    }
}

class Logger {
    private InMemoryLog mLog;
    private FileLog fLog;
    private DBLog dbLog;
    private RemoteServiceLog sLog;
    
    public Logger() {
        mLog = new InMemoryLog();
        fLog = new FileLog();
        dbLog = new DBLog();
        sLog = new RemoteServiceLog();
    }

    public void log(String message, String loggerMedium) {
        if (loggerMedium.equals("MEMORY")) {
            mLog.logToMemory(message);
        } else if (loggerMedium.equals("FILE")) {
            fLog.logToFile(message);
        } else if (loggerMedium.equals("DB")) {
            dbLog.logToDB(message);
        } else if (loggerMedium.equals("REMOTE_SERVICE")) {
            sLog.logToService(message);
        }
    }
}

在这个实现中,你已经将单独的代码分离到它们对应的文件中,但是
Logger
类与存储介质的具体实现紧密耦合,如
FileLog

DBLog
等。随着存储介质的增加,类中将引入更多的实例
Logger

还有什么更好的办法吗?

你想了想,上面的实现都是直接写具体的实现类,是面向实现编程,更合理的做法是面向接口编程,接口意味着协议,契约,是一种更加稳定的方式。

  1. 定义一个日志操作的接口
public interface LoggingOperation {
    void log(String message);
}
  1. 实现这个接口
class InMemoryLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}

class FileLog implements LoggingOperation {
    public void log(String message) {
        //Implementation
    }
}

class DBLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}

class RemoteServiceLog implements LoggingOperation {
    public void log(String message) {
        // Implementation
    }
}
  1. 你定义了一个类,据传递的参数,在运行时动态选择具体实现,这就是所谓的工厂类,不过是基础版。
class LoggerFactory {
    public static LoggingOperation getInstance(String loggerMedium) {
        LoggingOperation op = null;
        switch (loggerMedium) {
            case "MEMORY":
                op = new InMemoryLog();
                break;
            case "FILE":
                op = new FileLog();
                break;
            case "DB":
                op = new DBLog();
                break;
            case "REMOTE_SERVICE":
                op = new RemoteServiceLog();
                break;
        }

        return op;
    }
}
  1. 现在你的
    Logger
    类的实现就是下面这个样子了。
class Logger {
    public void log(String message, String loggerMedium) {
        LoggingOperation instance = LoggerFactory.getInstance(loggerMedium);
        instance.log(message);
    }
}

这里的代码变得非常统一,创建实际存储实例的责任已经转移到
LoggerFactory
,各个存储类只实现它们如何将消息记录到它们的特定介质,最后该类
Logger
只关心通过
LoggerFactory
将实际的日志记录委托给具体的实现。这样,代码就很松耦合了。你想要添加一个新的存储介质,例如
FLASH_DRIVE
,只需创建一个实现
LoggingOperation
接口的新类并将其注册到
LoggerFactory
中就好了。这就是工厂模式可以帮助您动态选择实现的方式。

还能做得更好吗?

你已经完成了一个松耦合的设计,但是想象一下假如有数百个存储介质的场景,所以我们最终会在工厂类
LoggerFactory
中的
switch case
部分
case
数百个。这看起来还是很糟糕,如果管理不当,它有可能成为技术债务,这该怎么办呢?

摆脱不断增长的
if else
或者
switch case
的一种方法是维护类中所有实现类的列表,
LoggerFactory
代码如下所示:

class LoggerFactory {
    private static final List<LoggingOperation> instances = new ArrayList<>();

    static {
        instances.addAll(Arrays.asList(
                new InMemoryLog(),
                new FileLog(),
                new DBLog(),
                new RemoteServiceLog()
        ));
    }

    public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
        for(LoggingOperation op : instances) {
            // 比如判断StrUtil.equals(loggerMedium, op.getType()) op本身添加一个type
        }

        return null;
    }
}

但是请注意,还不够,在所有上述实现中,无论
if else、switch case
还是上面的做法,都是让存储实现与
LoggerFactory
紧密耦合的。你添加一种实现,就要修改
LoggerFactory
,有什么更好的做法吗?

逆向思维一下,我们是不是让具体的实现主动注册上来呢?通过这种方式,工厂不需要知道系统中有哪些实例可用,而是实例本身会注册并且如果它们在系统中可用,工厂就会为它们提供服务。具体代码如下:

class LoggerFactory {
    private static final Map<String, LoggingOperation> instances = new HashMap<>();

    public static void register(String loggerMedium, LoggingOperation instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(String loggerMedium) {
        if (instances.containsKey(loggerMedium)) {
            return instances.get(loggerMedium);
        }
        return null;
    }
}

在这里,
LoggerFactory
提供了一个
register
注册的方法,具体的存储实现可以调用该方法注册上来,保存在工厂的
instances
map对象中。

我们来看看具体的存储实现注册的代码如下:

class RemoteServiceLog implements LoggingOperation {
    static {
        LoggerFactory.register("REMOTE", new RemoteServiceLog());
    }

    public void log(String message) {
        // Implementation
    }
}

由于注册应该只发生一次,所以它发生在
static
类加载器加载存储类时的块中。

但是又有一个问题,默认情况下JVM不加载类
RemoteServiceLog
,除非它由应用程序在外部实例化或调用。因此,尽管存储类有注册的代码,但实际上注册并不会发生,因为没有被JVM加载,不会调用static代码块中的代码, 你又犯难了。

你灵机一动,
LoggerFactory
是获取存储实例的入口点,能否在这个类上做点文章,就写下了下面的代码:

class LoggerFactory {
    private static final Map<String, LoggingOperation> instances = new HashMap<>();

    static {
        try {
            loadClasses(LoggerFactory.class.getClassLoader(), "com.alvin.storage.impl");
        } catch (Exception e) {
            // log or throw exception.
        }
    }

    public static void register(String loggerMedium, LoggingOperation instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(String loggerMedium) {
        if (instances.containsKey(loggerMedium)) {
            return instances.get(loggerMedium);
        }
        return null;
    }

    private static void loadClasses(ClassLoader cl, String packagePath) throws Exception {

        String dottedPackage = packagePath.replaceAll("[/]", ".");

        URL upackage = cl.getResource(packagePath);
        URLConnection conn = upackage.openConnection();

        String rr = IOUtils.toString(conn.getInputStream(), "UTF-8");

        if (rr != null) {
            String[] paths = rr.split("\n");

            for (String p : paths) {
                if (p.endsWith(".class")) {
                    Class.forName(dottedPackage + "." + p.substring(0, p.lastIndexOf('.')));
                }

            }
        }
    }
}

在上面的实现中,你使用了一个名为
loadClasses
的方法,该方法扫描提供的包名称
com.alvin.storage.impl
并将驻留在该目录中的所有类加载到类加载器。以这种方式,当类加载时,它们的
static
块被初始化并且它们将自己注册到
LoggerFactory
中。

如何在 SpringBoot 中实现此技术?

你突然发现你的应用是springboot应用,突然想到有更方便的解决方案。

因为你的存储实现类都被标记上注解
@Component
,这样
Spring
会在应用程序启动时自动加载类,它们会自行注册,在这种情况下你不需要使用
loadClasses
功能,
Spring
会负责加载类。具体的代码实现如下:

class LoggerFactory {
    private static final Map<String, Class<? extends LoggingOperation>> instances = new HashMap<>();

    public static void register(String loggerMedium, Class<? extends LoggingOperation> instance) {
        if (loggerMedium != null && instance != null) {
            instances.put(loggerMedium, instance);
        }
    }

    public static LoggingOperation getInstance(ApplicationContext context, String loggerMedium) {
        if (instances.containsKey(loggerMedium)) {
            return context.getBean(instances.get(loggerMedium));
        }
        return null;
    }
}

getInstance
需要传入
ApplicationContext
对象,这样就可以根据类型获取具体的实现了。

修改所有存储实现类,如下所示:

import org.springframework.stereotype.Component;

@Component
class RemoteServiceLog implements LoggingOperation {
    static {
        LoggerFactory.register("REMOTE", RemoteServiceLog.class);
    }

    public void log(String message) {
        // Implementation
    }
}

总结

我们通过一个例子,不断迭代带大家理解了工厂模式,工厂模式是一种创建型设计模式,用于创建同一类型的不同实现对象。我们来总结下这种动态选择对象工厂模式的优缺点。

优点:

  • 容易管理。在添加新的存储类时,只需将该类放入特定包中,在static代码块中注册它自己到工厂中。
  • 松耦合,当您添加新的存储实现时,您不需要在工厂类中进行任何更改。
  • 遵循SOLID编程原则。

缺点:

  • 如果是用原生通过类加载的方式,代价比较大,因为它涉及 I/O 操作。但是如果使用的是SpringBoot,则无需担心,因为框架本身会调用组件。
  • 需要额外编写一个
    static
    块,注册自己到工厂中,一不小心就遗漏了。

欢迎关注个人公众号【JAVA旭阳】交流沟通

基于内容的推荐算法是一种常用的推荐算法,它主要通过
分析物品的特征
(如文本、图片、视频等)来实现推荐。其核心思想是
利用物品属性的相似性,将已经喜欢的物品的特征作为输入,推荐与该物品相似度高的其他物品。
基于内容的推荐算法仅考虑了
单个用户对物品的偏好
,而未考虑多个用户之间的交互和影响。此外,该算法在特征提取方面也存在一定的局限性,因此需要根据具体应用场景选择合适的特征提取方法。
以下是基于内容的推荐算法的主要步骤:
  1. 特征提取:对每个物品进行特征提取,将其转换成可计算的数值向量,例如,对于文本数据可以使用词袋模型或TF-IDF方法提取特征,对于图像和音频数据可以使用卷积神经网络进行特征提取。
  2. 特征表示:将提取到的特征向量组成矩阵形式,并进行归一化处理,以便后续的相似度计算。
  3. 相似度计算:计算不同物品之间的相似度,可以使用余弦相似度、欧几里得距离或曼哈顿距离等方法进行计算。
  4. 推荐结果排序:根据用户已经喜欢的物品的特征向量,计算该物品与其他物品的相似度,并按照相似度降序排列,最后将排在前面的若干个物品推荐给用户。
需要注意的是,基于内容的推荐算法仅考虑了单个用户对物品的偏好,而未考虑多个用户之间的交互和影响。此外,该算法在特征提取方面也存在一定的局限性,因此需要根据具体应用场景选择合适的特征提取方法。

特征提取

特征提取是指从原始数据中选取最具代表性和区分性的属性或特征,以便用于机器学习、模式识别等任务。在实际应用中,特征提取一般是针对不同的任务和数据类型,选择合适的方法和特征集合,以提高机器学习算法的准确性和泛化能力。
  • 词袋模型
将文本中的每个词看成一个独立的特征,并将它们组成一个向量表示文本的特征。在构建词袋模型时,首先需要对所有文本进行分词,然后统计每个单词在整个文本集合中出现的次数,并将其转换为向量形式。这种方法虽然简单有效,但没有考虑到单词之间的顺序和语义关系。
  • TF-IDF方法
TF-IDF(Term Frequency-Inverse Document Frequency)指的是词频-逆文档频率,是一种常用的文本特征提取方法,可以用来评估一个词对于某篇文档的重要性。其中,TF指的是词频,表示该词在文档中出现的次数;IDF指的是逆文档频率,表示一个词的普遍重要性,计算方式为总文档数目除以包含该词的文档数目的对数。TF-IDF值越大,说明该词在文档中越重要。
TF-IDF方法的优点在于它能够衡量单词的重要程度,同时也考虑了单词的出现频率和单词在语料库中的普遍重要性。因此,在文本分类、信息检索和基于内容的推荐等领域中得到了广泛的应用。
  • 卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)是一种深度学习神经网络,主要用于处理具有网格状结构的数据,例如图像、视频和自然语言处理中的文本等。它可以通过卷积操作来提取输入数据的特征,并通过池化层对特征进行下采样,最后通过全连接层来进行分类或回归等任务。
1.卷积层 卷积层是卷积神经网络的核心组件,它可以将每个神经元与局部区域内的输入相连,然后通过共享权重来检测输入中的模式。具体而言,卷积层包含多个卷积核,每个卷积核在输入数据上滑动,计算出一个二维特征图,其中每个元素对应一个神经元的输出值。这样可以有效减少网络参数数量,避免过拟合问题。
2.池化层 池化层主要用于下采样,即减小特征图的尺寸,并保留重要信息。最常见的池化方式是最大池化,即在局部区域内选择最大值作为输出。此外,还有平均池化和L2-norm池化等方法。
3.全连接层 全连接层用于进行分类或回归等任务,将前面卷积和池化层得到的特征映射转换为输出结果。通常情况下,全连接层的神经元数目较多,需要使用激活函数来增加非线性表达能力。
4.激活函数 激活函数是一种非线性映射,用于引入非线性关系,增加模型的表达能力。常用的激活函数包括sigmoid、ReLU、Leaky ReLU等。
5.批量归一化 批量归一化是一种正则化方法,用于加速训练和提高模型泛化能力。它通过在每个批次上对输入数据进行标准化,使得每个神经元的输入分布具有相似的统计特性。
6.Dropout Dropout是一种随机失活技术,用于减少过拟合问题。它通过以一定的概率随机丢弃一些神经元的输出,使得模型在训练过程中不能过度依赖某些神经元的输出。

特征降维

特征降维是指将高维数据转化为低维表示的过程。在机器学习和数据挖掘中,通常需要处理高维数据集,例如图像、语音、文本等,这些数据通常包含大量冗余信息,而且难以可视化和理解。因此,通过将数据压缩到低维空间中,可以更好地进行分析和建模。
特征降维可以帮助我们减少计算复杂度和存储开销,提高模型训练速度和泛化能力,并且能够使得数据更易于可视化和理解。
线性降维和非线性降维区别
线性降维和非线性降维是两种常见的数据降维方法,它们之间的区别在于是否对数据进行了非线性变换。
线性降维方法(如主成分分析)通过矩阵变换将高维数据映射到低维空间中,其中每个新特征都是原始特征的线性组合。这意味着线性降维方法只能学习线性结构,并且无法捕捉非线性关系和复杂的拓扑关系。
非线性降维方法(如流形学习)则使用非线性变换将高维数据映射到低维空间中,以保留原始数据的非线性特征。这些非线性变换可以通过局部或全局方式来实现,例如通过在每个数据点周围建立局部坐标系或通过计算数据点之间的最短路径来估计它们在低维空间中的距离。非线性降维方法通常能够发现数据中的隐藏结构、拓扑形态和潜在含义等信息,从而提高机器学习模型的准确性和鲁棒性。
总之,线性降维方法适用于简单数据集并且计算效率高,而非线性降维方法则适用于复杂数据集,并且通常需要更多的计算资源和时间。
线性降维
线性降维是指通过线性变换将高维数据映射到低维空间中。最常见的线性降维方法是主成分分析(PCA),它通过找到原始数据中方差最大的方向来进行降维。其他常用的线性降维方法包括因子分析、独立成分分析(ICA)等。
主成分分析(PCA)
主成分分析(Principal Component Analysis,PCA)是一种常用的线性降维方法,用于将高维数据集投影到低维空间中。其基本思想是通过找到原始数据中方差最大的方向来进行降维,从而保留尽可能多的信息。
PCA的实现过程可以概括为以下几个步骤:
  1. 中心化数据 将每个特征减去对应的均值,使得数据在每个维度上的平均值为0。
  2. 计算协方差矩阵 计算中心化后的数据各维度之间的协方差矩阵,即数据集X的协方差矩阵C=X.T * X / (n-1),其中n为样本数。
  3. 计算特征值和特征向量 求解协方差矩阵的特征值和特征向量。特征值表示数据在该方向上的方差大小,而特征向量表示该方向的单位向量。
  4. 选择主成分 按照特征值的大小排序选择前k个主成分,这些主成分对应的特征向量组成了新的特征空间。
  5. 投影数据 通过将原始数据集投影到新的特征空间中,即将数据点乘以特征向量矩阵W,得到降维后的数据矩阵Y=X * W。
PCA可以帮助我们识别出数据中最重要的方向,并将其转换为新的特征空间,从而减少数据的维度和冗余,提高机器学习模型的训练效率和泛化能力。
因子分析
因子分析(Factor Analysis,FA)是一种常用的统计方法,用于分析多个变量之间的共性和相关性。其基本思想是将多个观测变量表示为少量潜在因子的线性组合形式,从而提取出数据中的主要因素并进行降维。
因子分析的实现过程可以概括为以下几个步骤:
  1. 建立模型 设有p个观测变量X1,X2,...,Xp,假设这些变量与m个潜在因子F1,F2,...,Fm有关,且每个观测变量与潜在因子之间存在线性关系,即Xi = a1iF1 + a2iF2 + ... + ami*Fm + ei,其中ai1,ai2,...,aim表示观测变量Xi与潜在因子Fj之间的权重,ei表示观测变量Xi与潜在因子之间未被解释的部分。
  2. 估计参数 通过极大似然估计等方法来估计模型参数,其中包括潜在因子的数量、权重系数以及误差项的方差。
  3. 提取因子 通过对估计得到的协方差矩阵或相关系数矩阵进行特征值分解或奇异值分解,得到因子载荷矩阵和旋转矩阵,从而确定每个变量与每个因子之间的关系。
  4. 解释因子 根据因子载荷矩阵和旋转矩阵来解释各个因子所表示的含义,例如某个因子可能与数据中的某个主题或属性相关联。
因子分析可帮助我们识别数据中的共性和相关性,提取出主要因素并进行降维,从而简化数据集并提高机器学习模型的训练效率和泛化能力。它在社会科学、经济学、生物学、心理学等领域中得到了广泛应用。
独立成分分析(ICA)
独立成分分析(Independent Component Analysis,ICA)是一种常用的盲源分离方法,用于将多个混合信号分解为不相关的独立成分。其基本思想是找到一个转换矩阵,使得经过转换后的信号之间不再具有统计相关性,从而提取出信号中的主要成分并进行降维。
ICA的实现过程可以概括为以下几个步骤:
  1. 建立模型 设有n个混合信号X1,X2,...,Xn,假设这些信号可以表示为独立成分S1,S2,...,Sm的线性组合形式,即X = AS + E,其中A为混合系数矩阵,S为独立成分矩阵,E为噪声误差矩阵。
  2. 中心化数据 将每个混合信号减去对应的均值,使得数据在每个维度上的平均值为0。
  3. 估计混合系数矩阵 通过最大似然估计等方法来寻找混合系数矩阵A,使得经过转换后的信号之间不再具有统计相关性。
  4. 提取成分 使用估计得到的混合系数矩阵A来预测独立成分S,并使用非高斯性衡量标准来确定哪些成分是不相关的。
  5. 旋转矩阵 对提取出的独立成分进行旋转变换,以改善成分的可解释性和物理意义。
ICA可帮助我们分离出混合信号中的独立成分,并进行降维和特征提取。它在语音处理、图像处理、生物医学工程等领域中有着广泛的应用。

非线性降维
非线性降维是指通过非线性变换将高维数据映射到低维空间中。最常见的非线性降维方法是流形学习,它可以识别数据中的流形结构,并将其映射到低维空间中。其他常用的非线性降维方法包括局部线性嵌入(LLE)、等距映射(Isomap)等。
流形学习
流形学习(Manifold Learning)是一种非线性降维方法,用于将高维数据映射到低维流形空间中。其基本思想是假设高维数据集在低维空间中呈现出某种结构或拓扑性质,通过寻找最优映射函数来保留原始数据的这些特征。
流形学习的实现过程可以概括为以下几个步骤:
  1. 建立模型 假设存在一个高维数据集X = {x1,x2,...,xn},其中每个样本xi都有d个特征,我们希望将其映射到低维流形空间Y = {y1,y2,...,yn},其中每个样本yi只有k(k < d)个特征。我们假设Y是X在低维空间中的表示,而不是简单地将数据投影到某个坐标系中。
  2. 寻找最优映射函数 通过最小化重构误差或最大化流形相似度等准则来寻找最优映射函数,常见的方法包括局部线性嵌入(LLE)、等距映射(Isomap)、拉普拉斯特征映射(Laplacian Eigenmaps)等。
  3. 降维和可视化 将原始数据映射到低维流形空间,并进行可视化和分析。
流形学习可以帮助我们识别数据中的流形结构和拓扑性质,从而在保留原始数据特征的同时进行降维和可视化。它在图像处理、语音处理、文本挖掘等领域中有着广泛的应用。
局部线性嵌入(LLE)
局部线性嵌入(Locally Linear Embedding,LLE)是一种常用的流形学习方法,用于将高维数据映射到低维流形空间中。其基本思想是通过在每个数据点周围找到最近邻的样本,并使用线性组合来重构数据点,从而保留原始数据的局部结构。
LLE的实现过程可以概括为以下几个步骤:
  1. 建立模型 设有n个数据点X = {x1,x2,...,xn},我们希望将其映射到低维流形空间Y = {y1,y2,...,yn},其中每个样本yi只有k(k < d)个特征。对于每个数据点xi,我们在它的最近邻集合中寻找权重系数wij,并使用这些系数来建立线性组合关系,使得xi可以被邻域内的其他点线性重构。
  2. 计算权重系数 对于每个数据点xi,我们在其最近邻集中寻找权重系数wij,使得xi可以线性重构为邻域内其他点的线性组合。通过最小化重构误差来计算权重系数,即minimize ||xi - sum(wij*xj)||^2,其中sum(wij)=1。
  3. 计算降维后的表示 通过求解权重系数矩阵W,并使用线性组合的方式计算每个数据点在低维流形空间中的表示,即minimize ||Y - W*Y||^2,其中Y为降维后的表示。
LLE可以帮助我们识别原始数据的局部结构,并在保留其全局拓扑结构的同时进行降维和可视化。它在图像处理、语音处理、生物医学等领域中得到了广泛应用。
等距映射(Isomap)
等距映射(Isomap)是一种常用的流形学习方法,用于将高维数据映射到低维流形空间中。其基本思想是通过计算数据点之间的最短路径来估计它们在低维空间中的距离,并使用多维缩放算法(MDS)来将它们嵌入到低维空间中。
Isomap的实现过程可以概括为以下几个步骤:
  1. 建立模型 设有n个数据点X = {x1,x2,...,xn},我们希望将其映射到低维流形空间Y = {y1,y2,...,yn},其中每个样本yi只有k(k < d)个特征。我们假设原始数据集是由一个非线性流形变换生成的,该流形在低维空间中保持等距性质,即点与点之间的距离应该与它们在流形上的距离相同。
  2. 计算最近邻图 对于每个数据点xi,在其k个最近邻中寻找所有可能的路径,并使用Floyd算法或Dijkstra算法计算出它们之间的最短路径。
  3. 估计距离矩阵 通过最短路径距离计算出数据点之间的距离矩阵D,即D[i,j]表示xi和xj之间的最短路径距离。
  4. 嵌入低维空间 使用多维缩放算法(MDS)将距离矩阵D嵌入到低维空间中,并得到降维后的表示Y。
Isomap可以帮助我们识别原始数据中的等距性质,从而在保留全局拓扑结构的同时进行降维和可视化。它在图像处理、语音处理、生物医学等领域中得到了广泛应用。

特征表示

特征表示(Feature Representation)是指将原始数据转换为一组有意义的特征向量,以便更好地描述和表达数据。在机器学习中,特征表示通常用于提取数据的重要特征并降低数据的维度,从而使数据更易于处理和分析。
特征表示的设计通常基于领域知识和数据结构的理解,包括对数据中存在的模式、结构和相关性等信息的分析和挖掘。常见的特征表示方法包括以下几种:

基于统计的特征表示:通过对数据的统计分析来提取关键特征,例如均值、方差、协方差等。

基于统计的特征表示是一种常见的特征提取方法,它通过对数据进行统计分析来提取代表性特征,该方法通常适用于处理数字数据。常见的基于统计的特征表示方法包括以下几种:
平均值:计算数据集中每个特征的平均值,并作为特征向量的元素。
方差:计算数据集中每个特征的方差,并作为特征向量的元素。
协方差矩阵:计算不同特征之间的协方差,并将其组合成一个矩阵,用于描述特征之间的相关性。
相关系数:计算不同特征之间的相关系数,并将其作为特征向量的元素。
直方图:将数据按照特定的区间划分并统计每个区间内的样本数量,然后将每个区间的样本数量作为特征向量的元素。
主成分分析(PCA):使用线性变换将数据投影到一个新的空间中,使得数据的方差最大化,并选取方差最大的前k个主成分作为特征向量的元素。
基于统计的特征表示方法通常简单、可靠且易于理解,在许多机器学习任务中都有着广泛的应用。但是,它们也具有一些局限性,例如无法捕捉数据的复杂结构和非线性关系等问题。因此,在实际应用中需要根据具体情况选择合适的特征表示方法。

基于频域的特征表示:通过离散傅里叶变换(DFT)或小波变换等方法将数据从时域转换到频域,并提取与问题相关的频率特征。

基于频域的特征表示是指将信号转换到频域,从频谱上提取特征表示。常见的基于频域的特征表示方法包括:
傅里叶变换(Fourier Transform, FT):将时域信号转换到频域,提取频谱能量、频率等特征。
离散傅里叶变换(Discrete Fourier Transform, DFT):对离散信号进行傅里叶变换。
短时傅里叶变换(Short
-time Fourier Transform, STFT):将长时间信号分割为短时间窗口,在每个窗口上进行傅里叶变换,提取时间-频率特征。
小波变换(Wavelet Transform, WT):使用小波基函数将信号分解成不同尺度和不同频率的子带,提取多尺度特征。
这些方法在音乐、语音识别、图像处理等领域都有广泛应用。

基于图像处理的特征表示:利用边缘检测、纹理分析、形态学处理等算法提取图像中的特征,例如边缘、角点、纹理等。

基于图像处理的特征表示是指对图像进行预处理和特征提取,将图像转换为可计算的特征向量。常见的基于图像处理的特征表示方法包括:
边缘检测:使用Canny、Sobel等边缘检测算法,提取图像轮廓信息。
尺度不变特征变换(Scale
-Invariant Feature Transform, SIFT):提取图像中的关键点,并通过局部自适应方向直方图描述其方向和尺度特征。
颜色直方图:统计图像颜色分布情况,提取颜色特征。
卷积神经网络(Convolutional Neural Network, CNN):通过多层卷积和池化操作,提取图像的视觉特征。
这些方法在图像识别、目标检测、人脸识别等领域都有广泛应用。

基于深度学习的特征表示:使用深度神经网络(DNN)等方法自动学习数据中的高层次特征表示,例如卷积神经网络(CNN)、循环神经网络(RNN)等。

基于深度学习的特征表示是指使用深度神经网络从原始数据中学习高层次的抽象特征表示。常见的基于深度学习的特征表示方法包括:
卷积神经网络(Convolutional Neural Network, CNN):通过多层卷积和池化操作,提取图像、视频等数据的视觉特征。
循环神经网络(Recurrent Neural Network, RNN):在处理序列数据时,可以使用RNN对序列进行建模,提取序列数据的语义特征。
自编码器(Autoencoder, AE):学习输入数据的压缩表示,同时尽量保留重构数据与原始数据的相似度,提取数据的潜在特征。
生成对抗网络(Generative Adversarial Network, GAN): 使用两个对抗的神经网络,一个生成器和一个判别器,在训练过程中生成器逐渐生成接近真实数据分布的样本,提取数据的分布特征。
这些方法在图像识别、自然语言处理、语音识别等领域都有广泛应用。
特征表示的选择和设计对机器学习模型的训练和预测结果有着至关重要的影响。一个优秀的特征表示应该具有更好的可解释性、更高的区分性和更好的鲁棒性,并能够充分捕捉数据的本质特征。

相似度计算

根据用户已经喜欢的物品的特征向量,计算该物品与其他物品的相似度,并按照相似度降序排列,最后将排在前面的若干个物品推荐给用户。
这些相似度和距离的计算方法在机器学习和数据分析中被广泛使用,例如基于内容的推荐算法中常用余弦相似度来计算物品之间的相似度,K近邻算法中常用欧几里得距离或曼哈顿距离来计算样本之间的距离,从而进行分类或聚类等任务。
  • 余弦相似度
用于衡量两个向量方向的差异程度,表示为两个向量的点积除以它们的模长乘积。计算公式为:similarity = (A·B) / (‖A‖ × ‖B‖),其中A和B分别表示需要比较的两个向量,·表示点积运算,|| ||表示向量的模长。
余弦相似度是一种用于衡量两个向量之间相似性的方法,它表示的是两个向量方向的夹角的余弦值,取值范围为-1到1之间。具体来说,如果两个向量的方向相同,则余弦相似度取最大值1;如果两个向量的方向完全相反,则余弦相似度取最小值-1;如果两个向量之间不存在任何关系,则余弦相似度接近于0。
在机器学习和数据分析中,余弦相似度常常用于计算不同样本之间的相似性,例如在基于内容的推荐系统中,可以利用余弦相似度来计算用户对物品的偏好程度,从而进行推荐。此外,在自然语言处理领域中,可以将文本表示为特征向量,然后使用余弦相似度来计算文本之间的相似性,例如对于搜索引擎中的查询文本,可以通过计算其与各篇网页之间的余弦相似度来确定排名等。
需要注意的是,余弦相似度只考虑了向量之间的方向信息,而没有考虑向量的长度或大小差异,因此可能会存在某些局限性。此外,当向量维度较高时,余弦相似度可能受到“维度灾难”的影响,导致相似度计算变得困难。
  • 欧几里得距离
用于衡量两个向量之间的空间距离,计算公式为:distance = √(Σ(xi - yi)^2),其中xi和yi分别表示两个向量在第i个维度上的坐标,Σ表示对所有维度的坐标做加和,√表示开根号。
欧几里得距离是指在数学中,两个n维向量之间的距离。具体来说,在二维或三维空间中,欧几里得距离表示为每个维度上差值的平方和再开平方根,如下所示:
d(p,q) = sqrt((p1 - q1)^2 + (p2 - q2)^2 + ... + (pn - qn)^2)
其中,p和q是包含n个元素的向量,pi和qi表示第i个元素的取值。
在实际应用中,欧几里得距离可以用于衡量不同样本之间的相似度或者差异性。例如,可以利用欧几里得距离来计算两张图片、两段音频或者两份文本之间的相似程度。当欧几里得距离较小时,表明两个向量之间的差异较小,相似度较高;当欧几里得距离较大时,则表示两个向量之间的差异较大,相似度较低。
  • 曼哈顿距离
也称为城市街区距离,用于衡量两个向量之间沿坐标轴方向的距离总和,计算公式为:distance = Σ| xi - yi |,其中xi和yi分别表示两个向量在第i个维度上的坐标,Σ表示对所有维度的坐标做加和,| |表示取绝对值。
曼哈顿距离是两个点在平面直角坐标系中的距离,即两点之间沿着网格线走的最短距离。它得名自纽约曼哈顿的街网规划,因为该城市的街道网格设计使得这种距离计算非常方便。在二维空间中,曼哈顿距离可以通过两点在横轴和纵轴上的坐标差的绝对值之和来计算。例如,如果一个人从 (1, 2) 要走到 (4, 6),那么他沿曼哈顿路线所需的步数就是 |4-1|+|6-2|=7 步。

推荐结果排序

推荐结果排序可以根据多种算法和指标进行,下面是几种常见的排序方式:
  1. 基于关键词匹配度的排序:将用户查询中的关键词与推荐结果进行匹配,并按照匹配度高低进行排序。
  2. 基于内容相似度的排序:利用自然语言处理技术对推荐结果进行文本相似度计算,将与用户查询最相关的结果排在前面。
  3. 基于用户历史行为的排序:通过分析用户过去的搜索和点击记录,推测用户的兴趣偏好,并向其推荐相关内容。
  4. 基于社交网络的排序:结合用户在社交网络中的关注、点赞、分享等行为,向其推荐可能感兴趣的内容。
  5. 基于机器学习的排序:使用机器学习算法对用户数据和推荐结果进行建模,预测用户对推荐结果的喜好程度,并按照预测得分进行排序。

pugixml XML格式处理库的介绍和使用(面向业务编程-格式处理)

介绍

pugixml是一个轻量级的C++ XML处理库。它的特点:

  • 类似dom的界面,具有丰富的遍历/修改功能

  • 非常快速的非验证XML解析器

  • 它从XML文件/缓冲区构造DOM树用于复杂数据驱动

  • 支持树查询的XPath 1.0

  • 实现Unicode接口变体和自动编码转换的完整Unicode支持

开源仓库地址:
https://github.com/zeux/pugixml

XML格式介绍

有很多配置文件的格式也是XML格式的,而且有些GUI也可以依据XML规则去执行一些渲染。

  • 结构化强,适合UI表达:类似HTML、XAML也是基于XML的。总之XML是一种结构化很强的语言。
  • 程序处理解析简单:因为有较强的结构化,所以基本上程序解析起来比较简单,不容易出错。
  • 基本单元是标签:有一般标签和自闭合标签,并且可以给标签加各种属性。

注释:XML支持注释,学过HTML的就知道,注释格式如下

<!-- 这是一条注释 -->


<!--
开始,并以
-->
结束

使用方式

引入头文件

pugixml的使用
很简单,只需要引入头文件,然后就可以开始用了

#include "pugixml.hpp"
using namespace pugi;

从XML文件中读取数据

准备XML文件(input.xml)内容如下

<platform>
    <ip>192.168.1.2</ip>
    <port>50000</port>
    <key>keyvalue</key>
</platform>

读取XML文件解析数据

(main.cpp) 如下

int parseXMLfromFile() {
    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_file("input.xml");
    if(!result)
        return -1;
    cout << "parse result: " << endl;
    xml_node root_node = doc.child("platform");
    xml_node ip_node = root_node.child("ip");
    xml_node port_node = root_node.child("port");
    xml_node key_node = root_node.child("key");
    cout << ip_node.text().as_string() << endl;
    cout << port_node.text().as_int() << endl;
    cout << key_node.text().as_string() << endl;
    return 0;
}

我们可以用迭代器访问列表,如下

    for (auto node : doc.child("Document").children("Data")) {
    	#if STRING_TYPE
        string item = node.text().as_string();
        cout << item << endl;
        #else
        int out = node.text().as_int();
        cout << out << endl;
        #endif
    }

XPath树访问

pugixml支持
XPath 1.0
,通过XPath可以快速对节点操作

XPath 是一门在 XML 文档中查找信息的语言,内容较为复杂,这里不展开讲了。有兴趣自行学习

int parseXMLUseXpathfromFile() {
    // 正常读取解析,和上面一样
    pugi::xml_document doc;
    pugi::xml_parse_result result = doc.load_file("input.xml");
    if(!result)
        return -1;
    //使用XPath访问
    pugi::xpath_node key_node = doc.select_node("/platform/key");
    cout << key_node.node().text().as_string() << endl;
    return 0;
}

将数据录入xml文件

int saveXMLtoFile() {
    pugi::xml_document doc;
    pugi::xml_node node_dec = doc.prepend_child(pugi::node_declaration);
    node_dec.append_attribute("version") = "1.0";
    node_dec.append_attribute("encoding") = "utf-8";
    pugi::xml_node node_comm = doc.append_child(pugi::node_comment);
    node_comm.set_value("this is a comment");
    pugi::xml_node root_node = doc.append_child("root");
    pugi::xml_node sub_node = root_node.append_child("key");
    // 添加一个属性
    sub_node.append_attribute("attribute").set_value("attributeValue");
    // 设置空标签内部值
    sub_node.append_child(pugi::node_pcdata).set_value("keyValue");
    doc.save_file("./output.xml");
    return 0;
}

以库的形式添加到项目中

以第三方库的形式添加到项目中

本文为作者原创文章,转载请注明出处:
https://www.cnblogs.com/nbtech/p/use_pugixml_library.html

首先我们下载pugixml源代码

mkdir UsePugixmlProject && cd UsePugixmlProject
git clone https://github.com/zeux/pugixml.git
vim CMakeLists.txt

CMakeLists.txt内容如下

# 下面3行是我们正常一个文件的CMake写法
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(pugixml-test LANGUAGES CXX)
add_executable(xml_test main.cpp)
# 添加pugixml库,1、包含头文件目录;2、添加库的源文件,声明这个库;3、将这个库链接到上面的目标文件中
include_directories(pugixml/src)
add_library(pugixml
    STATIC
    pugixml/src/pugixml.cpp
)
target_link_libraries(xml_test pugixml)

main.cpp文件就是我们上面小节的示例代码

// https://www.cnblogs.com/nbtech/p/use_pugixml_library.html
int main() {
    return parseXMLfromFile();
}

写完之后创建目录并编译

mkdir build && cd build
cmake .. && make

在我们项目中,
用CMake添加pugixml库
操作如上

如果想改成共享库,只需要把CMakeLists.txt的STATIC改成SHARED即可

交叉编译?

有时候我们希望它可以跨平台,那么只需要在cmake配置的时候指定交叉编译工具即可

cmake -D CMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ ..


一、解决bug:Selenium with PhantomJS,重构SeleniumeDownloader底层浏览器驱动

0、小背景:

想爬取外网steam的数据,但是steam官网在海外,加上steam处于反爬考虑,对于异步数据-json数据进行处理,导致如果直接去拿人家的ajax接口作为请求url进行爬取就爬取得到一堆乱码的没用数据。---解决:使用Selenium 模拟用户使用浏览器(通过js渲染),然后再解析处理selenium下载器下载下来的数据。
但是一开始,项目中selenium底层是使用phantomjs 作为驱动器(浏览器),出现了如下的一系列:

1、bug 截图

  • 找不到变量xxx

  • 报错信息:
[ERROR - 2023-03-07T12:00:29.232Z] Session [258893b0-bcdf-11ed-9fc2-3f99c08d4ed8] - page.onError - msg: ReferenceError: Can't find variable: InitMiniprofileHovers

  phantomjs://platform/console++.js:263 in error
[ERROR - 2023-03-07T12:00:29.233Z] Session [258893b0-bcdf-11ed-9fc2-3f99c08d4ed8] - page.onError - stack:
  global code (https://store.steampowered.com/charts/topselling/SG:671)

  phantomjs://platform/console++.js:263 in error
[ERROR - 2023-03-07T12:00:30.743Z] Session [258893b0-bcdf-11ed-9fc2-3f99c08d4ed8] - page.onError - msg: ReferenceError: Can't find variable: WebStorage

  phantomjs://platform/console++.js:263 in error
[ERROR - 2023-03-07T12:00:30.744Z] Session [258893b0-bcdf-11ed-9fc2-3f99c08d4ed8] - page.onError - stack:
  (anonymous function) (https://store.st.dl.eccdnx.com/public/shared/javascript/shared_responsive_adapter.js?v=TNYlyRmh1mUl&l=schinese&_cdn=china_eccdnx:43)
  l (https://store.st.dl.eccdnx.com/public/shared/javascript/jquery-1.8.3.min.js?v=.TZ2NKhB-nliU&_cdn=china_eccdnx:2)
  fireWith (https://store.st.dl.eccdnx.com/public/shared/javascript/jquery-1.8.3.min.js?v=.TZ2NKhB-nliU&_cdn=china_eccdnx:2)
  ready (https://store.st.dl.eccdnx.com/public/shared/javascript/jquery-1.8.3.min.js?v=.TZ2NKhB-nliU&_cdn=china_eccdnx:2)
  A (https://store.st.dl.eccdnx.com/public/shared/javascript/jquery-1.8.3.min.js?v=.TZ2NKhB-nliU&_cdn=china_eccdnx:2)

  phantomjs://platform/console++.js:263 in error
[ERROR - 2023-03-07T12:00:30.746Z] Session [258893b0-bcdf-11ed-9fc2-3f99c08d4ed8] - page.onError - msg: ReferenceError: Can't find variable: GetNavCookie

2、待爬取的页面是存在该变量的:InitMiniprofileHovers、GetNavCookie

3、调试-核心步骤

  • 断点入口
Page page = downloader.download(request, this);//爬虫任务的下载器,开始下载页面
  • SeleniumDownloader
//获取到web驱动器
webDriver = webDriverPool.get();
//驱动器下载页面
webDriver.get(request.getUrl());//这里出错

▪ webDriver变量的情况:

  • RemoteWebDriver
 this.execute("get", ImmutableMap.of("url", url));//执行下载命令

response = this.executor.execute(command);//响应体,即执行命令后的结果
//command 只是一个封装了sessionId, driverCommand-get, 请求参数url的对象
  • PhantomJSCommandExecutor
Response var2 = super.execute(command);

...


4、分析错误原因:

报错原因:是phantomis设计的不够合理: 在页面寻找不到dom元素的时候,合理设计应该返回nul,而不应该throw异常。

网友的错误原因--加密方式,理由:PhantomJS使用的加密方式是SSLv3,有些网站用的是TLS。

解决加密问题的方法:--ignore-ssl-errors=true 和 --ssl-protocol=any

▷ 自己的项目中的 web驱动器/浏览器(排除加密方式的原因):

5、小心得:

phantomjs在对ES6的支持上天生有坑,前端使用ES6的网站都不建议用phantomis去跑。

6、解决:使用 chrome 代替 PhantomJS

7、新的问题:chrome 解析外网的时候,不稳定

  • 解决---vpn
  • 现在思路就变成了Selenium 在调用浏览器 chrome 的时候,开vpn,默认集成到 Selenium中的浏览器,都是普通纯净的浏览器。

发现微软的浏览器Edge 打开steam 官网,不开vpn,也很流畅,不过要是steam的链接带上地理位置,例如香港,又打不开了,解决:vpn



二、改写Selenium的浏览器-目的为了添加代理

1、基本思路:先理清业务的逻辑

发现,在项目调用完爬虫框架的调度器后,下载器开始发挥作用。

case CHROME:
                    if (isWindows) {
                        System.setProperty("selenuim_config", "C:\\data\\config\\config-chrome.ini");
                        SeleniumDownloader seleniumDownloader = new SeleniumDownloader("C:\\data\\config\\chromedriver.exe");
                        // 浏览器打开10s后才开始爬取数据
                        seleniumDownloader.setSleepTime(10 * 1000);
                        autoSpider.setDownloader(seleniumDownloader);
                    }

业务中,我们是通过了创建了SeleniumDownloader的下载器来下载页面,但是确定就是底层的浏览器是纯净普通版的浏览器。

看到业务在创建SeleniumDownloader的下载器的时候,给它注入了一个配置文件config-chrome.ini,


2、个人解决思路1:考虑把代理的options 也通过这个配置文件注入

但是发现这个配置文件是一个启动文件,里面并没options的属性可以配置。

启动文件的配置,没法实现


3、个人解决思路2:看看SeleniumDownloader的下载器底层的浏览器驱动池WebDriverPool

是否有暴露给外界什么属性可以配置options,阅读源码后,发现它只暴露一个属性就是配置启动文件config-chrome.ini。

public void configure() throws IOException {
		// Read config file
		sConfig = new Properties();
		String configFile = DEFAULT_CONFIG_FILE;
		if (System.getProperty("selenuim_config")!=null){
			configFile = System.getProperty("selenuim_config");
		}
		sConfig.load(new FileReader(configFile));

		// Prepare capabilities
		sCaps = new DesiredCapabilities();
		sCaps.setJavascriptEnabled(true);
		sCaps.setCapability("takesScreenshot", false);

		String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS);

		// Fetch PhantomJS-specific configuration parameters
		......
}


4、个人解决思路3:重写底层的浏览器驱动池WebDriverPool,然后再重写一个调用该WebDriverPool的下载器

下载器和驱动器管理池都是在官网提供的源码的基础进行修改;

SeleniumDownloader2:在SeleniumDownloader基础上新增了代理枚举属性proxyEnum,并使用了自己重写的浏览器驱动池WebDriverPool2

WebDriverPool2:改写了 WebDriverPool的构造器,以及改写了初始化 WebDriver实例的configure方法(目的,就是为了增加上像代理等的options选项)

  • 当然,还增加了一个轮询方法incrForLoop,目的就是为了获得代理列表的索引

■ WebDriverPool2:

  • 用省略号表示代理和官网的是一摸一样的!

  • 细节: ChromeOptions需要设置ssl协议(官网给出的demo没加,导致我开vpn一直没成功,又没提示.....)

    ​ 分析和解决:因为https=http+ssl/tls,我们通过浏览器访问的时候,浏览器会把所有url地址都处理成安全通信协议,所以代码中需要配置ssl协议

public class WebDriverPool2 {
	......
    /** 代理枚举参数 */
    private final ProxyEnum proxyEnum;
    /** 代理列表 */
    private List<String> proxies;
    /** ip代理列表的索引 */
    private final AtomicInteger pointer = new AtomicInteger(-1);
	......
        
    /**
     * 初始化一个 WebDriver 实例
     * @throws IOException 异常
     */
    public void configure() throws IOException {
       ......
        if (isUrl(driver)) {
            sCaps.setBrowserName("phantomjs");
            mDriver = new RemoteWebDriver(new URL(driver), sCaps);
        } else if (driver.equals(DRIVER_FIREFOX)) {
            mDriver = new FirefoxDriver(sCaps);
        } else if (driver.equals(DRIVER_CHROME)) {
            if(proxyEnum == ProxyEnum.VPN_ENABLE || proxyEnum == ProxyEnum.PROXY_ENABLE){
                //给谷歌浏览器,添加上ip代理或vpn等options
                ChromeOptions options = new ChromeOptions();
                //禁止加载图片
                options.addArguments("blink-settings=imagesEnabled=false");
                Proxy proxy = new Proxy();
                String httpProxy = proxies.get(incrForLoop());
                // 需要设置ssl协议
                proxy.setHttpProxy(httpProxy).setSslProxy(httpProxy);
                options.setCapability("proxy",proxy);
                sCaps.setCapability(ChromeOptions.CAPABILITY, options);
                logger.info("chrome webDriver proxy is : " + proxy);
            }
            mDriver = new ChromeDriver(sCaps);
        } else if (driver.equals(DRIVER_PHANTOMJS)) {
            mDriver = new PhantomJSDriver(sCaps);
        }
    }


    /**
     * 轮询:从代理列表选出一个代理的索引
     * @return 索引
     */
    private int incrForLoop() {
        int p = pointer.incrementAndGet();
        int size = proxies.size();
        if (p < size) {
            return p;
        }
        while (!pointer.compareAndSet(p, p % size)) {
            p = pointer.get();
        }
        return p % size;
    }

    public WebDriverPool2(int capacity, ProxyEnum proxyEnum, MasterWebservice masterWebservice) {
        this.capacity = capacity;
        //设置代理的情况
        this.proxyEnum = proxyEnum;
        //vpn的情况
        if(proxyEnum == ProxyEnum.VPN_ENABLE){
            this.proxies = masterWebservice.getVpn();
        //ip代理的情况
        }else if(proxyEnum == ProxyEnum.PROXY_ENABLE){
            //获取动态生成的ip列表,带有端口的,参数形式举例 42.177.155.5:75114
            this.proxies = masterWebservice.getProxyIps();
        }
    }

}

■ SeleniumDownloader2:

/**
 * 在SeleniumDownloader基础上新增了代理枚举属性proxyEnum
 * 并且要把官网SeleniumDownloader代码中使用WebDriverPool(实际是使用上咱改写的WebDriverPool2)的方法引入,还有使用到WebDriverPool的方法
 * 中,需要的属性,要注意父类中被设置私有,需要重写一下(从父类copy到子类就行啦)
 */
public class SeleniumDownloader2 extends SeleniumDownloader {
    private volatile WebDriverPool2 webDriverPool;
    
    /** 代理枚举参数 */
    private ProxyEnum proxyEnum;
    /** 通过masterWebservice获得远程的动态ip列表 */
    private MasterWebservice masterWebservice;
    
    public SeleniumDownloader2(String chromeDriverPath, ProxyEnum proxyEnum, MasterWebservice masterWebservice) {
        System.getProperties().setProperty("webdriver.chrome.driver",
                chromeDriverPath);
        this.proxyEnum = proxyEnum;
        this.masterWebservice = masterWebservice;
    }
    
    ......
}


■ seleniume 包下的下载器和浏览器如下:

■ 官网提供的WebDriverPool:

package us.codecraft.webmagic.downloader.selenium;

import org.apache.log4j.Logger;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author code4crafter@gmail.com <br>
 *         Date: 13-7-26 <br>
 *         Time: 下午1:41 <br>
 */
class WebDriverPool {
	private Logger logger = Logger.getLogger(getClass());

	private final static int DEFAULT_CAPACITY = 5;

	private final int capacity;

	private final static int STAT_RUNNING = 1;

	private final static int STAT_CLODED = 2;

	private AtomicInteger stat = new AtomicInteger(STAT_RUNNING);

	/*
	 * new fields for configuring phantomJS
	 */
	private WebDriver mDriver = null;
	private boolean mAutoQuitDriver = true;

	private static final String DEFAULT_CONFIG_FILE = "/data/webmagic/webmagic-selenium/config.ini";
	private static final String DRIVER_FIREFOX = "firefox";
	private static final String DRIVER_CHROME = "chrome";
	private static final String DRIVER_PHANTOMJS = "phantomjs";

	protected static Properties sConfig;
	protected static DesiredCapabilities sCaps;

	/**
	 * Configure the GhostDriver, and initialize a WebDriver instance. This part
	 * of code comes from GhostDriver.
	 * https://github.com/detro/ghostdriver/tree/master/test/java/src/test/java/ghostdriver
	 * 
	 * @author bob.li.0718@gmail.com
	 * @throws IOException
	 */
	public void configure() throws IOException {
		// Read config file
		sConfig = new Properties();
		String configFile = DEFAULT_CONFIG_FILE;
		if (System.getProperty("selenuim_config")!=null){
			configFile = System.getProperty("selenuim_config");
		}
		sConfig.load(new FileReader(configFile));

		// Prepare capabilities
		sCaps = new DesiredCapabilities();
		sCaps.setJavascriptEnabled(true);
		sCaps.setCapability("takesScreenshot", false);

		String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS);

		// Fetch PhantomJS-specific configuration parameters
		if (driver.equals(DRIVER_PHANTOMJS)) {
			// "phantomjs_exec_path"
			if (sConfig.getProperty("phantomjs_exec_path") != null) {
				sCaps.setCapability(
						PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY,
						sConfig.getProperty("phantomjs_exec_path"));
			} else {
				throw new IOException(
						String.format(
								"Property '%s' not set!",
								PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY));
			}
			// "phantomjs_driver_path"
			if (sConfig.getProperty("phantomjs_driver_path") != null) {
				System.out.println("Test will use an external GhostDriver");
				sCaps.setCapability(
						PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY,
						sConfig.getProperty("phantomjs_driver_path"));
			} else {
				System.out
						.println("Test will use PhantomJS internal GhostDriver");
			}
		}

		// Disable "web-security", enable all possible "ssl-protocols" and
		// "ignore-ssl-errors" for PhantomJSDriver
		// sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, new
		// String[] {
		// "--web-security=false",
		// "--ssl-protocol=any",
		// "--ignore-ssl-errors=true"
		// });

		ArrayList<String> cliArgsCap = new ArrayList<String>();
		cliArgsCap.add("--web-security=false");
		cliArgsCap.add("--ssl-protocol=any");
		cliArgsCap.add("--ignore-ssl-errors=true");
		sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS,
				cliArgsCap);

		// Control LogLevel for GhostDriver, via CLI arguments
		sCaps.setCapability(
				PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_CLI_ARGS,
				new String[] { "--logLevel="
						+ (sConfig.getProperty("phantomjs_driver_loglevel") != null ? sConfig
								.getProperty("phantomjs_driver_loglevel")
								: "INFO") });

		// String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS);

		// Start appropriate Driver
		if (isUrl(driver)) {
			sCaps.setBrowserName("phantomjs");
			mDriver = new RemoteWebDriver(new URL(driver), sCaps);
		} else if (driver.equals(DRIVER_FIREFOX)) {
			mDriver = new FirefoxDriver(sCaps);
		} else if (driver.equals(DRIVER_CHROME)) {
			mDriver = new ChromeDriver(sCaps);
		} else if (driver.equals(DRIVER_PHANTOMJS)) {
			mDriver = new PhantomJSDriver(sCaps);
		}
	}

	/**
	 * check whether input is a valid URL
	 * 
	 * @author bob.li.0718@gmail.com
	 * @param urlString urlString
	 * @return true means yes, otherwise no.
	 */
	private boolean isUrl(String urlString) {
		try {
			new URL(urlString);
			return true;
		} catch (MalformedURLException mue) {
			return false;
		}
	}

	/**
	 * store webDrivers created
	 */
	private List<WebDriver> webDriverList = Collections
			.synchronizedList(new ArrayList<WebDriver>());

	/**
	 * store webDrivers available
	 */
	private BlockingDeque<WebDriver> innerQueue = new LinkedBlockingDeque<WebDriver>();

	public WebDriverPool(int capacity) {
		this.capacity = capacity;
	}

	public WebDriverPool() {
		this(DEFAULT_CAPACITY);
	}

	/**
	 * 
	 * @return
	 * @throws InterruptedException
	 */
	public WebDriver get() throws InterruptedException {
		checkRunning();
		WebDriver poll = innerQueue.poll();
		if (poll != null) {
			return poll;
		}
		if (webDriverList.size() < capacity) {
			synchronized (webDriverList) {
				if (webDriverList.size() < capacity) {

					// add new WebDriver instance into pool
					try {
						configure();
						innerQueue.add(mDriver);
						webDriverList.add(mDriver);
					} catch (IOException e) {
						e.printStackTrace();
					}

					// ChromeDriver e = new ChromeDriver();
					// WebDriver e = getWebDriver();
					// innerQueue.add(e);
					// webDriverList.add(e);
				}
			}

		}
		return innerQueue.take();
	}

	public void returnToPool(WebDriver webDriver) {
		checkRunning();
		innerQueue.add(webDriver);
	}

	protected void checkRunning() {
		if (!stat.compareAndSet(STAT_RUNNING, STAT_RUNNING)) {
			throw new IllegalStateException("Already closed!");
		}
	}

	public void closeAll() {
		boolean b = stat.compareAndSet(STAT_RUNNING, STAT_CLODED);
		if (!b) {
			throw new IllegalStateException("Already closed!");
		}
		for (WebDriver webDriver : webDriverList) {
			logger.info("Quit webDriver" + webDriver);
			webDriver.quit();
			webDriver = null;
		}
	}

}

■ 官网提供的SeleniumDownloader:

package us.codecraft.webmagic.downloader.selenium;

import org.apache.log4j.Logger;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.downloader.Downloader;
import us.codecraft.webmagic.selector.Html;
import us.codecraft.webmagic.selector.PlainText;

import java.io.Closeable;
import java.io.IOException;
import java.util.Map;

/**
 * 使用Selenium调用浏览器进行渲染。目前仅支持chrome。<br>
 * 需要下载Selenium driver支持。<br>
 *
 * @author code4crafter@gmail.com <br>
 *         Date: 13-7-26 <br>
 *         Time: 下午1:37 <br>
 */
public class SeleniumDownloader implements Downloader, Closeable {

	private volatile WebDriverPool webDriverPool;

	private Logger logger = Logger.getLogger(getClass());

	private int sleepTime = 0;

	private int poolSize = 1;

	private static final String DRIVER_PHANTOMJS = "phantomjs";

	/**
	 * 新建
	 *
	 * @param chromeDriverPath chromeDriverPath
	 */
	public SeleniumDownloader(String chromeDriverPath) {
		System.getProperties().setProperty("webdriver.chrome.driver",
				chromeDriverPath);
	}

	/**
	 * Constructor without any filed. Construct PhantomJS browser
	 * 
	 * @author bob.li.0718@gmail.com
	 */
	public SeleniumDownloader() {
		// System.setProperty("phantomjs.binary.path",
		// "/Users/Bingo/Downloads/phantomjs-1.9.7-macosx/bin/phantomjs");
	}

	/**
	 * set sleep time to wait until load success
	 *
	 * @param sleepTime sleepTime
	 * @return this
	 */
	public SeleniumDownloader setSleepTime(int sleepTime) {
		this.sleepTime = sleepTime;
		return this;
	}

	@Override
	public Page download(Request request, Task task) {
		checkInit();
		WebDriver webDriver;
		try {
			webDriver = webDriverPool.get();
		} catch (InterruptedException e) {
			logger.warn("interrupted", e);
			return null;
		}
		logger.info("downloading page " + request.getUrl());
		webDriver.get(request.getUrl());
		try {
			Thread.sleep(sleepTime);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		WebDriver.Options manage = webDriver.manage();
		Site site = task.getSite();
		if (site.getCookies() != null) {
			for (Map.Entry<String, String> cookieEntry : site.getCookies()
					.entrySet()) {
				Cookie cookie = new Cookie(cookieEntry.getKey(),
						cookieEntry.getValue());
				manage.addCookie(cookie);
			}
		}

		/*
		 * TODO You can add mouse event or other processes
		 * 
		 * @author: bob.li.0718@gmail.com
		 */

		WebElement webElement = webDriver.findElement(By.xpath("/html"));
		String content = webElement.getAttribute("outerHTML");
		Page page = new Page();
		page.setRawText(content);
		page.setHtml(new Html(content, request.getUrl()));
		page.setUrl(new PlainText(request.getUrl()));
		page.setRequest(request);
		webDriverPool.returnToPool(webDriver);
		return page;
	}

	private void checkInit() {
		if (webDriverPool == null) {
			synchronized (this) {
				webDriverPool = new WebDriverPool(poolSize);
			}
		}
	}

	@Override
	public void setThread(int thread) {
		this.poolSize = thread;
	}

	@Override
	public void close() throws IOException {
		webDriverPool.closeAll();
	}
}



三、关于Selenium 的介绍

0、官网参考资料:

1、Selenium 是什么

Selenium 是
Web的自动化测试工具,可以模拟用户与浏览器交互,进行访问网站。

Selenium是一个浏览器自动化的大型项目。

它提供用于
模拟用户与浏览器交互
的扩展、用于扩展浏览器分配的分发服务器,以及用于实现W3C WebDriver 规范的基础结构,使您可以为所有主要 Web 浏览器编写可互换的代码。Selenium 的核心是
WebDriver
,它是一个编写指令集的接口,可以在许多浏览器中互换运行。

2、Selenium 作用:

自动化测试:自动化测试工具,可以模拟用户与浏览器交互,进行访问网站。

爬虫:因为Selenium可以控制浏览器发送请求,并获取网页数据,因此可以应用于爬虫领域。

Selenium 可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。

3、Selenium 实际情况

Selenium是一个Web的自动化测试工具,最初是为网站自动化测试而开发的,Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器。

Selenium 自己不带浏览器,不支持浏览器的功能,它需要与第三方浏览器结合在一起才能使用。

■ 主流的浏览器驱动WebDriver:PhantomJS、chromedriver

▪ PhantomJS:

PhantomJS 是一个基于Webkit的“
无界面
”(headless)浏览器,它会把网站加载到内存并执行页面上的 JavaScript,因为不会展示图形界面,所以运行起来比完整的浏览器要高效。

如果我们把 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,这个爬虫可以处理 JavaScrip、Cookie、headers,以及任何我们真实用户需要做的事情。

▪ chromedriver:

注意 :chromedriver的版本要与你使用的chrome版本对应!

chromedriver版本	  支持的Chrome版本
v2.46				v71-73
v2.45				v70-72
v2.44				v69-71
v2.43				v69-71
v2.42				v68-70
v2.41				v67-69
v2.40				v66-68
v2.39				v66-68
v2.38				v65-67
v2.37				v64-66
v2.36				v63-65
v2.35				v62-64
v2.34				v61-63
v2.33				v60-62
v2.32				v59-61
v2.31				v58-60
v2.30				v58-60
v2.29				v56-58
v2.28				v55-57
v2.27				v54-56
v2.26				v53-55
v2.25				v53-55
v2.24				v52-54
v2.23				v51-53
v2.22				v49-52
v2.21				v46-50
v2.20				v43-48
v2.19				v43-47
v2.18				v43-46
v2.17				v42-43
v2.13				v42-45
v2.15				v40-43
v2.14				v39-42
v2.13				v38-41
v2.12				v36-40
v2.11				v36-40
v2.10				v33-36
v2.9				v31-34
v2.8				v30-33
v2.7				v30-33
v2.6				v29-32
v2.5				v29-32
v2.4				v29-32

4、Selenium+chromedriver 的使用:

(1) 准备工作:

Selenium:导入依赖包

chromedriver:看着你电脑的谷歌浏览器版本,下载对应的chromedriver 驱动包

(2) 使用:

public class FirstScriptTest {

    @Test
    public void eightComponents() {
        //通过DesiredCapabilities、options 可以给driver 配置一个选项,例如代理,禁止加载图片、去掉界面模式等
        //参考:ChromeDriver:https://sites.google.com/a/chromium.org/chromedriver/capabilities
        String downloadsPath = "d:\\data\\downloads";
		HashMap<String, Object> chromePrefs = new HashMap<String, Object>();
		chromePrefs.put("download.default_directory", downloadsPath);
		ChromeOptions options = new ChromeOptions();
		Proxy proxy = new Proxy();
		// 需要增加设置ssl协议
		proxy.setHttpProxy(VpnServerUtils.getVpnServer()).setSslProxy(VpnServerUtils.getVpnServer());
//		proxy.setHttpProxy(VpnServerUtils.getVpnServer());
		options.setCapability("proxy",proxy);
		System.out.println("~~~~~~~~~~~~~~~~~proxy: " + proxy.getHttpProxy());
		options.setExperimentalOption("prefs", chromePrefs);
		DesiredCapabilities caps = new DesiredCapabilities();
		caps.setCapability(ChromeOptions.CAPABILITY, options);
        
        WebDriver driver = new ChromeDriver(caps);
        //浏览器驱动器请求加载页面
        driver.get("https://www.selenium.dev/selenium/web/web-form.html");
		
        //查找元素
        String title = driver.getTitle();
        assertEquals("Web form", title);

        driver.manage().timeouts().implicitlyWait(Duration.ofMillis(500));

        WebElement textBox = driver.findElement(By.name("my-text"));
        WebElement submitButton = driver.findElement(By.cssSelector("button"));

        textBox.sendKeys("Selenium");
        submitButton.click();//点击事件

        WebElement message = driver.findElement(By.id("message"));
        String value = message.getText();
        assertEquals("Received!", value);
	    //结束会话
        driver.quit();
    }
}




如果本文对你有帮助的话记得给一乐点个赞哦,感谢!