文章 8:在 Android 中处理输入和存储,使用实时在线意见极性挖掘应用






4.83/5 (9投票s)
通过实时应用学习简单的 Android 数据管理、SqlLite 数据库、XML Web 服务和文件数据库。
- 下载 OpinionMining_Final_Project.zip - 2.4 MB
- 下载 OpinionMining_SharedPreferences_Test.zip - 1.5 MB
- 下载 OpinionMining_SQLite_Test.zip - 1.4 MB
- 下载 OpinionMining_With_XML_Record-Test.zip - 1.4 MB
- 下载 opinionxmldata.zip - 280 B
- 下载 FlatFile_Record_Simple_Opinion_Mining.zip - 1.4 MB
- 下载 AndroidFlatFileAccess.zip - 1.4 KB
- 下载 OpinionData.txt - 56 B
目录
2.3 简单的 Android 应用,用于检查给定陈述的积极性
2.4.1 从 assets 文件夹将文件解压到 SD 卡应用目录
3.2.3 从 XML 文件提取记录元数据
3.2.4 插入新记录
4.2.3 SELECT 查询
4.2.4 INSERT 查询
4.2.6 SQLite 的 UPDATE 操作
4.3 关于 SQLite 数据库的结语
5. SharedPreferences(共享偏好设置)
5.1 简介和技术规范
5.2.1 在 SharedPreferences 中创建和删除“数据记录”
6.1 使用 Android 原生方法访问原始 HTML 数据
7.1 编写可被 Android 使用的 ASP.Net Web 服务
1. 背景
在本教程中,我们的重点主要在于理解 Android 中的数据处理。数据可以是扁平文件或关系型数据,可以本地存储在 SqlLite 数据库、XML 数据等中。 我们还打算通过 Web 服务学习与远程服务器的数据交换,以了解如何使用远程方法或如何使用 SQL Server 通过中间件服务远程存储数据。 Android 中的数据处理可以使用下图进行总结。
图 1.1:Android 中的数据访问
因此,在本篇文章中,我们将学习所有这些数据访问技术。一如既往,独立地看待每个部分并不能带来直观的学习。更好的方法是始终考虑一个应用程序,并在构建应用程序的过程中学习所有这些技术。
在本教程中,我们将使用一个名为 OpinionMining 的应用程序,该应用程序应从 Web 获取数据,然后对数据进行意见挖掘,以告知您该页面的积极性。我们将使用 HTML 解析从 MyBB 驱动的社区网站获取数据。 在此过程中,我们将学习 Android 中的各种记录管理技术以及 Web 服务的数据处理。
那么,让我们一步一步开始构建应用程序,并学习每个步骤中的基本知识。
2.1 设置本地存储环境
2.1.1 开始使用 APP
在 Windows/Linux 系统中,您有一个固定的文件系统,可以使用绝对路径、相对地址甚至 URI 来访问文件/目录。这在 Android 中略有不同。您需要了解手机/移动设备中基本上有两种内存:手机内存,它总是相对较低;可扩展内存,在大多数情况下是 SD 卡。一些设备还支持可扩展内存,这意味着用户可以添加第二内存。那些熟悉 Linux 的人应该知道,CD ROM/读卡器等外部设备被加载到文件系统中,如 dev/tty0 等节点。在 Android 中几乎是相似的。 但是,由于不同的制造商可能使用不同的配置,因此在 Android 中使用绝对文件路径永远不被推荐。由于我们在这里的最终目标也是开发一个可以随时发布的应用程序,我们将学习 Android 中的本地文件访问,它应该能抽象底层硬件,并且适用于所有设备。
所以,像往常一样,让我们开始一个新的 Android 项目,并将应用程序命名为 **OpinionMining**。
图 2.1:Android 应用设置
正如我们在之前的教程 第 6 篇 - Android 资源组织/访问入门指南 中已经学到的,Google Play 不允许您提交带有 com.example 扩展名的应用程序,因为它已被保留,您必须选择一个合适的包名(理想情况下应包含您在 Google Play 上的发布者名称和应用程序名称)。 为了使您的应用程序能够广泛地被各种设备使用,同时又能利用更高 Android API 的直观性。
将其他所有设置保留为默认值,然后完成应用程序的创建过程。它将创建一个带有框架布局的名为 **activity_main** 的项目,其框架位于 **fragment_main** 中。对于这个特定的应用程序,我们不关心框架布局。因此,打开 fragment_main.xml,复制其 XML 内容,并如以下图 2.2 所示替换 activity_main 的 XML 内容。
图 2.2 将 activity_main 更新为相对布局而不是框架布局
现在,当您构建项目时,您会在 MainActivity.java 中看到多个错误。这是因为我们正在将一个 FrameLayout 转换为 RelativeLayout。因此,打开 MainActivity.java 并删除如图 2.3 所示的部分。
图 2.3 从 MainActivity 中删除 fragmentManager 实例部分
现在向下滚动并删除 PlaceholderFragment 类。
图 2.4 删除 PlaceholderFragment 类
注意,如果您默认已经创建了 RelativeLayout,那么您不必担心以上步骤。一旦完成此过程,您的项目就没有错误了,您就可以进行下一步,即开始编译和运行应用程序了。
然而,此刻我们必须强调,在本教程中,我们将进行许多无法在模拟器中进行测试的实时操作。因此,我们将花几分钟时间来设置我们的真实 Android 设备来运行我们的应用程序。 下一小节将指导您设置设备进行调试,而不是使用模拟器。本教程之所以添加这一小节,是因为我认为在真实设备上调试对于理解 Android 的数据处理(也包括通过互联网进行数据交换)非常重要。
由于您已选择 Android 4 作为最低 API 要求,我将假设您拥有一台运行至少 Android 4.0 的设备。Android 高版本不直接支持开发者选项。您可以在大多数 Android 4.0 设备上从 **设置->开发者选项-> USB 调试打开** 启用开发者选项,如图 2.5 所示。
图 2.6:设置 Android 设备进行调试和运行应用 在更新的版本中,例如 Android 4.2 及以上版本,您可能看不到开发者选项,因为它隐藏在主界面中,以防止意外更改手机。在这些设备上,您需要通过首先打开 **设置-> 关于** 来使开发者选项可见。 然后点击选项 7 次。一些 Galaxy 系列手机将“关于”部分隐藏在“**更多**”选项卡中。在这些设备上,您可以通过点击 **设置->更多选项卡->关于** 七次来激活开发者选项。 |
一旦您的设备启用了 USB 调试,您就应该 安装 Google USB 驱动程序 以供 Android 设备使用。一旦所有设置都已正确完成,请通过电缆将设备连接到系统。
现在转到您的 Eclipse 环境,然后单击 IDE 右上角“**Debug**”和“**Java**”选项卡旁边的 **DDMS**。您将在左侧列表中看到您的设备。如果单击相机图标,您将看到设备的家庭屏幕的快照。如果设备被锁定,请打开锁以查看选项。
2.6 Eclipse 中的 DDMS 视图
要返回代码模式,您可以选择 Java 选项卡。
Android 设备默认不带任何文件浏览器。您需要下载应用程序来查看文件夹的内容。由于我们将使用不同的数据,并且需要通过手动上传/下载/修改来验证数据,因此我建议您从 Google Play 下载一个好的文件浏览器应用程序。我使用 **文件管理器** 是因为它免费并且能满足我们对文件(尤其是开发者文件)的需求。但是,您可以选择任何您喜欢的文件管理器。安装文件管理器后,我们可以看到如图所示的文件系统。
图 2.7:Android 设备的文件系统
使用 FileManager,您可以在根目录或任何其他目录中创建任何新目录。然而,就像 Windows 应用程序一样,应用程序安装程序会创建一个包含所有应用程序数据的目录,我们应该为应用程序创建一个特定的目录,所有可读写的数据(任何类型)都应存储在该目录中。有一些只读资源,如图像、assets,用户操作不会更改它们。这些资源可以放在 Eclipse 项目目录中( 了解更多关于 Android 资源的内容)。但是对于用户数据,您绝对应该考虑有一个公共目录。
2.1.3 创建应用目录
您可以轻松选择一个现有的目录,也可以为应用程序创建一个自定义目录。第二种选择更可行,因为它将应用程序数据与其他用户数据隔离开来。
现在,假设我们要创建一个名为 **OpinionMining** 的目录,位于 Android 文件系统的某个位置。可以在此目录中存储、处理和修改各种数据。然后,应用程序应检查目录是否存在,如果不存在,则应创建该目录。
为此,您可以创建相对于应用程序目录的 **File** 对象,然后检查目录是否存在,如果不存在则创建它。我们希望在执行任何其他操作之前执行检查目录并根据需要创建它的任务。因此,我们将使用 main activity 的 **onCreate** 方法内的代码,在调用构造函数之后。因此,我们的 onCreate 方法如下所示。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try
{
Log.d("Starting", "Checking up directory");
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "OpinionMining");
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (! mediaStorageDir.exists())
{
if (! mediaStorageDir.mkdir())
{
Log.e("Directory Creation Failed",mediaStorageDir.toString());
}
else
{
Log.i("Directory Creation","Success");
}
}
}
catch(Exception ex)
{
Log.e("Directory Creation",ex.getMessage());
}
}
**Environment.getExternalStoragePublicDirectory()** 返回指定参数 **Type** 的路径的 File 对象。 ** 请注意,我们正在尝试在 Pictures 目录内查找名为“**OpinionMining**”的目录。您可以在 Alarms、Downloads、Music、Movies 等其他公共目录中创建您的应用程序目录。当您从
Environment.DIRECTORY_PICTURES
并填充选项,如图 2.8 所示。
图 2.8:创建应用目录的公共目录选项
保存,右键单击 Eclipse 中的 OpinionMining 项目,然后选择 **Run As->Android Application**。您是否期望应用程序在 Pictures 目录内成功创建一个目录?但它不会,您会看到如图 2.9 所示的调试消息。
图 2.9:创建目录时的调试错误
此消息显示目录创建已失败,尽管没有发生异常。它还显示新目录的路径将是:**storage/emulated/0/Pictures/OpinionMining**。您可以打开您的文件管理器并交叉验证路径,您会发现路径绝对没有问题。这里 Filemanager 应用程序很方便,因为它向您显示了目录的实际路径,这在调试时很有帮助。
回到错误,当您发现没有异常并且路径已被创建时,您必须理解此类错误通常与**权限**错误有关。处理外部目录时,您需要在 **AndroidManifest.xml** 的 use-permission 部分设置应用程序的权限,如图 2.10 所示。
图 2.10 设置目录写入访问权限
或者,您可以编辑 AndroidManifest.xml 并在 **<application>** 部分之前包含以下代码。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
现在,当您保存并运行应用程序时,您将在底部的 **LogCat** 窗口中看到“目录创建”“成功”消息,并且还将看到 Pictures 目录内的 OpinionMining 目录。
图 2.11 由应用程序创建的应用目录
2.2 扁平文件记录的插入、更新、删除、读取
2.2.1 在 Android 设备中上传扁平文件
首先,让我们学习访问 Android 扁平文件的具体细节。为了跟上我们的 OpinionMining 应用程序,让我们创建一个名为 **OpinionData.txt** 的记事本文件。其内容如下所示,如图 2.12 所示。
图 2.12 OpinionData.txt 文件
理解我们在这里要做什么并不难。我们创建了一个包含两列的文件。一行包含一个意见词及其相应的权重,用 **TAB** 分隔。如果您愿意,也可以 下载 OpinionData.txt。
请记住,检查光标是否在新行的记录旁边。如果没有,请按 Enter 键,使光标移到下一新行。这对于插入记录很重要。
我们将首先手动上传文件到新创建的 OpinionMining 目录,然后从 Android 读取内容。您可以通过 PC 浏览手机,并将文件粘贴到合适的目录中。如果 PC 目录浏览器中未显示新创建的目录,请拔下设备再重新插入。
图 2.13 手动加载到目录中的数据文件。
也有一个选项可以通过 Eclipse 来完成。转到 **DDMS** 视图并选择 **File Explorer** 选项卡。您将看到 Android 文件系统。将您的 OpinionData.txt 拖放到 **mnt->sdcard** 中,如图所示。
2.14 从 Eclipse 上传外部文件到设备
您会注意到的一件事是,这个特定视图隐藏了 Alarms、Download、DCIM 等所有目录。因此,上传的文件将存在于 sdcard 的根目录下,如 FileManager 快照所示。
2.2.2 读取文件内容
现在,为了访问文件内容,我们将创建一个名为 **AndroidFlatFileAccess** 的新类,并将所有文件相关的操作抽象到该类中。
首先,让我们创建一个静态的 **ReadFile(String path)** 方法,它应该将文件路径作为输入参数,并将文件内容作为字符串返回。
package com.integratedideas.opinionmining;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class AndroidFlatFileAccess
{
public static String ReadFile(String filePath)
{
File file = new File(filePath);
//Read text from file
StringBuilder text = new StringBuilder();
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\n');
}
}
catch (IOException e)
{
//You'll need to add proper error handling here
}
return text.toString();
}
}
请记住,我们在 MainActivity 类的 onCreate 方法中已经有用于检查和创建应用程序目录的代码。我们将仅使用 **Log.i(tag,text)** 命令来测试我们的文件访问是否正常工作,其结果将在 **LogCat** 窗口中可用。
if (! mediaStorageDir.exists())
{
Log.d("Trying to Create Directory", "App Directory Does not exists");
if (! mediaStorageDir.mkdir())
{
Log.e("Directory Creation Failed",mediaStorageDir.toString());
}
else
{
Log.i("Directory Creation","Success");
}
}
else
{
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
Log.i("Checking File Path", AndroidFlatFileAccess.ReadFile(filePath));
// We will check the contents of OpinioData.txt here
}
}
catch(Exception ex)
{
Log.e("Directory Creation",ex.getMessage());
}
观察一个额外的 else 部分。
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
上述两行代码创建了 OpinionData.txt 的 String 路径。首先,我们获取公共目录的路径,然后在此基础上附加应用程序目录的路径,即 **OpinionMining**,接着是文件 **OpinionData.txt**。最需要注意的一点是使用 **“/”**(**正斜杠**)作为目录分隔符,而不是我们在 Windows 文件系统中使用的“**\**”(**反斜杠**)。Android 基于 Linux,这很好地解释了这一点。
一旦获取了文件路径,就调用 ReadFile() 方法,并将结果与 Log.i 一起显示。现在,当您保存并运行项目时,您将看到如图 2.15 所示的 logcat 结果。很明显,每一行都显示了新的 logcat 信息。
图 2.15:显示我们文件内容的 LogCat 视图
2.2.3 将文件内容作为标记读取
现在,对于任何严肃的数据处理,我们必须理解,在 FlatFile 中,每一行代表一个记录。每一列代表记录的属性。因此,我们还必须有一个方法,该方法可以返回文件内容作为一个二维数组,其中第一个维度存储记录或行,第二个维度存储列(或标记)。
这是在 AndroidFlatFileAccess 类中创建的 **ReadFileTokens** 方法。
public static String[][] ReadFileTokens(String filePath,int noColumns)
{
File file = new File(filePath);
//Read text from file
StringBuilder text = new StringBuilder();
ArrayList<String[]> al=new ArrayList<String[]>();
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
String [] row=new String[noColumns];
while ((line = br.readLine()) != null)
{
row=line.split("\t");
al.add(row);
}
}
catch (IOException e)
{
//You'll need to add proper error handling here
}
String [][]retArray=new String[al.size()][noColumns];
return al.toArray(retArray);
}
在读取文件时,您不知道(可能知道,但明智的做法是您事先不知道总记录数)记录数或行数。因此,我们将利用 Java 的 **ArrayList** 类,它在各方面都与 **.Net ArrayList** 类相似,只是在反序列化时需要一个特定类型的变量。 ArrayList 对象可以存储任意数量的记录,就像任何链表都应该做的那样。您所要做的就是指定独立记录的类型。虽然这个文件有两列,但其他应用程序可能有不同列数的文件。因此,我们将 **noColumns** 列作为参数传递,指定该特定记录应有的列数。
ArrayList<String[]> al=new ArrayList<String[]>();
将 String 变量 **row** 声明在 while 循环之外的原因是为了提高性能。对于大文件,为每个循环创建和销毁一个变量不是一个好的编程实践。最后,我们修改了为 ReadFile 方法编写的 While 循环,添加了一行以 Tab 分隔标记,然后将标记赋值给 row 变量。然后,在每次循环中将 row 变量添加到 al 中。
while ((line = br.readLine()) != null)
{
row=line.split("\t");
al.add(row);
}
我们最终希望将 ArrayList 对象反序列化,并返回合适的数组,这里是二维字符串数组。对于反序列化,您需要声明一个合适类型的变量(不一定需要初始化变量),并使用该变量作为反序列化的模板。
String [][]retArray=new String[al.size()][noColumns];
return al.toArray(retArray);
我们更新了 onCreate 方法中的 else 部分,该方法用于测试 ReadFile 方法。首先,我们调用 ReadFileTokens() 方法,并将文件路径和列数(这里是两个)作为参数。
Log.i("Checking File Path", "---------------");
String[][] data=AndroidFlatFileAccess.ReadFileTokens(filePath,2);
for(int i=0;i<data.length;i++)
{
Log.i(data[i][0], data[i][1]);
}
以下是 TokenWise 文件读取的 LogCat 输出。
图 2.16:文件内容读取为标记
2.2.4 在文件中添加新记录
文件读取成功后,让我们将注意力转移到向文件添加记录。任何数据记录都将添加为新行。因此,Insert 方法可以接受一行作为输入,并将其附加到文件末尾。
这是执行相同操作的 **Insert** 方法。
public static int Insert(String filePath,String recordRow)
{
try {
FileWriter f = new FileWriter(filePath,true);
f.write(recordRow);
f.flush();
f.close();
return 1;
}
catch (IOException e)
{
return -1;
//You'll need to add proper error handling here
}
}
正如您所见,我们正在使用 **FileWriter** 对象将字符串写入文件。这里最重要的一点是使用 **true** ** **作为第二个参数,它表示 FileWriter 具有追加权限。请注意,如果不使用 **append=true**,您的现有内容将被清除。
添加新词“Pathetic”的结果如图 2.17 所示。
图 2.17:在文件中追加新记录的结果
2.2.5 从文件中删除记录
删除记录的逻辑可以从插入、逐标记选择、逐行数据选择结合简单线性搜索的逻辑中推导出来。我们将发送一个搜索标记,它可以是任何行中任何列的值。我们的方法应该在每一行的每一列中搜索该标记,最后删除该列中的值与搜索词匹配的整行。
可以通过选择文件中除匹配搜索词的行之外的所有行到变量中,然后以**非追加模式**(File 构造函数中的 append=false)将内容写回文件来执行删除。代码如下。
public static int DeleteRecord(String filePath,int noColumns,String searchTerm)
{
// Returning -1 means nothing was deleted, if deletre successful,
//it indicates the number of rows affected
/////////// 1. First we will Loop Through the File and Search for Tokens//////
File file = new File(filePath);
//Read text from file
String text = "";
int matched=0;
ArrayList<String[]> al=new ArrayList<String[]>();
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
String [] row=new String[noColumns];
while ((line = br.readLine()) != null)
{
row=line.split("\t");
/// Linear Search in each column/////////////
boolean flag=false;
for(int i=0;i<noColumns;i++)
{
if(row[i].toLowerCase().trim().equals(searchTerm.toLowerCase().trim()))
{
flag=true;
matched++;
}
}
////////////////// Don't store the row in linked list if any of the column has search term//
if(!flag)
{
text=text+line+"\n";
al.add(row);
}
}
//
br.close();
}
catch (IOException e)
{
//You'll need to add proper error handling here
}
///// If no data was matched variable will be 0. No need to write anything in file//////
if(matched==0)
{
return -1;
}
/////////////////// Collect the List in an Array
String [][]retArray=new String[al.size()][noColumns];
retArray=al.toArray(retArray);
//////////////2. Once entire data except the row to be deleted is in al, write back in file
try {
FileWriter f = new FileWriter(filePath,false); // remember to use append=false.
// Using append=false will wipe existing data and add new row
f.write(text);
f.flush();
f.close();
return matched;
}
catch (IOException e)
{
return -1;
//You'll need to add proper error handling here
}
}
这是删除 Word Nice 记录的结果。
图 2.18:从文件中删除记录的结果
2.2.6 更新扁平文件记录
在更新过程中,我们打算搜索一个标记来选择行,并用新内容替换该行的内容。因此,我们的输入是 a) 搜索词 和 b) 行的新值。此逻辑可以从删除逻辑中推导出来。唯一需要更改的是,不是“不复制”匹配的行,而是用新数据替换它。
public static int UpdateRecord(String filePath,int noColumns,String searchTerm, String newUpdatedRow)
{
// Returning -1 means nothing was deleted, if deletre successful,
//it indicates the number of rows affected
/////////// 1. First we will Loop Through the File and Search for Tokens//////
File file = new File(filePath);
//Read text from file
String text = "";
int matched=0;
ArrayList<String[]> al=new ArrayList<String[]>();
try {
BufferedReader br = new BufferedReader(new FileReader(file));
String line;
String [] row=new String[noColumns];
while ((line = br.readLine()) != null)
{
row=line.split("\t");
/// Linear Search in each column/////////////
boolean flag=false;
for(int i=0;i<noColumns;i++)
{
if(row[i].toLowerCase().trim().equals(searchTerm.toLowerCase().trim()))
{
flag=true;
matched++;
}
}
////////////////// Don't store the row in linked list if any of the column has search term//
if(!flag)
{
text=text+line+"\n";
}
else
{
///// Instead store the New Record Value/////////////
text=text+newUpdatedRow+"\n";
}
al.add(row);
}
//
br.close();
}
catch (IOException e)
{
//You'll need to add proper error handling here
}
///// If no data was matched variable will be 0. No need to write anything in file//////
if(matched==0)
{
return -1;
}
/////////////////// Collect the List in an Array
String [][]retArray=new String[al.size()][noColumns];
retArray=al.toArray(retArray);
//////////////2. Once entire data except the row to be deleted is in al, write back in file
try {
FileWriter f = new FileWriter(filePath,false); // remember to use append=false.
// Using append=false will wipe existing data and add new row
f.write(text);
f.flush();
f.close();
return matched;
}
catch (IOException e)
{
return -1;
//You'll need to add proper error handling here
}
}
很容易区分更新文件和删除文件。区别在于数据回写部分,在更新中我们用新数据替换旧数据,而在删除中我们不考虑数据。
if(!flag)
{
text=text+line+"\n";
}
else
{
///// Instead store the New Record Value/////////////
text=text+newUpdatedRow+"\n";
}
将包含 Pathetic -2 的行替换为 Patheticity -3 的结果如图所示。
图 2.18 扁平文件记录更新结果
2.2.7 Android 扁平文件记录访问用法
在插入、更新和删除部分,我们没有展示用法,而是留给您思考,以便您可以尝试使用这些方法。好吧,如果您遇到任何问题,以下是如何从 MainActivity 类中的 onCreate 方法的 else 部分使用这些方法。
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
/*********** Testing the Delete Operation************/
AndroidFlatFileAccess.DeleteRecord(filePath, 2, "nice");
//////////////////////////////////////////////////////
/**** Testing New Record Addition in File***********/
AndroidFlatFileAccess.Insert(filePath, "Pathetic\t-2\n");
/*************************************************/
/**** Testing New Record Addition in File***********/
AndroidFlatFileAccess.UpdateRecord(filePath, 2, "pathetic", "Patheticity\t3\n");
/*************************************************/
/******************** Reading File Content As Single STring*********/
Log.i("Checking OpinioData.txt Contents:", AndroidFlatFileAccess.ReadFile(filePath));
/*************-----------------------------------***********/
/******************** Reading File Content As Tokens*********/
Log.i("Checking File Path", "---------------");
String[][] data=AndroidFlatFileAccess.ReadFileTokens(filePath,2);
for(int i=0;i<data.length;i++)
{
Log.i(data[i][0], data[i][1]);
}
/******************** ---------------*********/
坦率地说,使用 Log.i 显示结果不是一个体面的 GUI 选项。但是,第 2.2 节,即 Android Flat File Record Access 的主要目的是指导您使用扁平文件系统作为 Android 的轻量级数据库。访问技术与任何 GUI 控件都无关。因此,本节的解释采用了最少的 GUI。您可以 下载 AndroidFlatFileAccess 类 并在任何其他应用程序中使用它。
轻量级的扁平文件数据库在游戏中非常有用,您可以使用扁平文件记录更新分数和统计信息。您可以收集应用程序的用户统计信息到扁平文件中。您可以将扁平文件系统用作日志等。
2.3 简单的 Android 应用,用于检查给定陈述的积极性
到目前为止,我们已经学习了处理扁平文件和扁平文件数据库的所有知识。现在,让我们回到应用程序的主题:即能够检测通过互联网获取的陈述的意见。在我们深入研究获取 Web 数据技术之前,让我们创建一个简单的 UI,用户将在文本框中输入文本,然后按按钮来检查意见。我们必须有一个检测陈述意见的方法。
现在,意见挖掘不是简单的字符串匹配。它有自己的算法和自动机。但由于我们受限于利用 GUI 进行文件访问知识,我们将限制自己使用一种基本的挖掘技术:字符串匹配。
让我们创建一个名为 MineOpinions 的类,以便稍后在我们推进应用程序时更新其方法和骨架。该类必须有一个单词及其相应权重的数据库。该类必须有一个搜索方法,可以返回给定文本中单词的出现次数。然后,它应该有一个接受文本、搜索意见数据库单词并更新权重总和的方法。
这是我们简单的意见挖掘类。
package com.integratedideas.opinionmining;
import java.util.ArrayList;
import java.util.StringTokenizer;
import android.util.Log;
public class MinePolarity
{
public String[][] OpinionDatabase=null;
public MinePolarity(String[][]database)
{
OpinionDatabase=database;
}
// Linear search a token in the list of Opinion term database
// If matched, return it's corresponding weight
// Else by Default return 0
public int SearchInDatabase(String tok)
{
for(int i=0;i<OpinionDatabase.length;i++)
{
if(tok.toLowerCase().trim().equals(OpinionDatabase[i][0].toLowerCase().trim()))
{
return Integer.parseInt(OpinionDatabase[i][1].trim());
}
}
return 0;
}
///////////// This is main method which returns the polarity score of a text//////////////
public int SimpleMine(String text)
{
// Tokenize the string based on all available patterns/////
StringTokenizer st = new StringTokenizer(text, ".\n\t ,:();");
int score=0;
while(st.hasMoreTokens())
{
String s=st.nextToken();
Log.i("In Simple Mine",s);
try{
//call Searchdatabase with current token to find the weight of current token
score+=SearchInDatabase(s);
}catch(Exception ex)
{
}
}
//return cumilative score
return score;
}
}
要了解这里发生的事情,不需要高级逻辑。当我们用文本调用 **SimpleMine** 方法时,该方法首先使用 **StringTokenizer** 类使用所有可能的定界符对文本进行标记。所有标记都在调用此类的构造函数时创建的数据库中搜索。数据库格式与 **ReadFileToken** 方法返回的类型格式(即 String[][])相同,以便于调用。搜索方法实现简单的线性搜索。
您可能已经注意到在许多地方使用了 **trim()** 方法。让我告诉您,很多时候 Android 控件会将截断字符附加到字符串。有时用户可能在两个单词之间使用两个空格而不是一个空格。使用 **trim()** 可以删除所有这些不必要的字符,并确保正在处理的字符串中没有垃圾。建议始终使用 trim 方法,尤其是在搜索、字符串到整数转换等过程中,因为额外的字符(有时也称为空字符)可能会导致异常并可能导致应用程序关闭。
我们将首先使用 LogCat 测试其功能。
/*********** Opinion Polarity Testing ***************/
String[][] datas=AndroidFlatFileAccess.ReadFileTokens(filePath,2);
MinePolarity mp=new MinePolarity(datas);
Log.i("Records Obtaind","Loading Database");
String testString="I am a good boy";
int score=mp.SimpleMine("I am a good boy");
if(score>0)
{
Log.i("Polarity of: "+testString+": ","Positive with score="+score);
}
else
{
if(score<0)
{
Log.i("Polarity of: "+testString+": ","Negative with score="+score);
}
else
{
Log.i("Polarity of: "+testString+": ","Nutral with score="+score);
}
}
//------------------------------------------------------------------------------
意见挖掘过程将分析分数。如果分数是积极的,那显然是积极的意见;如果是消极的,意见也是消极的。中性意见是指分数既不是积极也不是消极的,即 0。
图 2.20:意见极性挖掘结果
正如您所期望的,字符串“I am a good boy”的意见结果将是积极的。您可以更改字符串并测试结果。
完成所有繁重的工作后,终于到了设置我们的 GUI 并查看事物在 GUI 模式下是否顺利运行的时候了。
首先,请查看如图 2.21 所示的 UI。我们使用了一个 TextView 来显示意见测试结果,一个 EditText 用于获取用户输入,一个按钮用于触发挖掘过程。
图 2.21 用于测试意见挖掘过程的简单 GUI
activity_main.xml 的更新代码如下。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.integratedideas.opinionmining.MainActivity$PlaceholderFragment" >
<EditText
android:id="@+id/edInput"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="34dp"
android:layout_marginTop="25dp"
android:ems="10"
android:inputType="textMultiLine"
android:lines="8"
android:minLines="6"
android:gravity="top|left"
android:maxLines="10"
>
<requestFocus />
</EditText>
<Button
android:id="@+id/btnOpinionTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/edInput"
android:layout_centerHorizontal="true"
android:text="@string/opinion_test" />
<TextView
android:id="@+id/tvResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btnOpinionTest"
android:layout_centerHorizontal="true"
android:layout_marginTop="33dp"
android:text="@string/hello_world" />
</RelativeLayout>
设计完 UI 后,就该声明 MainActivity 中的相关变量并添加按钮的事件监听器了。
在类中声明以下变量。
MinePolarity mp=null;
EditText edInput;
TextView tvResult;
Button btnOpinion;
在 onCreate 方法中调用 setContentView 后初始化 UI 变量。
edInput=(EditText)findViewById(R.id.edInput);
tvResult=(TextView)findViewById(R.id.tvResult);
btnOpinion=(Button)findViewById(R.id.btnOpinionTest);
btnOpinion.setOnClickListener(this);
在之前测试所有文件操作的 else 部分初始化 mp 实例。
mp=new MinePolarity(datas);
更新 onClick 方法,以便每当点击按钮并调用 onClick 方法时,应用程序都会从 edInput 收集数据,将其传递给 SimpleMine 方法,并在 tvResult 中显示结果。
@Override
public void onClick(View arg0)
{
// TODO Auto-generated method stub
String testString=edInput.getText().toString();
int score=mp.SimpleMine(testString);
if(score>0)
{
tvResult.setText("Positive with score="+score);
}
else
{
if(score<0)
{
tvResult.setText("Negative with score="+score);
}
else
{
tvResult.setText("Nutral with score="+score);
}
}
如果一切顺利,您将获得所需的结果。
图 2.22:带有 GUI 的意见挖掘结果
2.4 将扁平文件记录打包到 .apk 文件中
2.4.1 从 assets 文件夹将文件解压到 SD 卡应用目录
到目前为止,我们做得很好。我们已经成功地将文本记录文件上传到了 Android 目录之一。实现了插入、删除、更新、查看方法。我们使用 LogCat 测试了这些方法。然后我们开发了一个简单的 OpinionMining 类,它是我们更大目标的一部分,然后通过 GUI 测试了结果。
现在,卸载您的应用程序,并删除您创建的 OpinionMining 目录。您的应用程序还能运行吗?
如果您在删除 OpinionMining 目录后测试您的应用程序,当您点击 Opinion 按钮时,您的应用程序将崩溃,因为没有 OpinionData.txt 文件,也没有数据库。
因此,理想情况下,您希望文本文件驻留在您的 apk 中,并在测试目录时,它还必须测试该目录中是否存在 OpinionData.txt。如果数据库文件不存在,应用程序必须将文件从 apk 复制到应用程序目录。换句话说,我们希望将扁平文件记录与 apk 本身一起打包。
我关于 Android 资源管理 的文章将为您提供有关如何管理资源的良好概念。由于这里的数据是文本文件,我们可以将其放在 **Asset** 目录中。但是,与所有其他 Android 资源一样,asset 名称必须是小写字母。因此,我们将“opiniondata.txt”文件上传到 asset 目录中。
图 2.23:将扁平文件记录放入 assets 文件夹以进行分发
我们希望应用程序能够打开 asset 作为流并将其复制到我们的项目目录中。您可能需要一个用于不同情况和 asset 的此类方法。因此,我决定在 AndroidFlatFileAccess 类中将逻辑作为一个独立的方法,名为 **CopyResourceFromAssetToSdcardDirectory**。
public static int CopyResourceFromAssetToSdcardDirectory(InputStream srcFileStream, String dstFile){
try{
File f2 = new File(dstFile);
InputStream in = srcFileStream;
//For Overwrite the file.
OutputStream out = new FileOutputStream(f2);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0){
out.write(buf, 0, len);
}
in.close();
out.close();
}
catch(Exception ex)
{
return -1;
}
return 1;
}
由于 asset 可以作为 InputStream 打开,因此我倾向于将输入参数设为 InputStream 类型。dstFile 是 OpinionMining/Opiniondata.txt 的路径,即 opiniondata.txt 应该位于该位置。
现在,正如讨论过的,在应用程序开始时检查目录是否存在时解压 asset 记录文件。
if (! mediaStorageDir.exists())
{
Log.d("Trying to Create Directory", "App Directory Does not exists");
if (! mediaStorageDir.mkdir())
{
Log.e("Directory Creation Failed",mediaStorageDir.toString());
}
else
{
Log.i("Directory Creation","Success");
//////////////////////////////
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
InputStream is =getResources().getAssets().open("opiniondata.txt");
AndroidFlatFileAccess.CopyResourceFromAssetToSdcardDirectory(is, filePath);
}
}
现在我们的应用程序已准备好分发。您可以打包 apk 并进行分发。
但有时数据库可能会损坏。有时您可能想刷新一个旧文件并用新文件替换它,或者有时您可能只想从设备中完全删除一个记录文件。在下一小节中,我们将介绍删除设备上文件的方法。
删除很简单。我们所需要做的就是用文件路径初始化一个 File 对象,然后通过调用 File 类的 **delete()** 方法来删除文件。这里的文件路径与我们用于所有其他文件操作的绝对路径相同。
public static int DeleteFile(String filePath)
{
try
{
File file = new File(filePath);
boolean deleted = file.delete();
if(deleted)
{
return 1;
}
else
{
return -1;
}
}catch(Exception ex)
{
return -1;
}
}
这里,为了与其他所有操作保持一致,我们返回一个整数。这个选择来自 SQL 方法,这些方法返回整数,表示是否有行受影响,以及如果受影响,有多少行受影响。您可以很好地设计您的方法以返回布尔值。但始终返回一个值是件好事。它会告知调用方操作的状态。
下载 FlatFile_Record_Simple_Opinion_Mining.zip,这是直到本节的完整项目。您可以随意使用该项目,也许可以添加新的 Intent 来添加、删除记录。
3. Android 的 XML 记录
3.1 XML 记录基础、优点、局限性和适用性
在我们之前的扁平文件记录系统中,我们能够创建一个通用类来处理几乎任何扁平文件数据库(诚实地说,只有一个表)。这种通用实现对于应用程序开发非常重要。您需要为不同的情况准备弹药,而这种实现总是很有帮助。它们就像即插即用的代码。您将它们导入任何项目,它们就能工作。在上一个 section 中,我们处理了由 Tab 分隔数据的扁平文件记录。之所以这样做,是因为这种数据可以轻松导出到 sql 表或转换为 Excel 文档。因此,我们的目标不仅是了解如何做事,而且还能够将概念泛化以供更广泛地使用。
XML 是一个非常重要的结构化数据处理器。XML 也像扁平文件一样,一个文件包含一个特定的表。 但 XML 的优点在于它支持嵌套。假设您想创建一个包含人员教育记录。在扁平文件中,您只能为每行(每条记录)指定一种教育。所以您将优先使用最后一种教育。但是,由于 XML 支持嵌套和层次结构,您可以在 education 字段下使用带有 tag 的多种教育。但是,如果没有嵌套,扁平记录将始终比它们的 XML 对等物占用更少的空间,因为在 XML 中,每个记录的列名都需要指定两次。
然而,XML 的一个主要优点是它非常平台独立,并且在每种现代编程语言中都有很好的 XML 解析器。Web 服务也以 XML 的形式返回其数据。但是在本节中,我们主要关注 XML 作为本地资源。本节获得的知识将在我们进行 Web 数据抓取时有所帮助。
在 XML 中,记录行由节点指定,列数据嵌套在行节点内。让我们构建与我们的意见数据相关的 XML。我们称之为 **OpinionXmlData.xml**。
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<OpinionWordTable>
<OpinionWord>
<Word>Good</Word>
<Weight>1</Weight>
</OpinionWord>
<OpinionWord>
<Word>Bad</Word>
<Weight>-1</Weight>
</OpinionWord>
</OpinionWordTable>
可以很容易地看出,上面的 XML 文件包含一个名为 **OpinionWordTable** 的表。该表中的每个项都称为 **OpinionWord**。 一行包含名为 **Word** 和 **Weight** 的两列。 然而,看着 XML 文件,您可能会想,既然我们已经学会了处理扁平文件记录,为什么还要使用这样的文件呢?我们对应的扁平文件记录将更紧凑。
要真正理解 XML 的优势所在,请看下面的 XML **OpinionXmlData_with_Phonetics.xml**。
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<OpinionWordTable>
<OpinionWord>
<Word>Good</Word>
<Weight>1</Weight>
<Phonetics>
<P1>Gud</P1>
<P2>Gd</P2>
</Phonetics>
</OpinionWord>
<OpinionWord>
<Word>Bad</Word>
<Weight>-1</Weight>
<Phonetics>
</Phonetics>
</OpinionWord>
</OpinionWordTable>
您可以看到第一个词有两个音标,而第二个词没有。 可能存在更复杂的嵌套结构,XML 可以轻松处理这些结构。
但是,为了保持学习的简单性,并与扁平文件处理的简单性相匹配,我们将限制自己使用非嵌套的 **OpinionXmlData.xml**,并在本节末尾讨论处理复杂数据的 XML 方法。
3.2 对 XML 记录进行操作
3.2.1 设置 XML 记录文件
首先 下载 opinionxmldata.zip,解压并将 opinionxmldata.xml 上传到 Eclipse 项目的 **assets** 文件夹。使用文件管理器,删除设备上已有的 **sdcard/Pictures/OpinionMining** 文件夹。现在,更新 MainActivity.xml 中检查目录是否存在的部分,如果不存在则创建 OpinionMining 目录,我们还将把 opiniondata.txt 从 assets 文件夹复制到应用程序文件夹。添加代码,使用我们已开发的 CopyFile 方法将 opinionxmldata.xml 从 assets 文件夹复制到应用程序文件夹。
if (! mediaStorageDir.exists())
{
Log.d("Trying to Create Directory", "App Directory Does not exists");
if (! mediaStorageDir.mkdir())
{
Log.e("Directory Creation Failed",mediaStorageDir.toString());
}
else
{
Log.i("Directory Creation","Success");
//////////////////////////////
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
InputStream is =getResources().getAssets().open("opiniondata.txt");
AndroidFlatFileAccess.CopyResourceFromAssetToSdcardDirectory(is, filePath);
//////////////// Now Copy The XML File/////////////////////////////
filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionXMLData.xml";
is =getResources().getAssets().open("opinionxmldata.xml");
AndroidFlatFileAccess.CopyResourceFromAssetToSdcardDirectory(is, filePath);
///////////////////////////////////////////////////////////////
}
复制 XML 文件部分与复制扁平文件类似。运行后,您的设备将在 Pictures 目录内重新创建 OpinionMining 文件夹,并且该文件夹现在将包含两个文件:OpinionData.txt 和 OpinionXmlData.xml,如图 3.1 所示。
图 3.1 XML 文件存储在 SD 卡 Pictures 中的 OpinionMining 文件夹内
无需赘言,尽管 XML 数据只有两行,但其大小远大于其扁平文件记录的对应物。
3.2.2 从 XML 文件读取记录
XML 记录的问题在于,由于它支持嵌套和复杂数据记录,提取记录并不那么简单。XML 是基于标签的数据,因此提取记录就是解析 XML 数据。在我们提供用于读取非嵌套数据类型记录行的完整通用方法之前,让我们编写一个简单的方法来理解 XML 的确切工作方式。
让我们创建一个名为 **AndroidXMLRecordAccess** 的类,我们在其中放置所有 XML 记录访问技术。让我们创建一个名为 **UnderstandXmlParsing(String filePath)** 的简单方法,用于执行其名称所暗示的操作:即理解 XML 记录的整个过程。
public static void UnderstandXMLRecords(String filePath)
{
// 1. First Open the XML File using File Method and Load it's contents in String
String data=AndroidFlatFileAccess.ReadFile(filePath);
Log.i("'Data Read",data);
///////////////////////////////////////
/// 2. Instantialize XmlPullParserFactory.....................
XmlPullParserFactory factory = null;
XmlPullParser xpp = null;
try{
factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
xpp = factory.newPullParser();
xpp.setInput(new StringReader(data));
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT)
{
if (eventType == XmlPullParser.START_DOCUMENT)
{
Log.i("Start document","START_OF_XML_DOCUMENT");
}
else if (eventType == XmlPullParser.START_TAG)
{
Log.i("Start tag ",xpp.getName());
}
else if (eventType == XmlPullParser.END_TAG)
{
Log.i("End tag ",xpp.getName());
}
else if(eventType == XmlPullParser.TEXT)
{
Log.i("Data:",xpp.getText());
}
eventType = xpp.next();
}
}catch(Exception ex)
{
}
}
XML 解析将在字符串数据上执行。因此,第一步是将 XML 文件内容读取到一个字符串中。请记住,我们已经在文件访问部分为此开发了一个方法。因此,我们将利用我们的通用方法将 XML 文件内容拉取到一个名为 data 的字符串变量中。
String data=AndroidFlatFileAccess.ReadFile(filePath);
**XMLPullParserFactory** 用于实例化解析实例。我们需要指定文档是否有命名空间声明:这是 XML 的第一行。一旦工厂实例初始化,它就用于实例化 **XMLPullParser** 的实例。一旦实例化,解析器对象通过 **setInput()** 方法被赋予包含 XML 文件内容的 data 变量。
factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
xpp = factory.newPullParser();
解析器现在作为一个 **Recordset** 对象,类似于 **JDBC**。**next()** 方法加载下一个标记并返回标记类型。标记类型被分类为 **START_TAG, END_TAG, START_DOCUMENT, TEXT**。
但问题是 **<OpinionWordTable>,<OpinionWord>,<Word>,<Weight>** 所有这些标签都将被归类为 **START_TAG**,它们各自的结束标签将被归类为 **END_TAG** 标签。 ** **START_TAG 和 END_TAG 之间的内容被归类为 Text。因此,尽管 XmlPullParser 对象可以检索 XML 标记及其类型,但它无法返回行数据,也无法返回记录的元数据(即表名、行或记录名、列名)。
结果如图 3.2 所示。
图:3.2 调用 UnderstandXmlParsing 的结果
现在,我们需要首先理解如何提取元数据,然后根据元数据提取结果。因此,让我们将 **tableName、rowName、columns** 声明为三个变量,它们分别是 String、String 和 ArrayList<String>。我们期望将 OpinionWordTable 作为 tableName,OpinionWord 作为 rowName,{Word,Weight} 作为 columns。一旦我们拥有了这些信息,我们就可以在 START_TAG 中查找 rowName,如果找到,则将所有 TEXT 存储在 ArrayList<String> 对象中,比如 **singleRow**,直到找到 rowName 的 END_TAG。
将 singleRow 类型转换为 String[**columns.size()**],因为我们有关于列的信息。然后将此信息添加到 ArrayList<String[]> 中,该列表将保存记录行,我们称之为 **allData**。 当到达 END_DOCUMENT 时,使用模板 String[**allData.size()**][**columns.size()**] 将 allData 类型转换为 String[][] 数组。allData.size() 返回正在读取的行数,columns.size() 当然返回列数。
这是可以读取任何 XML 扁平文件记录的完整方法,与列数或列类型无关。
public static String[][]ReadXMLRecords(String filePath)
{
ArrayList<String[]>allData=new ArrayList<String[]>();
ArrayList<String>singleRow=new ArrayList<String>();
String tableName="";
String rowName="";
ArrayList<String>columns=new ArrayList<String>();
boolean columnTracking=false;
// 1. First Open the XML File using File Method and Load it's contents in String
String data=AndroidFlatFileAccess.ReadFile(filePath);
Log.i("'Data Read",data);
///////////////////////////////////////
/// 2. Instantialize XmlPullParserFactory.....................
XmlPullParserFactory factory = null;
XmlPullParser xpp = null;
try
{
factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
xpp = factory.newPullParser();
xpp.setInput(new StringReader(data));
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT)
{
try{
if (eventType == XmlPullParser.START_DOCUMENT)
{
Log.i("Start document","START_OF_DOCUMENT");
}
else if (eventType == XmlPullParser.START_TAG)
{
//Log.i("Start tag ",xpp.getName());
// Initialize a row object when start tag is encountered: OpinionWord
if(tableName.length()<1)
{
tableName=xpp.getName().trim();
singleRow=new ArrayList<String>();
Log.i("Table Name",tableName);
}
else
{
if(tableName.trim().equals(xpp.getName().trim()))
{
singleRow=new ArrayList<String>();
}
else
{
if(rowName.length()<1)
{
rowName=xpp.getName().trim();
columnTracking=true;
Log.i("Row Name",rowName);
}
else
{
if(rowName.equals(xpp.getName().trim()))
{
singleRow=new ArrayList<String>();
}
else
{
if(columnTracking)
{
columns.add(xpp.getName().trim());
}
}
}
}
}
}
else if (eventType == XmlPullParser.END_TAG)
{
// Log.i("End tag ",xpp.getName());
// Convert the ArrayList singleRow to String[] and add inside all data.
if(rowName.equals(xpp.getName().trim()))
{
if(columnTracking)
{
columnTracking=false;
Log.i("Columns",columns.get(0)+" ,"+columns.get(1));
}
String [] row=new String[columns.size()];
row=singleRow.toArray(row);
allData.add(row);
singleRow=new ArrayList<String>();
Log.i("ROW DATA:"+row[0],row[1]);
}
if(tableName.equals(xpp.getName().trim()))
{
Log.i("All DONE","----------------------");
String [][] xmlData=new String[allData.size()][columns.size()];
xmlData=allData.toArray(xmlData);
return (xmlData);
}
}
else if(eventType == XmlPullParser.TEXT)
{
if(xpp.getText().trim().length()>=1)
singleRow.add(xpp.getText());
}
eventType = xpp.next();
// Log.i("ILoop over","1 loop done");
}
catch(Exception ex)
{
String [][] xmlData=null;
xmlData=allData.toArray(xmlData);
return (xmlData);
}
}
}
catch (Exception ex)
{
Log.i("Exception happened",ex.getMessage());
return null;
}
return null;
}
有趣的是,元数据的提取仅限于第一个实例。也就是说,我们将只提取一次 tableName、rowName 和 columns。对于复杂数据,您可以修改此方法,查找每个记录的元数据并相应地提取记录。
Android XMLParser 的一个问题是,在 TEXT 中,它倾向于提取换行符、制表符、空格等所有内容。为了防止读取任何垃圾数据,我们使用以下标准。
if(xpp.getText().trim().length()>=1)
singleRow.add(xpp.getText());
您可以测试删除 if 条件。您可能会注意到大量的垃圾换行符和空格字符数据。
现在为了测试,我们回到 MainActivity 并添加我们在用于测试文件操作的部分中的测试部分。
String filePathXML=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePathXML=filePathXML+"/"+"OpinionMining/OpinionXMLData.xml";
Log.i("Checking XML", "---------------");
String[][] data=AndroidXMLRecordAccess.ReadXMLRecords(filePathXML);
for(int i=0;i<data.length;i++)
{
Log.i(data[i][0], data[i][1]);
}
}
结果如图 3.3 所示。
图 3.3:XML 记录访问结果
此方法的通用性使我们能够提取几乎任何单例 XML 记录。在后面的部分中,我们将处理从 Web 获取 XML 数据。此方法在这些情况下将非常有用。
3.2.3 从 XML 文件提取记录元数据
在许多应用程序(如第 3.2.4 节中讨论的应用程序)中,我们需要表元数据。我们已经在此处打印了元数据,但让我们编写一个更结构化的方法来获取它。为了获取元数据,让我们首先声明一个名为 **RecordMetaData** 的类,以便我们可以使用它的对象在方法和类之间传递/返回元数据。
public class RecordMetaData
{
public String Tablename="";
public String RowName="";
public String[]Columns=null;
public RecordMetaData()
{
Tablename="";
RowName="";
Columns=new String[1];
}
public RecordMetaData(int noColumns)
{
Tablename="";
RowName="";
Columns=new String[noColumns];
}
}
我们的 **GetRecordMetaData** 方法应返回此类的对象。 我们已经知道如何提取元数据。因此,此方法只是我们之前开发的方法的修改版本,唯一的变化应该是我们在这里不关心读取 TEXT 标签。
public static RecordMetaData GetRecordMetaData(String filePath)
{
String tableName="";
String rowName="";
ArrayList<String>columns=new ArrayList<String>();
RecordMetaData rm=new RecordMetaData();
boolean columnTracking=false;
// 1. First Open the XML File using File Method and Load it's contents in String
String data=AndroidFlatFileAccess.ReadFile(filePath);
///////////////////////////////////////
/// 2. Instantialize XmlPullParserFactory.....................
XmlPullParserFactory factory = null;
XmlPullParser xpp = null;
try
{
factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
xpp = factory.newPullParser();
xpp.setInput(new StringReader(data));
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT)
{
try{
if (eventType == XmlPullParser.START_DOCUMENT)
{
Log.i("Start document","START_OF_DOCUMENT");
}
else if (eventType == XmlPullParser.START_TAG)
{
//Log.i("Start tag ",xpp.getName());
// Initialize a row object when start tag is encountered: OpinionWord
if(tableName.length()<1)
{
tableName=xpp.getName().trim();
Log.i("Table Name",tableName);
}
else
{
if(tableName.trim().equals(xpp.getName().trim()))
{
}
else
{
if(rowName.length()<1)
{
rowName=xpp.getName().trim();
columnTracking=true;
Log.i("Row Name",rowName);
}
else
{
if(rowName.equals(xpp.getName().trim()))
{
}
else
{
if(columnTracking)
{
columns.add(xpp.getName().trim());
}
}
}
}
}
}
else if (eventType == XmlPullParser.END_TAG)
{
// Log.i("End tag ",xpp.getName());
// Convert the ArrayList singleRow to String[] and add inside all data.
if(rowName.equals(xpp.getName().trim()))
{
if(columnTracking)
{
columnTracking=false;
rm=new RecordMetaData(columns.size());
rm.RowName=rowName;
rm.Tablename=tableName;
String [] colArray=new String[columns.size()];
colArray=columns.toArray(colArray);
rm.Columns=colArray;
Log.i("Columns",columns.get(0)+" ,"+columns.get(1));
}
}
}
else if(eventType == XmlPullParser.TEXT)
{
}
eventType = xpp.next();
// Log.i("ILoop over","1 loop done");
}
catch(Exception ex)
{
return rm;
}
}
}
catch (Exception ex)
{
Log.i("Exception happened",ex.getMessage());
return rm;
}
return rm;
}
最重要的一节是实例化 RecordMetaData 对象的部分。
if(rowName.equals(xpp.getName().trim()))
{
if(columnTracking)
{
columnTracking=false;
rm=new RecordMetaData(columns.size());
rm.RowName=rowName;
rm.Tablename=tableName;
String [] colArray=new String[columns.size()];
colArray=columns.toArray(colArray);
rm.Columns=colArray;
Log.i("Columns",columns.get(0)+" ,"+columns.get(1));
}
}
元数据测试结果如下。
图 3.4 元数据获取方法结果
有了这些信息,我们现在可以继续进行插入、删除和更新等其他操作。
3.2.4 插入新记录
插入或添加新记录是数据库/记录管理中最重要的一部分。插入新数据类似于**更新**文件记录中的数据。您提取所有数据,添加额外数据,然后将所有内容写回文件。但是,重要的是要知道,与文件不同,XML 是基于标签的。所以 与文件中的插入是一个简单的追加操作不同,在 XML 中,它就像更新一样,需要刷新旧信息并添加新信息。
重要的是要理解 新记录必须在 <rowName></rowName> 标签内。每个列应进一步包含在适当的列名标签中。最后,所有记录都必须包含在 <tableName></tableName> 标签内,该标签应放在 XML 文档内。为了成功做到这一点,我们需要完整的表元数据,我们已经通过上一小节获得了这些元数据。
让我们先通过 **InsertRecord()** 方法的完整逻辑进行检查。
1. InsertRecord() 方法必须接受一个包含您要插入的新数据的 String[] 和 XML 文件路径。
2. 在方法中,它首先应该在变量 **rm** 中获取元数据,在变量 **data** 中获取现有数据。
3. 现在打开 XML 文件进行写入。请记住不要交换 2 和 3,因为以写入模式打开文件将阻止元数据提取和记录提取方法以读取模式打开它。
4. 写数据的步骤
WRITE START_DOCUMENT
/* First Writeback Existing Data*/
WRITE START_TAG for TABLE_NAME
LOOP: i=0 to data.length()
WRITE START_TAG for ROW_NAME
LOOP:j=0 to rm.Columns.length
WRITE START_TAG for COLUMN_NAME[j]
WRITE TEXT data[i][j]
WRITE END_TAG for COLUMN_NAME[j]
END
WRITE END_TAG for ROW_NAME
END
/*Existing Data is Written*/
/* Add New Record*/
WRITE START_TAG for ROW_NAME
LOOP:j=0 to rm.Columns.length
WRITE START_TAG for COLUMN_NAME[j]
WRITE TEXT newRecord[j]
WRITE END_TAG for COLUMN_NAME[j]
END
WRITE END_TAG for ROW_NAME
/* New Record Addition Over*/
WRITE END_TAG for TABLE_NAME
WRITE END_DOCUMENT
之所以呈现此算法,是因为它易于理解新 XML 回写过程。最后,基于上述逻辑,我们有了 **InsertRecord** 方法。
public static int InsertRecord(String filePath,String[] newRow)
{
try
{
///////////////// Fetch Existing Rows//////////////////////
RecordMetaData rm=AndroidXMLRecordAccess.GetRecordMetaData(filePath);
String[][]data=AndroidXMLRecordAccess.ReadXMLRecords(filePath);
//////////////////////////////////////////////////////////////
////////////////////Initialise variables///////////////////
FileOutputStream fos = new FileOutputStream(filePath);
XmlSerializer xmlSerializer = Xml.newSerializer();
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
/////////////////////////////////////////////////////////
///////////// Begin Section//////////////////////
xmlSerializer.startDocument("UTF-8", true);
////////////////////////////////////////////
/////////////////// Row Wise Data////////////////
xmlSerializer.startTag(null, rm.Tablename);
///////////////// First write back existing data
for(int i=0;i<data.length;i++)
{
xmlSerializer.startTag(null, rm.RowName);
for(int j=0;j<rm.Columns.length;j++)
{
xmlSerializer.startTag(null, rm.Columns[j]);
xmlSerializer.text(data[i][j]);
xmlSerializer.endTag(null, rm.Columns[j]);
}
xmlSerializer.endTag(null, rm.RowName);
}
/////////////// Now Put back New Record///////////////////////
xmlSerializer.startTag(null, rm.RowName);
for(int j=0;j<rm.Columns.length;j++)
{
xmlSerializer.startTag(null, rm.Columns[j]);
xmlSerializer.text(newRow[j]);
xmlSerializer.endTag(null, rm.Columns[j]);
}
xmlSerializer.endTag(null, rm.RowName);
//////////////////////////////////////////////////////
xmlSerializer.endTag(null, rm.Tablename);
////////////////////////////////////////////////
/////////////////////////// End Section/////////////
xmlSerializer.endDocument();
xmlSerializer.flush();
String dataWrite = writer.toString();
fos.write(dataWrite.getBytes());
fos.close();
/////////////////////////////////
}catch(Exception ex)
{
return -1;
}
return 1;
}
您可以观察到 InitializeVariable 部分出现在获取元数据和记录之后。您可以交换这些部分来看效果。显然,您会得到 NullPointerException,但仍然值得测试。原因是读取元数据和读取记录方法都需要以读取模式打开文件,一旦您以写入模式打开文件,Android(或任何编程环境)将不允许这样做。
开发完方法后,就该进行测试了,我们将尝试将 Word Pathetic 和权重 -3 添加到记录中。
AndroidXMLRecordAccess.InsertRecord(filePathXML, new String[]{"Pathetic","-3"});
将更新您的记录,新行是 [Pathetic -3],如图 3.5 所示。
图 3.5 XML 数据库中新记录添加结果
3.2.5 删除 XML 记录
XML 记录的删除记录方法将与插入方法类似。唯一的区别是,我们不会发送一个完整的行,而是发送一个搜索词。该方法应检查记录中任何列的值是否与此搜索词匹配。如果任何列匹配,则不得写回该列。最后,当所有记录都被写回文件时,与搜索词匹配的行将不存在。该方法还必须返回被删除的行数。
public static int DeleteRecord(String filePath,String searchTerm)
{
int totAffected=0;
try
{
///////////////// Fetch Existing Rows//////////////////////
RecordMetaData rm=AndroidXMLRecordAccess.GetRecordMetaData(filePath);
String[][]data=AndroidXMLRecordAccess.ReadXMLRecords(filePath);
//////////////////////////////////////////////////////////////
////////////////////Initialise variables///////////////////
FileOutputStream fos = new FileOutputStream(filePath);
XmlSerializer xmlSerializer = Xml.newSerializer();
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
/////////////////////////////////////////////////////////
///////////// Begin Section//////////////////////
xmlSerializer.startDocument("UTF-8", true);
////////////////////////////////////////////
/////////////////// Row Wise Data////////////////
xmlSerializer.startTag(null, rm.Tablename);
///////////////// First write back existing data
for(int i=0;i<data.length;i++)
{
//////////////// Search the Search term in all columns of current row////////
boolean tobeIncluded=true;
for(int j=0;j<rm.Columns.length;j++)
{
if(data[i][j].toLowerCase().trim().equals(searchTerm.toLowerCase().trim()))
{
tobeIncluded=false;
// If searchTerm found the don't include current row
}
}
//////////////////////////////////////////////////////
if(tobeIncluded) // If search term not found
{
xmlSerializer.startTag(null, rm.RowName);
for(int j=0;j<rm.Columns.length;j++)
{
xmlSerializer.startTag(null, rm.Columns[j]);
xmlSerializer.text(data[i][j]);
xmlSerializer.endTag(null, rm.Columns[j]);
}
xmlSerializer.endTag(null, rm.RowName);
}
else
{
totAffected++;
}
}
xmlSerializer.endTag(null, rm.Tablename);
////////////////////////////////////////////////
/////////////////////////// End Section/////////////
xmlSerializer.endDocument();
xmlSerializer.flush();
String dataWrite = writer.toString();
fos.write(dataWrite.getBytes());
fos.close();
/////////////////////////////////
}catch(Exception ex)
{
return totAffected;
}
return totAffected;
}
为了测试,我删除了我在上一个示例中添加的单词 pathetic,并添加了新单词 Great。
AndroidXMLRecordAccess.DeleteRecord(filePathXML, "pathetic");
AndroidXMLRecordAccess.InsertRecord(filePathXML, new String[]{"Great","4"});
图 3.6:删除记录的结果(移除了单词“Pathetic”)
3.2.6 更新记录
更新记录与删除记录类似。这里我们必须指定 searchTerm 以及新的行数据。在删除中,我们不包括与搜索词匹配的行,而在 Update 方法中,我们需要为任何列与搜索词匹配的行写入替代行。
public static int UpdateRecord(String filePath,String searchTerm,String[] newRow)
{
int totAffected=0;
try
{
///////////////// Fetch Existing Rows//////////////////////
RecordMetaData rm=AndroidXMLRecordAccess.GetRecordMetaData(filePath);
String[][]data=AndroidXMLRecordAccess.ReadXMLRecords(filePath);
//////////////////////////////////////////////////////////////
////////////////////Initialise variables///////////////////
FileOutputStream fos = new FileOutputStream(filePath);
XmlSerializer xmlSerializer = Xml.newSerializer();
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
/////////////////////////////////////////////////////////
///////////// Begin Section//////////////////////
xmlSerializer.startDocument("UTF-8", true);
////////////////////////////////////////////
/////////////////// Row Wise Data////////////////
xmlSerializer.startTag(null, rm.Tablename);
///////////////// First write back existing data
for(int i=0;i<data.length;i++)
{
//////////////// Search the Search term in all columns of current row////////
boolean tobeReplaced=false;
for(int j=0;j<rm.Columns.length;j++)
{
if(data[i][j].toLowerCase().trim().equals(searchTerm.toLowerCase().trim()))
{
tobeReplaced=true;
// If searchTerm found the don't include current row
}
}
//////////////////////////////////////////////////////
if(!tobeReplaced) // If search term not found
{
xmlSerializer.startTag(null, rm.RowName);
for(int j=0;j<rm.Columns.length;j++)
{
xmlSerializer.startTag(null, rm.Columns[j]);
xmlSerializer.text(data[i][j]);
xmlSerializer.endTag(null, rm.Columns[j]);
}
xmlSerializer.endTag(null, rm.RowName);
}
else
{
xmlSerializer.startTag(null, rm.RowName);
for(int j=0;j<rm.Columns.length;j++)
{
xmlSerializer.startTag(null, rm.Columns[j]);
xmlSerializer.text(newRow[j]);
xmlSerializer.endTag(null, rm.Columns[j]);
}
xmlSerializer.endTag(null, rm.RowName);
totAffected++;
}
}
xmlSerializer.endTag(null, rm.Tablename);
////////////////////////////////////////////////
/////////////////////////// End Section/////////////
xmlSerializer.endDocument();
xmlSerializer.flush();
String dataWrite = writer.toString();
fos.write(dataWrite.getBytes());
fos.close();
/////////////////////////////////
}catch(Exception ex)
{
return -1;
}
return totAffected;
}
观察粗体 **else** 部分。这就是 Delete 和 Update 方法的区别。在更新中,您可以看到我们将 newRow 替换为数据的行,其列值与 searchTerm 匹配。
您可以通过以下方式测试 UpdateRecord 方法。
AndroidXMLRecordAccess.UpdateRecord(filePathXML,"good" ,new String[]{"Gud","3"});
正如您所见,我通过搜索术语“good”将 ["Good" 1] 更改为 ["Gud" "3"]。通过将搜索词和列值都转换为小写来实现不区分大小写的搜索。
图 3.7 XML 记录更新结果
您可以测试该方法以用于其他测试用例,例如受影响多行的案例等。
3.3 本地 XML 记录处理总结
在第 3.2 节中,我们看到了访问本地 XML 记录的方法。XML 记录访问的方法比扁平文件对应的方法要复杂一些。但是,由于 XML 文件是平台独立的,并且支持复杂数据类型,因此它们适用于许多应用程序,特别是数据不经常更改且数据大小不大的应用程序。大量数据将消耗大量资源,这对于移动设备来说是不推荐的,因为它们会消耗大量电池。因此,XML 是存储配置的一个不错的选择。
如果记录中的单个空格或制表符错位,扁平文件很容易损坏。在这种情况下,整个记录将无法工作。但是,XML 可以免受此类失败的影响。任何严重的错误都可能导致解析失败,可以通过异常处理程序进行跟踪。
与扁平文件记录相比,XML 记录中的数据完整性检查更为紧凑。为本地 XML 数据处理开发的技术可以通过一些调整来解析远程 XML 文件。
总而言之,我们已经开发了一个轻量级的通用 XML 数据库系统,就像我们开发文件访问系统一样。这个 XMl 类可以用于任何非复杂(非嵌套)记录的 XML 记录。
4. SQLite 数据库
4.1 SQLite 基础
4.1.1 SQLite 简介
网上有几个教程提供了关于 SQLite 的良好见解。因此,即使是初学者 Android 程序员可能也熟悉这个术语。SQLite 本质上是一个轻量级的关系数据库系统。独立隔离的数据库可用于处理本地数据,就像我们对 XML 和扁平文件所做的那样。
在哪些情况下应使用 SQLite?例如,一个支持多人(非多人游戏)从同一设备访问游戏的游戏。每个玩家可能有不同的级别和偏好。不同的配置可能是不同的表:例如,可能有名为 settings、scores、preferences 等的表,这些表都可以通过 {PRIMARY-FOREIGN_KEY} 关系链接。
我们可以考虑一个轻量级的会计软件包,商人可以用它来记录日常开支。当记录增长到超出设备支持的尺寸限制时,数据可以备份到云端。
SQLite 默认是安全的,这意味着数据不像扁平文件或 XML 文件那样可以直接被错误地或有意地操纵。 例如,您的一个朋友在玩您的手机,并找到了 OpinionData.txt 文件。他在文本编辑器中打开它并添加 [is 100]。很容易想象任何意见测试的结果!
SQLite 还支持存储过程和事务。因此,专业的数据库设计者在处理这个数据库时会感到更加“安全”。这就像任何时候 codeproject 开发者看到一篇很棒的 C# 文章一样,他们的眼睛都会发光。
因此,在开始使用 SQLite 之前,这里有一些需要了解和记住的事情,它们将帮助您在选择数据库时倾向于 SQLite。
因此,我们了解到 SQLite 可用于大量数据事务和关系型数据。
现在,既然已经有很多教程了,我们在这里还要学习什么呢?任何像 JDBC 那样的 ADO.NET 数据访问都非常依赖于表。实际上,SQL 查询取决于表、属性、键等等。因此,与其学习如何在 Android 中访问 SQLite 数据库,不如尝试创建一个通用类,可以被任何 SQLite 应用程序使用。
4.1.2 SQLite 技术特性
1) SQLite 是 Android 特有的,即此数据库不能与其他关系型数据库(如 MySQL)互操作。扁平文件和 XML 是非常可互操作的。它们的数据可以导入和导出到任何其他数据库。
2) SQLite 支持 ACID 属性。如果您是数据库程序员,这是您学到的第一件事;如果您不是数据库程序员,学习数据库的**原子性、一致性、隔离性、持久性**属性也没有坏处。
3) 幸运或不幸的是,该数据库只支持四种数据类型:**TEXT**(类似于 Java String)、**INTEGER** (非小数整数)、**REAL** (类似 Java 的 float/double 数据类型)、**BLOB** (用于存储图像、音乐等媒体的二进制数据)。任何其他数据类型都需要根据适用性转换为这些类型之一。这对于移动设备访问的大部分数据来说应该不是什么大问题。您可能会在日期类型数据上遇到问题,因为许多实际查询都使用日期,特别是选择日期范围内的记录。如果您想有一个表,您想根据日期范围提取一个元组,您可以随时创建三个字段,分别是 day、month、year,对应于日期数据。这可能不是最优雅的,但相信我,这有助于保持数据类型较小。
4) SQLite 支持 **PRIMARY_KEY** 和 **REFERENCE_KEY**。
5) 如果一个列创建为 **INTEGER PRIMARY_KEY**,那么当我们将 **NULL** 存储到该列时,它将**自动递增**。
6) 它不强制执行**数据类型约束**。任何类型的数据(通常)都可以插入到任何列中。您可以将任意长度的字符串放入整数列。您在 CREATE TABLE 命令中为列指定的数据类型不会限制可以放入该列的数据。每个列都可以保存任意长度的字符串,除了 INTEGER PRIMARY KEY,它只能保存 64 位有符号整数。此功能称为**类型亲和性 (TYPE AFFINITY)**。 数据库使用创建表时指定的数据类型或多或少作为提示。所以,如果您有一个声明为 Integer 的列,并且您输入了一个值“12”,它会将其转换为整数 12 并保存。如果您尝试将“we are part of lovely world”存储在列中,它只会存储字符串,因为它无法将字符串转换为数字。
7) SQLite 在真正意义上支持**并行性**或**并发性**。这是一种多进程或应用程序可以同时访问同一数据库的特性。但一次只能有一个进程拥有**写入(INSERT、UPDATE、DELETE)**权限。一致性属性确保 READ 仅在 WRITE 之后执行,如果两者同时到达。
8) SQLite 是无服务器的 (serverless)**,这意味着没有独立的服务器进程运行,所以没有所谓的启动和停止数据库。这与其他两个数据库系统(到目前为止已讨论过)类似,即插即用。
9) SQLite 数据是**持久的 (Persistent)**。 即,即使应用程序关闭,数据也不会被刷新。
在对 SQLite 的优点和缺点有了一些了解,并且看到了它相对于扁平文件和 XML 记录的优势之后,终于到了开始第一步:创建 SQLite 数据库。
4.2.1 创建 SQLite 数据库
请记住,对于扁平文件和 XML 记录,我们在 PC 上创建了文件,然后上传了它。然而,由于 SQLite 中的存储和数据处理完全不同,创建这样的数据库不像其他两个那样直接。
SQLite 数据库可以通过两种方式创建:使用程序或使用 **SQLite Browser**。 SQLiteBrowser 是一款软件,允许您在 PC 上离线创建和测试数据库。由于这是一个像 MS-Access 一样的 GUI 驱动软件,我们将首先采用这种简单的选项,并将我们的注意力转向使用我们的 APP 创建数据库作为另一个子部分。
安装它,这只需要一两分钟。最好的方面是:它非常简单,您不需要任何培训。
在我们继续之前,让我们有一个与 OpinionXMLData 等效的表。我们想创建一个名为 **OpinionSQLiteDatabase** 的数据库。该数据库包含一个名为 **OpinionDataTable** 的表,其类型为 {No,Word,Weight},其中 No 是整数主键,Word 是 TEXT 类型,Weight 是 REAL 类型。
图 4.1 在 SQLite Browser 中创建数据库
要创建数据库,只需选择 New Database 选项。选择要保存数据库的目录(我将其保存在桌面上)。给它一个合适的名字,然后像图 4.1 一样保存。
一旦创建了数据库,通过选择 Create Table 选项来创建 OpinionDataTable,然后使用 Add Field 选项继续添加字段。 如前所述,我们使用 No、Word 和 Weight 字段以及适当的数据类型。我们还将 No 设置为主键,方法是选中 PK 复选框。
表的创建如图 4.2 所示。
4.2 在 SQLite 数据库中创建表
对于我们的 OpinionMining,我们实际上不需要主键,但我添加了这个额外的字段来演示 SQLite 中主键的功能。
图 4.3 从查询中插入值
要将数据插入表中,您需要选择 Execute SQL 选项卡并执行适当的 INSERT 查询,如图 4.3 所示。在浏览器中键入查询后,单击如图所示的 |> 图标。查询结果将在窗口底部显示为查询状态。
现在我们想查看数据库的主键约束。如果您尝试插入 {1,'Bad',-3},您将看到一个错误,因为 No=1 违反了主键约束,因为表中已有 No=1 对应于 Word='Good'。
图 4.4 主键约束冲突示例
在第 4.1.2 节中,我们了解到如果插入 NULL,主键将充当自动递增。您可以在图 4.5 中验证这一点。
图 4.5 主键字段对 NULL 数据的自动递增
最后,您可以从 Browse Data 选项卡浏览表中的数据。您可以在图 4.6 中看到 No=2 已自动为 Word='Bad' 的行存储。
图 4.6:SQLite 中的 Browse Data 选项验证所有 INSERT 查询
创建数据库后,就该将其放入我们的项目中了。生成的应用程序必须在首次运行时将数据库部署到设备中,就像应用程序对扁平文件和 XML 所做的那样。这里又有两个选项:将数据库保存在 INTERNAL 存储器中,例如 DDMS 视图中的 data 目录(如图 2.14 所示)。但是,正如我们对其他所有选项所做的那样,我们希望将其保留在 EXTERNAL 中,位于我们 SDCARD 的 Pictures 目录中的 OpinionMining 文件夹中。
请记住,在将数据库上传到 assets 文件夹之前,请保存并关闭您的 SQLite Browser。
在此之前,将您的 OpinionSQLiteDatabase 保存到项目(当然,文件名首选小写字母)的 **assets** 目录中。查看我们 assets 目录的结构,如图 4.7 所示。
图 4.7:将 SQLite 数据库加载到项目的 assets 文件夹中
4.2.2 部署和测试数据库
好的,现在我们准备使用数据库了。第一项任务是将数据库存储在设备中,我们称之为**部署**。部署后,我们可以继续使用我们的通用类,该类可以处理几乎任何 SQLite 数据库和表。
像往常一样,我们继续创建一个名为 **SQLiteDatabaseAccess** 的类,并让该类继承 **android.database.sqlite.SQLiteOpenHelper**。
创建类后,它会提示您实现未实现的方��,请继续使用默认设置。您的类将如下所示。
public class SQLiteDatabaseAccess extends SQLiteOpenHelper
{
public SQLiteDatabaseAccess(Context context)
{
super(context, DB_NAME, null, 1);
// TODO Auto-generated constructor stub
}
@Override
public synchronized void close() {
super.close();
}
@Override
public void onCreate(SQLiteDatabase arg0) {
// TODO Auto-generated method stub
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
}
}
我已经添加了 DB_NAME,它当然还没有声明。但不用担心,我们将看到它是如何工作的。
现在我们的第一个任务是将数据库从 assets 文件夹复制到设备中。基本上有两种选择:
1) 您可以将数据库放在设备内部存储的 **data/data/** 文件夹中:如果您想使用此选项,那么您的数据库路径变量(例如 **DB_PATH**)应该如下所示。
public static String DB_PATH = "/data/data/com.integratedideas.opinionmining/databases/";
请注意,data 文件夹中的数据库必须采用 **data/data/COMPLETE_APP_PACKAGE/databases** 格式。作为开发人员,您可能会想做的一件事是离线检查数据以进行验证和调试,就像我们对扁平文件和 XML 记录选项所做的那样。但是,如果您的设备未 root,您将无法从 DDMS 中看到数据。
2) 第二个数据库部署选项是位于我们在此应用程序中一直使用的 **/Pictures/OpinionMining** 文件夹中。为了避免 root 设备并方便调试,我们将坚持这个选项,在这种情况下,我们的 DB_PATH 变量将是。
public static String DB_PATH = "/storage/emulated/0/Pictures/OpinionMining/";
现在您可以随时在测试部分更新路径。与其硬编码 DB_PATH 变量,不如像我们为其他记录处理所做的那样获取路径。
让我们声明一个名为 **DB_NAME** 的数据库名称变量,因为我们需要一个通用的 SQLite 数据处理解决方案。同样,我们将使其保持 public static,可以从外部更新。
public static String DB_NAME = "OpinionSQLiteDatabase";
所以,现在目标路径是 **DB_PATH+DB_NAME**。请注意路径变量末尾的 **/**。如果您不使用它,您的目标部署文件将是 **DB_PATH+"/"+DB_NAME**。
请记住,在复制文件时,我们打开了源文件作为 FileInputStream 对象?我们在这里也将这样做。
myContext.getAssets().open(DB_NAME.toLowerCase());// Remember inside asset folder we have put lowercase //-name file
其中 myContext 是 MainActivity 的上下文,因为只有从 Activity 类才能访问 getAssets() 方法。因此,我们必须有一个名为 myContext 的 Context 对象,它应该用 MainActivity 的实例进行实例化。
我们的逻辑是将数据库从 assets 复制到 OpinionMining 数据库,即首先检查数据库是否存在于部署位置,如果不存在,我们将执行复制操作。
与文件访问不同,任何文件名与数据库名称匹配都不够,我们应该进行数据库完整性检查,可以通过打开和关闭数据库来完成。
让我们完成我们的类。
package com.integratedideas.opinionmining;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class SQLiteDatabaseAccess extends SQLiteOpenHelper
{
//public static String DB_PATH = "/data/data/com.integratedideas.opinionmining/databases/";
public static String DB_PATH = "/storage/emulated/0/Pictures/OpinionMining/";
public static String DB_NAME = "OpinionSQLiteDatabase";
private SQLiteDatabase db;
private final Context myContext;
public SQLiteDatabaseAccess(Context context)
{
super(context, DB_NAME, null, 1);
this.myContext = context;
// TODO Auto-generated constructor stub
}
public void createDataBase() throws IOException{
boolean dbExist = checkDataBase();
Log.i("Does Database Exists?",""+dbExist);
if(dbExist){
//do nothing - database already exist
}else{
//By calling this method an empty database will be created into the default system path
//of your application so we are gonna be able to overwrite that database with our dat //abase.
this.getReadableDatabase();
Log.i("Default Database Creation","SUCCESS");
try {
Log.i("PREPARING TO COPY DATA","Initializing CopyDatabase");
copyDataBase();
} catch (IOException e) {
throw new Error("Error copying database");
}
}
}
// Check if the database already exist to avoid re-copying the file each time you open the application.
// * @return true if it exists, false if it doesn't
// */
private boolean checkDataBase(){
SQLiteDatabase checkDB = null;
try{
String myPath = DB_PATH + DB_NAME;
checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY);
}catch(Exception e){
//database does't exist yet.
}
if(checkDB != null){
checkDB.close();
}
return checkDB != null ? true : false;
}
/**
* Copies your database from your local assets-folder to the just created empty database in the
* system folder, from where it can be accessed and handled.
* This is done by transfering bytestream.
* */
private void copyDataBase() throws IOException{
//Open your local db as the input stream
InputStream myInput = myContext.getAssets().open(DB_NAME.toLowerCase());
// Path to the just created empty db
String outFileName = DB_PATH + DB_NAME;
//Open the empty db as the output stream
OutputStream myOutput = new FileOutputStream(outFileName);
//transfer bytes from the inputfile to the outputfile
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer))>0){
myOutput.write(buffer, 0, length);
}
Log.i("Database Deployment","Database Created Successfully");
//Close the streams
myOutput.flush();
myOutput.close();
myInput.close();
}
}
因此,**createDatabase** 是我们需要调用的方法,它将调用 **checkDatabase** 方法来查看数据库是否存在于部署位置。如果已存在,则不执行任何操作,否则调用 **copyDatabase** 方法将数据库从源 assets 文件夹复制到 sdcard 的 /Pictures/OpinionMining 文件夹。createDatabase 方法在各个方面都与我们为部署扁平文件而开发的 **CopyFiles()** 方法相似。而 checkDatabase 通过 **SQLiteDatabase.openDatabase** 使用位于部署位置的数据库实例来打开数据库。如果数据库存在且可用,它将返回一个可用的数据库实例。否则,它将返回 null。
完成所有繁重的工作后,就该测试数据库访问了。
String filePathDatabase=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
SQLiteDatabaseAccess.DB_PATH=filePathDatabase;
SQLiteDatabaseAccess db=new SQLiteDatabaseAccess(this);
db.createDataBase();
您可以通过文件管理器检查您的设备,一个名为 OpinionSQLiteDatabase 的新文件已部署在您的 OpinionMining 文件夹中。当您在文本编辑器中打开文件时,您会看到类似图 4.8 的数据。
图 4.8:文本编辑器中的 SQLite 数据库
由于 SQLite 数据库不完全是您的扁平文件,您无法看到任何有意义的数据。但您仍然可以找出插入的字符串和查询。
因此,我们部署数据库的第一项任务是成功的。
4.2.3 SELECT 查询
如果您已经查看了互联网上关于 SQLite 的其他教程,您可能已经预先知道如何声明特定于您的表架构的类或如何声明适配器。如果您是初学者,您肯定会讨厌遵循这些内容;如果您是专业人士,您可能一直在寻找一些容易重复使用的代码,而不考虑表架构。在偶然看到这个 codeproject 教程后,您一定会感谢您花时间阅读和查看这篇文章。因为我们将在这里学到的将是一种真正简单的数据库处理方式,它将非常通用。这意味着:这些方法几乎可以用于所有表,而不考虑它们的字段或字段的数据类型。
public String[][] ReadTable(String tableName)
{
//db=this.getReadableDatabase();
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
String[] columnNames = dbCursor.getColumnNames();
ArrayList<String[]>allData=new ArrayList<String[]>();
String[] singleRow=new String [columnNames.length];
dbCursor.moveToFirst();
do
{
for(int i=0;i<columnNames.length;i++)
{
singleRow[i]=dbCursor.getString(i).trim();
}
allData.add(singleRow);
singleRow=new String [columnNames.length];
}while(dbCursor.moveToNext());
db.close();
String [][]data=new String[allData.size()][columnNames.length];
data=allData.toArray(data);
return data;
}
首先,我们在只读模式下打开数据库实例,因为我们的目的是只读取数据。db.query() 使用表名查询数据库。其余参数用于您要获取数据的列、选择列、选择列的标准、groupby、having 和 orderby。由于我们的目的是拉取所有行,我们将所有其他字段都设为 null。
查询结果在 Cursor 对象中返回。这就像一个记录集对象,我们可以在其中遍历并选择每个遍历中的一个元组。有趣的是,Cursor 对象也返回列名。因此,在循环之前,我们存储列名,这有助于我们获取一行的列数据。
singleRow 数组的大小与 columnNames 数组相同。对于一个循环,我们遍历所有列并将值提取到 singleRow 中。然后将 singleRow 记录添加到 allData ArrayList 对象中。因此,无论行数大小如何,我们都可以获取它们。
在将每个记录提取到 ArrayList 对象后,我们使用
String[**allData.size()**][**columnNames.length**]
将它类型转换为 String[][]。因此,我们最终可以为表返回一个二维数组,其中第一个维度是行(记录),第二个维度存储行的列值。
它的测试和结果将在下一小节与 Insert Query 一起呈现。但在我们进入 Insert 部分之前,我们需要讨论复杂查询。SQLite 被认为是一个关系型数据库。因此,很明显,在许多实际应用中,您可能需要通过链接多个表来拉取数据。如何使用我们的通用方法获取此类数据?
答案是您不能,因为这实际上是“Select * from”@tableName 实现的通用实现。但是,您无需担心。Android 为 **SQLiteDatabase** 对象提供了一个很棒的方法,称为 **rawQuery**,您可以将整个原始 SQL 查询字符串作为参数传递。因此,您可以使用 db 对象调用该方法,如下所示。
//selectionFlags is second argument which is set to none
db.rawQuery("select * from OpinionDataTable where abs(Weight)>1 order by Word", null;)
我将留给读者开发另一个执行此操作的通用方法。您需要做的就是更改 ReadTable 方法中的 db.query 部分。
4.2.4 INSERT 查询
我们可以坦率地设计一个与表无关(嗯,几乎)的插入查询。您可能会问,为什么对于不同的数据类型可以做到?请回忆我们在 4.1.2 节的 6) 中讨论过的 SQLite 数据库的**类型亲和性 (Type Affinity)** 属性。所以我们知道我们可以将字符串放在任何数据类型的前面,SQLite 会自动转换。你不相信我吗?您可以使用 SQLite Browser 进行验证,或者直接跳到我们最令人惊叹的 Insert 方法,该方法几乎适用于所有 SQLite 数据类型。
public int InsertRecord(String tableName,String []newData)
{
//db=this.getReadableDatabase();
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
String[] columnNames = dbCursor.getColumnNames();
db.close();
/// Now Create Content Based On Columns ..............
ContentValues values = new ContentValues();
for(int i=0;i<columnNames.length;i++)
{
values.put(columnNames[i], newData[i]);
}
////////// Once Content is created, reopen database to insert values///
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
db.insert(tableName, null, values);
db.close();
/////////////////////////////////////
return 1;
}
SQLite 将**内容 (Contents)** 插入到数据表中。Contents 就像键值对。key 是列名,value 是您要存储的相应值。因此,首先我们需要构建一个 **ContentValues** 对象,它本质上是一个 Contents 列表。由于我们首先需要列名,我们以只读模式打开数据库并执行 db.query。它的结果在 Cursor 对象中获得,该对象返回列名。列名及其从数组中选择的相应值。
一旦您的内容准备就绪,就以读写模式打开数据库,并使用 insert 方法写回新的 Content。就这么简单。
现在您可以看到,我们可以传递 String 数组形式的新行数据,而无需考虑它们的数据类型。所以,即使 No 是 INTEGER,Weight 是 REAL,我们也可以为它们传递 String 值。
插入后跟 Select 查询都可以通过以下方式进行测试。
db.InsertRecord("OpinionDataTable", new String[]{"4","Pathetic","-3"});
Log.i("Displaying Content of Table:----","OpinionDataTable");
String[][]data=db.ReadTable("OpinionDataTable");
for(int i=0;i<data.length;i++)
{
String s="";
for(int j=0;j<data[i].length;j++)
{
s=s+data[i][j]+"-";
}
Log.i(""+i,s);
}
如果您有兴趣检查**自动递增**属性,请在 **InsertRecord** 调用中将 "4" 替换为 **null**。
最后,您可以在 LogCat 中看到如下结果。
4.9 Type Affinity Proof下InsertRecord和ReadTable的结果
4.2.5 SQLite 的 DELETE 操作
让我们设计一个方法,该方法将根据搜索词执行通用的删除操作。
例如,我提供一个名为“good”的搜索词,上面表的查询应自动变为:
“delete from OpinionDataTable where No='good' OR Word='good' OR Weight='good' ”
请在此关注where子句。如果表架构不同,查询应自动更新。通过SQLiteDatabase对象调用delete 方法,我们可以删除一行(或任何匹配条件的行集)。它接受三个参数:第一个是表名,第二个是where子句,第三个是where子句的参数。
请记住,在指定WHERE子句(第二个参数)时,您无需指定WHERE关键字,而是必须将字符串构建为一个包含其余部分的术语。
因此,上述查询的delete调用将是
db.delete( tableName,"No='good' OR Word='good' OR Weight='good' ",null);
但请记住,“good”仅是此示例中的数据。实际上,我们确实希望使用一个名为searchTerm的变量,而不是good。
在这种情况下,查询将看起来像
db.delete( tableName,"No='"+searchTerm+"' OR Word='"+searchTerm+"' OR Weight='"+searchTerm+""' ",null);
但这看起来并不优雅。是吗?此外,您不一定想发送字符串。其次,对于大量的列,构建这样的查询很繁琐。那么呢?
delete方法支持第三个参数,即搜索词的参数。因此,在要放置变量的位置使用通配符? 在搜索词中,然后传递一个字符串数组或任何合适数据类型的数组。因此,我们的查询变成
db.delete( tableName,"No=? OR Word=? OR Weight=? ",new String[]{"good","good","good",});
但是,我们的目标不是特定的表删除方法,而是更通用的删除方法,可以用于任何表。因此,我们需要动态地构建查询的第二和第三个术语。从上述方法调用的结构可以清楚地看出,第二个术语应为每个列名提供通配符?。然后,它应创建一个大小与列数相同的字符串数组,并将搜索词作为数组中出现的次数附加,就像我们为具有三列的表看到的包含三个“good”的字符串数组一样。
这是我们的方法。
public int DeleteRecord(String tableName,String searchTerm)
{
// 1. Get the column names................................//
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
String[] columnNames = dbCursor.getColumnNames();
db.close();
/////////////////////////////////////////////////////////
/// Now Create Where clause and Argument part Based On Columns ..............
String WHERE="";
String []args=new String[columnNames.length];
for(int i=0;i<columnNames.length-1;i++)
{
WHERE=WHERE+columnNames[i]+"=? OR ";//Note a SPACE after OR
args[i]=searchTerm;
}
WHERE=WHERE+columnNames[columnNames.length-1]+"=?";
//The above line is separated as there is no OR after the last appearance of column
args[columnNames.length-1]=searchTerm;
////////// Once Where part and arguments are ready, reopen database in RW mode to delete///
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
int n=db.delete(tableName, WHERE, args);
db.close();
/////////////////////////////////////
return n;
}
删除之前添加的“Pathetic”,然后使用AutoIncreament功能添加新词“Worst”的测试如下所示。
db.DeleteRecord("OpinionDataTable", "Pathetic");
db.InsertRecord("OpinionDataTable", new String[]{null,"Worst","-3"});
图 4.10 SQLite数据库上删除的结果
4.2.6 SQLite 的 UPDATE 操作
Android中的SQLiteDatabase类的update方法具有以下结构
update(table, contentValues, whereClause, whereArgs)
我们从Insert方法中了解了Content Values,并且我们还知道如何从Delete查询中构建where子句。因此,Update本质上是Insert和delete方法的组合。我们的通用方法的结构与往常一样,有两个参数:一个是搜索词,第二个是指定更新行值的字符串数组,正如我们对平面文件和xml更新方法所做的那样。
这是我们的方法
public int UpdateRecord(String tableName,String searchTerm, String[]updateRow)
{
// 1. Get the column names................................//
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READONLY);
Cursor dbCursor = db.query(tableName, null, null, null, null, null, null);
String[] columnNames = dbCursor.getColumnNames();
db.close();
///////////////////Logic Similar to Delete//////////////////////////////////////
/// Now Create Where clause and Argument part Based On Columns ..............
String WHERE="";
String []args=new String[columnNames.length];
for(int i=0;i<columnNames.length-1;i++)
{
WHERE=WHERE+columnNames[i]+"=? OR ";//Note a SPACE after OR
args[i]=searchTerm;
}
WHERE=WHERE+columnNames[columnNames.length-1]+"=?";
//The above line is separated as there is no OR after the last appearance of column
args[columnNames.length-1]=searchTerm;
///////////////////////// Logic Similar to Insert for praparing Content////////
ContentValues values = new ContentValues();
for(int i=0;i<columnNames.length;i++)
{
values.put(columnNames[i], updateRow[i]);
}
////////////////////////////////////////////////////////////////////////////////
////////// Once Where part and arguments are ready, reopen database in RW mode to UPDATE///
db=SQLiteDatabase.openDatabase(DB_PATH + DB_NAME, null, SQLiteDatabase.OPEN_READWRITE);
int n=db.update(tableName,values, WHERE, args);
//db.update(table, values, whereClause, whereArgs)
db.close();
/////////////////////////////////////
return n;
}
测试可以按以下方式进行
db.UpdateRecord("OpinionDataTable","Good" ,new String[]{"1","Great","5"});
这里有一点很重要,您必须记住。在Insert中,您可以通过将null传递给主键字段来逃避,从而利用其自动增量属性。但是在update方法中,您需要指定准确的数据。不用说,查询不接受主键字段的null。但是,您可以注意到,Type Affinity仍然有效,并且我们可以将“1”而不是数字1传递给INTEGER主键No字段。
图 4.11 Update Query的结果
4.3 关于 SQLite 数据库的结语
SQLite数据库部分已通过SQLite数据处理最简单的方式向您介绍了。该部分试图帮助您构建更通用的即插即用方法,即适用于您要使用的几乎任何表或数据库。我们已使用LogCat测试了这些方法。我们传递的参数很大程度上取决于数据库的状态。您可以 下载OpinionMining_SQLite_Test.zip 并测试这些方法。我还敦促您构建GUI并将方法调用从GUI触发器附加,以更适当地查看结果。
5. SharedPreferences(共享偏好设置)
5.1 简介和技术规范
共享偏好设置顾名思义,即共享偏好设置的手段。在谁之间?应用程序之间,但请注意SharedPreference只能在两个包名相同或其中一个包名是另一个包名的子级的应用程序之间共享数据 。
如果您希望应用程序B能够访问应用程序A的偏好设置,则应用程序B的包名必须是应用程序A的包名的子集(例如,应用程序A:com.example.pkg 应用程序B:com.example.pkg.stuff)
“Shared Preferences”是一个轻量级的数据库系统,通过它可以跨应用程序共享数据。它本质上是一个{KEY,VALUE}数据系统,Android将其用作全局数据库。我们可以添加新字段,更新/删除字段或获取特定字段的值。
正如我们所理解的,“Shared Preferences”为所有应用程序提供了一个全局数据库的单一实例。因此,没有像数据库部署这样的事情。“数据库”或“存储”(如果我们被允许使用这两个术语中的任何一个)与设备一起部署,您所要做的就是获取它的实例并继续使用。因此,这可能是我们迄今为止讨论过的所有存储和数据访问技术中最简单的。
那么它的特点是什么?我们可以在哪里使用它,在哪里不能使用?有哪些缺点?
1) Shared Preferences每个键只允许一个值。因此,它可以被认为是单行 - 单列数据库。所以,我们无法凭空想象直接将意见表存储在Shared Preferences中。但是,您可以将表保存为单个分隔字符串,方法是实现提取分隔行和列的方法。然而,这种数据存储需要一个完整的解析器设计,就像我们为平面文件记录设计的那个一样。这就是我们将在本节中尝试做的。
2) Shared Preferences可用于用户存储简单的、快速的和高效的游戏分数、应用程序偏好设置(如主题等)。
3) Shared Preferences提供了一个创建应用程序特定节点(类似于创建数据库)的选项。当前应用程序的所有数据都可以存储在此节点中。
4) Shared Preferences目前不是多进程的。这意味着没有并发。如果两个应用程序请求访问Shared Preferences,则先到的应用程序将被提供服务,另一个应用程序必须等待。因此,这不太优雅。
5) Shared Preferences不支持Type Affinity。也就是说,它期望一个整数值用于一个用于存储整数值的键。
6) Android对应用程序可以访问的内部内存量有一些限制。例如,2.3要求设备为应用程序数据分配至少100Mb的数据,而Android 4.3已增加到512Mb。因此,虽然旧设备上的内部存储可能是一个限制,但对于较新设备来说,这很少是一个问题。
考虑到这些要点,让我们开始实现。
5.2 SharedPreferences 中的记录管理
5.2.1 在 SharedPreferences 中创建和删除“数据记录”
虽然Shared Preferences是键值对,但我们将使用Shared Preferences实现完整的记录管理。这种方法的优点是,一旦您准备好了记录管理系统,您就可以通用地使用开发的方法。如果您愿意,您始终可以有一个单行单列的记录,它充当单个键值对。另一方面,您也可以将这些方法用于更广泛的数据定义和架构。
与其他记录处理技术一样,我们将创建一个用于Shared Preferences的类来处理操作。让我们称此类为SharedPreferencesRecordAccess 我们将假设SharedPreference节点为Database (这是为了跟上我们正在处理的架构和数据定义)。正如我们已经知道的,设备上有一个SharedPreference的单一实例。可以使用应用程序上下文访问此实例。因此,该类必须有一个Context 对象,该对象以MainActivity的上下文进行初始化。
SharedPreferences上有两种数据操作:写入和读取。可以使用SharedPreferences 对象来获取节点特定共享实例的实例。如果节点不存在,Android将创建节点并返回SharedPreferences对象的句柄。android.content.SharedPreferences.Editor 对象可用于在Shared Preferences节点中写入/修改/删除数据。必须通过SharedPreferences对象调用edit() 方法来初始化此编辑器对象。
所以我们的类看起来像这样
package com.integratedideas.opinionmining;
import java.util.ArrayList;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
public class SharedPreferencesRecordAccess
{
public Context context;
public String DB_NAME="OpinionMiningSP";
private SharedPreferences sp=null;
private Editor spEditor=null;
public SharedPreferencesRecordAccess(Context context)
{
this.context=context;
sp = context.getSharedPreferences(DB_NAME, 0);
spEditor = sp.edit();
}
public SharedPreferencesRecordAccess(Context context,String dbName)
{
this.DB_NAME=dbName;
this.context=context;
sp = context.getSharedPreferences(DB_NAME, 0);
spEditor = sp.edit();
}
}
请注意,我们正在使用一个名为OpinionMiningSP 的SharedPreferences节点,如果将SharedPreferences可视化为数据/记录管理系统,则可以将其视为数据库名称或记录名称。OpinionMiningSP可能包含多个记录,其中每个记录都可以看作是一个数据库表。由于SharedPreferences是键值对存储,所以记录名称(或表名称)将用作键。一个节点可以有多个键。所以我们可以在一个节点中有多个表。值可以是字符串、整数、双精度、二进制等等。由于我们打算将整个表存储为分隔格式,因此我们只处理字符串值。
有两种构造函数,它们是不言而喻的。SharedPreferences类的对象被初始化为访问当前应用程序上下文的SharedPreferences,spEditor可以写入实例。
我们的表将是图 2.12的格式,第一行是列名,每一行都存储为新行。每列元素都用TAB('\t')分隔。
CreateDatabaseTable 方法在节点中创建一个键,其值为一个记录。为了在技术理解上简单起见,我们将此视为表。为了创建表,我们传递了我们在XML记录访问部分开发的RecordMetaData类的实例。由于在这种情况下我们不需要为独立行提供名称,因此我们无需为类的RowName变量赋值。 这是该方法的实现。
public int CreateDataTable(RecordMetaData rm)
{
// 1st check if the key exists by the name of table name
if(sp.getAll().containsKey(rm.Tablename))
{
Log.i("Shared Preferences","Table Exists");
return 0;
}
else
{
String value="";
for(int i=0;i<rm.Columns.length;i++)
{
value=value+"\t"+rm.Columns[i];
}
value=value+"\n";
spEditor.putString(rm.Tablename, value);
spEditor.commit();
Log.i("Shared Preference","Table Created");
}
return 1;
}
行
if(sp.getAll().containsKey(rm.Tablename))
获取当前SharedPreference上下文的所有键的列表,并在键中搜索表名。如果找到,则表示表已存在,并且不会创建表。
否则,它准备将以'\t'分隔的列存储为键的初始值,该键就是表名。观察包含制表符分隔的列名的字符串末尾的“\n”。这是为了让下一个记录轻松地适合光标所在的空白行。
SharedPreferences是持久存储。要使任何更改持久化,您需要提交更改。PutString方法检查给定名称的键是否存在,如果存在,则覆盖该值。如果不存在给定名称的键,则putString创建一个具有给定名称的新键并存储该值。
spEditor.putString(rm.Tablename, value);
spEditor.commit();
此方法可以从MainActivity使用LogCat进行测试。
SharedPreferencesRecordAccess sp=new SharedPreferencesRecordAccess(this);
RecordMetaData rm=new RecordMetaData(2);
rm.Tablename="OpinionSPTable";
rm.Columns=new String[]{"Word","Weight"};
sp.CreateDataTable(rm);
您可能有时想删除旧表以创建一个具有新数据定义的新表。您也可能想删除一个表来重新创建它。这样的过程会清除所有以前的数据。
spEditor.remove(KEY_NAME) 可以从当前shared preference上下文删除一个键。当一个键被删除时,它的值也会自动被清除。因此,我们RemoveDatabase的实现是
public int RemoveDataTable(RecordMetaData rm)
{
// 1st check if the key exists by the name of table name
if(sp.getAll().containsKey(rm.Tablename))
{
spEditor.remove(rm.Tablename);
spEditor.commit();
return 1;
}
else
{
Log.i("Shared Preference","Table Does not exists in deletion");
return 0;
}
}
我们首先检查记录类型或表是否存在。如果存在,则删除tableName键并提交更改,这将清除表或记录。
5.2.2 INSERT 方法
我们已经创建了带有两列的OpinionDataTable记录。如何插入数据?要插入,首先需要获取现有数据作为字符串。然后需要添加新行。新行是通过连接输入参数数组newData的元素形成的字符串,该数组就是行的列元素。
public int InsertRecord(String tableName,String [] newData)
{
// 1st check if the key exists by the name of table name
if(sp.getAll().containsKey(tableName))
{
String data="";
data=sp.getString(tableName, data);
for(int i=0;i<newData.length;i++)
{
data=data+newData[i]+"\t";
}
data=data.trim();
data=data+"\n";
spEditor.putString(tableName, data);
spEditor.commit();
return 1;
}
return 0;
}
请注意这里的For循环。您可以看到每个标记都被添加并且制表符被附加。这样,在最后一个标记(新记录的最后一个列元素)之后,将有一个额外的制表符。我们使用trim()将其删除。修剪后,将附加一个新行字符,以便下一个记录可以放在下一行。一如既往,您可以看到sp用于获取数据,spEditor用于写回数据。另外请注意,与以前的情况一样,任何写入操作 都需要在最后进行提交。
Insert方法可以通过以下方式测试:
sp.InsertRecord(rm.Tablename, new String[]{"Great","2" });
sp.InsertRecord(rm.Tablename, new String[]{"Bad","-1" });
5.2.3 Select方法(获取所有行)
public String[][] ReadRecord(String tableName)
{
if(sp.getAll().containsKey(tableName))
{
String data="";
data=sp.getString(tableName, data);
String [] rows=data.split("\n");
String [][]allData=new String[rows.length][rows[0].split("\t").length];// Because First row is column names
for(int i=0;i<rows.length;i++)
{
Log.i("Length="+rows.length,"Index="+(i-1));
try{
String[] singleRow=rows[i].split("\t");
for(int j=0;j<allData[i].length;j++)
{
allData[(i)][j]=singleRow[j];
}
}catch(Exception ex)
{
}
}
String [][]myData=new String[allData.length-1][allData[0].length];
for(int i=0;i<myData.length;i++)
{
for(int j=0;j<myData[i].length;j++)
{
myData[i][j]=allData[i+1][j];
}
}
return myData;
}
else
{
return null;
}
}
ReadRecord 返回String[][],就像我们的其他数据访问方法一样。我们首先将数据(存储在键表名下的整个字符串值)获取到名为data的字符串中。首先,我们使用通配符“\n”来分割它。该过程返回包含0行(我们的列名)在内的行数。现在,我们循环遍历此数组,然后通过't'分割每个数组元素,我们现在获得另一个数组,即当前行号对应的行数组。我们将数据放入allData变量中,这是一个双精度数组。但是,我们不希望将列名作为数据返回。因此,我们声明另一个名为myData的数组,其大小比allData小一。我们将数据从allData复制到myData,排除第一行(即列名)。
为了测试,请使用与我们用于所有记录类别相同的格式。
String [][]data=sp.ReadRecord(rm.Tablename);
for(int i=0;i<data.length;i++)
{
String s="";
for(int j=0;j<data[i].length;j++)
{
s=s+data[i][j]+"-";
}
Log.i(""+i,s);
}
5.2.4 DELETE 方法
Delete方法从平面文件记录的Delete方法派生其逻辑。首先,我们需要提取所有数据。该方法将接收一个searchTerm。它所需要做的就是线性搜索所有行的所有列中的searchTerm,并标记找到搜索词的行。将数据复制到临时ArrayList中,因为我们不知道模式会在多少行中找到。最后,将ArrayList转换为与分隔模式相同的字符串,并将其作为键(表名)的值写回,覆盖现有值。然后进行提交,使更改持久化。该方法应返回被删除的行数。
public int DeleteRecord(String tableName,String searchTerm)
{
int matched=0;
String s="";
if(sp.getAll().containsKey(tableName))
{
/////////////// First obtain all data including columns///////
String data="";
data=sp.getString(tableName, data);
String [] rows=data.split("\n");
String[][]record=new String[rows.length][rows[0].split("\t").length];
for(int i=0;i<rows.length;i++)
{
Log.i("Length="+rows.length,"Index="+(i-1));
try{
String[] singleRow=rows[i].split("\t");
for(int j=0;j<record[i].length;j++)
{
record[(i)][j]=singleRow[j];
}
}catch(Exception ex)
{
}
}
///////////////// Search if searchTerm is present in current row//////////////////////////////
for(int i=0;i<record.length;i++)
{
boolean shouldInclude=true;
for(int j=0;j<record[i].length;j++)
{
if(record[i][j].trim().toLowerCase().equals(searchTerm.trim().toLowerCase()))
{
shouldInclude=false;// If found disable copying current row
}
}
if(shouldInclude) // If current row does not have search term, select the record for write back
{
for(int j=0;j<record[i].length;j++)
{
s=s+record[i][j]+"\t";
}
s=s.trim();
s=s+"\n";
}
else // If current row had the search term, increase match count
{
matched++;
}
}
}
spEditor.putString(tableName, s);
spEditor.commit();
return matched;
}
5.2.5 UPDATE 方法
正如我们已经看到的,update方法是删除现有记录并插入新记录,我们可以通过简单地利用delete和insert方法来设计一个update方法。首先调用带有searchTerm的delete,如果删除了记录,则插入新行。如果未删除记录,则表示WHERE子句不满足,在这种情况下,无需插入新记录。
public int UpdateRecord(String tableName,String searchTerm,String[] updateRow)
{
int i=DeleteRecord(tableName, searchTerm);
if(i>0)
{
i=InsertRecord(tableName, updateRow);
return i;
}
else
{
return 0;
}
}
最后,所有内容都可以通过以下方式进行检查:
SharedPreferencesRecordAccess sp=new SharedPreferencesRecordAccess(this);
RecordMetaData rm=new RecordMetaData(2);
rm.Tablename="OpinionSPTable";
rm.Columns=new String[]{"Word","Weight"};
sp.RemoveDataTable(rm);
sp.CreateDataTable(rm);
sp.InsertRecord(rm.Tablename, new String[]{"Great","2" });
sp.InsertRecord(rm.Tablename, new String[]{"Bad","-1" });
String [][]data=sp.ReadRecord(rm.Tablename);
for(int i=0;i<data.length;i++)
{
String s="";
for(int j=0;j<data[i].length;j++)
{
s=s+data[i][j]+"-";
}
Log.i(""+i,s);
}
int n=sp.DeleteRecord(rm.Tablename, "Bad");
sp.InsertRecord(rm.Tablename, new String[]{"Good","2" });
sp.UpdateRecord(rm.Tablename, "Good",new String[]{"Awesome","2" });
data=sp.ReadRecord(rm.Tablename);
for(int i=0;i<data.length;i++)
{
String s="";
for(int j=0;j<data[i].length;j++)
{
s=s+data[i][j]+"-";
}
Log.i(""+i,s);
}
图 5.1:使用共享偏好设置进行记录管理
图 5.1:使用共享偏好设置进行记录管理
您可以 下载OpinionMining_SharedPreferences_Test.zip 并测试我们在本节中开发的这些方法。
到目前为止,我们已经学习了使用Flat File、XML、SQLite和Shared Preferences的Android中的数据访问技术。我们试图理解在什么情况下应该使用哪种类型的数据或记录管理技术。我们已经了解了数据库的部署位置以及内部和外部存储的优缺点。此外,我们以OpinionMining数据表为例,该表在所有部分中都可见,这使我们对性能和编码复杂性有了很好的了解。对于所有机制,我们都创建了通用方法,这些方法几乎可以用于任何表或数据库,只需少量努力即可重用。虽然ReadRecord方法是我们唯一关心的,但我们已经为所有设备内数据管理服务构建了一个完整的框架。我们还使用UI测试了平面文件访问的意见管理。我留给读者使用所有其他技术来测试相同的方法。我还建议初学者创建名为Intents的UI窗体,用于我们学习的所有四种方法,然后将我们开发的方法连接到UI。
在查看了本地数据存储之后,我们将把焦点转移到使用WebServices进行在线数据访问。
6.从Web获取数据
Web数据主要可分为:1)获取原始HTML内容 2)使用Twitter或Facebook等API从云服务获取内容,3)访问WebServices中的数据。以及 4)获取XML 数据,如RSS Feed。这些技术中的每一种都有其自身的应用、优点和缺点。 例如,HTML数据对于从技术论坛(如myBB)获取内容非常有用,并且文本最少。 这项技术与文本转语音结合使用,可以成为呈现网站作为语音文档的应用程序的强大工具。
Facebook、Google Drive、Twitter、Dropbox、Skydrive以及类似的云服务使用RESTful web services进行消息交换,这些消息可以使用JSON进行解析。WebServices实现了跨设备和平台实现互操作性的绝佳方法。
所有这些数据处理都需要设备具有互联网连接。为了允许设备访问网络,您需要启用Use Permission internet,如图 6.1所示。
图 6.1:为您的应用启用INTERNET权限。
有了这个先决条件,我们将继续处理来自Web的数据。
6.1 使用 Android 原生方法访问原始 HTML 数据
HTML内容进一步分为文本和多媒体内容,如图像。典型的HTML页面包含许多标签,这些标签因网站而异。这些标签的范围从CSS标签到CMS系统中的类。尽管如此,当我们获得html文档时,我们需要解析它以从文档中分离出任何有意义的信息。
在Android中,可以使用简单的HTTP请求-响应对来获取网页。 还有其他方法,我们将在适当的时候看到。
我们还将朝着构建Opinion Mining系统的最终目标前进。在继续之前,让我们在UI中进行一些更改,以添加一个用于处理URL的EditText和一个用于触发页面获取的按钮。我们将首先在edInput中获取页面,然后借助opinion按钮获取数据中的意见。
图 6.2 修改后的布局
您也可以通过将activity_main.xml代码替换为以下内容来更改布局。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.integratedideas.opinionmining.MainActivity$PlaceholderFragment" >
<EditText
android:id="@+id/edUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/edInput"
android:layout_alignParentTop="true"
android:layout_marginTop="62dp"
android:layout_marginRight="66dp"
android:ems="10"
android:inputType="text|textUri" >
<requestFocus />
</EditText>
<TextView
android:id="@+id/tvResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btnOpinionTest"
android:layout_centerHorizontal="true"
android:layout_marginTop="34dp"
android:text="@string/hello_world" />
<EditText
android:id="@+id/edInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="17dp"
android:ems="10"
android:gravity="top|left"
android:inputType="textMultiLine"
android:lines="8"
android:maxLines="10"
android:minLines="6"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
<Button
android:id="@+id/btnOpinionTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/edInput"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="@string/opinion_test" />
<Button
android:id="@+id/btnUrlGo"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/edUrl"
android:layout_alignRight="@+id/edInput"
android:text="@string/Go" />
</RelativeLayout>
现在,让我们在MainActivity.java中声明两个名为btnUrlGo和edUrl的变量,以代表相应的布局组件。btnUrlGo将添加到MainActivity类的setAction监听器中,以便在单击按钮时,它会触发onClick(View arg0)的代码。
在MainActivity类中声明以下变量
Button btnUrlGo;
EditText edUrl;
在mainActivity类的onCreate方法中调用super之后初始化它们。
edUrl=(EditText)findViewById(R.id.edUrl);
btnUrlGo=(Button)findViewById(R.id.btnUrlGo);
btnUrlGo.setOnClickListener(this);
现在,让我们修改onClick 的代码,以便在单击Go按钮时触发获取网页,并在单击Opinion Test按钮时触发OpinionMining。
@Override
public void onClick(View arg0)
{
Button b=(Button)arg0;
if(b.getText().toString().trim().equals("Go"))
{
try
{
String url=edUrl.getText().toString().trim();
// HHTP Request response must be invoked from here and result to be assigned to s
String s="RESULT OF HTTP METHOD";
edInput.setText(s);
}catch(Exception ex)
{
edInput.setText(ex.getMessage());
}
}
else
{
String testString=edInput.getText().toString();
int score=mp.SimpleMine(testString);
if(score>0)
{
tvResult.setText("Positive with score="+score);
}
else
{
if(score<0)
{
tvResult.setText("Negative with score="+score);
}
else
{
tvResult.setText("Nutral with score="+score);
}
}
}
就像我们所有的其他功能一样,让我们创建一个新类来处理与Web相关的数据。让我们称此类为HTMLRawDataReader 如果您能找到更好的名字,我将坚持使用Raw 因为它提醒我“黑魔法”。
让我们有一个名为FetchWebDataUsingRequestResponse 的简单方法,它应该使用Android的原生服务来返回原始网页。
public class HTMLRawDataReader
{
HttpClient client = new DefaultHttpClient();
HttpGet request = null;
public static String RSLT="START";
public String FetchWebDataUsingRequestResponse(String url)
{
request=new HttpGet(url);
try
{
HttpResponse response = client.execute(request);
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;
StringBuilder str = new StringBuilder();
while((line = reader.readLine()) != null)
{
str.append(line);
}
return str.toString();
}
catch(Exception e)
{
return e.getMessage();
}
}
}
FetchWebDataUsingRequestResponse 接受用户的URL并使用url初始化HttpRequest对象。然后,使用HttpClient对象,通过调用execute方法获取相应的HttpResponse。 响应内容在缓冲读取器中读取。 最后,使用StringBuilder将响应内容转换为字符串并返回。
现在,让我们从我们已经为调用此预留了占位符的onClick方法中调用此方法。
Button b=(Button)arg0;
if(b.getText().toString().trim().equals("Go"))
{
try
{
String url=edUrl.getText().toString().trim();
HTMLRawDataReader hrd=new HTMLRawDataReader();
String s=hrd.FetchWebDataUsingRequestResponse(url);
edInput.setText(s);
}catch(Exception ex)
{
edInput.setText(ex.getMessage());
}
}
现在保存、重新构建并运行您的应用程序。 键入一个URL,如https://codeproject.org.cn。您看到了什么?
我知道,您什么也没看到。那是因为从Android API 9开始,Android已阻止从主线程调用任何网络服务。 所以?
别担心,我们有解决办法!只需在MainActivity的onCreate方法中的变量初始化之后添加以下两行:
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
这只是绕过了Android施加的限制。现在,当您运行时,您将看到如下屏幕输出。
图 6.3 使用Http Request-Response获取Web数据的结果
虽然我们的调整有助于我们获得结果,但您可能会注意到,从生成请求到在Edittext中获取响应之间,UI实际上会挂起。这是从主线程调用任何网络服务(或任何资源密集型服务)的主要缺点之一。
在执行任何其他与网络相关的操作之前,我们必须找到解决此问题的方法。下一节将帮助您做到这一点。
6.2 Android 线程和 AsyncTask 复习
之所以将此主题涵盖在Web数据访问中,是因为这可能是多线程和后台执行作业最重要的概念。
6.2.1 AsyncTask
AsyncTask是一个围绕线程的辅助类,它允许后台执行方法。但在您为发现阻塞任务的解决方案而欢欣鼓舞之前,请记住AsyncTask旨在轻量级,并且仅在等待时间约为几秒钟时使用。对于更广泛的进程,Android提供了Executor和ThreadPoolExecutor等类。
主线程可以有一个扩展AsyncThread类的内部类。这提供了三个重写的方法:doInBackground,onPostExecute 和onPreExecute。 doInBackground方法是您可以调用资源密集型方法(如我们的FetchWebDataUsingRequestResponse 方法)的地方。一旦方法完成,代码块将自动进入onPostExecute ,它可以访问UI组件。因此,一旦结果可用,此方法就可以更新UI中的结果。
让我们在MainActivity中创建一个名为AsyncWebMethodInvoker 的内部类,并使其扩展AsyncTask。从doInBackground我们将调用远程方法。在onPreExecute中,我们将向textView显示一条消息,说明可能的等待时间,并在任务完成后,我们将将结果分配给edInput。
private class AsyncWebMethodInvoker extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
//hrd.start();
try
{
webResult=hrd.FetchWebDataUsingRequestResponse(url);
}catch(Exception ex)
{
}
return null;
}
@Override
protected void onPostExecute(Void result)
{
edInput.setText(webResult);
tvResult.setText("task Completed");
}
@Override
protected void onPreExecute() {
tvResult.setText("Please be patient.. Operation may take time");
}
}
其中webResult、hrd和url变量是在类中声明的,以便我们的Async类能够访问它们。
String url="";
HTMLRawDataReader hrd=null;
String webResult="";
当单击Go按钮时,在事件处理程序内部,我们将实例化我们的异步类的实例并调用execute()方法,该方法应自动调用doInBackground过程。
url=edUrl.getText().toString().trim();
hrd=new HTMLRawDataReader();
AsyncWebMethodInvoker awm=new AsyncWebMethodInvoker();
awm.execute();
现在,当您执行代码时,您将看到一个漂亮、流畅且优雅的应用程序执行。
图 6.4 使用AsyncTask调用远程方法
正如我们在本节开头讨论过的,AsyncTask更多的是一个线程辅助,更适合轻量级应用程序。 如果您仔细观察,您会发现另一个问题。AsyncTask基本上不发送preExecute和postExecute之间的任何消息。因此,UI线程不知道方法的状态。 而且,重要的是要知道一个AsyncTask对象只能执行一项后台任务。对于多个任务,您需要每个任务有多个对象。
对于每个方法调用,都有一个对象并不是一种干净的编程实践。在这种情况下,Executor会派上用场。
现在想象一下,您正在下载一个图像,并且您想显示下载进度。在这种情况下,AsyncTask根本无法满足我们的要求。是吗?在这种情况下,使用Android的并发执行 服务是明智的。 要更新进度条或计数器,您可以在等待期间寻求计时器的帮助。
现在,让我们看看如何使用传统的Java线程支持来调用网络方法。
6.2.2 Executor
Executor可以并行(更准确地说,是并发)执行多个已提交的任务。 根据定义,executor可以异步执行多个任务。但是,异步实现不是强制性的。 默认情况下,Executor的初始化会引入一个内联的run()方法,该方法可用于立即执行任务。让我们使用Executor调用我们的FetchWebDataUsingRequestResponse 方法。
Executor exe=new Executor() {
@Override
public void execute(Runnable command) {
// TODO Auto-generated method stub
url=edUrl.getText().toString().trim();
String s=hrd.FetchWebDataUsingRequestResponse(url);
edInput.setText(s);
} };
exe.execute(null);
当您调用execute(null)时,它会调用特定实例的run()方法。当您想同时调用多个不同方法时,可以使用以下结构。
exe.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
Android中还有其他有用的服务有助于设计良好的后台执行和多线程。但是,为了简化我们当前的讨论,我们将把后台和并发执行仅限于AsyncTask和Executor。
6.3 原始 HTML Web 文件读取的实际应用
当您看文本时,您并不觉得很好。因为这个文本带有所有标签、编码、CSS等等。那么我们为什么要学习它呢?因为您可以在您的网站中保留纯HTML,并允许应用程序获取数据。您可以设计轻量级的套装来发布消息或更新应用程序策略或重要通知。
我们现在迫切地想看看我们的OpinionMining是如何工作的,不是吗?好的,如果您如此渴望测试我们正在做的事情,让我们来测试一下。
我专门为大家创建了一个测试html
http://integratedideas.co.in/op.html
将opinion测试部分的onClick方法更新为如下:
String filePath=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
filePath=filePath+"/"+"OpinionMining/OpinionData.txt";
//return;
/*********** Opinion Polarity Testing ***************/
String[][] datas=AndroidFlatFileAccess.ReadFileTokens(filePath,2);
mp=new MinePolarity(datas);
String testString=edInput.getText().toString();
int score=mp.SimpleMine(testString);
if(score>0)
{
tvResult.setText("Positive with score="+score);
}
else
{
if(score<0)
{
tvResult.setText("Negative with score="+score);
}
else
{
tvResult.setText("Nutral with score="+score);
}
}
这真的没什么新东西,我们只是用我们的平面文件数据库初始化了MinePolarity类的对象。您必须验证其他数据库。在应用程序的URL文本框中键入上述URL。获取数据并测试意见。
图 6.5 从Web数据进行实际Opinion Testing的结果
此屏幕清楚地说明了很多问题。首先,它告诉我们我们应用程序的最终预期结果是什么,其次,它为我们提供了如何使用不带标签的HTML远程推送消息的提示。
6.4 解析和提取实际网页中的信息
我们已经学会了如何处理来自Web的纯HTML文件。但是,如何处理真实的网页呢?我们能从中提取有意义的信息吗?为了处理有意义的信息,我们需要一个好的解析器。
Jsoup 是我遇到的一个非常稳定且出色的解析器。用于解析实际网页,我们将使用Jsoup。首先,请下载Jsoup Jar
解压并将jar文件拖到Android项目中的lib 目录中,如图 6.6所示。
图 6.6 配置以使用Jsoup
现在构建您的项目一次,您就可以进行HTML解析了。
在本例中,我们将考虑Mybb ,这是一个免费且流行的论坛CMS。 此CMS的一个迷人功能是它为每个网站提供了轻量级(存档模式)以及主链接。 您可以打开任何“由mybb驱动”的网站,然后查找Light Archive Mode。 这是一个例子
http://community.mybb.com/archive/index.php?thread-158687.html
HTML解析将因网站而异,因为每个网站都会为文本部分使用不同的标签。 为了理解我们需要提取什么,请右键单击网页并查找包含您感兴趣的消息或文本的标签。
对于上面的例子,这是页面源代码的截图。
图 6.7 MyBB Light网站的页面源代码
正如您所看到的,为了获取消息,我们需要解析一个名为message 的类。 让我们在我们的HTMLRawDataReader中开发一个名为ParseHTMLPageWithJsoupForMyBBLight 的方法,它应该接受一个url,检查它是否是一个mybb存档站点,如果是,它将解析消息。
这是方法的实现
public String ParseHTMLPageWithJsoupForMyBBLight(String url)
{
try
{
if(!(url.contains("archive")&&url.contains("thread-")))
{
return "not mybb lite site";
}
//Document document = Jsoup.connect("http://community.mybb.com/archive/index.php?thread-158687.html").get();
Document document = Jsoup.connect(url).get();
// Get the html document title
Elements elements=document.getElementsByClass("message");
String s="";
for(Element ele: elements)
{
s =s+ ele.text();
}
return s;
}
catch(Exception e)
{
return e.getMessage();
}
}
Jsoup.connect(url).get()返回一个Jsoup Document,它像任何带标签的Document解析器一样包含元素。所有message类元素都被分开。然后代码循环遍历元素并将元素的文本部分附加到字符串中。
这是它的惊人结果
图 6.8 HTML解析的结果
因此,您可以看到,一旦我们将意见数据库更新为包含具有权重的真实意见术语并稍微更新算法,我们应该就能够找到任何MyBB Lite网站的意见。您还可以添加一些有趣的功能,如使用语音合成说出内容等等。
7. 使用 WebServices(Web 服务)
Web服务本质上是一个Web方法,可以从另一个Web服务、网页、Windows窗体、移动应用程序调用。因此,Web服务是一种中间件,可以轻松地用作跨平台业务逻辑开发。Web服务使用WSDL 发布服务接口。客户端(或调用方法)可以传递XML作为参数,并以序列化的XML数据作为结果。
正如您所知,序列化是一个过程,它将结构数据等复杂数据类型打包成流,然后在接收方进行解包。图像和视频等数据不可序列化。在通过Web服务通信之前,需要将此类数据转换为字符串。
在本节中,我们将学习两件事:首先是如何从Web服务获取数据,其次是编写支持Android的ASP.Net Web服务的一些策略。
大约三年前,我写了一篇关于从Android调用Asp.Net WebService(ASMX)的文章。这些年来技术变化很大,但技术仍然一样。
您在互联网上看到的大多数教程都展示了如何调用Celcious到Farenhite或货币转换器Web服务。但我们的目标和目的是提出一个好的基于Web的Opinion Mining系统,对吗?因此,与其使用那些您一生中永远不会使用的无用Web服务,不如考虑到Android应用程序将使用它这一事实,将我们的重点转移到开发WebService上。然后,我们将展示如何使用这些服务。
7.1 编写可被 Android 使用的 ASP.Net Web 服务
有人可能会对为什么需要知道如何为Android编写服务感到有些惊讶?我们想学的只是Android基础知识。是吗?如果您偶然发现了本教程,或者设法阅读了本节,那么我有充分的理由相信您不仅仅是为了完成高中作业。我敢肯定,您只想学习Web服务的实际用法和强大功能,以及如何通过这些服务赋予Android应用程序强大的功能。对吗?
所以,我们将在这里通过我们的Web服务提供实时的Opinion Database,然后允许Android应用程序使用它。然后,我们将测试我们的Opinion Mining系统在实际HTML数据上的表现。
在我们开始之前,这里有一些在处理真实Web服务时会有帮助的要点。在您查看本节之前,您必须查看一些关于Web服务数据处理的精彩Codeproject教程。由于我们的角色仅限于涵盖Android特定的服务接口,因此我们假设您已经了解Asp.Net Web服务的基础知识。
1) Android中没有DataGrid这样的东西。因此,当您需要一个Android服务时,请始终在数据库查询中使用DataReader对象。DataReader对象可以循环遍历并访问记录的独立元素。当您想返回整个表时,返回一个String[][] 就像我们在所有上述数据相关部分中所做的那样。为了使客户端更简单,如果您可以将String [][]编码为单个字符串并在Android客户端中解码,那将更简单。
2) .Net的DateTime模式与Java不同。因此,在您的.Net Web服务中使用DateTime作为字符串对象非常重要。如果您的数据库字段严格为Date格式,则应在Web服务中执行转换,而不是将其留给客户端。
3) 难以转换和序列化类对象的类数据。
例如,您有一个类,名为
class Employee
{
string EmpName;
string Eno;
DateTime JoinDate;
}
那么,不要编写一个接受此类对象作为参数或返回该对象作为返回数据的Web方法。虽然将类的对象转换为字符串并非不可能,但维护这样的代码非常痛苦。没有书会教你这个,但请相信我。
使用String[]来传输类的字段很容易。在Web服务和客户端中分别执行对象初始化,而不是让传输层来处理。
4) 如果您想让Android访问.Net Web服务中的图像,那么您可以使用Image到Base64String 转换。Android可以轻松地将Base64String解组为图像,反之亦然。
5) Core Android实用程序类可以将任何数据转换为字符串,并通过与字符串连接来轻松地将任何数据类型转换为字符串。因此,您可以开发主要处理字符串数据的.Net Web服务。Android将最舒适地处理此类数据。
为了本文的缘故,我使用了一个SentiStrength 数据集的一部分,并将其加载到我的网站的Sqlserver中。
所以,这是我们的SentiWords Web服务
<%@ WebService language="C#" class="SentiWordProvider" %>
using System;
using System.Web.Services;
using System.Xml.Serialization;
public class SentiWordProvider {
[WebMethod]
public String SentiDataset() {
string connectionString = "server=\'-,1234\'; user id=\'USER_ID\'; password=\'PASSWORD" +
"\'; database=\'technicalresearch\'";
System.Data.IDbConnection dbConnection = new System.Data.SqlClient.SqlConnection(connectionString);
string queryString = "select * from OpinionDb";
System.Data.IDbCommand dbCommand = new System.Data.SqlClient.SqlCommand();
dbCommand.CommandText = queryString;
dbCommand.Connection = dbConnection;
dbConnection.Open();
System.Data.IDataReader dataReader = dbCommand.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
string s="";
while(dataReader.Read())
{
s=s+dataReader[0].ToString()+"#"+dataReader[1].ToString()+"\n";
}
return s;
}
}
请注意,我们正在使用DataReader对象来提取命令对象返回的查询结果。我们循环遍历数据并将整个表转换为单个字符串。每列由#分隔,行由换行符'\n'分隔。
以下是本地测试Web服务的测试结果
图 7.1 SentiDataset WebService在localhost上运行的结果
请记住,一旦Web服务部署到服务器,您就无法测试它。
如果您没有Web服务器来测试它,请不要担心。我为每个人提供了这个Web服务进行测试!
http://grasshoppernetwork.com/SentiData.asmx
您可以从任何语言/平台创建服务引用并使用该服务。不要忘记为此文章和SentiStrength提供归因。
此Web服务的架构和设计肯定证明了本节的价值!
7.2 通过Android消耗Web Service
任何封装在XML和HTTP等标签中的Web数据都需要解析器。对吗?由于Web服务通过XML交互数据,因此也需要解析器,对吗?是的,它需要,正如我之前提到的,Ksoap仍然是使用Web服务的最佳选择之一。您可以从这里>> 下载Ksoap jar文件。
有趣的事实之一是,Ksoap不仅对Android应用程序有用,它的Java版本同样稳定,您可以使用此库来消耗Java应用程序中的Asp.Net Web服务。
下载Ksoap库后,将其加载到libs文件夹中,如以下图所示,然后清理并构建您的项目。您必须记住的一件事是,在将jar文件拖到libs文件夹中时,Eclipse会询问您链接文件还是复制文件的选项。始终选择复制文件而不是链接文件。
图 7.2 使用Project加载KSoap库
现在让我们开发一个Android类来处理所有SOAP操作。让我们看看我们的类CallSoap。
package com.integratedideas.opinionmining;
import java.sql.Date;
import java.util.StringTokenizer;
import org.ksoap2.SoapEnvelope;
import org.ksoap2.serialization.PropertyInfo;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransportSE;
public class CallSoap
{
public static String SOAP_ACTION = "http://tempuri.org/SentiData";
public static final String WSDL_TARGET_NAMESPACE = "http://tempuri.org/";
public static final String SOAP_ADDRESS = "http://grasshoppernetwork.com/SentiData.asmx";
public CallSoap()
{
}
public String GetSentiData()
{
// TODO Auto-generated method stub
OPERATION_NAME = "SentiDataset";
Object response=null;
// TODO Auto-generated method stub
SoapObject request = new SoapObject(WSDL_TARGET_NAMESPACE,OPERATION_NAME);
SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(
SoapEnvelope.VER11);
envelope.dotNet = true;
envelope.setOutputSoapObject(request);
HttpTransportSE httpTransport = new HttpTransportSE(SOAP_ADDRESS);
try
{
httpTransport.call(SOAP_ACTION, envelope);
response = envelope.getResponse();
}
catch (Exception exception)
{
response=response+"Here it is"+exception.toString();
}
return response.toString();
}
}
现在,让我们进入GetSentiData 方法。OPERATION_NAME是要调用的WebMethod的名称。WSDL_TARGET_NAMESPACE默认情况下始终是http://tempuri.org,除非另有说明。但是,您如何知道是否指定了其他内容?在为任何WebService编写客户端之前,请单击服务以获取其WSDL描述。我们的SentiDataset方法的WSDL描述。
POST /SentiData.asmx HTTP/1.1 Host: grasshoppernetwork.com Content-Type: application/soap+xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <SentiDataset xmlns="http://tempuri.org/" /> </soap12:Body> </soap12:Envelope>
HTTP/1.1 200 OK Content-Type: application/soap+xml; charset=utf-8 Content-Length: length <?xml version="1.0" encoding="utf-8"?> <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <SentiDatasetResponse xmlns="http://tempuri.org/"> <SentiDatasetResult>string</SentiDatasetResult> </SentiDatasetResponse> </soap12:Body> </soap12:Envelope>
查找xmlns,它就是命名空间。正如您所看到的,这里是http://tempuri.org xmlns前面的词是SOAP方法。 在这里,正如您设计了服务一样,您知道它的返回类型和参数。但是,如果您不是Web服务的所有者,而是使用第三方服务,那么从wsdl描述中获取参数知识总是很有帮助的。
回到我们的方法,我们使用命名空间和方法来构建SoapRequest对象。
SoapObject request = new SoapObject(WSDL_TARGET_NAMESPACE,OPERATION_NAME);
SoapEnvelope将在传输层上传输SoapRequest和SoapResponse。如上WSDL描述所示,文件顶部提到了版本1.1。因此, SOAP Envelope也必须告知版本。重要的是,必须告知它正在处理C# WebMethod。
调用WebMethod以获取对象作为响应。
httpTransport.call(SOAP_ACTION, envelope);
response = envelope.getResponse();
顺便说一句,假设您有一个带有参数的WebMethod,其中您发送了一些参数。您的Android方法将如何改变?
为了适应属性,您需要按以下方式添加它们:
PropertyInfo pi=new PropertyInfo();
pi.setName("variable1");
pi.setValue(VALUE1_FROM_SOME_ANDROID_UI_ELEMENT);
pi.setType(String.class);
request.addProperty(pi);
pi=new PropertyInfo();
pi.setName("variable2");
pi.setValue(VALUE2_FROM_SOME_ANDROID_UI_ELEMENT);
pi.setType(String.class);
request.addProperty(pi);
其中variable1和two是Web方法的两个参数,它们可能看起来像下面
[WebMethod]
public String MyWebMethod(String variable1, String Variable2){
return "Some String";
}
您可以使用另一个WebService来测试参数传递
http://grasshoppernetwork.com/NewFile.asmx
它提供了Add方法以及Encrypt和Decrypt方法,您可以随意使用。
现在是时候测试了。我们将要做的是从OpinionMining Button调用此方法,获取返回的字符串,然后将其格式化为String[][]。使用String[][]初始化MiningPolarity类对象,就像我们之前为测试由MyByy驱动的Community Forum Site在lite Archive模式下的Opinion Mining数据所做的那样。我们将使用Executor来调用我们的服务。executor的优点是,一旦声明并初始化了它的对象,void run及其主体就会自动创建。因此,它工作量小,回报高。
Executor exe =new Executor() {
@Override
public void execute(Runnable command)
{
CallSoap cs=new CallSoap();
webResult=cs.GetSentiData();
// TODO Auto-generated method stub
}
};
exe.execute(null);
// String[][] datas=AndroidFlatFileAccess.ReadFileTokens(filePath,2);
//mp=new MinePolarity(datas);
Log.i("Database",webResult);
String [] rows=webResult.split("\n");
String [][]datas=new String [rows.length][2];
for(int i=0;i<rows.length;i++)
{
datas[i]=rows[i].split("#");
}
mp=new MinePolarity(datas);
我们首先根据换行符分割获得的字符串。这是记录的数量。然后,我们使用“#”分割行以提取每行的Word和Weight列实体。调试会话的结果显示了返回的结果,在LogCat中将其拆分为行,并在调试器弹出窗口中准备String[][]数组。
图 7.3 将Web服务结果从String解组为String[][]的结果
这是实时Opinion Mining的结果。
图 7.4 使用真实数据集进行Opinion Mining的结果
8.让Opinion Mining正常工作
到目前为止,我们已经学习了Android中几乎所有可能的记录管理技术,以及从Web获取和解析记录。虽然这个主题本来应该在这里结束,但我们渴望看到一个很棒的应用程序问世,这 realmente很重要。所以,我们将继续改进我们的Opinion Mining任务,目前它只是计算正面和负面词语。
所以,让我们从一些简单的句子开始。在输入框中键入“I am good and the world is great” 。现在找到意见。
它将显示正面,得分为10,并带有以下LogCat跟踪,在SimpleMine方法中。
图 8.1 对“I am good and the world is great”进行Opinion Polarity mining的跟踪
不要搜索“I am not good and the world is not great”。您期望它是负面的,对吗?但它显示为正面,因为SimpleMine方法所做的只是寻找正面和负面词语。因为该方法是无状态的,所以它不知道前一个词是什么。现在,我们将引入一个简单的规则,即如果在与极性匹配的词之前出现“not”或“no”,我们将改变极性。
所以,我们一直将令牌存储在一个列表中,直到出现意见术语。然后,我们检查这些令牌中是否存在word not或no,如果存在,我们则改变极性,然后释放列表。
以下是我们修改后的SimpleMine方法。
public int SimpleMine(String text)
{
text=text.toLowerCase().trim();
StringTokenizer st = new StringTokenizer(text, ".\n\t ,:();");
int score=0;
ArrayList<String> prevTokens=new ArrayList<String>();
while(st.hasMoreTokens())
{
String s=st.nextToken().trim();
Log.i("In Simple Mine",s);
try{
int i=SearchInDatabase(s);
if(i!=0)
{
if(prevTokens.contains("not") ||prevTokens.contains("no"))
{
i=i*-1;
}
}
Log.i(s, ""+i);
score+=i;
if(i!=0)
{
prevTokens.clear();
}
else
{
prevTokens.add(s);
}
}catch(Exception ex)
{
}
}
return score;
}
现在,当您检查“I am no good and world is not great”时,您将获得预期的精确极性。即,负面,得分为-10。
如果not没有紧挨着意见词出现怎么办?
“I am not at all good and the world is not good”。请注意,在not之后有一个“at all”,但结果仍然是完美的。这是因为我们不是在意见词之前查找令牌,而是在查找所有令牌。
那么,如果我 想测试
“I am bad but world is too good a place.”观察“too”的使用。我想说的是,“nice”这个词是有权重的。这类词语应增加极性词的权重,但极性的绝对值必须保持在[0-5]范围内。
因此,我们在当前极性词之前绝对搜索加权项,如果存在,我们将增加该项的权重。但此测试必须在测试前导not之前出现,以便not可以考虑到该决定。
所以,这是我们SimpleMine方法的另一个变化,如果当前词是意见词。
if(i!=0)
{
if(prevTokens.size()>0)
{
if(prevTokens.get(prevTokens.size()-1).equals("too") ||prevTokens.get(prevTokens.size()-1).equals("very")||prevTokens.get(prevTokens.size()-1).equals("really")||prevTokens.get(prevTokens.size()-1).equals("honestly"))
{
int j=Math.abs(i);
j=j*2;
if(j>5)
{
j=5;
}
if(i<0)
{
i=j*-1;
}
else
{
i=j;
}
} }
if(prevTokens.contains("not") ||prevTokens.contains("no"))
{
i=i*-1;
}
}
最后,我们使用n't 与许多动词结合来改变极性,例如isn't、doesn't、couldn't等。 这些词的出现不可避免地会改变极性,而不是如果该词没有出现。
例如,“It isn't looking good”是一个负面极性的句子,而“it is looking good”是正面极性的句子。
所以我们编写了一个简单的线性搜索方法,它可以循环遍历ArrayList<String>的元素,并检查字符串是否以“n't”结尾,表示not。如果是,则在主逻辑部分,我们将改变极性。
boolean SearchForNt(ArrayList<String> words)
{
if(words.size()<1)
{
return false;
}
for(int i=0;i<words.size();i++)
{
if(words.get(i).endsWith("n't"))
return true;
}
return false;
}
我们整体的SimpleMine() 方法,现在已经不再那么简单了,如下所示。
public int SimpleMine(String text)
{
text=text.toLowerCase().trim();
StringTokenizer st = new StringTokenizer(text, ".\n\t ,:();");
int score=0;
ArrayList<String> prevTokens=new ArrayList<String>();
while(st.hasMoreTokens())
{
String s=st.nextToken().trim();
Log.i("In Simple Mine",s);
try{
int i=SearchInDatabase(s);
if(i!=0)
{
if(prevTokens.size()>0)
{
if(prevTokens.get(prevTokens.size()-1).equals("too") ||prevTokens.get(prevTokens.size()-1).equals("very")||prevTokens.get(prevTokens.size()-1).equals("really")||prevTokens.get(prevTokens.size()-1).equals("honestly"))
{
int j=Math.abs(i);
j=j*2;
if(j>5)
{
j=5;
}
if(i<0)
{
i=j*-1;
}
else
{
i=j;
}
} }
if(prevTokens.contains("not") ||prevTokens.contains("no"))
{
i=i*-1;
}
if(SearchForNt(prevTokens))
{
i=i*-1;
}
}
Log.i(s, ""+i);
score+=i;
if(i!=0)
{
prevTokens.clear();
}
else
{
prevTokens.add(s);
}
}catch(Exception ex)
{
}
}
return score;
}
}
最后,让我们用一个真实的网址进行测试!
图 8.2 在真实的MyBB页面上进行Opinion Polarity Mining
下载OpinionMining_Final_Project.zip 来玩耍并将其开发成一个很棒的应用程序。
9. 结论
在本教程中,我们设计了一个简单但功能强大的Android Opinion Polarity mining工具。该算法并非没有缺陷,但总的来说,它能很好地给出网页的Opinion和Sentiment的正确结果。然而,重要的是我们学习了Android中的几种数据处理技术。本教程涵盖了Flat File、XML、Shared Preferences、SQLite等数据(或记录)管理技术。对于每种记录管理,我们都试图开发通用方法,这些方法可以即插即用,用于您的其他应用程序。例如,您几乎不可能有任何数据库,并且可以使用我们涵盖的任何数据库管理技术来处理它们,而无需太多工作或代码更改。这是本教程最迷人的方面。因此,您不仅为示例数据库学习了记录处理,而且实际上为将来修改方法以适应不同数据定义节省了时间。
我在涵盖这些主题时,试图涵盖我能想到或想到的每一个技巧和窍门。但我敢肯定,可能还有更多。我期待听到您关于在文章的未来更新中纳入您的建议。
本文是Android领域多年工作的成果。我试图以最简单易懂的方式为初学者提供解决方案。在每个主题中,我试图回答“为什么”和“何时”,以及“如何”。因此,本文不仅教您如何做事情,同时还告诉您为什么要使用特定的概念以及何时使用它们。技术上,最复杂的代码不是本文的目标,而是代码的简洁性。 在写作过程中,我反复问自己,如果我是一名只有Android工作经验的新手程序员,我是否会理解这一部分的内容,并相应地更新了内容。
我几乎在每个部分都提供了代码,以便您可以分析特定部分。您可以做很多事情,例如在第一次从Web获取Opinion词时,创建一个本地Opinion词数据库。
我希望这篇文章能让您轻松学习数据库。祝您编码愉快,学习愉快。