本文主要讲解在Android开发中ContentProvider的常规用法,仅供学习分享使用,如有不足之处,还请指正。

访问一个ContentProvider

在Android开发中,应用程序通过ContentResolver(内容解析器)从ContentProvider(内容提供者)中获取数据,ContentResolver提供访问ContentProvider中同名方法,ContentProvider包括ContentProvider和它的子类,ContentResolver对ContentProvider的持久层存储提供了基本的CRUD(Create,Retrieve,Update,Delete)方法进行访问。客户端App的ContentResolver对象自动处理和ContentProvider的App之间的进程间通信。ContentProvider还充当数据库和外部数据视图表现之间的抽象层。

备注:如果要访问一个ContentProvider,App需要在清单文件中请求对应的权限。

例如:从User Dictionary Provider中获取单词和区域的列表,可以调用ContentResolver.query()方法,如下图所示:

 // 查询用户定义字典并返回结果
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 单词表的内容URI
mProjection, // 查询的数据列名数组
mSelectionClause //查询条件,可以为null
mSelectionArgs, // 查询参数,可以为null
mSortOrder); // 返回数据对象的排序条件

下表显示了query(Uri,projection,selection,selectionArgs,sortOrder) 如何与SQL语句进行匹配:

Content URIs

Content URI是Provider中标识数据的URI,包括整个Provider(其权限)的符号名和指向表(或路径)的名称,Content URI是访问ContentProvider的参数之一。

在前面的代码行中,常量_uri包含了用户词典“Word”表的Content URI。ContentResolver对象通过将权限与已知提供者的系统表进行比较,将查询参数发送到正确的Provider。

ContentProvider使用URI的路径部分来选择要访问的表,通常为公开的每个表设置路径。

在前面的代码行中,“Word”表的全称为:

 content://user_dictionary/words

其中user_dictionary 字符串是Provider的权限,而 words是表的路径。content:// (the scheme)始终存在,并将其标识为Content URI。

许多Provider允许将id值附加到URI的末尾来访问表中的单个行。例如,要从User Dictionary中检索_id为4的行,可以使用Content URI:

 Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

当要修改或删除其中一行时,经常使用ID值。

备注:Uri 和 Uri.Builder类包含了用字符串构造形式良好的uri对象的方法。ContentUris包含了将ID附加到uri的方法。前面片段使用withAppendedId() 将ID附加到UserDictionary.Words.CONTENT_URI。

从Provider获取数据

本节介绍如何使用User Dictionary Provider作为示例,从中检索数据。

为了清晰起见,本节中的代码段调用“UI线程”上的ContentResolver.query()。在实际代码中,应该在非UI线程上异步地进行查询。

要从Provider获取数据,请遵循以下基本步骤:

  1. 请求读取Provider的访问权限。
  2. 定义查询Provider的代码。

访问权限

要从Provider中检索数据,应用程序需要Provider的“读取访问权限”。不能在运行时请求此权限;必须在您的清单中指定需要此权限,使用<uses-permission>元素和由Provider定义的权限名称。当在清单中指定此元素时,实际上是在为App“请求”此权限。当用户安装App时,会隐式地批准这个请求。

User Dictionary Provider在清单文件中定义的权限名称为android.permission.READ_USER_DICTIONARY,所以App中想要从Provider中获取数据,需要请求这个权限。

构造查询

查询数据的下一步是构造查询。以下片段定义了访问User Dictionary Provider的一些变量:

 //  "projection" 定义每行返回的列名数组
String[] mProjection =
{
UserDictionary.Words._ID, // _ID column name
UserDictionary.Words.WORD, // word column name
UserDictionary.Words.LOCALE // locale column name
}; // 定义查询条件
String mSelectionClause = null; // 定义查询条件参数
String[] mSelectionArgs = {""};

下一个片段显示如何使用ContentResolver.query(),以User Dictionary Provider 为例,查询类似于sql查询,它包含要返回的列名、查询条件和排序。

查询返回的列集合称为投影(变量投影)。

查询条件表达式被拆分为选择子句和选择参数。选择子句是逻辑表达式和布尔表达式、列名称和值的组合。如果指定可替换参数“?”,查询条件不再是一个值,而是从条件参数数组(mSelectionArgs)中查询该值。

如果用户没有输入一个单词,则选择子句设置为空,查询返回Provider中的所有单词。

如果用户输入了一个单词,查询条件将设置UserDictionary.Words.WORD + " = ?"。参数数组的第一个元素设置为用户输入的单词。

 /*
* 定义查询参数
*/
String[] mSelectionArgs = {""}; // 获取界面输入的查询条件
mSearchString = mSearchWord.getText().toString(); //此处插入代码校验数据是否有效
//如果条件为空,则查询所有
if (TextUtils.isEmpty(mSearchString)) {
// 如果查询条件为空,则返回所有内容
mSelectionClause = null;
mSelectionArgs[0] = "";
} else {
// 构造查询条件,匹配用户输入的数据.
mSelectionClause = UserDictionary.Words.WORD + " = ?";
// 查询参数.
mSelectionArgs[0] = mSearchString;
} // 对表进行查询并返回Cursor对象
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // URI
mProjection, // 查询数据列
mSelectionClause // 查询条件
mSelectionArgs, // 查询参数
mSortOrder); // 返回结果排序行 // 如果出现查询异常,则返回空
if (null == mCursor) {
/*
* 插入代码捕获异常
*/
// 如果返回为空,则没有匹配的内容
} else if (mCursor.getCount() < 1) {
/*
* 通知用户查询不成功. 但这不是必须的*/
} else {
// 插入代码处理结果
}

类似于sql语句:

 SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

在这个sql语句中,使用的是实际的列名称,而不是Contract类常量。

防止恶意输入

如果content provider管理的数据在sql数据库中,外部不受信任的数据输入到原始sql语句中,就会导致sql注入。

考虑这个查询条件:

 //通过拼接用户输入和列名的方式构造查询条件
String mSelectionClause = "var = " + mUserInput;

如果这样做,用户可能将恶意sql连接到您的sql语句中。例如,用户可以输入"nothing; DROP TABLE *;"用于mUserInput,这将导致选择子句var = nothing; DROP TABLE *;。由于选择条件被视为sql语句,这可能会导致Provider删除sqlite数据库中的所有表。

为了避免此问题,请使用可替换的参数和单独的选择参数数组的查询条件。采用这种方式,用户输入将直接绑定到查询,而不是被解释为sql语句的一部分,用户无法注入恶意sql。如下所示:

 // 用一个可替换参数来包含用户输入
String mSelectionClause = "var = ?";

如下设置查询参数数组:

 // 定义一个查询条件的数组
String[] selectionArgs = {""};

在查询参数数组中进行赋值:

 // 将用户数据作为参数数据
selectionArgs[0] = mUserInput;

显示查询结果

ContentResolver.query()客户端方法总是返回一个Cursor。Cursor对象提供对其包含的行和列的读取访问权。使用Cursor中的方法可以迭代行数据,确定每列的数据类型,将数据从列中取出,并检查结果的其他属性。有些Cursor实现会在提供者的数据变更时自动更新,或在Cursor变更时触发对应的事件,或两者兼而有之。

如果没有行符合查询条件,provider将返回一个Cursor, 其Cursor.getCount()为0(空光标)。

如果发生内部错误,查询的结果取决于特定的Provider。它可以返回null,也可以抛出异常。

由于Cursor是行的“列表”,显示Cursor内容的一个好方法是通过SimpleCursorAdapter绑定到ListView。

如下代码所示:它创建一个SimpleCursorAdapter对象,包含查询到的Cursor,并将此对象设置到ListView的适配器

 // 定义从Cursor中检索并加载到输出行的列名
String[] mWordListColumns =
{
UserDictionary.Words.WORD, // word column name
UserDictionary.Words.LOCALE // locale column name
}; //定义一个视图ID列表,该列表将接收每行的Cursor列
int[] mWordListItems = { R.id.dictWord, R.id.locale}; // 创建一个SimpleCursorAdapter对象
mCursorAdapter = new SimpleCursorAdapter(
getApplicationContext(), // 应用程序上下文对象
R.layout.wordlistrow, // ListView单行配置文件
mCursor, // query函数返回的结果
mWordListColumns, // Cursor中的列名数组
mWordListItems, // ListView中Item项的布局文件
0); // Flags (usually none are needed) // 设置 adapter到ListView
mWordList.setAdapter(mCursorAdapter);

备注:要使用Cursor支持ListView,Cursor必须包含一个名为_id的列。这个限制也解释了为什么大多数Provider的每个表都有一个_id列。

从查询结果中获取数据

您可以将查询结果用于其他任务,而不是简单地显示查询结果。要做到这一点,需要迭代Cursor中的行:

 // 定义"word"列的索引
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /*
* 当cursor有效的时候才执行. User Dictionary Provider如果发生内部错误,将返回null,其他provider可能会抛出异常
*/ if (mCursor != null) {
/*
* 移动到cursor的下一行.在第一行移动之前, 行指向是-1,如果试图去查询此位置上的内容,将会抛出一个异常
*/
while (mCursor.moveToNext()) {
//获取对应的列的值.
newWord = mCursor.getString(index);
// 插入代码处理获取的值.
...
// while 循环结束
}
} else {
// 展示错误和异常信息
}

Cursor实现包含检索不同类型数据的几种“get”方法。例如,上一个片段使用getString()。同时也有一个gettype()方法,该方法返回列的数据类型。

Content Provider权限

访问Provider中的数据,调用方必须具有相应的权限,这些权限确保用户知道应用程序试图访问哪些数据,用户在安装App时会看到请求的权限。

如前所述,User Dictionary Provider要求使用android.permission.READ_USER_DICTIONARY权限获取数据。Provider需要android.permission.WRITE_USER_DICTIONARY权限来插入、更新或删除数据。

为了获得访问provider所需的权限,App在其清单文件中以<uses-permission>元素请求它们。当安装App时,用户必须允许应用程序请求的所有权限。如果用户全部允许,将继续安装;如果用户不允许,Package Manager将中止安装。

以下<uses-permission>元素请求读取 User Dictionary Provider的访问权限:

 <uses-permission android:name="android.permission.READ_USER_DICTIONARY">

Inserting, Updating, and Deleting Data

与从provider获取数据的方式相同,还可以使用provider客户端与provider's 提供方之间的交互来修改数据。provider 和provider客户端自动处理安全以及进程间通信。

插入数据(Inserting data)

将数据插入到provider中,请调用ContentResolver.insert()。此方法将新行插入到provider中,并返回新增行的 content URI。此片段显示如何将新词插入到User Dictionary Provider中:

 // 定义一个新的 Uri对象,接收插入新行放回的内容
Uri mNewUri; // 要插入的新值
ContentValues mNewValues = new ContentValues(); /*
* 设置每列对应的值
*/
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100"); mNewUri = getContentResolver().insert(
UserDictionary.Word.CONTENT_URI, // 内容 URI
mNewValues // 插入的值
);

新行的数据对应单个ContentValues对象,该对象在形式上类似于单行cursor。此对象中的列不需要具有相同的数据类型,如果不想指定值,则可以使用ContentValues.putNull()设置列为空。

代码段不会添加_id列,因为此列是自动维护的。provider为添加的每一行指定一个唯一_id,通常使用_id作为表的主键。

返回的新行的newUri,格式如下:

 content://user_dictionary/words/<id_value>

<id_value>是新行的_id。大多数provider可以自动检测到这种形式的内容,然后在该特定行上执行请求的操作。

若要从返回的Uri中得到_id值,请调用ContentUris.parseId()。

更新数据(Updating data)

要更新行,将使用带有更新值的ContentValues对象,就像使用插入时一样,选择条件也与使用查询时一样。调用方法是ContentResolver.update()。您只需要为需要更新的列向ContentValues对象添加值。如果要清除列的内容,请将值设置为null。

下面的片段将locale设置有语言"en"的所有行更改为locale为空。返回值是更新的行数:

 // 包含更新的内容的对象
ContentValues mUpdateValues = new ContentValues(); // 定义需要更新的查询条件
String mSelectionClause = UserDictionary.Words.LOCALE + "LIKE ?";
String[] mSelectionArgs = {"en_%"}; // 定义更新行得到的行数
int mRowsUpdated = 0; /*
* 设置更新的内容.
*/
mUpdateValues.putNull(UserDictionary.Words.LOCALE); mRowsUpdated = getContentResolver().update(
UserDictionary.Words.CONTENT_URI, // URI
mUpdateValues // 更新的内容
mSelectionClause //查询条件
mSelectionArgs // 查询内容参数
);

在调用ContentResolver.update()时,对用户输入进行处理。

删除数据(Deleting data)

删除行类似于查询行数据:为要删除的行指定选择条件,而客户端方法返回已删除行的数目如下所示:

 // 定义需要删除的条件
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"}; //定义删除掉行数
int mRowsDeleted = 0; // 删除匹配条件的内容
mRowsDeleted = getContentResolver().delete(
UserDictionary.Words.CONTENT_URI, // URI
mSelectionClause // 删除条件
mSelectionArgs // 删除参数
);

在调用 ContentResolver.delete()方法时,对用户输入进行处理。

Provider数据类型

Content providers可以提供许多不同的数据类型。User Dictionary Provider只提供文本,但也可以提供以下格式:

  • integer
  • long integer (long)
  • floating point
  • long floating point (double)

providers经常使用的另一种数据类型是Binary Large OBject (BLOB),它是64kb字节数组。通过查看Cursor类“get”方法,可以看到可用的数据类型。

provider中每一列的数据类型通常在其文档中列出。User Dictionary Provider 的数据类型在其contract类UserDictionary.Words的参考文档中列出。也可以通过Cursor.getType()来确定数据类型。

Provider访问的替代形式

在应用程序开发中,三种可供选择的Provider访问形式非常重要:

  1. 批量访问:可以在ContentProviderOperation中使用方法创建批量处理访问调用,然后用ContentResolver.applyBatch()应用它们。
  2. 异步查询:应该在单独的线程中进行查询,其中一种方法是使用CursorLoader对象。
  3. 通过intents访问数据:虽然不能直接向提供者发送intent,但可以向provider's application发送intent,而provider's application序通常是最适合修改provider数据的应用程序。

批量访问(Batch access)

对provider的批量访问用于插入多行,或在同一方法中在多个表中插入行,或通常用于作为事务(原子操作)执行一组跨进程的操作。

要以“batch mode”访问provider,您可以创建一组 ContentProviderOperation 对象,然后通过ContentResolver.applyBatch()方法将对象分发到provider。将provider的权限传递给此方法,而不是特定的内容。这允许数组中的每个ContentProviderOperation对象对不同的表操作。ContentResolver.applyBatch() 返回结果数组。

通过Intent进行数据访问(Data access via intents)

Intents可以提供对 content provider的间接访问。允许用户访问provider中的数据,即使您的App没有访问权限,也可以从有权限的App获得结果Intent,或者通过激活有权限的App并在其中工作。

合同类别(Contract Classes)

contract类定义了帮助App处理content URIs、列名称、意图操作和 content provider的其他特性的常量。Contract类不自动包含在provider中;provider的开发人员必须定义它们,然后将其提供给其他开发人员。android平台中的许多提供商在android.provider中都有相应的contract类。

例如,User Dictionary Provider有一个包含内容URI和列名常量的contract类用户词典。“单词”表的内容以“常量”为定义。UserDictionary.Words.CONTENT_URI,在以下示例片段中使用。例如,查询投影可以定义为:

 String[] mProjection =
{
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};

ContentProvider示例

读取通话记录

 //通讯记录URI
private String call_uri = "content://call_log/calls"; //内容解析器
private ContentResolver mResolver; //列表
private ListView lvCall; //获取的通讯记录的列名
private String[] columns = new String[]{
CallLog.Calls._ID, CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER, CallLog.Calls.TYPE, CallLog.Calls.DATE,CallLog.Calls.DURATION
}; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化内容解析器
mResolver = getContentResolver();
lvCall = (ListView) this.findViewById(R.id.lv_call);
} /**
* 获取通讯记录事件
* @param v
*/
public void bn_call(View v) {
List<Map<String, String>> list = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Cursor cursor = mResolver.query(Uri.parse(call_uri), columns, null, null, CallLog.Calls.DEFAULT_SORT_ORDER);
//以下是为了转换数据格式
if(cursor!=null){
while (cursor.moveToNext()){
long dt=cursor.getLong(cursor.getColumnIndex("date"));
Date callDate = new Date(dt);
String callDateStr = sdf.format(callDate);
String name=cursor.getString(cursor.getColumnIndex("name"));
String number=cursor.getString(cursor.getColumnIndex("number"));
String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s";
Map<String, String> map=new HashMap<String, String>() ;
map.put("name",name);
map.put("number",number);
map.put("date",callDateStr);
map.put("duration",duration);
list.add(map);
}
}
//将数据填充到Adapter
SimpleAdapter adapter=new SimpleAdapter(this,list,R.layout.list_item,
new String[]{"name", "number", "date","duration"},
new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time,R.id.tv_duration}); /*SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.list_item, cursor,
new String[]{"name", "number", "date"},
new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time},
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);*/
//绑定Adapter到ListView
lvCall.setAdapter(adapter);
}

读取短信记录

 private String sms_uri="content://sms";

     private String[] columns=new String[]{
Telephony.Sms._ID, Telephony.Sms.ADDRESS,Telephony.Sms.CREATOR, Telephony.Sms.BODY, Telephony.Sms.DATE, Telephony.Sms.PERSON, Telephony.Sms.STATUS, Telephony.Sms.DATE_SENT
}; private ContentResolver mResolver; private ListView lvMsg; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mResolver=getContentResolver();
lvMsg= (ListView) this.findViewById(R.id.lv_sms);
} public void bn_sms(View view) {
List<Map<String, String>> list = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Cursor cursor = mResolver.query(Uri.parse(sms_uri), columns, null, null, Telephony.Sms.DEFAULT_SORT_ORDER);
if (cursor != null) {
while (cursor.moveToNext()) {
Log.e("TAG", "bn_sms: "+cursor.getColumnIndex("person")+"---"+ cursor.getColumnIndex("date")+"---"+cursor.getColumnIndex("creator")+"---"+cursor.getColumnIndex("address"));
long dt = cursor.getLong(cursor.getColumnIndex("date"));
Date callDate = new Date(dt);
String callDateStr = sdf.format(callDate);
String person = cursor.getString(cursor.getColumnIndex("address"));
String creator = cursor.getString(cursor.getColumnIndex("creator"));
//String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s";
String body = cursor.getString(cursor.getColumnIndex("body"));
Map<String, String> map = new HashMap<String, String>();
map.put("person", person);
map.put("creator", creator);
map.put("date", callDateStr);
//map.put("duration",duration);
map.put("body", body);
list.add(map);
}
}
//将数据填充到Adapter
SimpleAdapter adapter = new SimpleAdapter(this, list, R.layout.msg_item,
new String[]{"person", "creator", "date", "body"},
new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time, R.id.tv_msg}); //绑定Adapter到ListView
lvMsg.setAdapter(adapter);
}

备注

千里之行,始于足下。

一起学Android之ContentProvider的更多相关文章

  1. 23、从头学Android之ContentProvider .

    http://blog.csdn.net/jiahui524/article/details/7016430 应用场景: 在Android官方指出的Android的数据存储方式总共有五种,分别是:Sh ...

  2. 学Android开发 这19个开发工具助你顺风顺水

    学Android开发 这19个开发工具助你顺风顺水 要想快速开发一个Android应用,通常会用到很多工具,巧妙利用这些工具,能让我们的开发工作事半功倍,节省大量时间,下面大连Android开发培训小 ...

  3. Android之ContentProvider数据存储

    一.ContentProvider保存数据介绍 一个程序可以通过实现一个ContentProvider的抽象接口将自己的数据完全暴露出去,而且ContentProvider是以类似数据库中表的方式将数 ...

  4. Android开发学习之路-该怎么学Android(Service和Activity通信为例)

    在大部分地方,比如书本或者学校和培训机构,教学Android的方式都基本类似,就是告诉先上原理方法,然后对着代码讲一下. 但是,这往往不是一个很好的方法,为什么? ① 学生要掌握这个方法的用途,只能通 ...

  5. 菜鸟学Android编程——简单计算器《一》

    菜鸟瞎搞,高手莫进 本人菜鸟一枚,最近在学Android编程,网上看了一些视频教程,于是想着平时手机上的计算器应该很简单,自己何不尝试着做一个呢? 于是就冒冒失失的开撸了. 简单计算器嘛,功能当然很少 ...

  6. 学Android开发,入门语言java知识点

    学Android开发,入门语言java知识点 Android是一种以Linux为基础的开源码操作系统,主要使用于便携设备,而linux是用c语言和少量汇编语言写成的,如果你想研究Android,就去学 ...

  7. DoNet屌丝学Android(一)——Android开发准备工作 & No HelloWord & (真机)调试

    先乱扯淡一下吧,本人一.net屌丝,手持Android 4.2.2手机,Win7 x64本本,闲来无聊学习一下Android的开发,至于要开发啥玩意目前没有什么想法,就是想学学,搞不好是三分热度也有可 ...

  8. 从头学Android系列

    从头学Android系列 http://blog.csdn.net/worker90/article/category/888358

  9. Android中ContentProvider的简单使用

    1.新建继承ContentProvider的类 package com.wangzhu.demo; import android.content.ContentProvider; import and ...

随机推荐

  1. Java中接口和抽象类的区别?

    抽象类 抽象类必须用 abstract 修饰,子类必须实现抽象类中的抽象方法,如果有未实现的,那么子类也必须用 abstract 修饰.抽象类默认的权限修饰符为 public,可以定义为 public ...

  2. CQRS+ES项目解析-Equinox

    今天我们来分析另一个开源的CQRS+ES项目:Equinox.该项目可以在github上下载并直接本地运行,项目地址:https://github.com/EduardoPires/EquinoxPr ...

  3. 一起学Spring之Web基础篇

    概述 在日常的开发中Web项目集成Spring框架,已经越来越重要,而Spring框架已经成为web开发的主流框架之一.本文主要讲解Java开发Web项目集成Spring框架的简单使用,以及使用Spr ...

  4. java面向对象 - 匿名对象

    一.匿名对象 1. 创建的对象,没有显示的赋给一个变量名,即为匿名对象. 2. 匿名对象只能调用一次 二.匿名对象使用 class Phone { private int price; public ...

  5. Cocos Creator 资源加载流程剖析【五】——从编辑器到运行时

    我们在编辑器中看到的资源,在构建之后会进行一些转化,本章将揭开Creator对资源进行的处理. 资源处理的整体规则 首先我们将Creator的开发和运行划分为以下几个场景: 编辑器 当我们将资源放到编 ...

  6. Android Studio出现Failed to open zip file问题的解决方法

    直接在网上找到gradle-3.3-all.zip下载下来,不要解压缩,放在类似下面的目录中 C:\Users\Administrator\.gradle\wrapper\dists\gradle-3 ...

  7. Python小技巧:打印出来的文本文档中间有空格

    问题描述: 在file.txt中存了内容如下 AAAAAA BBBBBB CCCCCC 然后采用python显示,发现显示出来的是这样的 A A A A A A B B B B B B C C C C ...

  8. JS---DOM---为元素绑定事件的引入,为元素绑定多个代码,兼容代码

    1. 为元素绑定事件的引入: 用src直接绑定多个,只实现最后一个(programmer2.js) <input type="button" value="按钮&q ...

  9. 推荐一款好看的Hexo主题Ayer

    介绍 Ayer 是一个干净且优雅的Hexo主题,自带响应式,加载速度很快,该有的功能都有,可配置项也很多,非常适合作为你的博客主题,主题内还附送了6张精美的高清壁纸.欢迎使用和Star支持,如果你在使 ...

  10. Kali Linux install "Veil-Evasion"

    Xx_Step wget https://github.com/ChrisTruncer/Veil/archive/master.zip unzip master.zip cd Veil-Evasio ...