Java 数据科学(一)
零、前言
在本书中,我们研究了数据科学领域中基于 Java 的方法。数据科学是一个广泛的主题,包括数据挖掘、统计分析、音频和视频分析以及文本分析等子主题。许多 Java APIs 都支持这些主题。应用这些特定技术的能力允许创建新的、创新的应用程序,这些应用程序能够处理可用于分析的大量数据。
这本书对数据科学的各个方面采取了广泛而粗略的方法。第一章简要介绍了这一领域。后续章节涵盖数据科学的重要方面,如数据清洗和神经网络的应用。最后一章结合了整本书讨论的主题,以创建一个全面的数据科学应用。
这本书涵盖了什么
第 1 章,数据科学入门,介绍了本书涵盖的技术。给出了每种技术的简要说明,随后是 Java 提供的支持的简短概述和演示。
第 2 章、数据采集,演示了如何从多个来源采集数据,包括 Twitter、Wikipedia 和 YouTube。数据科学应用的第一步是获取数据。
第 3 章、数据清理解释了一旦获得数据,就需要对其进行清理。这可能涉及删除停用词、验证数据和数据转换等活动。
第 4 章、数据可视化表明,虽然数值处理是许多数据科学任务中的关键步骤,但人们通常更喜欢分析结果的可视化描述。本章演示了完成这项任务的各种 Java 方法。
第 5 章,统计数据分析技术,回顾基本的统计技术,包括回归分析,并演示各种 Java APIs 如何提供统计支持。统计分析是许多数据分析任务的关键。
第六章、*、*机器学习,涵盖了几种机器学习算法,包括决策树和支持向量机。大量的可用数据为应用机器学习技术提供了机会。
第 7 章、神经网络,解释了神经网络可以应用于解决各种数据科学问题。在这一章中,我们将解释它们是如何工作的,并演示几种不同类型的神经网络的使用。
第八章、深度学习表明深度学习算法通常被描述为多级神经网络。Java 在这方面提供了重要的支持,我们将举例说明这种方法的使用。
第九章、文本分析解释了的可用数据集的很大一部分以文本格式存在。自然语言处理领域已经取得了相当大的进步,并且经常用于数据科学应用中。我们展示了用于支持这种类型分析的各种 Java APIs。
第十章、*、*视觉和听觉分析告诉我们,数据科学并不局限于文本处理。许多社交媒体网站广泛使用视觉数据。本章说明了可用于此类分析的 Java 支持。
第 11 章,数据分析的数学和并行技术,研究为低级数学运算提供的支持,以及如何在多处理器环境中支持它们。数据分析的核心是处理和分析大量数字数据的能力。
第 12 章、将所有这些整合在一起,探讨了如何将本书中介绍的各种技术整合起来,以创建数据科学应用。本章从数据采集开始,结合了后续章节中使用的许多技术来构建一个完整的应用程序。
这本书你需要什么
书中的许多例子都使用了 Java 8 的特性。演示了许多 Java APIs,每一个都是在应用之前介绍的。IDE 不是必需的,但却是理想的。
这本书是给谁的
这本书的目标读者是有经验的 Java 程序员,他们有兴趣更好地理解数据科学领域以及 Java 如何支持底层技术。不需要该领域的先前经验。
习俗
在这本书里,你会发现许多区分不同种类信息的文本样式。下面是这些风格的一些例子和它们的含义的解释。
文本中的代码如下所示:“getResult方法返回一个保存处理结果的SpeechResult实例。”数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:“KevinVoiceDirectory包含两种声音:kevin和kevin16”
代码块设置如下:
Voice[] voices = voiceManager.getVoices();
for (Voice v : voices) {
out.println(v);
}
任何命令行输入或输出都按如下方式编写:
Name: kevin16
Description: default 16-bit diphone voice
Organization: cmu
Age: YOUNGER_ADULT
Gender: MALE
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中看到的单词,会出现在文本中,如下所示:“选择 Images 类别,然后过滤 Labeled for reuse 。”
注意
警告或重要提示出现在这样的框中。
Tip
提示和技巧是这样出现的。
读者反馈
我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们来说很重要,因为它有助于我们开发出真正让你受益匪浅的图书。要给我们发送总体反馈,只需发送电子邮件feedback@packtpub.com,并在邮件主题中提及书名。如果有一个你擅长的主题,并且你有兴趣写一本书或者为一本书投稿,请在www.packtpub.com/authors查看我们的作者指南。
客户支持
既然您已经是 Packt book 的骄傲拥有者,我们有许多东西可以帮助您从购买中获得最大收益。
下载示例代码
你可以从你在www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 的并注册,让文件直接通过电子邮件发送给你。
您可以按照以下步骤下载代码文件:
- 使用您的电子邮件地址和密码登录或注册我们的网站。
- 将鼠标指针悬停在顶部的
SUPPORT标签上。 - 点击
Code Downloads & Errata。 - 在
Search框中输入书名。 - 选择您要下载代码文件的书。
- 从下拉菜单中选择您购买这本书的地方。
- 点击
Code Download。
下载文件后,请确保使用最新版本的解压缩或解压文件夹:
- WinRAR / 7-Zip for Windows
- 适用于 Mac 的 Zipeg / iZip / UnRarX
- 用于 Linux 的 7-Zip / PeaZip
这本书的代码包也托管在 GitHub 上,地址是github.com/PacktPublis…T2【Java-for-Data-Science】T3。我们在 github.com/PacktPublis…](github.com/PacktPublis…)
勘误表
尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。如果您在我们的某本书中发现了一个错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。这样做,你可以让其他读者免受挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误表,请访问www.packtpub.com/submit-erra…,选择您的图书,点击 Errata Submission Form 链接,并输入勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,该勘误表将被上传到我们的网站或添加到该标题的勘误表部分下的任何现有勘误表列表中。
要查看之前提交的勘误表,请前往www.packtpub.com/books/conte…,在搜索栏中输入图书名称。所需信息将出现在 Errata 部分。
盗版
互联网上版权材料的盗版是所有媒体都存在的问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现我们作品的任何形式的非法拷贝,请立即向我们提供地址或网站名称,以便我们采取补救措施。
请通过copyright@packtpub.com联系我们,并提供可疑盗版材料的链接。
我们感谢您帮助保护我们的作者,以及我们为您带来有价值内容的能力。
问题
如果您对本书的任何方面有问题,可以通过questions@packtpub.com联系我们,我们将尽最大努力解决问题。
一、数据科学入门
数据科学不是一门单一的科学,而是为了分析数据而集成的各种科学学科的集合。这些学科包括各种统计和数学技术,包括:
- 计算机科学
- 数据工程
- 形象化
- 特定领域的知识和方法
随着更便宜的存储技术的出现,越来越多的数据被收集和存储,允许以前不可行的数据处理和分析。随着这种分析,需要各种技术来理解数据。这些大型数据集在用于分析数据并识别趋势和模式时,被称为大数据。
这反过来又催生了云计算和并发技术,如 **map-reduce、**将分析过程分布在大量处理器上,充分利用了并行处理的能力。
分析大数据的过程并不简单,并且演变为被称为数据科学家的开发人员的专业化。利用无数的技术和专业知识,他们能够分析数据来解决以前没有预见到或难以解决的问题。
早期的大数据应用以搜索引擎的出现为代表,这些搜索引擎比它们的前辈能够进行更强大和更准确的搜索。例如, AltaVista 是早期流行的搜索引擎,最终被谷歌取代。虽然大数据应用不仅限于这些搜索引擎功能,但这些应用为未来的大数据工作奠定了基础。
数据科学这一术语自 1974 年开始使用,并随着时间的推移发展到包括数据的统计分析。数据挖掘和数据分析的概念已经与数据科学联系在一起。大约在 2008 年,数据科学家一词出现,用来描述执行数据分析的人。关于数据科学历史的更深入的讨论可以在http://www . Forbes . com/sites/Gil press/2013/05/28/a-very-short-history-of-data-science/# 3d 9 ea 08369 FD找到。
这本书旨在用 Java 对数据科学有一个广泛的了解,并将简要涉及许多主题。读者可能会找到感兴趣的话题,并独立地更深入地探讨这些话题。然而,本书的目的只是向读者介绍重要的数据科学主题,并说明如何使用 Java 解决这些问题。
数据科学中使用了很多算法。在本书中,我们不试图解释它们是如何工作的,除非是在一个介绍性的水平上。相反,我们更感兴趣的是解释它们如何被用来解决问题。具体来说,我们有兴趣知道它们如何与 Java 一起使用。
利用数据科学解决问题
我们将展示的各种数据科学技术已经被用来解决各种问题。这些技术中有许多是为了获得一些经济利益,但是它们也被用来解决许多紧迫的社会和环境问题。使用这些技术的问题领域包括金融、优化业务流程、了解客户需求、执行 DNA 分析、挫败恐怖阴谋、发现交易之间的关系以检测欺诈,以及许多其他数据密集型问题。
数据挖掘是数据科学的一个热门应用领域。在这项活动中,大量的数据被处理和分析,以收集关于数据集的信息,提供有意义的见解,并得出有意义的结论和预测。它已被用于分析客户行为,检测看似不相关的事件之间的关系,并对未来行为做出预测。
机器学习是数据科学的一个重要方面。这种技术允许计算机解决各种问题,而不需要显式编程。它已经被用于自动驾驶汽车、语音识别和网络搜索。在数据挖掘中,数据被提取和处理。通过机器学习,计算机使用数据采取某种行动。
了解数据科学问题解决方法
数据科学涉及对大量数据的处理和分析,以创建可用于预测或支持特定目标的模型。这个过程通常涉及模型的建立和训练。解决问题的具体方法取决于问题的性质。但是,一般来说,以下是分析过程中使用的高级任务:
-
获取数据:在我们处理数据之前,必须先获取数据。数据经常以各种格式存储,并且将来自广泛的数据源。
-
清理数据:数据一旦被采集,往往需要转换成不同的格式才能使用。此外,还需要对数据进行处理或清理,以便消除错误、解决不一致的地方,或者将数据转换成可供分析的形式。
-
Analyzing the data: This can be performed using a number of techniques including:
-
统计分析:这使用多种统计方法来提供对数据的洞察。它包括简单的技术和更高级的技术,如回归分析。
-
AI analysis: These can be grouped as machine learning, neural networks, and deep learning techniques:
- 机器学习方法的特点是程序可以学习,而无需专门编程来完成特定任务
- 神经网络是围绕模仿大脑神经连接的模型建立的
- 深度学习试图在一组数据中识别更高层次的抽象
-
文本分析:这是一种常见的分析形式,它与自然语言一起识别特征,如人名和地名、文本各部分之间的关系以及文本的隐含意义。
-
数据可视化:这是一个重要的分析工具。通过以视觉形式显示数据,一组难以理解的数字可以更容易理解。
-
视频、图像和音频处理和分析:这是一种更专业的分析形式,随着更好的分析技术的发现和更快的处理器的出现,这种形式变得越来越普遍。这与更常见的文本处理和分析任务形成对比。
-
开发高效的应用程序是对这组任务的补充。采用多处理器和 GPU 的机器极大地促进了最终结果。
虽然使用的确切步骤会因应用程序而异,但了解这些基本步骤为构建许多数据科学问题的解决方案提供了基础。
使用 Java 支持数据科学
Java 及其相关的第三方库为数据科学应用程序的开发提供了一系列支持。有许多核心 Java 功能可以使用,比如基本的字符串处理方法。Java 8 中 lambda 表达式的引入有助于实现构建应用程序的更强大和更具表现力的方法。在后续章节的许多例子中,我们将展示使用 lambda 表达式的替代技术。
为基础数据科学任务提供了充足的支持。这些包括获取数据的多种方式、用于清理数据的库,以及用于自然语言处理和统计分析等任务的多种分析方法。还有无数支持神经网络类型分析的库。
对于数据科学问题,Java 可能是一个非常好的选择。该语言为解决问题提供了面向对象和函数的支持。有一个庞大的开发人员社区可以利用,并且存在多个支持数据科学任务的 API。这些只是为什么应该使用 Java 的几个原因。
本章的剩余部分将概述书中演示的数据科学任务和 Java 支持。每个部分只能提供对主题和可用支持的简要介绍。下一章将更深入地讨论这些话题。
为应用程序获取数据
数据采集是数据分析过程中的一个重要步骤。当数据被获取时,它通常是一种特殊的形式,其内容可能与应用程序的需求不一致或不同。有许多数据来源,可以在互联网上找到。几个例子将在第二章、数据采集中演示。
数据可以以各种格式存储。文本数据的流行格式包括 HTML、逗号分隔值 ( CSV )、 JavaScript 对象符号 ( JSON )和 XML。图像和音频数据以多种格式存储。但是,经常需要将一种数据格式转换成另一种格式,通常是纯文本格式。
例如,JSON(www.JSON.org/)使用包含键值对的花括号块来存储。在以下示例中,显示了 YouTube 结果的一部分:
{
"kind": "youtube#searchResult",
"etag": etag,
"id": {
"kind": string,
"videoId": string,
"channelId": string,
"playlistId": string
},
...
}
使用诸如处理直播流、下载压缩文件以及通过屏幕抓取等技术获取数据,在那里提取网页上的信息。Web 爬行是一种技术,程序检查一系列网页,从一个页面移动到另一个页面,获取它需要的数据。
对于许多流行的媒体网站,需要获得用户 ID 和密码才能访问数据。一种常用的技术是 OAuth,,这是一种开放标准,用于对许多不同网站的用户进行身份验证。该技术委托对服务器资源的访问,并在 HTTPS 上工作。一些公司使用 OAuth 2.0,包括 PayPal、脸书、Twitter 和 Yelp。
清洗数据的重要性和过程
一旦获取了数据,就需要对其进行清理。通常,数据会包含错误、重复条目或不一致。通常需要将其转换为更简单的数据类型,如文本。数据清洗通常被称为数据角力、整形、或蒙骗。它们实际上是同义词。
清理数据时,通常需要执行几项任务,包括检查其有效性、准确性、完整性、一致性和统一性。例如,当数据不完整时,可能需要提供替代值。
考虑 CSV 数据。可以用几种方法中的一种来处理。我们可以使用简单的 Java 技术,比如String class' split方法。在下面的序列中,假定字符串数组csvArray保存逗号分隔的数据。split方法填充第二个数组tokenArray。
for(int i=0; i<csvArray.length; i++) {
tokenArray[i] = csvArray[i].split(",");
}
更复杂的数据类型需要 API 来检索数据。例如,在第三章、数据清理中,我们将使用杰克森项目(【github.com/FasterXML/j… JSON 文件中检索字段。该示例使用一个文件,该文件包含一个人的 JSON 格式的演示,如下所示:
{
"firstname":"Smith",
"lastname":"Peter",
"phone":8475552222,
"address":["100 Main Street","Corpus","Oklahoma"]
}
下面的代码序列显示了如何提取一个人的字段值。创建一个解析器,它使用getCurrentName来检索字段名称。如果名称是firstname,那么getText方法返回该字段的值。其他字段以类似的方式处理。
try {
JsonFactory jsonfactory = new JsonFactory();
JsonParser parser = jsonfactory.createParser(
new File("Person.json"));
while (parser.nextToken() != JsonToken.END_OBJECT) {
String token = parser.getCurrentName();
if ("firstname".equals(token)) {
parser.nextToken();
String fname = parser.getText();
out.println("firstname : " + fname);
}
...
}
parser.close();
} catch (IOException ex) {
// Handle exceptions
}
该示例的输出如下:
firstname : Smith
简单的数据清理可能包括将文本转换成小写,用空格替换某些文本,以及用一个空格删除多个空白字符。下面显示了这样做的一种方法,其中组合使用了String class' toLowerCase、replaceAll和trim方法。这里,处理包含脏文本的字符串:
dirtyText = dirtyText
.toLowerCase()
.replaceAll("[\\d[^\\w\\s]]+", "
.trim();
while(dirtyText.contains(" ")){
dirtyText = dirtyText.replaceAll(" ", " ");
}
停用词是诸如*、、和或但*之类的词,它们并不总是有助于文本分析。删除这些停用词通常可以改善结果并加快处理速度。
LingPipe API 可以用来移除停用词。在下一个代码序列中,使用了一个TokenizerFactory类实例来标记文本。标记化是返回单个单词的过程。EnglishStopTokenizerFactory类是一个特殊的记号赋予器,它删除常见的英语停用词。
text = text.toLowerCase().trim();
TokenizerFactory fact = IndoEuropeanTokenizerFactory.INSTANCE;
fact = new EnglishStopTokenizerFactory(fact);
Tokenizer tok = fact.tokenizer(
text.toCharArray(), 0, text.length());
for(String word : tok){
out.print(word + " ");
}
想想下面这段摘自《莫比·迪克》一书的文字:
Call me Ishmael. Some years ago- never mind how long precisely - having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.
输出如下所示:
call me ishmael . years ago - never mind how long precisely - having little money my purse , nothing particular interest me shore , i thought i sail little see watery part world .
这些只是在第 3 章、数据清理中讨论的几个数据清理任务。
可视化数据以增强理解
数据分析通常会产生一系列代表分析结果的数字。然而,对于大多数人来说,这种表达结果的方式并不总是直观的。理解结果的更好方法是创建图形和图表来描述结果以及结果元素之间的关系。
人类的大脑通常善于在视觉表现中看到模式、趋势和异常值。许多数据科学问题中存在的大量数据可以使用可视化技术进行分析。可视化适用于从分析师到高层管理人员到客户的广泛受众。在这一章中,我们将介绍各种可视化技术,并演示 Java 是如何支持这些技术的。
在第 4 章、数据可视化、中,我们展示了如何创建不同类型的图形、曲线图和图表。这些例子使用 JavaFX,使用一个名为【trac.erichseifert.de/gral/】()的免费…
可视化允许用户以提供大量数据中不存在的洞察力的方式来检查大型数据集。可视化工具帮助我们识别潜在的问题或意外的数据结果,并对数据进行有意义的解释。
例如,离群值,即超出正常值范围的值,可能很难从大量数字中识别出来。根据数据创建图表,用户可以快速发现异常值。它还可以帮助快速发现错误,并更容易地对数据进行分类。
例如,下面的图表可能表明上面两个值应该是需要处理的异常值:
数据科学中统计方法的使用
统计分析是许多数据科学任务的关键。它用于多种类型的分析,从简单平均值和中间值的计算到复杂的多元回归分析。第 5 章,统计数据分析技术,介绍了这种类型的分析和可用的 Java 支持。
统计分析并不总是一件容易的事情。此外,高级统计技术通常需要特定的心态才能完全理解,这可能很难学习。幸运的是,许多技术并不难使用,各种库减轻了这些技术固有的复杂性。
特别地,回归分析是一种分析数据的重要技术。该技术试图绘制一条与一组数据相匹配的线。计算表示该线的方程,并可用于预测未来的行为。回归分析有几种类型,包括简单回归和多元回归。它们因所考虑的变量数量而异。
下图显示了一条与代表比利时几十年人口的一组数据点非常匹配的直线:
简单的统计技术,如均值和标准差,可以使用 basic Java 来计算。它们也可以由 Apache Commons 之类的库来处理。例如,为了计算中位数,我们可以使用 Apache Commons DescriptiveStatistics类。这将在下一个计算 doubles 数组的中值的地方进行说明。这些数字被添加到该类的一个实例中,如下所示:
double[] testData = {12.5, 18.3, 11.2, 19.0, 22.1, 14.3, 16.2,
12.5, 17.8, 16.5, 12.5};
DescriptiveStatistics statTest =
new SynchronizedDescriptiveStatistics();
for(double num : testData){
statTest.addValue(num);
}
getPercentile方法返回存储在其参数中指定的百分点值。为了找到中间值,我们使用50的值。
out.println("The median is " + statTest.getPercentile(50));
我们的输出如下:
The median is 16.2
在第五章、统计数据分析技术中,我们将展示如何使用 Apache Commons SimpleRegression类执行回归分析。
机器学习应用于数据科学
机器学习对于数据科学分析已经变得越来越重要,就像它对于许多其他领域一样。机器学习的一个定义性特征是模型能够根据一组代表性数据进行训练,然后用于解决类似的问题。不需要显式地编写应用程序来解决问题。模型是现实世界对象的表示。
例如,客户购买可以用于训练模型。随后,可以对客户随后可能进行的购买类型进行预测。这允许组织为客户定制广告和优惠券,并可能提供更好的客户体验。
培训可以用几种不同的方法之一来进行:
- 监督学习:用显示相应正确结果的带注释、带标签的数据训练模型
- 无监督学习:数据不包含结果,但是模型被期望自己发现关系
- 半监督:少量标记数据与大量未标记数据相结合
- 强化学习:这类似于监督学习,但是对好的结果提供奖励
有几种方法支持机器学习。在第六章、*、*机器学习中,我们将举例说明三种技术:
- 决策树:使用问题的特征作为内部节点,结果作为叶子来构建一棵树
- 支持向量机:它通过创建一个超平面来划分数据集,然后进行预测,从而用于分类
- 贝叶斯网络:用于描述事件之间的概率关系
一个支持向量机 ( SVM )主要用于分类类型问题。该方法创建了一个超平面来对数据进行分类,该超平面可以被想象为分隔两个区域的几何平面。在二维空间中,它将是一条线。在三维空间中,它将是一个二维平面。在第 6 章、*机器学习、*中,我们将使用一组与个人露营倾向相关的数据来演示如何使用该方法。我们将使用 Weka 类SMO来演示这种类型的分析。
下图描述了一个使用两种数据点分布的超平面。这些线代表分隔这些点的可能的超平面。除了一个异常值,这些线清楚地分隔了数据点。
一旦模型被训练,可能的超平面被考虑,然后可以使用类似的数据进行预测。
在数据科学中使用神经网络
一个人工神经网络 ( 安),我们称之为神经网络,是基于大脑中发现的神经元。一个神经元是一个有树突将其连接到输入源和其他神经元的细胞。根据输入源,分配给源的权重,神经元被激活,然后发射信号沿着树突到另一个神经元。可以训练一组神经元对一组输入信号作出反应。
人工神经元是具有一个或多个输入和单个输出的节点。每个输入都有一个分配给它的权重,该权重可以随时间变化。神经网络可以通过向网络输入输入、调用激活函数并比较结果来进行学习。该函数组合输入并创建输出。如果多个神经元的输出与预期结果匹配,那么网络已经被正确训练。如果它们不匹配,则网络被修改。
如下图所示,可以可视化一个神经网络,其中隐藏层用于增强该过程:
数据集被分成训练集和测试集。读取数据后,将使用方法创建并初始化 MLP 实例,以配置模型的属性,包括模型学习的速度和训练模型所花费的时间。
String trainingFileName = "dermatologyTrainingSet.arff";
String testingFileName = "dermatologyTestingSet.arff";
try (FileReader trainingReader = new FileReader(trainingFileName);
FileReader testingReader =
new FileReader(testingFileName)) {
Instances trainingInstances = new Instances(trainingReader);
trainingInstances.setClassIndex(
trainingInstances.numAttributes() - 1);
Instances testingInstances = new Instances(testingReader);
testingInstances.setClassIndex(
testingInstances.numAttributes() - 1);
MultilayerPerceptron mlp = new MultilayerPerceptron();
mlp.setLearningRate(0.1);
mlp.setMomentum(0.2);
mlp.setTrainingTime(2000);
mlp.setHiddenLayers("3");
mlp.buildClassifier(trainingInstances);
...
} catch (Exception ex) {
// Handle exceptions
}
然后使用测试数据对模型进行评估:
Evaluation evaluation = new Evaluation(trainingInstances);
evaluation.evaluateModel(mlp, testingInstances);
然后可以显示结果:
System.out.println(evaluation.toSummaryString());
此处显示了该示例的截断输出,其中列出了正确和错误识别的疾病数量:
Correctly Classified Instances 73 98.6486 %
Incorrectly Classified Instances 1 1.3514 %
可以调整模型的各种属性来改进模型。在第 7 章、*神经网络、*中,我们将更深入地讨论这一技术和其他技术。
深度学习方法
深度学习网络通常被描述为使用多个中间层的神经网络。每一层将在前一层的输出上训练,潜在地识别数据集的特征和子特征。特征是指可能感兴趣的数据的那些方面。在第 8 章、深度学习中,我们将考察这些类型的网络,以及它们如何支持几种不同的数据科学任务。
这些网络通常使用非结构化和未标记的数据集,这是当今可用数据的绝大部分。一种典型的方法是获取数据,识别要素,然后使用这些要素及其对应的图层来重构原始数据集,从而验证网络。受限玻尔兹曼机 ( RBM )就是应用这种方法的一个很好的例子。
深度学习网络需要确保结果是准确的,并最大限度地减少任何可能蔓延到过程中的错误。这是通过基于所谓的梯度下降来调整分配给神经元的内部权重来实现的。这代表重量变化的斜率。该方法修改权重以最小化误差,并且还加速了学习过程。
有几种类型的网络被归类为深度学习网络。其中之一是一个自动编码器网络。在这个网络中,各层是对称的,其中输入值的数量与输出值的数量相同,中间层有效地将数据压缩到一个更小的内部层。自动编码器的每一层都是一个 RBM。
下面的例子反映了这种结构,它将提取一组包含手写数字的图像中的数字。这里没有显示完整示例的细节,但是请注意,1,000 个输入和输出值与由 RBM 组成的内部层一起使用。层的大小在nOut和nIn方法中指定。
MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
.seed(seed)
.iterations(numberOfIterations)
.optimizationAlgo(
OptimizationAlgorithm.LINE_GRADIENT_DESCENT)
.list()
.layer(0, new RBM.Builder()
.nIn(numberOfRows * numberOfColumns).nOut(1000)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(1, new RBM.Builder().nIn(1000).nOut(500)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(2, new RBM.Builder().nIn(500).nOut(250)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(3, new RBM.Builder().nIn(250).nOut(100)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(4, new RBM.Builder().nIn(100).nOut(30)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build()) //encoding stops
.layer(5, new RBM.Builder().nIn(30).nOut(100)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build()) //decoding starts
.layer(6, new RBM.Builder().nIn(100).nOut(250)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(7, new RBM.Builder().nIn(250).nOut(500)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(8, new RBM.Builder().nIn(500).nOut(1000)
.lossFunction(LossFunctions.LossFunction.RMSE_XENT)
.build())
.layer(9, new OutputLayer.Builder(
LossFunctions.LossFunction.RMSE_XENT).nIn(1000)
.nOut(numberOfRows * numberOfColumns).build())
.pretrain(true).backprop(true)
.build();
一旦模型经过训练,就可以用于预测和搜索任务。通过搜索,压缩的中间层可以用于匹配其他需要分类的压缩图像。
执行文本分析
自然语言处理 ( NLP )领域用于许多不同的任务,包括文本搜索、语言翻译、情感分析、语音识别和分类等等。由于许多原因,包括自然语言固有的模糊性,处理文本是困难的。
有几种不同类型的处理可以执行,例如:
- 识别停用词:这些是常见的词,可能不需要处理
- 命名实体识别 ( NER ):这是识别文本元素的过程,例如人名、地点或事物
- 词性 ( 词性):这标识了一个句子的语法部分,如名词、动词、形容词等等
- 关系:这里我们关心的是识别文本的各个部分是如何相互关联的,比如句子的主语和宾语
与大多数数据科学问题一样,预处理和清理文本非常重要。在第 9 章、文本分析中,我们考察了 Java 为这一数据科学领域提供的支持。
例如,我们将使用 Apache 的 OpenNLP(opennlp.apache.org/)库来查找词性。这只是我们可以使用的几个 NLP APIs 中的一个,包括 LingPipe(【alias-i.com/lingpipe/】[… UIMA(](alias-i.com/lingpipe/))… Standford NLP(【http://nlp.stanford.edu/】)。我们选择 OpenNLP 是因为它在这个例子中很容易使用。
在下面的例子中,在en-pos-maxent.bin文件中找到了一个用于识别 POS 元素的模型。初始化单词数组并创建 POS 模型:
try (InputStream input = new FileInputStream(
new File("en-pos-maxent.bin"));) {
String sentence = "Let's parse this sentence.";
...
String[] words;
...
list.toArray(words);
POSModel posModel = new POSModel(input);
...
} catch (IOException ex) {
// Handle exceptions
}
向tag方法传递一个words数组,并返回一个标签数组。然后显示单词和标签。
String[] posTags = posTagger.tag(words);
for(int i=0; i<posTags.length; i++) {
out.println(words[i] + " - " + posTags[i]);
}
该示例的输出如下:
Let's - NNP
parse - NN
this - DT
sentence. - NN
缩写NNP和DT分别代表单数专有名词和限定词。我们在第 9 章、文本分析中考察了其他几种 NLP 技术。
视觉和听觉分析
在第十章、视听分析中,我们展示了几种处理声音和图像的 Java 技术。我们首先演示声音处理技术,包括语音识别和文本到语音转换 API。具体来说,我们将使用 FreeTTS(【freetts.sourceforge.net/docs/index.… 将文本转换为语音。我们还展示了用于语音识别的 CMU 斯芬克斯工具包。
Java 语音 API(JSAPI)(【www.oracle.com/technetwork… API 支持语音识别和语音合成器。FreeTTS 和 Festival(www.cstr.ed.ac.uk/projects/fe…)就是支持 JSAPI 的供应商的例子。
在本章的第二部分,我们研究图像处理技术,如面部识别。这个演示包括识别图像中的人脸,使用 OpenCV(opencv.org/)很容易完成。
此外,在第十章、*、*中,我们演示了如何从图像中提取文本,这个过程被称为 OCR 。一个常见的数据科学问题涉及提取和分析图像中嵌入的文本。例如,包含在牌照、路标和方向中的信息可能非常重要。
在下面的例子中,在第 11 章中有更详细的解释,用于数据分析的数学和并行技术使用 Tess4j(tess4j.sourceforge.net/)一个用于 Tesseract OCR API 的 Java JNA 包装器来实现 OCR。我们对从维基百科关于 OCR 的文章(https://en . Wikipedia . org/wiki/Optical _ character _ recognition # Applications)中捕获的图像执行 OCR,如下所示:
ITesseract接口提供了许多 OCR 方法。doOCR方法获取一个文件并返回一个包含文件中的单词的字符串,如下所示:
ITesseract instance = new Tesseract();
try {
String result = instance.doOCR(new File("OCRExample.png"));
System.out.println(result);
} catch (TesseractException e) {
System.err.println(e.getMessage());
}
下面显示了输出的一部分:
OCR engines nave been developed into many lunds oiobiectorlented OCR applicatlons, sucn as reoeipt OCR, involoe OCR, check OCR, legal billing document OCR
They can be used ior
- Data entry ior business documents, e g check, passport, involoe, bank statement and receipt
- Automatic number plate recognnlon
如您所见,这个示例中有许多错误需要解决。我们在第 11 章数据分析的数学和并行技术中建立了这个例子,讨论了增强功能和注意事项,以确保 OCR 过程尽可能有效。
我们将以 NeurophStudio 的讨论来结束本章,NeurophStudio 是一个基于 Java 的神经网络编辑器,用于对图像进行分类和执行图像识别。我们在这一部分训练一个神经网络来识别和分类人脸。
使用并行技术提高应用性能
在第 11 章、数据分析的数学和并行技术中,我们考虑了一些可用于数据科学应用的并行技术。程序的并发执行可以显著提高性能。与数据科学相关,这些技术包括从低级数学计算到高级 API 特定选项。
本章包括对基本性能增强注意事项的讨论。算法和应用程序架构与增强代码一样重要,当试图集成并行技术时,应该考虑这一点。如果应用程序没有以预期或想要的方式运行,并行优化的任何好处都是无关紧要的。
矩阵运算对于许多数据应用程序和支持 API 是必不可少的。我们将在本章中讨论矩阵乘法,以及如何用各种方法处理它。尽管这些操作通常隐藏在 API 中,但是理解它们是如何被支持的还是很有用的。
我们演示的一种方法利用了 Apache Commons 数学 API(commons.apache.org/proper/comm…)。这个 API 支持大量的数学和统计操作,包括矩阵乘法。以下示例说明了如何执行矩阵乘法。
我们首先声明并初始化矩阵A和B:
double[][] A = {
{0.1950, 0.0311},
{0.3588, 0.2203},
{0.1716, 0.5931},
{0.2105, 0.3242}};
double[][] B = {
{0.0502, 0.9823, 0.9472},
{0.5732, 0.2694, 0.916}};
Apache Commons 使用RealMatrix类来存储矩阵。接下来,我们使用Array2DRowRealMatrix构造函数为A和B创建相应的矩阵:
RealMatrix aRealMatrix = new Array2DRowRealMatrix(A);
RealMatrix bRealMatrix = new Array2DRowRealMatrix(B);
我们简单地使用multiply方法执行乘法:
RealMatrix cRealMatrix = aRealMatrix.multiply(bRealMatrix);
最后,我们使用一个for循环来显示结果:
for (int i = 0; i < cRealMatrix.getRowDimension(); i++) {
System.out.println(cRealMatrix.getRowVector(i));
}
输出如下所示:
{0.02761552; 0.19992684; 0.2131916}
{0.14428772; 0.41179806; 0.54165016}
{0.34857924; 0.32834382; 0.70581912}
{0.19639854; 0.29411363; 0.4963528}
并发处理的另一种方法是使用 Java 线程。当多个 CPU 或 GPU 不可用时,线程由 api(如 Aparapi)使用。
数据科学应用程序通常利用 map-reduce 算法。我们将通过使用 Apache 的 Hadoop 来执行 map-reduce 来演示并行处理。Hadoop 专为大型数据集而设计,可减少大规模数据科学项目的处理时间。我们演示了一种计算大型数据集平均值的技术。
我们还包括支持多处理器的 API 的例子,包括 CUDA 和 OpenCL。使用 Java bindings for CUDA(JCuda)(jcuda.org/)来支持 CUDA。我们还将讨论 OpenCL 及其 Java 支持。Aparapi API 为使用多个 CPU 或 GPU 提供了高级支持,并且我们包括了一个 Aparapi 支持矩阵乘法的演示。
组装零件
在本书的最后一章,我们将把前几章探索的许多技术结合在一起。我们将创建一个简单的基于控制台的应用程序,用于从 Twitter 获取数据并执行各种类型的数据操作和分析。我们在本章中的目标是展示一个探索各种数据科学概念的简单项目,并为未来的项目提供见解和考虑。
具体来说,在最后一章中开发的应用程序执行几个高级任务,包括数据采集、数据清理、情感分析和基本的统计数据收集。我们使用 Java 8 流演示这些技术,并尽可能关注 Java 8 方法。
总结
数据科学是一个广泛多样的研究领域,不可能在本书中进行详尽的探讨。我们希望提供对重要数据科学概念的坚实理解,并使读者做好进一步学习的准备。特别是,这本书将为数据科学相关调查的所有阶段提供不同技术的具体例子。这包括从数据采集和清理到详细的统计分析。
因此,让我们从讨论数据采集和 Java 如何支持它开始,如下一章所示。
二、数据采集
使用格式不正确的代码或使用的变量名不能表达其预期目的的代码从来都不是什么有趣的事情。数据也是如此,只是坏数据会导致不准确的结果。因此,数据采集是数据分析中的一个重要步骤。数据可以从许多来源获得,但是在它有用之前必须进行检索和最终处理。它可以从各种来源获得。我们可以在众多的公共数据源中找到简单的文件,也可以在互联网上找到更复杂的形式。在这一章中,我们将演示如何从这些网站中获取数据,包括各种互联网网站和一些社交媒体网站。
我们可以通过下载特定的文件或者通过一个叫做网络抓取的过程从互联网上获取数据,这个过程包括提取网页的内容。我们还探索了一个被称为网络爬行的相关主题,它涉及到应用程序检查一个网站以确定它是否是感兴趣的,然后跟随嵌入的链接以识别其他潜在的相关页面。
我们也可以从社交媒体网站提取数据。如果我们知道如何访问,这些类型的网站通常拥有现成的数据宝库。在本章中,我们将演示如何从几个站点提取数据,包括:
- 推特
- 维基百科(一个基于 wiki 技术的多语言的百科全书协作计划ˌ也是一部用不同语言写成的网络百科全书ˌ 其目标及宗旨是为全人类提供自由的百科全书)ˌ开放性的百科全书
- 闪烁(光)
- 油管(国外视频网站)
从站点提取数据时,可能会遇到许多不同的数据格式。我们将研究三种基本类型:文本、音频和视频。然而,即使在文本、音频和视频数据中,也存在许多格式。单就音频数据来说,在https://en . Wikipedia . org/wiki/Comparison _ of _ audio _ coding _ formats就比较了 45 种音频编码格式。对于文本数据,在 fileinfo.com/filetypes/t… 的列出了将近 300 种格式。在这一章中,我们将着重于如何下载和提取这些类型的文本作为纯文本,以供最终处理。
我们将简要分析不同的数据格式,然后分析可能的数据源。我们需要这些知识来演示如何使用不同的数据采集技术获取数据。
了解数据科学应用中使用的数据格式
当我们讨论数据格式时,我们指的是内容格式,而不是底层的文件格式,后者对大多数开发人员来说可能是不可见的。由于可用的格式数量巨大,我们无法检查所有可用的格式。相反,我们将处理几种更常见的格式,提供足够的例子来解决最常见的数据检索需求。具体来说,我们将演示如何检索以下列格式存储的数据:
- 超文本标记语言
- 便携文档格式
- CSV/TSV
- 电子表格
- 数据库
- JSON
- 可扩展标记语言
其中一些格式在其他地方得到了很好的支持和记录。例如,XML 已经使用了很多年,并且有几种成熟的技术可以在 Java 中访问 XML 数据。对于这些类型的数据,我们将概述可用的主要技术,并展示一些示例来说明它们是如何工作的。这将为那些不熟悉该技术的读者提供一些对其本质的洞察。
最常见的数据格式是二进制文件。例如,Word、Excel 和 PDF 文档都是以二进制存储的。这些需要特殊的软件来从中提取信息。文本数据也很常见。
CSV 数据概述
逗号分隔值 ( CSV )文件,包含以行列格式组织的表格数据。以明文形式存储的数据按行存储,也称为记录。每条记录都包含用逗号分隔的字段。这些文件也与其他分隔文件密切相关,最显著的是制表符分隔值 ( TSV )文件。以下是一个简单 CSV 文件的一部分,这些数字并不代表任何特定类型的数据:
JURISDICTION NAME,COUNT PARTICIPANTS,COUNT FEMALE,PERCENT FEMALE
10001,44,22,0.5
10002,35,19,0.54
10003,1,1,1
请注意,第一行包含描述后续记录的标题数据。每个值由逗号分隔,并对应于相同位置的标题。在第 3 章、数据清理中,我们将更深入地讨论 CSV 文件,并检查对不同类型分隔符的支持。
电子表格概述
电子表格是表格数据的一种形式,其中信息存储在行和列中,很像二维数组。它们通常包含数字和文本信息,并使用公式来总结和分析其内容。大多数人都熟悉 Excel 电子表格,但它们也是其他产品套件的一部分,如 OpenOffice。
电子表格是一种重要的数据源,因为在过去的几十年中,它们被用于在许多行业和应用中存储信息。它们的表格性质使它们易于处理和分析。了解如何从这个无处不在的数据源中提取数据非常重要,这样我们就可以利用存储在数据源中的大量信息。
对于我们的一些示例,我们将使用一个简单的 Excel 电子表格,它由一系列包含 ID 的行以及最小值、最大值和平均值组成。这些数字并不代表任何特定类型的数据。电子表格如下所示:
| ID | 最小值 | 最大值 | 平均值 |
| 12345 | 45 | 89 | 65.55 |
| 23456 | 78 | 96 | 86.75 |
| 34567 | 56 | 89 | 67.44 |
| 45678 | 86 | 99 | 95.67 |
在第 3 章、数据清理中,我们将学习如何从电子表格中提取数据。
数据库概述
数据可以在数据库管理系统 ( DBMS )中找到,这些系统和电子表格一样,无处不在。Java 为访问和处理 DBMS 中的数据提供了丰富的选项。本节的目的是提供使用 Java 访问数据库的基本介绍。
我们将演示使用 JDBC 连接到数据库、存储信息和检索信息的本质。对于这个例子,我们使用 MySQL 数据库管理系统。但是,它也适用于其他数据库管理系统,只需更改数据库驱动程序。我们在 MySQL 工作台中使用以下命令创建了一个名为example的数据库和一个名为URLTABLE的表。还有其他工具可以达到同样的效果:
CREATE TABLE IF NOT EXISTS `URLTABLE` (
`RecordID` INT(11) NOT NULL AUTO_INCREMENT,
`URL` text NOT NULL,
PRIMARY KEY (`RecordID`)
);
我们从处理异常的try块开始。连接到 DBMS 需要一个驱动程序。在这个例子中,我们使用了com.mysql.jdbc.Driver。为了连接到数据库,使用了getConnection方法,传递数据库服务器位置、用户 ID 和密码。这些值取决于所使用的 DBMS:
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/example";
connection = DriverManager.getConnection(url, "user ID",
"password");
...
} catch (SQLException | ClassNotFoundException ex) {
// Handle exceptions
}
接下来,我们将说明如何向数据库添加信息,然后如何读取它。SQL INSERT命令是在字符串中构造的。MySQL 数据库的名字是example。该命令将在数据库的URLTABLE表中插入值,其中问号是要插入值的占位符:
String insertSQL = "INSERT INTO `example`.`URLTABLE` "
+ "(`url`) VALUES " + "(?);";
PreparedStatement类表示要执行的 SQL 语句。prepareStatement方法使用INSERT SQL 语句创建该类的一个实例:
PreparedStatement stmt = connection.prepareStatement(insertSQL);
然后,我们使用setString方法和execute方法向表中添加 URL。setString方法有两个论点。第一个指定要插入数据的列索引,第二个是要插入的值。execute方法执行实际的插入。我们在下一个序列中添加两个 URL:
stmt.setString(1, "https://en.wikipedia.org/wiki/Data_science");
stmt.execute();
stmt.setString(1,
"https://en.wikipedia.org/wiki/Bishop_Rock,_Isles_of_Scilly");
stmt.execute();
为了读取数据,我们使用在selectSQL字符串中声明的 SQL SELECT语句。这将从URLTABLE表中返回所有的行和列。createStatement方法创建了一个Statement类的实例,用于INSERT类型的语句。executeQuery方法执行查询并返回保存表内容的ResultSet实例:
String selectSQL = "select * from URLTABLE";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(selectSQL);
下面的序列遍历表,一次显示一行。getString方法的参数指定我们想要使用结果集的第二列,它对应于 URL 字段:
out.println("List of URLs");
while (resultSet.next()) {
out.println(resultSet.getString(2));
}
该示例在执行时的输出如下:
List of URLs
https://en.wikipedia.org/wiki/Data_science
https://en.wikipedia.org/wiki/Bishop_Rock,_Isles_of_Scilly
如果需要清空表中的内容,请使用以下顺序:
Statement statement = connection.createStatement();
statement.execute("TRUNCATE URLTABLE;");
这是对使用 Java 访问数据库的简单介绍。有许多资源可以提供关于这个主题的更深入的报道。例如,甲骨文在docs.oracle.com/javase/tuto…提供了关于这个主题的更深入的介绍。
PDF 文件概述
可移植文档格式 ( PDF )是一种不依赖于特定平台或软件应用的格式。PDF 文档可以保存格式化的文本和图像。PDF 是一种开放标准,使它在许多地方都很有用。
有大量的文档存储为 PDF 格式,这使得它成为一个有价值的数据来源。有几个 Java APIs 允许访问 PDF 文档,包括 Apache POI 和 PDFBox 。从 PDF 文档中提取信息的技术在第 3 章、数据清理中有说明。
JSON 概述
JavaScript 对象符号(JSON)(www.JSON.org/)是一种用于交换数据的数据格式。对于人类或机器来说,读和写很容易。JSON 受到许多语言的支持,包括 Java,Java 有几个 JSON 库列在www.JSON.org/上。
JSON 实体由一组用花括号括起来的名称-值对组成。我们将在几个例子中使用这种格式。在处理 YouTube 时,我们将使用一个 JSON 对象,下面显示了其中的一部分,它代表了一个 YouTube 视频请求的结果:
{
"kind": "youtube#searchResult",
"etag": "etag",
"id": {
"kind": string,
"videoId": string,
"channelId": string,
"playlistId": string
},
...
}
访问这样一个文档的字段和值并不难,在第 3 章、数据清理中有说明。
XML 概述
可扩展标记语言 ( XML )是一种指定标准文档格式的标记语言。XML 广泛用于应用程序之间和互联网上的通信,由于它相对简单和灵活,所以很受欢迎。用 XML 编码的文档是基于字符的,很容易被机器和人阅读。
XML 文档包含标记和内容字符。这些字符允许解析器对文档中包含的信息进行分类。文档由标签组成,元素存储在标签中。元素还可以包含其他标记标签并形成子元素。此外,元素可能包含存储为名称-值对的属性或特定特征。
XML 文档必须是格式良好的。这意味着它必须遵循一定的规则,比如总是使用结束标签并且只使用一个根标签。其他规则在https://en . Wikipedia . org/wiki/XML # Well-formed ness _ and _ error-handling讨论。
用于 XML 处理的 Java API 由三个用于解析 XML 数据的接口组成。文档对象模型 ( DOM )接口解析 XML 文档并返回描述文档结构的树形结构。DOM 接口将整个文档作为一个整体来解析。或者,XML 的简单 API(SAX)一次解析一个文档元素。当内存使用是个问题时,SAX 是更好的选择,因为 DOM 需要更多的资源来构建树。然而,DOM 比 SAX 更灵活,因为任何元素都可以在任何时间以任何顺序访问。
第三个 Java API 被称为 XML 的流 API**(StAX)。这种流模型旨在通过在不牺牲资源的情况下提供灵活性来容纳 DOM 和 SAX 模型的最佳部分。StAX 表现出更高的性能,代价是一次只能访问文档中的一个位置。如果您已经知道如何处理文档,StAX 是首选技术,但是对于可用内存有限的应用程序,它也很受欢迎。**
下面是一个简单的 XML 文件。每个<text>代表一个标签,标记标签中包含的元素。在这种情况下,我们文件中最大的节点是<music>,其中包含歌曲数据集。一个<song>标签中的每个标签描述了另一个对应于那首歌的元素。每个标签最终都会有一个结束标签,比如</song>。请注意,第一个标记包含应该使用哪个 XML 版本来解析文件的信息:
<?xml version="1.0"?>
<music>
<song id="1234">
<artist>Patton, Courtney</artist>
<name>So This Is Life</name>
<genre>Country</genre>
<price>2.99</price>
</song>
<song id="5678">
<artist>Eady, Jason</artist>
<name>AM Country Heaven</name>
<genre>Country</genre>
<price>2.99</price>
</song>
</music>
还有许多其他与 XML 相关的技术。例如,我们可以使用 DTD 文档或专门为 XML 文档编写的 XML 模式来验证特定的 XML 文档。使用 XLST 可以将 XML 文档转换成不同的格式。
流数据概述
流数据指的是以连续流的形式生成并以顺序、逐段的方式访问的数据。普通互联网用户访问的大部分数据都是流媒体,包括视频和音频频道,或者社交媒体网站上的文本和图像数据。当数据是新的且变化很快时,或者当需要收集大量数据时,流式数据是首选方法。
流式数据通常是数据科学研究的理想选择,因为它通常以大量的原始格式存在。许多公共流数据都是免费的,并得到 Java APIs 的支持。在这一章中,我们将研究如何从流媒体来源获取数据,包括 Twitter、Flickr 和 YouTube。尽管使用了不同的技术和 API,但是您会注意到从这些站点获取数据的技术之间的相似之处。
Java 中的音频/视频/图像概述
有大量的格式用于表示图像、视频和音频。这种类型的数据通常以二进制格式存储。模拟音频流被采样和数字化。图像通常只是代表像素颜色的位的集合。以下链接提供了对其中一些格式的更深入的讨论:
通常,这种类型的数据可能非常大,必须进行压缩。当数据被压缩时,使用两种方法。第一种是无损压缩,使用更少的空间并且没有信息损失。第二种是有损,即信息丢失。丢失信息并不总是一件坏事,因为有时这种丢失对人类来说并不明显。
正如我们将在第 3 章、数据清理中演示的那样,这种类型的数据通常会以一种不方便的方式遭到破坏,可能需要清理。例如,录音中可能有背景噪声,或者图像可能需要在处理之前进行平滑处理。图像平滑在第 3 章、数据清理中演示,使用 OpenCV 库。
**# 数据采集技术
在这一节中,我们将说明如何从网页中获取数据。网页包含大量潜在的有用信息。我们将演示如何使用几种技术访问 web 页面,从由HttpUrlConnection类支持的低级方法开始。为了找到页面,经常使用网络爬虫应用程序。一旦确定了有用的页面,就需要从页面中提取信息。这通常使用 HTML 解析器来执行。提取这些信息非常重要,因为它们通常隐藏在杂乱的 HTML 标记和 JavaScript 代码中。
使用 HttpUrlConnection 类
可以使用HttpUrlConnection类访问网页的内容。这是一种低级的方法,需要开发人员做大量的工作来提取相关的内容。然而,他或她能够对如何处理内容进行更大的控制。在某些情况下,这种方法可能比使用其他 API 库更好。
我们将演示如何使用这个类下载维基百科数据科学页面的内容。我们从一个try / catch块开始处理异常。URL 对象是使用数据科学 URL 字符串创建的。openConnection方法将创建一个到维基百科服务器的连接,如下所示:
try {
URL url = new URL(
"https://en.wikipedia.org/wiki/Data_science");
HttpURLConnection connection = (HttpURLConnection)
url.openConnection();
...
} catch (MalformedURLException ex) {
// Handle exceptions
} catch (IOException ex) {
// Handle exceptions
}
用 HTTP GET命令初始化connection对象。然后执行connect方法来连接服务器:
connection.setRequestMethod("GET");
connection.connect();
假设没有遇到错误,我们可以使用getResponseCode方法确定响应是否成功。一个正常返回值是200。网页的内容可能会有所不同。例如,getContentType方法返回一个描述页面内容的字符串。getContentLength方法返回它的长度:
out.println("Response Code: " + connection.getResponseCode());
out.println("Content Type: " + connection.getContentType());
out.println("Content Length: " + connection.getContentLength());
假设我们得到了一个 HTML 格式的页面,下一个序列说明了如何得到这个内容。创建一个BufferedReader实例,从 web 站点一次读入一行并附加到一个BufferedReader实例。然后显示缓冲区:
InputStreamReader isr = new InputStreamReader((InputStream)
connection.getContent());
BufferedReader br = new BufferedReader(isr);
StringBuilder buffer = new StringBuilder();
String line;
do {
line = br.readLine();
buffer.append(line + "\n");
} while (line != null);
out.println(buffer.toString());
这里显示了简短的输出:
Response Code: 200
Content Type: text/html; charset=UTF-8
Content Length: -1
<!DOCTYPE html>
<html lang="en" dir="ltr" class="client-nojs">
<head>
<meta charset="UTF-8"/>
<script>document.documentElement.className =
...
"wgHostname":"mw1251"});});</script>
</body>
</html>
虽然这是可行的,但是有更简单的方法来获取网页的内容。下一节将讨论其中一种技术。
Java 中的网络爬虫
Web 爬行是遍历一系列相互连接的网页并从这些网页中提取相关信息的过程。它通过隔离然后跟随页面上的链接来做到这一点。虽然有许多现成的预编译数据集,但仍有必要直接从互联网上收集数据。一些来源,如新闻网站,不断更新,需要不时地重新访问。
网络爬虫是访问各种站点并收集信息的应用程序。web 爬行过程由一系列步骤组成:
- 选择要访问的 URL
- 获取页面
- 解析页面
- 提取相关内容
- 提取相关网址进行访问
对每个访问的 URL 重复这个过程。
在获取和解析页面时,需要考虑几个问题,例如:
- 页面重要性:我们不想处理不相关的页面。
- 例如,我们通常不会关注图片的链接。
- 蜘蛛陷阱(Spider traps):我们希望绕过那些可能导致无限请求的网站。在动态生成的页面中,一个请求导致另一个请求,就会发生这种情况。
- 重复:避免多次抓取同一页面很重要。
- 礼貌:不要向网站发出过多的请求。观察
robot.txt文件;它们指定网站的哪些部分不应该被爬网。
创建一个网络爬虫的过程可能是令人生畏的。对于除了最简单的需求之外的所有需求,建议使用几种开源网络爬虫中的一种。部分列表如下:
- 纳特:【nutch.apache.org】T2
- 爬虫军:【github.com/yasserg/cra…
- JSpider:【j-spider.sourceforge.net/】T2
- WebSPHINX:【www.cs.cmu.edu/~rcm/websph…
- 赫莉特里克斯:【webarchive.jira.com/wiki/displa…
我们既可以创建自己的网络爬虫,也可以使用现有的爬虫。在本章中,我们将研究这两种方法。对于专门的处理,最好使用定制的爬行器。我们将演示如何用 Java 创建一个简单的网络爬虫,以便更深入地了解网络爬虫是如何工作的。接下来是对其他网络爬虫的简单讨论。
创建自己的网络爬虫
现在我们对网络爬虫有了基本的了解,我们准备创建自己的爬虫。在这个简单的网络爬虫中,我们将使用ArrayList实例跟踪被访问的页面。此外,jsoup 将用于解析网页,我们将限制我们访问的页面数量。jsoup(jsoup.org/)是一个开源的 HTML 解析器。这个例子演示了一个网络爬虫的基本结构,也强调了创建一个网络爬虫所涉及的一些问题。
我们将使用SimpleWebCrawler类,如下所示:
public class SimpleWebCrawler {
private String topic;
private String startingURL;
private String urlLimiter;
private final int pageLimit = 20;
private ArrayList<String> visitedList = new ArrayList<>();
private ArrayList<String> pageList = new ArrayList<>();
...
public static void main(String[] args) {
new SimpleWebCrawler();
}
}
实例变量的详细信息如下:
| 变量 | 使用 |
| topic | 要使页面被接受,页面中需要包含的关键字 |
| startingURL | 第一页的 URL |
| urlLimiter | 必须包含在链接中才能被跟踪的字符串 |
| pageLimit | 要检索的最大页数 |
| visitedList | 包含已经访问过的页面的ArrayList |
| pageList | 一个包含感兴趣页面的 URL 的ArrayList |
在SimpleWebCrawler构造函数中,我们初始化实例变量,从维基百科页面开始搜索意大利海岸附近的 Bishop Rock 岛。这是为了最大限度地减少可能检索到的页面数量。正如我们将会看到的,维基百科中关于主教洛克的页面比我们想象的要多得多。
urlLimiter变量被设置为Bishop_Rock,这将限制嵌入的链接只跟随那些包含该字符串的链接。每个感兴趣的页面必须包含存储在topic变量中的值。visitPage方法执行实际的爬行:
public SimpleWebCrawler() {
startingURL = https://en.wikipedia.org/wiki/Bishop_Rock, "
+ "Isles_of_Scilly";
urlLimiter = "Bishop_Rock";
topic = "shipping route";
visitPage(startingURL);
}
在visitPage方法中,检查pageList ArrayList 以查看是否超过了接受页面的最大数量。如果超出了限制,则搜索终止:
public void visitPage(String url) {
if (pageList.size() >= pageLimit) {
return;
}
...
}
如果页面已经被访问过,那么我们忽略它。否则,它将被添加到已访问列表中:
if (visitedList.contains(url)) {
// URL already visited
} else {
visitedList.add(url);
...
}
Jsoup用于解析页面并返回一个Document对象。可能会出现许多不同的异常和问题,例如格式错误的 URL、检索超时或简单的坏链接。catch区块需要处理这些类型的问题。我们将在 Java 的 web 抓取中对 jsoup 进行更深入的解释:
try {
Document doc = Jsoup.connect(url).get();
...
}
} catch (Exception ex) {
// Handle exceptions
}
如果文档包含主题文本,则显示链接并添加到pageList数组列表中。获取每个嵌入的链接,如果链接包含限制文本,那么递归调用visitPage方法:
if (doc.text().contains(topic)) {
out.println((pageList.size() + 1) + ": [" + url + "]");
pageList.add(url);
// Process page links
Elements questions = doc.select("a[href]");
for (Element link : questions) {
if (link.attr("href").contains(urlLimiter)) {
visitPage(link.attr("abs:href"));
}
}
}
这种方法只检查那些包含主题文本的页面中的链接。将for循环移出 if 语句将测试所有页面的链接。
输出如下:
1: [https://en.wikipedia.org/wiki/Bishop_Rock,_Isles_of_Scilly]
2: [https://en.wikipedia.org/wiki/Bishop_Rock_Lighthouse]
3: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&oldid=717634231#Lighthouse]
4: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&diff=prev&oldid=717634231]
5: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&oldid=716622943]
6: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&diff=prev&oldid=716622943]
7: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&oldid=716608512]
8: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&diff=prev&oldid=716608512]
...
20: [https://en.wikipedia.org/w/index.php?title=Bishop_Rock,_Isles_of_Scilly&diff=prev&oldid=716603919]
在本例中,我们没有将爬网结果保存在外部源中。通常这是必要的,可以存储在文件或数据库中。
使用 crawler4j 网络爬虫
这里我们将举例说明爬虫 4j(【github.com/yasserg/cra… . com/yasserg/crawler 4j/tree/master/src/test/Java/edu/UCI/ics/crawler 4j/examples/basi](github.com/yasserg/cra…](github.com/yasserg/cra…
和我们之前的爬虫一样,我们将抓取维基百科中关于主教岩的文章。使用这个爬虫的结果会更小,因为许多无关的页面会被忽略。
我们先来看一下CrawlerController类。crawler 使用了几个参数,详情如下:
- 抓取存储文件夹:抓取数据存储的位置
- 爬虫数量:控制用于爬行的线程数量
- 礼貌延迟:请求之间暂停多少秒
- 爬行深度:爬行的深度
- 要读取的最大页数:要读取多少页
- 二进制数据:是否抓取 PDF 文件等二进制数据
这里显示了基本类:
public class CrawlerController {
public static void main(String[] args) throws Exception {
int numberOfCrawlers = 2;
CrawlConfig config = new CrawlConfig();
String crawlStorageFolder = "data";
config.setCrawlStorageFolder(crawlStorageFolder);
config.setPolitenessDelay(500);
config.setMaxDepthOfCrawling(2);
config.setMaxPagesToFetch(20);
config.setIncludeBinaryContentInCrawling(false);
...
}
}
接下来,创建并配置CrawlController类。注意用于处理robot.txt文件的RobotstxtConfig和RobotstxtServer类。这些文件包含供网络爬虫阅读的指令。它们提供了帮助爬虫做得更好的方向,例如指定站点的哪些部分不应该被爬行。这对自动生成的页面很有用:
PageFetcher pageFetcher = new PageFetcher(config);
RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
RobotstxtServer robotstxtServer =
new RobotstxtServer(robotstxtConfig, pageFetcher);
CrawlController controller =
new CrawlController(config, pageFetcher, robotstxtServer);
爬行器需要从一个或多个页面开始。addSeed方法添加开始页面。虽然我们在这里只使用了一次该方法,但它可以根据需要多次使用:
controller.addSeed(
"https://en.wikipedia.org/wiki/Bishop_Rock,_Isles_of_Scilly");
start方法将开始爬行过程:
controller.start(SampleCrawler.class, numberOfCrawlers);
SampleCrawler类包含两个有趣的方法。第一个是确定页面是否被访问的shouldVisit方法和实际处理页面的visit方法。我们从类声明和 Java 正则表达式类Pattern对象的声明开始。这将是确定一个页面是否会被访问的一种方式。在此声明中,指定了标准图像,并将忽略这些图像:
public class SampleCrawler extends WebCrawler {
private static final Pattern IMAGE_EXTENSIONS =
Pattern.compile(".*\\.(bmp|gif|jpg|png)$");
...
}
向shouldVisit方法传递了一个对找到该 URL 的页面的引用。如果任何图像匹配,该方法返回false并且页面被忽略。另外,网址必须以 en.wikipedia.org/wiki/的[开头。我…:](en.wikipedia.org/wiki/)
public boolean shouldVisit(Page referringPage, WebURL url) {
String href = url.getURL().toLowerCase();
if (IMAGE_EXTENSIONS.matcher(href).matches()) {
return false;
}
return href.startsWith("https://en.wikipedia.org/wiki/");
}
向visit方法传递一个表示被访问页面的Page对象。在这个实现中,只有那些包含字符串shipping route的页面才会被处理。这进一步限制了访问的页面。当我们找到这样一个页面时,它的URL、Text和Text length会显示出来:
public void visit(Page page) {
String url = page.getWebURL().getURL();
if (page.getParseData() instanceof HtmlParseData) {
HtmlParseData htmlParseData =
(HtmlParseData) page.getParseData();
String text = htmlParseData.getText();
if (text.contains("shipping route")) {
out.println("\nURL: " + url);
out.println("Text: " + text);
out.println("Text length: " + text.length());
}
}
}
以下是该程序执行时的截断输出:
URL: https://en.wikipedia.org/wiki/Bishop_Rock,_Isles_of_Scilly
Text: Bishop Rock, Isles of Scilly...From Wikipedia, the free encyclopedia ... Jump to: ... navigation, search For the Bishop Rock in the Pacific Ocean, see Cortes Bank. Bishop Rock Bishop Rock Lighthouse (2005)
...
Text length: 14677
请注意,只返回了一页。该网络爬虫能够识别并忽略主网页的先前版本。
我们可以执行进一步的处理,但是这个例子提供了一些关于 API 如何工作的见解。访问一个页面可以获得大量的信息。在这个例子中,我们只使用了 URL 和文本的长度。以下是您可能有兴趣获取的其他数据的示例:
- path
- 父 URL
- 锚
- HTML 文本
- 传出链接
- 文档 ID
Java 中的网页抓取
网页抓取是从网页中提取信息的过程。该页面通常使用一系列 HTML 标记进行格式化。HTML 解析器用于浏览一个页面或一系列页面,并访问页面的数据或元数据。
jsoup(jsoup.org/)是一个开源的 Java 库,它使用 HTML 解析器来帮助提取和操作 HTML 文档。它有许多用途,包括 web 抓取、从 HTML 页面中提取特定元素以及清理 HTML 文档。
有几种方法可以获得有用的 HTML 文档。HTML 文档可以从以下位置提取:
- 统一资源定位器
- 线
- 文件
第一种方法在下面举例说明,其中数据科学的维基百科页面被加载到一个Document对象中。这个Jsoup对象代表 HTML 文档。connect方法连接到站点,get方法检索document:
try {
Document document = Jsoup.connect(
"https://en.wikipedia.org/wiki/Data_science").get();
...
} catch (IOException ex) {
// Handle exception
}
从文件加载使用如下所示的File类。重载的parse方法使用文件来创建document对象:
try {
File file = new File("Example.html");
Document document = Jsoup.parse(file, "UTF-8", "");
...
} catch (IOException ex) {
// Handle exception
}
Example.html文件如下:
<html>
<head>
<body>
<p>The body of the document</p>
Interesting Links:
<br>
<a href="https://en.wikipedia.org/wiki/Data_science">Data Science</a>
<br>
<a href="https://en.wikipedia.org/wiki/Jsoup">Jsoup</a>
<br>
Images:
<br>
<img src="eyechart.jpg" alt="Eye Chart">
</body>
</html>
为了从一个字符串创建一个Document对象,我们将使用下面的序列,其中parse方法处理复制前面 HTML 文件的字符串:
String html = "<html>\n"
+ "<head>
+ "<body>\n"
+ "<p>The body of the document</p>\n"
+ "Interesting Links:\n"
+ "<br>\n"
+ "<a href="https://en.wikipedia.org/wiki/Data_science">" +
"DataScience</a>\n"
+ "<br>\n"
+ "<a href="https://en.wikipedia.org/wiki/Jsoup">" +
"Jsoup</a>\n"
+ "<br>\n"
+ "Images:\n"
+ "<br>\n"
+ " <img src="eyechart.jpg" alt="Eye Chart"> \n"
+ "</body>\n"
+ "</html>";
Document document = Jsoup.parse(html);
Document类拥有许多有用的方法。title方法返回标题。为了获取文档的文本内容,使用了select方法。此方法使用指定要检索的文档元素的字符串:
String title = document.title();
out.println("Title: " + title);
Elements element = document.select("body");
out.println(" Text: " + element.text());
这里显示了 Wikipedia 数据科学页面的输出。为了节省空间,它被缩短了:
Title: Data science - Wikipedia, the free encyclopedia
Text: Data science From Wikipedia, the free encyclopedia Jump to: navigation, search Not to be confused with information science. Part of a
...
policy About Wikipedia Disclaimers Contact Wikipedia Developers Cookie statement Mobile view
select方法的参数类型是字符串。通过使用字符串,选择的信息类型很容易改变。在 jsoup.org/apidocs/[的`… jsoup Javadocs 中可以找到关于如何构造这个字符串的细节:](jsoup.org/apidocs/)
我们可以使用select方法来检索文档中的图像,如下所示:
Elements images = document.select("img[src$=.png]");
for (Element image : images) {
out.println("\nImage: " + image);
}
这里显示了 Wikipedia 数据科学页面的输出。为了节省空间,它被缩短了:
Image: <img alt="Data Visualization" src="//upload.wikimedia.org/...>
Image: <img alt="" src="//upload.wikimedia.org/wikipedia/commons/thumb/b/ba/...>
如下所示,可以轻松检索链接:
Elements links = document.select("a[href]");
for (Element link : links) {
out.println("Link: " + link.attr("href")
+ " Text: " + link.text());
}
这里显示了Example.html页面的输出:
Link: https://en.wikipedia.org/wiki/Data_science Text: Data Science
Link: https://en.wikipedia.org/wiki/Jsoup Text: Jsoup
jsoup 拥有许多附加功能。然而,这个例子演示了网页抓取过程。还有其他可用的 Java HTML 解析器。在en.wikipedia.org/wiki/Compar…可以找到 Java HTML 解析器的比较。
使用 API 调用访问常见的社交媒体网站
社交媒体包含大量可以处理的信息,并被许多数据分析应用程序使用。在这一节中,我们将说明如何使用 Java APIs 访问其中的一些资源。它们中的大多数需要某种访问密钥,这通常很容易获得。我们从讨论OAuth类开始,它提供了一种认证访问数据源的方法。
当使用这种类型的数据源时,请记住数据并不总是公开的,这一点很重要。虽然数据可能是可访问的,但数据的所有者可能是不一定希望信息被共享的个人。大多数 API 都提供了一种方法来决定如何分配数据,这些请求应该被接受。当使用私人信息时,必须获得作者的许可。
此外,这些网站对可以发出的请求数量有限制。从一个站点提取数据时,请记住这一点。如果需要超出这些限制,那么大多数网站都提供了一种方法。
使用 OAuth 认证用户
OAuth 是一种开放标准,用于对许多不同网站的用户进行身份验证。资源所有者有效地委托对服务器资源的访问,而不必共享他们的凭证。它适用于 HTTPS。OAuth 2.0 继承了 OAuth,并且不向后兼容。它为客户端开发人员提供了一种简单的身份验证方式。一些公司使用 OAuth 2.0,包括 PayPal、Comcast 和暴雪娱乐。
OAuth 2.0 提供商列表可在 en.wikipedia.org/wiki/List_o…](en.wikipedia.org/wiki/List_o…)
递推特
庞大的数据量和该网站在名人和公众中的受欢迎程度,使 Twitter 成为挖掘社交媒体数据的宝贵资源。Twitter 是一个流行的社交媒体平台,允许用户阅读和发布名为 tweets 的短信。Twitter 为发布和拉取推文提供 API 支持,包括来自所有公共用户的流数据。虽然有一些服务可用于提取整个公共 tweet 数据集,但我们将研究其他选项,这些选项虽然限制了一次检索的数据量,但却是免费的。
我们将关注用于检索流数据的 Twitter API。从特定用户那里检索推文以及向特定账户发布数据还有其他选择,但我们不会在本章中讨论这些。默认访问级别的公共流 API 允许用户提取当前在 Twitter 上流动的公共 tweets 的样本。通过指定参数来跟踪关键字、特定用户和位置,可以细化数据。
对于这个例子,我们将使用 Java HTTP 客户端 HBC。你可以在 github.com/twitter/hbc 下载一个示例 HBC 应用程序。如果您喜欢使用不同的 HTTP 客户端,请确保它将返回增量响应数据。Apache HTTP 客户机是一种选择。在创建 HTTP 连接之前,您必须首先创建一个 Twitter 帐户和该帐户中的一个应用程序。若要开始使用该应用程序,请访问 apps.twitter.com。创建应用程序后,将为您分配一个消费者密钥、消费者密码、访问令牌和访问密码令牌。我们也将使用 OAuth,正如本章前面所讨论的。
首先,我们将编写一个方法来执行身份验证并从 Twitter 请求数据。我们方法的参数是创建应用程序时 Twitter 给我们的认证信息。我们将创建一个BlockingQueue对象来保存我们的流数据。在本例中,我们将默认容量设置为 10,000。我们还将指定端点并关闭停止警告:
public static void streamTwitter(
String consumerKey, String consumerSecret,
String accessToken, String accessSecret)
throws InterruptedException {
BlockingQueue<String> statusQueue =
new LinkedBlockingQueue<String>(10000);
StatusesSampleEndpoint ending =
new StatusesSampleEndpoint();
ending.stallWarnings(false);
...
}
接下来,我们使用OAuth1创建一个Authentication对象,它是OAuth类的变体。然后,我们可以构建我们的连接客户端并完成 HTTP 连接:
Authentication twitterAuth = new OAuth1(consumerKey,
consumerSecret, accessToken, accessSecret);
BasicClient twitterClient = new ClientBuilder()
.name("Twitter client")
.hosts(Constants.STREAM_HOST)
.endpoint(ending)
.authentication(twitterAuth)
.processor(new StringDelimitedProcessor(statusQueue))
.build();
twitterClient.connect();
出于这个例子的目的,我们将简单地读取从流中接收到的消息,并将它们打印到屏幕上。消息以 JSON 格式返回,如何在实际应用程序中处理它们将取决于该应用程序的目的和限制:
for (int msgRead = 0; msgRead < 1000; msgRead++) {
if (twitterClient.isDone()) {
out.println(twitterClient.getExitEvent().getMessage());
break;
}
String msg = statusQueue.poll(10, TimeUnit.SECONDS);
if (msg == null) {
out.println("Waited 10 seconds - no message received");
} else {
out.println(msg);
}
}
twitterClient.stop();
为了执行我们的方法,我们只需将我们的认证信息传递给streamTwitter方法。为了安全起见,我们在这里更换了我们的私人钥匙。应该始终保护身份验证信息:
public static void main(String[] args) {
try {
SampleStreamExample.streamTwitter(
myKey, mySecret, myToken, myAccess);
} catch (InterruptedException e) {
out.println(e);
}
}
下面是使用上面列出的方法检索的截断样本数据。您的数据会因 Twitter 的实时流而异,但应该类似于以下示例:
{"created_at":"Fri May 20 15:47:21 +0000 2016","id":733685552789098496,"id_str":"733685552789098496","text":"bwisit si em bahala sya","source":"\u003ca href="http:\/\/twitter.com" rel="nofollow"\u003eTwitter Web
...
ntions":[],"symbols":[]},"favorited":false,"retweeted":false,"filter_level":"low","lang":"tl","timestamp_ms":"1463759241660"}
Twitter 还支持为一个特定的用户帐户提取所有数据,以及将数据直接发布到一个帐户。REST API 也是可用的,它通过 search API 为特定的查询提供支持。这些也使用 OAuth 标准,并在 JSON 文件中返回数据。
处理维基百科
维基百科(www.wikipedia.org/)是文本和图像类型信息的有用来源。这是一个互联网百科全书,拥有超过 250 种语言的 3800 万篇文章(【en.wikipedia.org/wiki/Wikipe…
MediaWiki 是一个开源的 Wiki 应用程序,支持 wiki 类型的站点。它用于支持维基百科和许多其他网站。MediaWiki API(www.mediaWiki.org/wiki/API)通过 HTTP 提供对 wiki 数据和元数据的访问。使用该 API 的应用程序可以登录、读取数据以及向站点发布更改。
有几个 Java APIs 支持对维基站点的编程访问,如在www.mediawiki.org/wiki/API:Cl…所列。为了演示 Java 对 wiki 的访问,我们将使用 bitbucket.org/axelclk/inf… Bliki。它提供了良好的访问,并且易于用于大多数基本操作。](bitbucket.org/axelclk/inf…)
MediaWiki API 很复杂,有许多特性。本节的目的是说明使用这个 API 从维基百科文章中获取文本的基本过程。这里不可能完全涵盖 API。
我们将使用info.bliki.api和info.bliki.wiki.model包中的以下类:
Page:表示检索到的页面User:代表一个用户WikiModel:代表维基
Bliki 的 Javadocs 可以在 www.javadoc.io/doc/info.bl… 找到。
以下例子改编自http://www . integrating stuff . com/2012/04/06/hook-into-Wikipedia-using-Java-and-the-mediawiki-API/。这个例子将访问主题为数据科学的英文维基百科页面。我们首先创建一个User类的实例。三参数构造函数的前两个参数分别是user ID和password。在这种情况下,它们是空字符串。这种组合允许我们在不建立账户的情况下阅读页面。第三个参数是 MediaWiki API 页面的 URL:
User user = new User("", "",
"http://en.wikipedia.org/w/api.php");
user.login();
帐户将使我们能够修改文件。queryContent方法返回在字符串数组中找到的主题的Page对象列表。每个字符串应该是一个页面的标题。在本例中,我们访问一个页面:
String[] titles = {"Data science"};
List<Page> pageList = user.queryContent(titles);
每个Page对象包含一个页面的内容。有几种方法可以返回页面的内容。对于每个页面,使用双参数构造函数创建一个WikiModel实例。第一个参数是图像基本 URL,第二个参数是链接基本 URL。这些 URL 使用名为image和title的维基变量,这些变量将在创建链接时被替换:
for (Page page : pageList) {
WikiModel wikiModel = new WikiModel("${image}",
"${title}");
...
}
render方法将获取 wiki 页面并将其呈现为 HTML。还有一种将页面呈现为 PDF 文档的方法:
String htmlText = wikiModel.render(page.toString());
然后显示 HTML 文本:
out.println(htmlText);
输出的部分列表如下:
<p>PageID: 35458904; NS: 0; Title: Data science;
Image url:
Content:
{{distinguish}}
{{Use dmy dates}}
{{Data Visualization}}</p>
<p><b>Data science</b> is an interdisciplinary field about processes and systems to extract <a href="Knowledge" >knowledge</a>
...
我们还可以使用如下所示的几种方法之一来获取文章的基本信息:
out.println("Title: " + page.getTitle() + "\n" +
"Page ID: " + page.getPageid() + "\n" +
"Timestamp: " + page.getCurrentRevision().getTimestamp());
还可以获得文章中的参考文献列表和标题列表。这里显示了一个参考列表:
List <Reference> referenceList = wikiModel.getReferences();
out.println(referenceList.size());
for(Reference reference : referenceList) {
out.println(reference.getRefString());
}
下面说明了获取节标题的过程:
ITableOfContent toc = wikiModel.getTableOfContent();
List<SectionHeader> sections = toc.getSectionHeaders();
for(SectionHeader sh : sections) {
out.println(sh.getFirst());
}
维基百科的全部内容都可以下载。这个过程将在en.wikipedia.org/wiki/Wikipe…进行讨论。
建立自己的维基百科服务器来处理你的请求可能是可取的。
处理 Flickr
Flickr(www.flickr.com/)是一款在线照片管理和分享应用。它可能是图像和视频的来源。Flickr 开发者指南(【www.flickr.com/services/de… Flickr API 的一个很好的起点。
使用 Flickr API 的第一步是请求一个 API 密钥。这个密钥用于签署您的 API 请求。获取密钥的过程从www.flickr.com/services/ap…开始。商业密钥和非商业密钥都可用。当你获得一个密钥时,你也将获得一个“秘密”这两者都是使用 API 所必需的。
我们将举例说明从 Flickr 定位和下载图像的过程。该流程包括:
- 创建 Flickr 类实例
- 指定查询的搜索参数
- 执行搜索
- 下载图像
在此过程中可能会抛出FlickrException或IOException。有几个 API 支持 Flickr 访问。我们将使用位于 github.com/callmeal/Fl… 的 Flickr4Java。Flickr4Java Javadocs 可以在 flickrj.sourceforge.net/api/[找到。我们将…:](flickrj.sourceforge.net/api/)
try {
String apikey = "Your API key";
String secret = "Your secret";
} catch (FlickrException | IOException ex) {
// Handle exceptions
}
接下来创建Flickr实例,其中apikey和secret作为前两个参数被提供。最后一个参数指定了用于访问 Flickr 服务器的传输技术。目前,使用REST类支持 REST 传输:
Flickr flickr = new Flickr(apikey, secret, new REST());
为了搜索图像,我们将使用SearchParameters类。这个类支持许多标准,这些标准可以缩小查询返回的图像数量,包括纬度、经度、媒体类型和用户 ID 等标准。在下面的序列中,setBBox方法指定了搜索的经度和纬度。这些参数是(按顺序):最小经度、最小纬度、最大经度和最大纬度。setMedia方法指定了媒体的类型。有三个可能的参数— "all"、"photos"和"videos":
SearchParameters searchParameters = new SearchParameters();
searchParameters.setBBox("-180", "-90", "180", "90");
searchParameters.setMedia("photos");
PhotosInterface类拥有一个search方法,该方法使用SearchParameters实例来检索照片列表。getPhotosInterface方法返回PhotosInterface类的一个实例,如下所示。SearchParameters实例是第一个参数。第二个参数决定每页检索多少张照片,第三个参数是偏移量。返回一个PhotoList类实例:
PhotosInterface pi = new PhotosInterface(apikey, secret,
new REST());
PhotoList<Photo> list = pi.search(searchParameters, 10, 0);
下一个序列说明了几种方法的使用,以获取有关图像检索的信息。使用get方法访问每个Photo实例。将显示标题、图像格式、公共标志和照片 URL:
out.println("Image List");
for (int i = 0; i < list.size(); i++) {
Photo photo = list.get(i);
out.println("Image: " + i +
`"\nTitle: " + photo.getTitle() +
"\nMedia: " + photo.getOriginalFormat() +
"\nPublic: " + photo.isPublicFlag() +
"\nUrl: " + photo.getUrl() +
"\n");
}
out.println();
此处显示了部分列表,其中许多特定值已被修改以保护原始数据:
Image List
Image: 0
Title: XYZ Image
Media: jpg
Public: true
Url: https://flickr.com/photos/7723...@N02/269...
Image: 1
Title: IMG_5555.jpg
Media: jpg
Public: true
Url: https://flickr.com/photos/2665...@N07/264...
Image: 2
Title: DSC05555
Media: jpg
Public: true
Url: https://flickr.com/photos/1179...@N04/264...
这个例子返回的图片列表会有所不同,因为我们使用了一个相当大的搜索范围,而且图片一直在增加。
有两种方法可以用来下载图像。第一个使用图像的 URL,第二个使用一个Photo对象。图像的 URL 可以从许多来源获得。我们在这个例子中使用了Photo类getUrl方法。
在下面的序列中,我们使用其构造函数获得了一个PhotosInterface的实例,以说明另一种方法:
PhotosInterface pi = new PhotosInterface(apikey, secret,
new REST());
我们从前面的列表中获取第一个Photo实例,然后用它的getUrl获取图像的 URL。PhotosInterface类的getImage方法返回一个代表图像的BufferedImage对象,如下所示:
Photo currentPhoto = list.get(0);
BufferedImage bufferedImage =
pi.getImage(currentPhoto.getUrl());
然后使用ImageIO类将图像保存到一个文件中:
File outputfile = new File("image.jpg");
ImageIO.write(bufferedImage, "jpg", outputfile);
getImage方法被重载。这里,Photo实例和所需图像的大小被用作参数来获得BufferedImage实例:
bufferedImage = pi.getImage(currentPhoto, Size.SMALL);
可以使用前面的技术将图像保存到文件中。
Flickr4Java API 支持许多其他处理 Flickr 图像的技术。
处理 YouTube
YouTube 是一个受欢迎的视频网站,用户可以上传和分享视频(www.youtube.com/)。它已经被用来分享幽默视频,提供如何做任何事情的指令,并在其观众之间分享信息。这是一个有用的信息来源,因为它捕捉了不同人群的思想和观点。这为分析和洞察人类行为提供了一个有趣的机会。
YouTube 可以作为视频和视频元数据的有用来源。Java API 可用于访问其内容(developers.google.com/youtube/v3/)。API 的详细文档可以在 developers.google.com/youtube/v3/…](developers.google.com/youtube/v3/…)
在本节中,我们将演示如何通过关键字搜索视频并检索感兴趣的信息。我们还将展示如何下载视频。要使用 YouTube API,你需要一个谷歌账户,可以在www.google.com/accounts/Ne…获得。接下来,在谷歌开发者控制台中创建一个账户(【console.developers.google.com/】T2)。使用 API 密钥或 OAuth 2.0 凭证支持 API 访问。在https://developers . Google . com/YouTube/registrating _ an _ application # create _ project讨论项目创建过程和关键点。
按关键字搜索
按关键字搜索视频的过程改编自https://developers . Google . com/YouTube/v3/code _ samples/Java # search _ by _ keyword。其他可能有用的代码示例可以在developers.google.com/youtube/v3/…找到。该过程已经过简化,因此我们可以专注于搜索过程。我们从 try 块和一个YouTube实例的创建开始。这个类提供了对 API 的基本访问。这个 API 的 Javadocs 可以在https://developers . Google . com/resources/API-libraries/documentation/YouTube/v3/Java/latest/找到。
YouTube.Builder类用于构造一个YouTube实例。它的构造函数有三个参数:
Transport:用于 HTTP 的对象JSONFactory:用于处理 JSON 对象- 这个例子不需要任何东西
许多 API 响应将以 JSON 对象的形式出现。YouTube 类的'setApplicationName方法给它一个名字,build方法创建一个新的 YouTube 实例:
try {
YouTube youtube = new YouTube.Builder(
Auth.HTTP_TRANSPORT,
Auth.JSON_FACTORY,
new HttpRequestInitializer() {
public void initialize(HttpRequest request)
throws IOException {
}
})
.setApplicationName("application_name")
...
} catch (GoogleJSONResponseException ex) {
// Handle exceptions
} catch (IOException ex) {
// Handle exceptions
}
接下来,我们初始化一个字符串来保存感兴趣的搜索词。在这种情况下,我们将查找包含单词cats的视频:
String queryTerm = "cats";
类YouTube.Search.List维护一个搜索结果的集合。YouTube类的search方法指定了要返回的资源类型。在这种情况下,字符串指定了要返回的搜索结果的id和snippet部分:
YouTube.Search.List search = youtube
.search()
.list("id,snippet");
搜索结果是一个 JSON 对象,其结构如下。在https://developers . Google . com/YouTube/v3/docs/playlist items # methods中有更详细的描述。在前面的序列中,只返回搜索的id和snippet部分,从而提高了操作效率:
{
"kind": "youtube#searchResult",
"etag": etag,
"id": {
"kind": string,
"videoId": string,
"channelId": string,
"playlistId": string
},
"snippet": {
"publishedAt": datetime,
"channelId": string,
"title": string,
"description": string,
"thumbnails": {
(key): {
"url": string,
"width": unsigned integer,
"height": unsigned integer
}
},
"channelTitle": string,
"liveBroadcastContent": string
}
}
接下来,我们需要指定 API 键和各种搜索参数。指定查询术语以及要返回的媒体类型。在这种情况下,只会返回视频。另外两个选项包括channel和playlist:
String apiKey = "Your API key";
search.setKey(apiKey);
search.setQ(queryTerm);
search.setType("video");
此外,我们进一步指定要返回的字段,如下所示。这些对应于 JSON 对象的字段:
search.setFields("items(id/kind,id/videoId,snippet/title," +
"snippet/description,snippet/thumbnails/default/url)");
我们还指定了使用setMaxResults方法检索的结果的最大数量:
search.setMaxResults(10L);
execute方法将执行实际的查询,返回一个SearchListResponse对象。它的getItems方法返回一个SearchResult对象列表,每个对象对应一个检索到的视频:
SearchListResponse searchResponse = search.execute();
List<SearchResult> searchResultList =
searchResponse.getItems();
在这个例子中,我们没有遍历每个返回的视频。相反,我们检索第一个视频并显示关于该视频的信息。SearchResult video 变量允许我们访问 JSON 对象的不同部分,如下所示:
SearchResult video = searchResultList.iterator().next();
Thumbnail thumbnail = video
.getSnippet().getThumbnails().getDefault();
out.println("Kind: " + video.getKind());
out.println("Video Id: " + video.getId().getVideoId());
out.println("Title: " + video.getSnippet().getTitle());
out.println("Description: " +
video.getSnippet().getDescription());
out.println("Thumbnail: " + thumbnail.getUrl());
一种可能的输出如下,其中部分输出已被修改:
Kind: null
Video Id: tntO...
Title: Funny Cats ...
Description: Check out the ...
Thumbnail: https://i.ytimg.com/vi/tntO.../default.jpg
为了简化示例,我们已经跳过了许多错误检查步骤,但是在业务应用程序中实现时,应该考虑这些步骤。
如果我们需要下载视频,最简单的方法之一就是使用 axet/wget 在github.com/axet/wget找到。它提供了一种使用视频 ID 下载视频的简单易用的技术。
在下面的示例中,使用视频 ID 创建了一个 URL。您需要提供视频 ID 才能正常工作。文件以视频标题作为文件名保存到当前目录:
String url = "http://www.youtube.com/watch?v=videoID";
String path = ".";
VGet vget = new VGet(new URL(url), new File(path));
vget.download();
GitHub 网站上还有其他更复杂的下载技术。
总结
在这一章中,我们讨论了对数据科学有用的数据类型,这些数据在互联网上很容易获得。这一讨论包括关于最常见类型数据源的文件规范和格式的细节。
我们还研究了 Java APIs 和其他检索数据的技术,并用多个数据源说明了这个过程。我们特别关注基于文本的文档格式和多媒体文件的类型。我们使用网络爬虫来访问网站,然后执行网络抓取来从我们遇到的网站中检索数据。
最后,我们从社交媒体网站提取数据,并检查可用的 Java 支持。我们从 Twitter、Wikipedia、Flickr 和 YouTube 检索数据,并检查可用的 API 支持。**
三、数据清理
真实世界的数据通常是脏的和非结构化的,并且在可用之前必须重新处理。数据可能包含错误、重复条目、格式错误或不一致。解决这些类型问题的过程称为数据清理。数据清洗又被称为数据角力、按摩、整形,或者蒙皮。数据合并是指将来自多个来源的数据进行合并,通常被认为是一种数据清理活动。
我们需要清理数据,因为任何基于不准确数据的分析都会产生误导性的结果。我们希望确保我们处理的数据是高质量的数据。数据质量包括:
- 有效性:确保数据具有正确的形式或结构
- **准确性:**数据中的值真正代表数据集
- **完整性:**没有缺失的元素
- 一致性:数据的变化是同步的
- 一致性:使用相同的测量单位
有几种用于清理数据的技术和工具。我们将研究以下方法:
- 处理不同类型的数据
- 清理和操作文本数据
- 填补缺失数据
- 验证数据
此外,我们将简要检查几种图像增强技术。
通常有许多方法来完成相同的清洁任务。例如,有许多支持数据清理的 GUI 工具,比如 open refine(openrefine.org/)。该工具允许用户读入数据集,并使用各种技术对其进行清理。然而,对于需要清理的每个数据集,它需要用户与应用程序进行交互。它不利于自动化。
我们将关注如何使用 Java 代码清理数据。即便如此,也可能有不同的技术来清理数据。我们将展示多种方法,为读者提供如何做到这一点的见解。有时,这将使用核心 Java 字符串类,而在其他时候,它可能会使用专门的库。
这些库通常更具表现力和效率。但是,有时使用简单的字符串函数就足以解决问题了。展示赞美的技巧会提高读者的技能。
基于文本的基本任务包括:
- 数据转换
- 数据插补(处理缺失数据)
- 子集数据
- 分类数据
- 验证数据
在这一章中,我们感兴趣的是清理数据。然而,这个过程的一部分是从各种数据源中提取信息。数据可以明文或二进制形式存储。在开始清理过程之前,我们需要了解用于存储数据的各种格式。这些格式中的许多已在第 2 章、数据采集中介绍过,但我们将在以下章节中更详细地介绍。
处理数据格式
数据以各种形式出现。我们将研究更常用的格式,并展示如何从各种数据源中提取它们。在我们清理数据之前,需要从数据源(如文件)中提取数据。在本节中,我们将建立在第 2 章、数据采集中的数据格式介绍的基础上,并展示如何提取全部或部分数据集。例如,从一个 HTML 页面中,我们可能只想提取没有标记的文本。或者也许我们只对它的数字感兴趣。
这些数据格式可能相当复杂。本节的目的是说明该数据格式常用的基本技术。对特定数据格式的全面论述超出了本书的范围。具体来说,我们将介绍如何从 Java 处理以下数据格式:
- CSV 数据
- 电子表格
- 可移植文档格式或 PDF 文件
- Javascript 对象符号或 JSON 文件
还有许多其他文件类型在这里没有提到。例如,jsoup 对于解析 HTML 文档很有用。因为我们已经在第二章、数据采集的Java 部分介绍了这是如何完成的,所以我们在此不再赘述。
处理 CSV 数据
分隔信息的常用技术是使用逗号或类似的分隔符。了解如何处理 CSV 数据使我们能够在分析工作中利用这种类型的数据。当我们处理 CSV 数据时,有几个问题,包括转义数据和嵌入的逗号。
我们将研究一些处理逗号分隔数据的基本技术。由于 CSV 数据的行列结构,这些技术将从文件中读取数据,并将数据放在二维数组中。首先,我们将结合使用Scanner类读入令牌和String类split方法来分离数据并将其存储在数组中。接下来,我们将探索使用第三方库 OpenCSV,它提供了一种更有效的技术。
然而,第一种方法可能只适用于快速和肮脏的数据处理。我们将逐一讨论这些技术,因为它们在不同的情况下都很有用。
我们将使用从www.data.gov/下载的数据集,其中包含按邮政编码排序的美国人口统计数据。该数据集可在https://catalog . data . gov/dataset/demographic-statistics-by-zip-code-acfc 9下载。出于我们的目的,这个数据集已经存储在文件Demographics.csv中。在这个特定的文件中,每一行都包含相同数量的列。然而,并不是所有的数据都如此干净,接下来显示的解决方案考虑了交错数组的可能性。
注意
交错数组是指不同行的列数可能不同的数组。例如,行 2 可以具有 5 个元素,而行 3 可以具有 6 个元素。当使用交错数组时,你必须小心你的列索引。
首先,我们使用Scanner类从数据文件中读入数据。我们将数据暂时存储在一个ArrayList中,因为我们并不总是知道我们的数据包含多少行。
try (Scanner csvData = new Scanner(new File("Demographics.csv"))) {
ArrayList<String> list = new ArrayList<String>();
while (csvData.hasNext()) {
list.add(csvData.nextLine());
} catch (FileNotFoundException ex) {
// Handle exceptions
}
使用toArray方法将列表转换为数组。这个版本的方法使用一个String数组作为参数,这样该方法就知道要创建什么类型的数组。然后创建一个二维数组来保存 CSV 数据。
String[] tempArray = list.toArray(new String[1]);
String[][] csvArray = new String[tempArray.length][];
split方法用于为每行创建一个由String组成的数组。这个数组被分配给csvArray的一行。
for(int i=0; i<tempArray.length; i++) {
csvArray[i] = tempArray[i].split(",");
}
我们的下一项技术将使用第三方库来读入和处理 CSV 数据。有多种选择,但我们将重点关注流行的 OpenCSV(opencsv.sourceforge.net)。这个库比我们以前的技术提供了几个优势。我们可以在每行上有任意数量的条目,而不用担心处理异常。我们也不需要担心数据标记中嵌入的逗号或回车。该库还允许我们选择一次读取整个文件还是使用迭代器逐行处理数据。
首先,我们需要创建一个CSVReader类的实例。注意,第二个参数允许我们指定分隔符,例如,如果我们有由制表符或破折号分隔的类似文件格式,这是一个有用的特性。如果我们想一次读取整个文件,我们使用readAll方法。
CSVReader dataReader = new CSVReader(new FileReader("Demographics.csv"),',');
ArrayList<String> holdData = (ArrayList)dataReader.readAll();
然后我们可以像上面一样处理数据,通过使用String类方法将数据分割成一个二维数组。或者,我们可以一次处理一行数据。在下面的示例中,每个标记都是单独打印出来的,但是标记也可以存储在二维数组或其他适当的数据结构中。
CSVReader dataReader = new CSVReader(new FileReader("Demographics.csv"),',');
String[] nextLine;
while ((nextLine = dataReader.readNext()) != null){
for(String token : nextLine){
out.println(token);
}
}
dataReader.close();
我们现在可以清理或处理阵列。
处理电子表格
电子表格已经被证明是一种非常流行的处理数字和文本数据的工具。由于过去几十年来电子表格中存储了大量信息,知道如何从电子表格中提取信息使我们能够利用这一广泛可用的数据源。在本节中,我们将演示如何使用 Apache POI API 来实现这一点。
Open Office 还支持电子表格应用程序。Open Office 文档以 XML 格式存储,这使得使用 XML 解析技术很容易访问它。然而,阿帕奇 ODF 工具包(incubator.apache.org/odftoolkit/)提供了一种在不知道 OpenOffice 文档格式的情况下访问文档内数据的方法。这目前是一个孵化器项目,还没有完全成熟。还有许多其他 API 可以帮助处理 OpenOffice 文档,详见面向开发人员的开放文档格式(ODF)(www.opendocumentformat.org/developers/)页面。
处理 Excel 电子表格
Apache POI(poi.apache.org/index.html)是一组 API,提供对包括 Excel 和 Word 在内的许多微软产品的访问。它由一系列旨在访问特定 Microsoft 产品的组件组成。这些组件的概述可在 poi.apache.org/overview.ht… 的找到。
在本节中,我们将演示如何使用 XSSF 组件读取一个简单的 Excel 电子表格来访问 Excel 2007+电子表格。Apache POI API 的 Javadocs 可以在poi.apache.org/apidocs/ind…找到。
我们将使用一个简单的 Excel 电子表格,它由一系列包含 ID 以及最小值、最大值和平均值的行组成。这些数字并不代表任何特定类型的数据。电子表格如下:
| ID | 最小值 | 最大值 | 平均值 |
| 12345 | 45 | 89 | 65.55 |
| 23456 | 78 | 96 | 86.75 |
| 34567 | 56 | 89 | 67.44 |
| 45678 | 86 | 99 | 95.67 |
我们从 try-with-resources 块开始处理任何可能发生的IOExceptions:
try (FileInputStream file = new FileInputStream(
new File("Sample.xlsx"))) {
...
}
} catch (IOException e) {
// Handle exceptions
}
使用电子表格创建一个XSSFWorkbook类的实例。由于一个工作簿可能包含多个电子表格,我们使用getSheetAt方法选择第一个。
XSSFWorkbook workbook = new XSSFWorkbook(file);
XSSFSheet sheet = workbook.getSheetAt(0);
下一步是遍历电子表格的行,然后遍历每一列:
for(Row row : sheet) {
for (Cell cell : row) {
...
}
out.println();
电子表格的每个单元格可以使用不同的格式。我们使用getCellType方法确定其类型,然后使用适当的方法提取单元格中的数据。在这个例子中,我们只处理数字和文本数据。
switch (cell.getCellType()) {
case Cell.CELL_TYPE_NUMERIC:
out.print(cell.getNumericCellValue() + "\t");
break;
case Cell.CELL_TYPE_STRING:
out.print(cell.getStringCellValue() + "\t");
break;
}
执行时,我们得到以下输出:
ID Minimum Maximum Average
12345.0 45.0 89.0 65.55
23456.0 78.0 96.0 86.75
34567.0 56.0 89.0 67.44
45678.0 86.0 99.0 95.67
POI 支持其他更复杂的类和方法来提取数据。
处理 PDF 文件
有几个 API 支持从 PDF 文件中提取文本。这里我们将使用 PDFBox。Apache PDF box(pdfbox.apache.org/)是一个开源 API,允许 Java 程序员处理 PDF 文档。在这一节中,我们将演示如何从 PDF 文档中提取简单的文本。在 pdfbox.apache.org/docs/2.0.1/… PDFBox API 的 javadocs。](pdfbox.apache.org/docs/2.0.1/…)
这是一个简单的 PDF 文件。它由几个项目符号组成:
- 第一行
- 第二行
- 第 3 行
这是文件的结尾。
一个try块用于抓住IOExceptions。PDDocument类将代表正在处理的 PDF 文档。它的load方法将加载到由File对象指定的 PDF 文件中:
try {
PDDocument document = PDDocument.load(new File("PDF File.pdf"));
...
} catch (Exception e) {
// Handle exceptions
}
一旦加载完毕,PDFTextStripper类getText方法将从文件中提取文本。然后显示文本,如下所示:
PDFTextStripper Tstripper = new PDFTextStripper();
String documentText = Tstripper.getText(document);
System.out.println(documentText);
该示例的输出如下。请注意,项目符号以问号的形式返回。
This is a simple PDF file. It consists of several bullets:
? Line 1
? Line 2
? Line 3
This is the end of the document.
这是对 PDFBox 使用的简单介绍。当我们需要提取和操作 PDF 文档时,它是一个非常强大的工具。
处理 JSON
在第 2 章、数据采集中,我们了解到某些 YouTube 搜索会返回 JSON 格式的结果。具体来说,SearchResult类保存与特定搜索相关的信息。在这一节中,我们说明了如何使用 YouTube 特定的技术来提取信息。在这一节中,我们将说明如何使用 Jackson JSON 实现提取 JSON 信息。
JSON 支持三种数据处理模型:
- 流 API -逐令牌处理 JSON 数据
- 树模型——JSON 数据完全保存在内存中,然后进行处理
- 数据绑定——JSON 数据被转换成 Java 对象
使用 JSON 流 API
我们将说明前两种方法。第一种方法效率更高,在处理大量数据时使用。第二种方法很方便,但是数据不能太大。当使用特定的 Java 类处理数据更方便时,第三种技术很有用。例如,如果 JSON 数据代表一个地址,那么可以定义一个特定的 Java 地址类来保存和处理数据。
有几个 Java 库支持 JSON 处理,包括:
- flex JSON(flexjson.sourceforge.net/)
- genson(http://owlike . github . io/genson/
- Google-Gson(github.com/google/gson)
- 杰克逊图书馆(github.com/FasterXML/j…)
- JSON-io(https://github . com/jdereg/JSON-io
- JSON-lib(json-lib.sourceforge.net/
我们将使用杰克逊项目(github.com/FasterXML/j…)。文件可以在 github.com/FasterXML/j… 找到。我们将使用两个 JSON 文件来演示如何使用它。接下来显示的是第一个文件Person.json,其中存储了个人数据。它由四个字段组成,其中最后一个字段是位置信息数组。
{
"firstname":"Smith",
"lastname":"Peter",
"phone":8475552222,
"address":["100 Main Street","Corpus","Oklahoma"]
}
下面的代码序列显示了如何提取每个字段的值。在 try-catch 块中,创建了一个JsonFactory实例,然后基于Person.json文件创建了一个JsonParser实例。
try {
JsonFactory jsonfactory = new JsonFactory();
JsonParser parser = jsonfactory.createParser(new File("Person.json"));
...
parser.close();
} catch (IOException ex) {
// Handle exceptions
}
nextToken方法返回一个token。然而,JsonParser对象跟踪当前的令牌。在while循环中,nextToken方法返回并让解析器前进到下一个标记。getCurrentName方法返回token的字段名称。当到达最后一个令牌时,while循环终止。
while (parser.nextToken() != JsonToken.END_OBJECT) {
String token = parser.getCurrentName();
...
}
循环体由一系列根据字段名称处理字段的if语句组成。由于address字段是一个数组,另一个循环将提取它的每个元素,直到到达结束数组token。
if ("firstname".equals(token)) {
parser.nextToken();
String fname = parser.getText();
out.println("firstname : " + fname);
}
if ("lastname".equals(token)) {
parser.nextToken();
String lname = parser.getText();
out.println("lastname : " + lname);
}
if ("phone".equals(token)) {
parser.nextToken();
long phone = parser.getLongValue();
out.println("phone : " + phone);
}
if ("address".equals(token)) {
out.println("address :");
parser.nextToken();
while (parser.nextToken() != JsonToken.END_ARRAY) {
out.println(parser.getText());
}
}
此示例的输出如下:
firstname : Smith
lastname : Peter
phone : 8475552222
address :
100 Main Street
Corpus
Oklahoma
然而,JSON 对象通常比前一个例子更复杂。这里一个Persons.json文件由三个persons组成:
{
"persons": {
"groupname": "school",
"person":
[
{"firstname":"Smith",
"lastname":"Peter",
"phone":8475552222,
"address":["100 Main Street","Corpus","Oklahoma"] },
{"firstname":"King",
"lastname":"Sarah",
"phone":8475551111,
"address":["200 Main Street","Corpus","Oklahoma"] },
{"firstname":"Frost",
"lastname":"Nathan",
"phone":8475553333,
"address":["300 Main Street","Corpus","Oklahoma"] }
]
}
}
为了处理这个文件,我们使用了与前面所示类似的一组代码。我们创建解析器,然后像以前一样进入一个循环:
try {
JsonFactory jsonfactory = new JsonFactory();
JsonParser parser = jsonfactory.createParser(new File("Person.json"));
while (parser.nextToken() != JsonToken.END_OBJECT) {
String token = parser.getCurrentName();
...
}
parser.close();
} catch (IOException ex) {
// Handle exceptions
}
然而,我们需要找到persons字段,然后提取它的每个元素。提取并显示groupname字段,如下所示:
if ("persons".equals(token)) {
JsonToken jsonToken = parser.nextToken();
jsonToken = parser.nextToken();
token = parser.getCurrentName();
if ("groupname".equals(token)) {
parser.nextToken();
String groupname = parser.getText();
out.println("Group : " + groupname);
...
}
}
接下来,我们找到person字段并调用一个parsePerson方法来更好地组织代码:
parser.nextToken();
token = parser.getCurrentName();
if ("person".equals(token)) {
out.println("Found person");
parsePerson(parser);
}
接下来的parsePerson方法与第一个例子中使用的过程非常相似。
public void parsePerson(JsonParser parser) throws IOException {
while (parser.nextToken() != JsonToken.END_ARRAY) {
String token = parser.getCurrentName();
if ("firstname".equals(token)) {
parser.nextToken();
String fname = parser.getText();
out.println("firstname : " + fname);
}
if ("lastname".equals(token)) {
parser.nextToken();
String lname = parser.getText();
out.println("lastname : " + lname);
}
if ("phone".equals(token)) {
parser.nextToken();
long phone = parser.getLongValue();
out.println("phone : " + phone);
}
if ("address".equals(token)) {
out.println("address :");
parser.nextToken();
while (parser.nextToken() != JsonToken.END_ARRAY) {
out.println(parser.getText());
}
}
}
}
输出如下:
Group : school
Found person
firstname : Smith
lastname : Peter
phone : 8475552222
address :
100 Main Street
Corpus
Oklahoma
firstname : King
lastname : Sarah
phone : 8475551111
address :
200 Main Street
Corpus
Oklahoma
firstname : Frost
lastname : Nathan
phone : 8475553333address :
300 Main Street
Corpus
Oklahoma
使用 JSON 树 API
第二种方法是使用树模型。一个ObjectMapper实例用于使用Persons.json文件创建一个JsonNode实例。fieldNames方法返回Iterator,允许我们处理文件的每个元素。
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(new File("Persons.json"));
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
...
fieldNames.next();
}
} catch (IOException ex) {
// Handle exceptions
}
由于 JSON 文件包含一个persons字段,我们将获得一个表示该字段的JsonNode实例,然后遍历它的每个元素。
JsonNode personsNode = node.get("persons");
Iterator<JsonNode> elements = personsNode.iterator();
while (elements.hasNext()) {
...
}
一次处理一个元素。如果元素类型是字符串,我们假设这是groupname字段。
JsonNode element = elements.next();
JsonNodeType nodeType = element.getNodeType();
if (nodeType == JsonNodeType.STRING) {
out.println("Group: " + element.textValue());
}
如果元素是一个数组,我们假设它包含一系列人,每个人都由parsePerson方法处理:
if (nodeType == JsonNodeType.ARRAY) {
Iterator<JsonNode> fields = element.iterator();
while (fields.hasNext()) {
parsePerson(fields.next());
}
}
parsePerson方法如下所示:
public void parsePerson(JsonNode node) {
Iterator<JsonNode> fields = node.iterator();
while(fields.hasNext()) {
JsonNode subNode = fields.next();
out.println(subNode.asText());
}
}
输出如下:
Group: school
Smith
Peter
8475552222
King
Sarah
8475551111
Frost
Nathan
8475553333
JSON 的内容比我们在这里能够说明的要多得多。但是,这应该会让您了解如何处理这种类型的数据。
清理文本的本质
字符串用于支持文本处理,所以使用一个好的字符串库是很重要的。不幸的是,java.lang.String类有一些限制。要解决这些限制,您可以根据需要实现自己的特殊字符串函数,也可以使用第三方库。
创建你自己的库是有用的,但是你基本上是在重新发明轮子。编写一个简单的代码序列来实现某些功能可能会更快,但是为了正确地完成任务,您需要对它们进行测试。第三方库已经过测试,并在数百个项目中使用过。它们提供了一种更有效的处理文本的方式。
除了 Java 中的那些之外,还有几个文本处理 API。我们将演示其中的两个:
- Apache common:https://commons . Apache . org/
- 番石榴:【github.com/google/guav…
Java 为清理文本数据提供了许多支持,包括String类中的方法。这些方法非常适合简单的文本清理和少量数据,但对于较大的复杂数据集也很有效。我们稍后将演示几个String类方法。下表总结了一些最有用的String类方法:
| 方法名 | 返回类型 | 描述 |
| trim | String | 删除前导空格和尾随空格 |
| toUpperCase / toLowerCase | String | 更改整个字符串的大小写 |
| replaceAll | String | 替换字符串中出现的所有字符序列 |
| contains | boolean | 确定字符串中是否存在给定的字符序列 |
| compareTo``compareToIgnoreCase | int | 对两个字符串进行词法比较,并返回表示它们之间关系的整数 |
| matches | boolean | 确定字符串是否与给定的正则表达式匹配 |
| join | String | 用指定的分隔符组合两个或多个字符串 |
| split | String[] | 使用指定的分隔符分隔给定字符串的元素 |
正则表达式的使用简化了许多文本操作。正则表达式使用标准化的语法来表示文本中的模式,这可用于定位和操作与模式匹配的文本。
正则表达式本身就是一个字符串。例如,字符串Hello, my name is Sally可以用作正则表达式来查找给定文本中的精确单词。这是非常具体的,并不广泛适用,但我们可以使用不同的正则表达式,使我们的代码更有效。Hello, my name is \\w将匹配任何以Hello, my name is开头并以单词字符结尾的文本。
我们将使用几个更复杂的正则表达式的例子,下表总结了一些更有用的语法选项。注:在 Java 应用程序中使用时,每个都必须双转义。
| 选项 | 描述 |
| \d | 任意数字: 0-9 |
| \D | 任何非数字 |
| \s | 任何空白字符 |
| \S | 任何非空白字符 |
| \w | 任意单词字符(包括数字): A-Z 、 a-z 、 0-9 |
| \W | 任何非单词字符 |
文本数据的大小和来源因应用程序而异,但用于转换数据的方法是相同的。你可能真的需要从一个文件中读取数据,但是为了简单起见,我们将使用一个包含赫尔曼·梅尔维尔的《莫比·迪克》开头句子的字符串作为本章的几个例子。除非另有说明,否则将假定文本如下所示:
String dirtyText = "Call me Ishmael. Some years ago- never mind how";
dirtyText += " long precisely - having little or no money in my purse,";
dirtyText += " and nothing particular to interest me on shore, I thought";
dirtyText += " I would sail about a little and see the watery part of the world.";
使用 Java 分词器提取单词
通常,将文本数据作为标记进行分析是最有效的。核心 Java 库中有多个可用的记号赋予器,第三方记号赋予器也是如此。我们将在这一章中演示各种记号化器。理想的记号赋予器将取决于单个应用程序的限制和要求。
Java 核心令牌化器
是第一个也是最基本的记号赋予器,从 Java 1 开始就有了。不推荐在新的开发中使用,因为String类的split方法被认为更有效。虽然它确实为具有窄定义和集合分隔符的文件提供了速度优势,但它不如其他记号赋予器选项灵活。下面是一个简单的StringTokenizer类的实现,它在空格上分割一个字符串:
StringTokenizer tokenizer = new StringTokenizer(dirtyText," ");
while(tokenizer.hasMoreTokens()){
out.print(tokenizer.nextToken() + " ");
}
当我们设置dirtyText变量来保存来自莫比·迪克的文本时,如前所示,我们得到以下截断的输出:
Call me Ishmael. Some years ago- never mind how long precisely...
StreamTokenizer是另一个核心的 Java 标记器。StreamTokenizer提供了更多关于检索到的令牌的信息,并允许用户指定要解析的数据类型,但被认为比StreamTokenizer或split方法更难使用。String类split方法是基于分隔符拆分字符串的最简单方法,但是它不提供解析拆分后的字符串的方法,并且您只能为整个字符串指定一个分隔符。由于这些原因,它不是一个真正的记号赋予器,但是它对于数据清理很有用。
Scanner类被设计成允许你将字符串解析成不同的数据类型。我们之前在处理 CSV 数据部分使用过它,我们将在移除停用词部分再次处理它。
第三方标记器和库
Apache Commons 由一组开源 Java 类和方法组成。这些提供了补充标准 Java APIs 的可重用代码。公地中包含的一个受欢迎的类是StrTokenizer。这个类提供了比标准的StringTokenizer类更高级的支持,特别是更多的控制和灵活性。下面是StrTokenizer的一个简单实现:
StrTokenizer tokenizer = new StrTokenizer(text);
while (tokenizer.hasNext()) {
out.print(tokenizer.next() + " ");
}
这与StringTokenizer的操作方式类似,默认情况下解析空格上的标记。构造函数可以指定分隔符以及如何处理数据中包含的双引号。
当我们使用前面显示的来自莫比·迪克的字符串时,第一个记号赋予器实现产生以下截断的输出:
Call me Ishmael. Some years ago- never mind how long precisely - having little or no money in my purse...
我们可以如下修改我们的构造函数:
StrTokenizer tokenizer = new StrTokenizer(text,",");
该实现的输出是:
Call me Ishmael. Some years ago- never mind how long precisely - having little or no money in my purse
and nothing particular to interest me on shore
I thought I would sail about a little and see the watery part of the world.
注意每一行是如何在原文中逗号的地方被分割的。这个定界符可以是一个简单的字符,正如我们已经展示的,也可以是一个更复杂的StrMatcher对象。
Google Guava 是一组开源的实用 Java 类和方法。与许多 API 一样,Guava 的主要目标是减轻编写基本 Java 实用程序的负担,以便开发人员可以专注于业务流程。我们将在本章中讨论 Guava 中的两个主要工具:Joiner类和Splitter类。标记化是在 Guava 中使用其Splitter类的split方法完成的。下面是一个简单的例子:
Splitter simpleSplit = Splitter.on(',').omitEmptyStrings().trimResults();
Iterable<String> words = simpleSplit.split(dirtyText);
for(String token: words){
out.print(token);
}
这会将每个标记用逗号分开,并产生类似于我们上一个示例的输出。我们可以修改on方法的参数来分割我们选择的字符。注意 chaining 方法,它允许我们省略空字符串并修剪前导和尾随空格。由于这些原因以及其他高级功能,Google Guava 被一些人认为是 Java 可用的最好的标记器。
LingPipe 是一个可用于 Java 语言处理的语言工具包。它通过它的TokenizerFactory接口为文本分割提供了更专业的支持。我们在简单文本清理部分实现了一个 LingPipe IndoEuropeanTokenizerFactory记号化器。
将数据转换成可用的形式
一旦获得数据,通常需要对其进行清理。数据集通常不一致,缺少信息,并且包含无关信息。在这一节中,我们将研究一些简单的方法来转换文本数据,使其更有用,更易于分析。
简单的文本清理
我们将使用之前显示的来自莫比·迪克的字符串来演示一些基本的String类方法。注意toLowerCase和trim方法的使用。数据集通常有非标准的大小写和额外的前导或尾随空格。这些方法确保了数据集的一致性。我们也使用两次replaceAll方法。在第一个实例中,我们使用一个正则表达式用一个空格替换所有数字和任何不是单词或空白字符的内容。第二个实例用一个空格替换所有连续的空白字符:
out.println(dirtyText);
dirtyText = dirtyText.toLowerCase().replaceAll("[\\d[^\\w\\s]]+", " ");
dirtyText = dirtyText.trim();
while(dirtyText.contains(" ")){
dirtyText = dirtyText.replaceAll(" ", " ");
}
out.println(dirtyText);
执行时,代码会产生以下截断的输出:
Call me Ishmael. Some years ago- never mind how long precisely -
call me ishmael some years ago never mind how long precisely
我们的下一个例子产生了相同的结果,但是用正则表达式解决了这个问题。在这种情况下,我们首先替换所有的数字和其他特殊字符。然后,我们使用方法链接来标准化我们的大小写,删除前导和尾随空格,并将我们的单词拆分到一个String数组中。split方法允许您在给定的分隔符上拆分文本。在这种情况下,我们选择使用正则表达式\\W,它表示任何不是单词字符的东西:
out.println(dirtyText);
dirtyText = dirtyText.replaceAll("[\\d[^\\w\\s]]+", "");
String[] cleanText = dirtyText.toLowerCase().trim().split("[\\W]+");
for(String clean : cleanText){
out.print(clean + " ");
}
这段代码产生与前面所示相同的输出。
尽管数组对许多应用程序都很有用,但在清理后重新组合文本通常很重要。在下一个例子中,一旦我们清理了单词,我们就使用join方法来组合它们。我们使用与前面相同的链接方法来清理和分割我们的文本。join方法连接数组words中的每个单词,并在每个单词之间插入一个空格:
out.println(dirtyText);
String[] words = dirtyText.toLowerCase().trim().split("[\\W\\d]+");
String cleanText = String.join(" ", words);
out.println(cleanText);
同样,这段代码产生与前面所示相同的输出。使用谷歌番石榴可以获得另一种版本的join方法。下面是我们之前使用的相同过程的一个简单实现,但是使用了 Guava Joiner类:
out.println(dirtyText);
String[] words = dirtyText.toLowerCase().trim().split("[\\W\\d]+");
String cleanText = Joiner.on(" ").skipNulls().join(words);
out.println(cleanText);
这个版本提供了额外的选项,包括跳过空值,如前所示。输出保持不变。
删除停用词
文本分析有时需要省略常见的、非特定的单词,如*、、和,或但*。这些词被称为停用词,有几种工具可以将它们从文本中删除。有各种方法来存储停用词列表,但是对于下面的例子,我们将假设它们包含在一个文件中。首先,我们创建一个新的Scanner对象来读取我们的停用词。然后,我们使用Arrays类的asList方法将我们希望转换的文本存储在ArrayList中。这里我们假设文本已经被清理和规范化。在使用String类方法时,考虑大小写是很重要的,因为和不同于和或者和,尽管这三个可能都是您希望消除的停用词:
Scanner readStop = new Scanner(new File("C://stopwords.txt"));
ArrayList<String> words = new ArrayList<String>(Arrays.asList((dirtyText));
out.println("Original clean text: " + words.toString());
我们还创建了一个新的ArrayList来保存在我们的文本中实际找到的停用词列表。这将允许我们很快使用ArrayList类removeAll方法。接下来,我们使用我们的Scanner来通读我们的停用词文件。注意我们也是如何针对每个停用词调用toLowerCase和trim方法的。这是为了确保我们的停用词与文本中的格式相匹配。在这个例子中,我们使用contains方法来确定我们的文本是否包含给定的停用词。如果是这样,我们将它添加到我们的foundWords数组列表中。一旦我们处理完所有的停用词,我们调用removeAll将它们从我们的文本中删除:
ArrayList<String> foundWords = new ArrayList();
while(readStop.hasNextLine()){
String stopWord = readStop.nextLine().toLowerCase();
if(words.contains(stopWord)){
foundWords.add(stopWord);
}
}
words.removeAll(foundWords);
out.println("Text without stop words: " + words.toString());
输出将取决于被指定为停止字的字。如果停用字词文件包含的字词不同于本示例中使用的字词,则输出会略有不同。我们的输出如下:
Original clean text: [call, me, ishmael, some, years, ago, never, mind, how, long, precisely, having, little, or, no, money, in, my, purse, and, nothing, particular, to, interest, me, on, shore, i, thought, i, would, sail, about, a, little, and, see, the, watery, part, of, the, world]
Text without stop words: [call, ishmael, years, ago, never, mind, how, long, precisely
标准 Java 库之外也支持删除停用词。我们来看一个例子,使用 LingPipe。在这个例子中,我们首先确保我们的文本被规范化为小写并被修剪。然后我们创建一个TokenizerFactory类的新实例。我们将工厂设置为使用默认的英语停用词,然后对文本进行标记。注意,tokenizer方法使用了一个char数组,所以我们针对我们的文本调用toCharArray。第二个参数指定文本中开始搜索的位置,最后一个参数指定结束位置:
text = text.toLowerCase().trim();
TokenizerFactory fact = IndoEuropeanTokenizerFactory.INSTANCE;
fact = new EnglishStopTokenizerFactory(fact);
Tokenizer tok = fact.tokenizer(text.toCharArray(), 0, text.length());
for(String word : tok){
out.print(word + " ");
}
输出如下:
Call me Ishmael. Some years ago- never mind how long precisely - having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world.
call me ishmael . years ago - never mind how long precisely - having little money my purse , nothing particular interest me shore , i thought i sail little see watery part world .
请注意我们之前的例子之间的差异。首先,我们没有彻底清理文本,并允许特殊字符(如连字符)保留在文本中。其次,LingPipe 的停用词列表不同于我们在前面的例子中使用的文件。一些单词被删除了,但是 LingPipe 限制更少,允许更多的单词保留在文本中。您使用的停用字词的类型和数量将取决于您的特定应用。
在文本中查找单词
标准 Java 库支持在文本中搜索特定的标记。在前面的例子中,我们已经演示了matches方法和正则表达式,它们在搜索文本时会很有用。然而,在这个例子中,我们将演示一个简单的技术,使用contains方法和equals方法来定位一个特定的字符串。首先,我们规范化我们的文本和我们正在搜索的单词,以确保我们可以找到匹配。我们还创建了一个整数变量来保存单词被找到的次数:
dirtyText = dirtyText.toLowerCase().trim();
toFind = toFind.toLowerCase().trim();
int count = 0;
接下来,我们调用contains方法来确定这个单词是否存在于我们的文本中。如果是,我们将文本分割成一个数组,然后循环遍历,使用equals方法来比较每个单词。如果我们遇到这个单词,我们就把计数器加 1。最后,我们显示输出,以显示我们的单词被遇到的次数:
if(dirtyText.contains(toFind)){
String[] words = dirtyText.split(" ");
for(String word : words){
if(word.equals(toFind)){
count++;
}
}
out.println("Found " + toFind + " " + count + " times in the text.");
}
在这个例子中,我们将toFind设置为字母I。这产生了以下输出:
Found i 2 times in the text.
我们还可以选择使用Scanner类来搜索整个文件。一个有用的方法是findWithinHorizon方法。这使用了一个Scanner来解析文本直到一个给定的 horizon 规范。如果第二个参数为 0,如下所示,默认情况下将搜索整个Scanner:
dirtyText = dirtyText.toLowerCase().trim();
toFind = toFind.toLowerCase().trim();
Scanner textLine = new Scanner(dirtyText);
out.println("Found " + textLine.findWithinHorizon(toFind, 10));
这种技术可以更有效地定位特定的字符串,但它确实使确定在哪里找到该字符串以及找到该字符串的次数变得更加困难。
使用BufferedReader搜索整个文件也会更有效。我们指定要搜索的文件,并使用 try-catch 块来捕获任何 IO 异常。我们从我们的路径创建一个新的BufferedReader对象,只要下一行不为空,就处理我们的文件:
String path = "C:// MobyDick.txt";
try {
String textLine = "";
toFind = toFind.toLowerCase().trim();
BufferedReader textToClean = new BufferedReader(
new FileReader(path));
while((textLine = textToClean.readLine()) != null){
line++;
if(textLine.toLowerCase().trim().contains(toFind)){
out.println("Found " + toFind + " in " + textLine);
}
}
textToClean.close();
} catch (IOException ex) {
// Handle exceptions
}
我们再次通过在莫比·迪克的第一句话中搜索单词I来测试我们的数据。截断的输出如下:
Found i in Call me Ishmael...
查找和替换文本
我们经常不仅想找到文本,还想用其他东西替换它。我们开始下一个例子,就像我们之前的例子一样,通过指定我们的文本,我们要定位的文本,并调用contains方法。如果我们找到了文本,我们调用replaceAll方法来修改我们的字符串:
text = text.toLowerCase().trim();
toFind = toFind.toLowerCase().trim();
out.println(text);
if(text.contains(toFind)){
text = text.replaceAll(toFind, replaceWith);
out.println(text);
}
为了测试这段代码,我们将toFind设置为单词I,将replaceWith设置为Ishmael。我们的输出如下:
call me ishmael. some years ago- never mind how long precisely - having little or no money in my purse, and nothing particular to interest me on shore, i thought i would sail about a little and see the watery part of the world.
call me ishmael. some years ago- never mind how long precisely - having little or no money in my purse, and nothing particular to interest me on shore, Ishmael thought Ishmael would sail about a little and see the watery part of the world.
Apache Commons 还提供了一个在StringUtils类中有几个变体的replace方法。这个类提供了与String类相同的功能,但是有更多的灵活性和选项。在下面的例子中,我们使用来自莫比·迪克的字符串,将单词me的所有实例替换为X来演示replace方法:
out.println(text);
out.println(StringUtils.replace(text, "me", "X"));
截断的输出如下:
Call me Ishmael. Some years ago- never mind how long precisely -
Call X Ishmael. SoX years ago- never mind how long precisely -
请注意me的每个实例是如何被替换的,甚至是那些包含在其他单词中的实例,例如some.这可以通过在me周围添加空格来避免,尽管这将忽略任何 me 位于句子末尾的实例,例如 me。我们将在一会儿用谷歌番石榴检查一个更好的选择。
StringUtils类还提供了一个replacePattern方法,允许您基于正则表达式搜索和替换文本。在以下示例中,我们用一个空格替换所有非单词字符,如连字符和逗号:
out.println(text);
text = StringUtils.replacePattern(text, "\\W\\s", " ");
out.println(text);
这将产生以下截断的输出:
Call me Ishmael. Some years ago- never mind how long precisely -
Call me Ishmael Some years ago never mind how long precisely
Google Guava 使用CharMatcher类为匹配和修改文本数据提供了额外的支持。CharMatcher不仅允许您查找匹配特定字符模式的数据,还提供了如何处理数据的选项。这包括允许您保留数据、替换数据以及从特定字符串中修剪空白。
在这个例子中,我们将使用replace方法简单地用一个空格替换单词me的所有实例。这将在我们的文本中产生一系列的空白。然后,我们将使用trimAndCollapseFrom方法折叠多余的空格,并再次打印我们的字符串:
text = text.replace("me", " ");
out.println("With double spaces: " + text);
String spaced = CharMatcher.WHITESPACE.trimAndCollapseFrom(text, ' ');
out.println("With double spaces removed: " + spaced);
我们的输出被截断如下:
With double spaces: Call Ishmael. So years ago- ...
With double spaces removed: Call Ishmael. So years ago- ...
数据插补
数据插补是指识别和替换给定数据集中缺失数据的过程。在几乎所有的数据分析案例中,缺失数据都是一个问题,需要在正确分析数据之前解决这个问题。试图处理缺失信息的数据就像试图理解一场偶尔会漏掉一个词的对话。有时我们能理解意图是什么。在其他情况下,我们可能完全不知道想要传达什么。
在统计分析人员中,对于如何处理缺失数据存在不同意见,但最常见的方法是用合理的估计值或空值替换缺失数据。
为了防止数据偏斜和错位,许多统计学家主张用代表该数据集平均值或期望值的值来替换缺失的数据。确定代表值并将其分配到数据中某个位置的方法会因数据而异,我们无法在本章中一一举例说明。但是,例如,如果数据集包含某个日期范围内的温度列表,并且某个日期缺少温度,则可以为该日期指定一个温度,该温度是数据集内温度的平均值。
我们将研究一个相当琐碎的例子来说明围绕数据插补的问题。让我们假设变量tempList包含一年中每个月的平均温度数据。然后,我们执行简单的平均值计算,并打印出结果:
double[] tempList = {50,56,65,70,74,80,82,90,83,78,64,52};
double sum = 0;
for(double d : tempList){
sum += d;
}
out.printf("The average temperature is %1$,.2f", sum/12);
请注意,对于该执行中使用的数字,输出如下:
The average temperature is 70.33
接下来,在计算我们的sum之前,我们将通过将数组的第一个元素更改为零来模拟缺失数据:
double sum = 0;
tempList[0] = 0;
for(double d : tempList){
sum += d;
}
out.printf("The average temperature is %1$,.2f", sum/12);
这将改变我们输出中显示的平均温度:
The average temperature is 66.17
请注意,虽然这种变化看起来很小,但在统计上却很重要。根据给定数据集内的变化以及平均值与零或其他替代值的差距,统计分析的结果可能会有很大偏差。这并不意味着不应该用零来代替空值或其他无效值,而是应该考虑其他替代值。
一种替代方法是计算数组中值的平均值,排除零或空值,然后用缺失数据替换每个位置的平均值。在做出这些决定时,考虑数据的类型和数据分析的目的是很重要的。例如,在前面的例子中,零是否总是无效的平均温度?如果南极洲的温度是平均的,也许不会。
当需要处理空数据时,Java 的Optional类提供了有用的解决方案。考虑下面的例子,我们有一个以数组形式存储的名字列表。为了演示这些方法,我们给null设置了一个值:
String useName = "";
String[] nameList =
{"Amy","Bob","Sally","Sue","Don","Rick",null,"Betsy"};
Optional<String> tempName;
for(String name : nameList){
tempName = Optional.ofNullable(name);
useName = tempName.orElse("DEFAULT");
out.println("Name to use = " + useName);
}
我们首先创建了一个名为useName的变量来保存我们将实际打印出来的名称。我们还创建了一个名为tempName的Optional类的实例。我们将用它来测试数组中的值是否为空。然后我们遍历数组,创建并调用Optional类ofNullable方法。这个方法测试一个特定的值是否为空。在下一行,我们调用orElse方法将数组中的一个值赋给useName,或者如果元素为空,则赋给DEFAULT。我们的输出如下:
Name to use = Amy
Name to use = Bob
Name to use = Sally
Name to use = Sue
Name to use = Don
Name to use = Rick
Name to use = DEFAULT
Name to use = Betsy
Optional类包含其他几个用于处理潜在空数据的方法。尽管有其他方法来处理这样的实例,但是 Java 8 的这一新增功能为常见的数据分析问题提供了更简单、更优雅的解决方案。
子集化数据
处理一整套数据并不总是实际可行的,也不总是可取的。在这些情况下,我们可能希望检索数据的子集,以便处理或从数据集中完全删除。标准 Java 库支持几种方法。首先,我们将使用SortedSet接口的subSet方法。我们将从在一个TreeSet中存储一个数字列表开始。然后我们创建一个新的TreeSet对象来保存从列表中检索到的子集。接下来,我们打印出原始列表:
Integer[] nums = {12, 46, 52, 34, 87, 123, 14, 44};
TreeSet<Integer> fullNumsList = new TreeSet<Integer>(new
ArrayList<>(Arrays.asList(nums)));
SortedSet<Integer> partNumsList;
out.println("Original List: " + fullNumsList.toString()
+ " " + fullNumsList.last());
subSet方法有两个参数,它们指定了我们想要检索的数据中的整数范围。第一个参数包含在结果中,而第二个参数是唯一的。在下面的例子中,我们希望检索数组中第一个数字12和46之间所有数字的子集:
partNumsList = fullNumsList.subSet(fullNumsList.first(), 46);
out.println("SubSet of List: " + partNumsList.toString()
+ " " + partNumsList.size());
我们的输出如下:
Original List: [12, 14, 34, 44, 46, 52, 87, 123]
SubSet of List: [12, 14, 34, 44]
另一种选择是结合使用stream方法和skip方法。stream方法返回一个 Java 8 流实例,该实例对集合进行迭代。我们将使用与上一个例子相同的numsList,但是这一次我们将使用skip方法指定跳过多少个元素。我们还将使用collect方法创建一个新的Set来保存新元素:
out.println("Original List: " + numsList.toString());
Set<Integer> fullNumsList = new TreeSet<Integer>(numsList);
Set<Integer> partNumsList = fullNumsList
.stream()
.skip(5)
.collect(toCollection(TreeSet::new));
out.println("SubSet of List: " + partNumsList.toString());
当我们打印出新的子集时,我们得到下面的输出,其中跳过了排序集的前五个元素。因为它是一个SortedSet,我们实际上将省略五个最低的数字:
Original List: [12, 46, 52, 34, 87, 123, 14, 44]
SubSet of List: [52, 87, 123]
有时,数据会以空行或标题行开始,我们希望从要分析的数据集中删除这些空行或标题行。在我们的最后一个例子中,我们将从文件中读取数据并删除所有的空行。我们使用一个BufferedReader来读取数据,并使用一个 lambda 表达式来测试空行。如果该行不为空,我们将该行打印到屏幕上:
try (BufferedReader br = new BufferedReader(new FileReader("C:\\text.txt"))) {
br
.lines()
.filter(s -> !s.equals(""))
.forEach(s -> out.println(s));
} catch (IOException ex) {
// Handle exceptions
}
排序文本
有时候在清理过程中需要对数据进行排序。标准 Java 库为完成不同类型的排序提供了一些资源,Java 8 的发布增加了一些改进。在我们的第一个例子中,我们将结合 lambda 表达式使用Comparator接口。
我们从声明变量compareInts开始。等号后面的第一组括号包含要传递给我们的方法的参数。在 lambda 表达式中,我们调用compare方法,它决定哪个整数更大:
Comparator<Integer> compareInts = (Integer first, Integer second) ->
Integer.compare(first, second);
我们现在可以像以前一样调用sort方法:
Collections.sort(numsList,compareInts);
out.println("Sorted integers using Lambda: " + numsList.toString());
我们的输出如下:
Sorted integers using Lambda: [12, 14, 34, 44, 46, 52, 87, 123]
然后我们用wordsList模拟这个过程。注意使用了compareTo方法,而不是compare:
Comparator<String> compareWords = (String first, String second) -> first.compareTo(second);
Collections.sort(wordsList,compareWords);
out.println("Sorted words using Lambda: " + wordsList.toString());
当执行这段代码时,我们应该会看到以下输出:
Sorted words using Lambda: [boat, cat, dog, house, road, zoo]
在下一个例子中,我们将使用Collections类对String和整数数据进行基本排序。在本例中,wordList和numsList都是ArrayList,初始化如下:
List<String> wordsList
= Stream.of("cat", "dog", "house", "boat", "road", "zoo")
.collect(Collectors.toList());
List<Integer> numsList = Stream.of(12, 46, 52, 34, 87, 123, 14, 44)
.collect(Collectors.toList());
首先,我们将打印每个列表的原始版本,然后调用sort方法。然后我们显示我们的数据,按升序排列:
out.println("Original Word List: " + wordsList.toString());
Collections.sort(wordsList);
out.println("Ascending Word List: " + wordsList.toString());
out.println("Original Integer List: " + numsList.toString());
Collections.sort(numsList);
out.println("Ascending Integer List: " + numsList.toString());
输出如下:
Original Word List: [cat, dog, house, boat, road, zoo]
Ascending Word List: [boat, cat, dog, house, road, zoo]
Original Integer List: [12, 46, 52, 34, 87, 123, 14, 44]
Ascending Integer List: [12, 14, 34, 44, 46, 52, 87, 123]
接下来,我们将在整数数据示例中用Collections类的reverse方法替换sort方法。这个方法简单地获取元素并以相反的顺序存储它们:
out.println("Original Integer List: " + numsList.toString());
Collections.reverse(numsList);
out.println("Reversed Integer List: " + numsList.toString());
输出显示我们新的numsList:
Original Integer List: [12, 46, 52, 34, 87, 123, 14, 44]
Reversed Integer List: [44, 14, 123, 87, 34, 52, 46, 12]
在下一个例子中,我们使用Comparator接口来处理排序。我们将继续使用我们的numsList,并假设排序还没有发生。首先我们创建两个实现Comparator接口的对象。sort方法将在比较两个元素时使用这些对象来确定所需的顺序。表达式Integer::compare是一个 Java 8 方法引用。这可用于使用 lambda 表达式的情况:
out.println("Original Integer List: " + numsList.toString());
Comparator<Integer> basicOrder = Integer::compare;
Comparator<Integer> descendOrder = basicOrder.reversed();
Collections.sort(numsList,descendOrder);
out.println("Descending Integer List: " + numsList.toString());
执行这段代码后,我们将看到以下输出:
Original Integer List: [12, 46, 52, 34, 87, 123, 14, 44]
Descending Integer List: [123, 87, 52, 46, 44, 34, 14, 12]
在最后一个例子中,我们将尝试一个更复杂的排序,包括两个比较。让我们假设有一个包含两个属性name和age的Dog类,以及必要的访问方法。我们将开始向一个新的ArrayList添加元素,然后打印每个Dog的名字和年龄:
ArrayList<Dogs> dogs = new ArrayList<Dogs>();
dogs.add(new Dogs("Zoey", 8));
dogs.add(new Dogs("Roxie", 10));
dogs.add(new Dogs("Kylie", 7));
dogs.add(new Dogs("Shorty", 14));
dogs.add(new Dogs("Ginger", 7));
dogs.add(new Dogs("Penny", 7));
out.println("Name " + " Age");
for(Dogs d : dogs){
out.println(d.getName() + " " + d.getAge());
}
我们的输出应该类似于:
Name Age
Zoey 8
Roxie 10
Kylie 7
Shorty 14
Ginger 7
Penny 7
接下来,我们将使用方法链接和双冒号操作符来引用来自Dog类的方法。我们首先调用comparing,然后调用thenComparing,以指定比较发生的顺序。当我们执行代码时,我们希望看到Dog对象首先按照Name排序,然后按照Age排序:
dogs.sort(Comparator.comparing(Dogs::getName).thenComparing(Dogs::getAge));
out.println("Name " + " Age");
for(Dogs d : dogs){
out.println(d.getName() + " " + d.getAge());
}
我们的输出如下:
Name Age
Ginger 7
Kylie 7
Penny 7
Roxie 10
Shorty 14
Zoey 8
现在我们将交换比较的顺序。请注意,在这个版本中,狗的年龄优先于名字:
dogs.sort(Comparator.comparing(Dogs::getAge).thenComparing(Dogs::getName));
out.println("Name " + " Age");
for(Dogs d : dogs){
out.println(d.getName() + " " + d.getAge());
}
我们的输出是:
Name Age
Ginger 7
Kylie 7
Penny 7
Zoey 8
Roxie 10
Shorty 14
数据验证
数据验证是数据科学的重要组成部分。在我们能够分析和操作数据之前,我们需要验证数据是预期的类型。我们已经将代码组织成简单的方法,用于完成非常基本的验证任务。这些方法中的代码可以适用于现有的应用程序。
验证数据类型
有时我们只需要验证一段数据是否属于特定类型,比如整数或浮点数据。我们将在下一个例子中演示如何使用validateIn t 方法验证整数数据。对于标准 Java 库中支持的其他主要数据类型,包括Float和Double,这种技术很容易修改。
我们需要在这里使用一个 try-catch 块来捕捉一个NumberFormatException。如果抛出异常,我们知道我们的数据不是有效的整数。我们首先将待测试的文本传递给Integer类的parseInt方法。如果文本可以被解析为一个整数,我们只需打印出这个整数。如果抛出一个异常,我们会显示相应的信息:
public static void validateInt(String toValidate){
try{
int validInt = Integer.parseInt(toValidate);
out.println(validInt + " is a valid integer");
}catch(NumberFormatException e){
out.println(toValidate + " is not a valid integer");
}
我们将使用以下方法调用来测试我们的方法:
validateInt("1234");
validateInt("Ishmael");
输出如下:
1234 is a valid integer
Ishmael is not a valid integer
Apache Commons 包含一个具有额外有用功能的IntegerValidator类。在第一个例子中,我们简单地重复之前的过程,但是使用IntegerValidator方法来实现我们的目标:
public static String validateInt(String text){
IntegerValidator intValidator = IntegerValidator.getInstance();
if(intValidator.isValid(text)){
return text + " is a valid integer";
}else{
return text + " is not a valid integer";
}
}
我们再次使用下面的方法调用来测试我们的方法:
validateInt("1234");
validateInt("Ishmael");
输出如下:
1234 is a valid integer
Ishmael is not a valid integer
IntegerValidator类还提供了一些方法来确定一个整数是大于还是小于一个特定值,将该数字与一组数字进行比较,并将Number对象转换为Integer对象。Apache Commons 有许多其他的验证器类。我们将在本节的剩余部分研究更多的内容。
验证日期
很多时候,我们的数据验证比简单地确定一段数据是否是正确的类型更复杂。例如,当我们想要验证数据是一个日期时,仅仅验证它是由整数组成的是不够的。我们可能需要包括连字符和斜线,或者确保年份是两位数或四位数的格式。
为此,我们创建了另一个简单的方法validateDate。该方法有两个String参数,一个保存要验证的日期,另一个保存可接受的日期格式。我们使用参数中指定的格式创建了一个SimpleDateFormat类的实例。然后我们调用parse方法将我们的String日期转换成一个Date对象。就像我们前面的整数示例一样,如果数据不能被解析为日期,就会抛出一个异常,方法返回。但是,如果可以将String解析为日期,我们只需将测试日期的格式与我们可接受的格式进行比较,以确定日期是否有效:
public static String validateDate(String theDate, String dateFormat){
try {
SimpleDateFormat format = new SimpleDateFormat(dateFormat);
Date test = format.parse(theDate);
if(format.format(test).equals(theDate)){
return theDate.toString() + " is a valid date";
}else{
return theDate.toString() + " is not a valid date";
}
} catch (ParseException e) {
return theDate.toString() + " is not a valid date";
}
}
我们进行以下方法调用来测试我们的方法:
String dateFormat = "MM/dd/yyyy";
out.println(validateDate("12/12/1982",dateFormat));
out.println(validateDate("12/12/82",dateFormat));
out.println(validateDate("Ishmael",dateFormat));
输出如下:
12/12/1982 is a valid date
12/12/82 is not a valid date
Ishmael is not a valid date
这个例子强调了考虑对数据的限制的重要性。我们的第二个方法调用包含一个合法的日期,但是它不是我们指定的格式。如果我们正在寻找非常特殊格式的数据,这是很好的。但是,如果我们在验证中过于严格,我们也有遗漏有用数据的风险。
验证电子邮件地址
还经常需要验证电子邮件地址。虽然大多数电子邮件地址都有@符号,并要求符号后至少有一个句点,但也有许多变体。请考虑以下每个示例都可以是有效的电子邮件地址:
myemail@mail.comMyEmail@some.mail.comMy.Email.123!@mail.net
一种选择是使用正则表达式来尝试捕获所有允许的电子邮件地址。请注意,下面的方法中使用的正则表达式非常长且复杂。这很容易出错,错过有效的电子邮件地址,或者将无效地址视为有效地址。但是一个精心制作的正则表达式可能是一个非常强大的工具。
我们使用Pattern和Matcher类来编译和执行我们的正则表达式。如果我们传入的电子邮件模式与我们定义的正则表达式匹配,我们将认为该文本是有效的电子邮件地址:
public static String validateEmail(String email) {
String emailRegex = "^[a-zA-Z0-9.!$'*+/=?^_`{|}~-" +
"]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\." +
"[0-9]{1,3}\\])|(([a-zAZ\\-0-9]+\\.)+[a-zA-Z]{2,}))$";
Pattern.compile(emailRegex);
Matcher matcher = pattern.matcher(email);
if(matcher.matches()){
return email + " is a valid email address";
}else{
return email + " is not a valid email address";
}
}
我们调用以下方法来测试我们的数据:
out.println(validateEmail("myemail@mail.com"));
out.println(validateEmail("My.Email.123!@mail.net"));
out.println(validateEmail("myEmail"));
输出如下:
myemail@mail.com is a valid email address
My.Email.123!@mail.net is a valid email address
myEmail is not a valid email address
还有一个用于验证电子邮件地址的标准 Java 库。在这个例子中,我们使用InternetAddress类来验证给定的字符串是否是有效的电子邮件地址:
public static String validateEmailStandard(String email){
try{
InternetAddress testEmail = new InternetAddress(email);
testEmail.validate();
return email + " is a valid email address";
}catch(AddressException e){
return email + " is not a valid email address";
}
}
当使用与上例相同的数据进行测试时,我们的输出是相同的。但是,请考虑下面的方法调用:
out.println(validateEmailStandard("myEmail@mail"));
尽管不是标准的电子邮件格式,但输出如下:
myEmail@mail is a valid email address
此外,validate方法默认接受本地电子邮件地址作为有效地址。根据数据的用途,这并不总是可取的。
我们要看的最后一个选项是 Apache Commons EmailValidator类。这个类的isValid方法检查一个电子邮件地址,并确定它是否有效。我们之前展示的validateEmail方法修改如下以使用EmailValidator:
public static String validateEmailApache(String email){
email = email.trim();
EmailValidator eValidator = EmailValidator.getInstance();
if(eValidator.isValid(email)){
return email + " is a valid email address.";
}else{
return email + " is not a valid email address.";
}
}
验证邮政编码
邮政编码通常根据其所在国家或当地的要求进行格式化。因此,正则表达式非常有用,因为它们可以适应任何所需的邮政编码。下面的示例演示了如何验证标准的美国邮政编码,包括或不包括连字符和最后四位数字:
public static void validateZip(String zip){
String zipRegex = "^[0-9]{5}(?:-[0-9]{4})?$";
Pattern pattern = Pattern.compile(zipRegex);
Matcher matcher = pattern.matcher(zip);
if(matcher.matches()){
out.println(zip + " is a valid zip code");
}else{
out.println(zip + " is not a valid zip code");
}
}
我们调用以下方法来测试我们的数据:
out.println(validateZip("12345"));
out.println(validateZip("12345-6789"));
out.println(validateZip("123"));
输出如下:
12345 is a valid zip code
12345-6789 is a valid zip code
123 is not a valid zip code
验证姓名
名称可能特别难以验证,因为有太多的变体。除了键盘上可用的字符之外,没有行业标准或技术限制。对于这个例子,我们选择在正则表达式中使用 Unicode,因为它允许我们匹配任何语言的任何字符。Unicode 属性\\p{L}提供了这种灵活性。我们还使用 \\s-',允许名称字段中有空格、撇号、逗号和连字符。在尝试匹配名称之前,可以执行字符串清理,如本章前面所讨论的。这将简化所需的正则表达式:
public static void validateName(String name){
String nameRegex = "^[\\p{L}\\s-',]+$";
Pattern pattern = Pattern.compile(nameRegex);
Matcher matcher = pattern.matcher(name);
if(matcher.matches()){
out.println(name + " is a valid name");
}else{
out.println(name + " is not a valid name");
}
}
我们调用以下方法来测试我们的数据:
validateName("Bobby Smith, Jr.");
validateName("Bobby Smith the 4th");
validateName("Albrecht Müller");
validateName("François Moreau");
输出如下:
Bobby Smith, Jr. is a valid name
Bobby Smith the 4th is not a valid name
Albrecht Müller is a valid name
François Moreau is a valid name
注意,Bobby Smith, Jr.中的逗号和句号是可以接受的,但是4th中的4是不可以接受的。另外,François和Müller中的特殊字符被认为是有效的。
清洗图像
虽然图像处理是一项复杂的任务,我们将介绍一些技术来清理和提取图像中的信息。这将使读者对图像处理有所了解。我们还将演示如何使用光学字符识别(OCR)从图像中提取文本数据。
有几种技术可用于提高图像质量。其中许多需要调整参数来获得期望的改善。我们将演示如何:
-
增强图像的对比度
-
平滑图像
-
使图像变亮
-
调整图像大小
-
将图像转换为不同的格式
我们将使用 OpenCV(opencv.org/),一个用于图像处理的开源项目。我们将使用几个类:
Mat:这是一个保存图像数据的 n 维数组,比如通道、灰度或颜色值- 拥有许多处理图像的方法
Imgcodecs:拥有读写图像文件的方法
OpenCV Javadocs 位于docs.opencv.org/java/2.4.9/。在下面的例子中,我们将使用维基百科的图片,因为它们可以免费下载。具体来说,我们将使用以下图像:
- 鹦鹉图片:https://en . Wikipedia . org/wiki/gray #/media/File:gray _ 8 bits _ palette _ sample _ image . png
- 猫咪形象:https://en . Wikipedia . org/wiki/Cat #/media/File:kitty ply _ edit 1 . jpg
改变图像的对比度
这里我们将演示如何增强鹦鹉的黑白图像。Imgcodecs类的imread方法读入图像。它的第二个参数指定图像使用的颜色类型,在本例中是灰度。使用与原始图像相同的大小和颜色类型为增强图像创建一个新的Mat对象。
实际工作由equalizeHist方法执行。这均衡了图像的直方图,具有归一化亮度的效果,并增加了图像的对比度。图像直方图是表示图像色调分布的直方图。色调又称明度。它表示图像中亮度的变化。
最后一步是写出图像。
Mat source = Imgcodecs.imread("GrayScaleParrot.png",
Imgcodecs.CV_LOAD_IMAGE_GRAYSCALE);
Mat destination = new Mat(source.rows(), source.cols(), source.type());
Imgproc.equalizeHist(source, destination);
Imgcodecs.imwrite("enhancedParrot.jpg", destination);
以下是原图:
增强图像如下:
平滑图像
平滑图像,也称为模糊,会使图像的边缘更加平滑。模糊是使图像变得不那么清晰的过程。当我们在相机失焦的情况下拍照时,我们能识别模糊的物体。模糊可以用于特殊效果。在这里,我们将使用它来创建一个图像,然后我们将锐化。
下面的示例加载一幅猫的图像,并对该图像重复应用blur方法。在该示例中,该过程重复了25次。增加迭代次数将导致更多的模糊或平滑。
模糊方法的第三个参数是模糊核的大小。内核是一个像素矩阵,在本例中为 3×3,用于卷积。这是将图像的每个元素乘以其邻居的加权值的过程。这允许相邻值影响元素的值:
Mat source = Imgcodecs.imread("cat.jpg");
Mat destination = source.clone();
for (int i = 0; i < 25; i++) {
Mat sourceImage = destination.clone();
Imgproc.blur(sourceImage, destination, new Size(3.0, 3.0));
}
Imgcodecs.imwrite("smoothCat.jpg", destination);
以下是原图:
增强图像如下:
使图像变亮
convertTo方法提供了一种使图像变亮的方法。原始图像被复制到新图像,在新图像中对比度和亮度被调整。第一个参数是目标图像。第二个指定不应该更改图像的类型。第三和第四个参数分别控制对比度和亮度。第一个值与此值相乘,第二个值与相乘后的值相加:
Mat source = Imgcodecs.imread("cat.jpg");
Mat destination = new Mat(source.rows(), source.cols(),
source.type());
source.convertTo(destination, -1, 1, 50);
Imgcodecs.imwrite("brighterCat.jpg", destination);
增强图像如下:
调整图像大小
有时需要调整图像的大小。接下来显示的resize方法说明了这是如何实现的。读入图像并创建一个新的Mat对象。然后应用resize方法,在Size对象参数中指定宽度和高度。然后保存调整后的图像:
Mat source = Imgcodecs.imread("cat.jpg");
Mat resizeimage = new Mat();
Imgproc.resize(source, resizeimage, new Size(250, 250));
Imgcodecs.imwrite("resizedCat.jpg", resizeimage);
增强图像如下:
将图像转换成不同的格式
另一个常见的操作是将使用一种格式的图像转换为使用不同格式的图像。在 OpenCV 中,这很容易实现,如下所示。图像被读入,然后立即被写出。imwrite方法使用文件的扩展名将图像转换成新格式:
Mat source = Imgcodecs.imread("cat.jpg");
Imgcodecs.imwrite("convertedCat.jpg", source);
Imgcodecs.imwrite("convertedCat.jpeg", source);
Imgcodecs.imwrite("convertedCat.webp", source);
Imgcodecs.imwrite("convertedCat.png", source);
Imgcodecs.imwrite("convertedCat.tiff", source);
如果需要,这些图像现在可以用于专门的处理。
总结
很多时候,数据科学中一半的战斗是操纵数据,使它足够干净,可以使用。在这一章中,我们考察了许多获取真实世界中杂乱数据并将其转换成可用数据集的技术。这个过程通常被称为数据清理、争论、整形或蒙戈。我们的重点是核心 Java 技术,但是我们也研究了第三方库。
在清理数据之前,我们需要对数据的格式有一个坚实的理解。我们讨论了 CSV 数据、电子表格、PDF 和 JSON 文件类型,并提供了几个操作文本文件数据的例子。当我们检查文本数据时,我们查看了处理数据的多种方法,包括标记化器、Scanners和BufferedReaders。我们展示了执行简单清理操作、删除停用词以及执行查找和替换功能的方法。
本章还讨论了数据估算以及确定和纠正缺失数据情况的重要性。缺失数据会在数据分析过程中引起问题,我们提出了不同的方法来处理这个问题。我们演示了如何检索数据子集以及对数据进行排序。
最后,我们讨论了图像清理,并演示了几种修改图像数据的方法。这包括改变对比度、平滑、增亮和调整信息大小。最后,我们讨论了如何提取图像上的文字。
有了这个背景,我们将在下一章介绍基本的统计方法及其 Java 支持。