文章目錄
- HBase 過濾器
- IDEA 匯入Maven依賴
- 過濾器
- 過濾器的兩類引數
- 運算子
- 比較器
- Bloom Filter 布隆過濾器
- Bloom Filter 作業原理
- Bloom Filter 在HBase中的應用
- HBase 過濾器作用
- ```比較過濾器```
- 比較運算子
- 常見的六大比較過濾器
- BinaryComparator
- BinaryPrefixComparator
- NullComparator
- BitComparator
- RegexStringComparator
- SubstringComparator
- 示例代碼
- rowKey過濾器:RowFilter
- 列簇過濾器:FamilyFilter
- 列過濾器:QualifierFilter
- 列值過濾器:ValueFilter
- ```專用過濾器```
- 單列值過濾器:SingleColumnValueFilter
- 列值排除過濾器:SingleColumnValueExcludeFilter
- rowkey前綴過濾器:PrefixFilter
- 分頁過濾器PageFilter
- 多過濾器綜合查詢
HBase 過濾器
IDEA 匯入Maven依賴
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.4.6</version>
</dependency>
過濾器
??HBase 的基本API,包括增、刪、改、查等,
??增、刪都是相對簡單的操作,與傳統的RDBMS相比,這里的查詢操作略顯蒼白,只能根據特性的行鍵進行查詢(Get)或者根據行鍵的范圍來查詢(Scan),
??HBase不僅提供了這些簡單的查詢,而且提供了更加高級的過濾器(Filter)來查詢,
過濾器的兩類引數
??過濾器可以根據列簇、列、版本等更多的條件來對資料進行過濾,基于HBase可以高效地完成查詢過濾的任務,帶有過濾器條件的RPC查詢請求會把過濾器分發到各個RegionServer(這是一個服務端過濾器),這樣也可以降低網路傳輸的壓力,
??使用過濾器至少需要兩類引數:
- 一類是抽象的運算子,另一類是比較器,
運算子
- HBase 提供了列舉型別的變數來表示這些抽象的運算子:
- LESS :小于
- LESS_OR_EQUAL :小于等于
- EQUAL :等于
- NOT_EQUAL :不等于
- GREATER_OR_EQUAL :大于等于
- GREATER :大于
- NO_OP :不比較
比較器
??比較器作為過濾器的核心組件之一,用于處理具體的比較邏輯,例如位元組級的比較,字串級的比較等,
RegexStringComparator :支持正則運算式的值比較
Scan scan = new Scan();
RegexStringComparator comp = new RegexStringComparator(“文科*”); // 以 文科 開頭的字串
SingleColumnValueFilter filter = new SingleColumnValueFilter(Bytes.toBytes("info"), Bytes.toBytes("clazz"), CompareOp.EQUAL, comp);
scan.setFilter(filter);
SubStringComparator:用于監測一個子串是否存在于值中,并且不區分大小寫,
Scan scan = new Scan();
SubstringComparator comp = new SubstringComparator("1129"); // 查找包含 1129 的字串
SingleColumnValueFilter filter = new SingleColumnValueFilter(Bytes.toBytes("info"), Bytes.toBytes("clazz"), CompareOp.EQUAL, comp);
scan.setFilter(filter);
BinaryPrefixComparator:前綴二進制比較器,與二進制比較器不同的是,只比較前綴是否相同,
Scan scan = new Scan();
BinaryPrefixComparator comp = new BinaryPrefixComparator(Bytes.toBytes("yting")); //
SingleColumnValueFilter filter = new SingleColumnValueFilter(Bytes.toBytes("family"), Bytes.toBytes("qualifier"), CompareOp.EQUAL, comp);
scan.setFilter(filter);
BinaryComparator:二進制比較器,用于按字典順序比較 Byte 資料值,
Scan scan = new Scan();
BinaryComparator comp = new BinaryComparator(Bytes.toBytes("xmei")); //
ValueFilter filter = new ValueFilter(CompareOp.EQUAL, comp);
scan.setFilter(filter);
-
列值過濾器:效率較低,需要做全表掃描
SingleColumnValueFilter:用于測驗值的情況(相等,不等,范圍 、、、) -
列簇過濾器:
FamilyFilter:用于過濾列族(通常在 Scan 程序中通過設定某些列族來實作該功能,而不是直接使用該過濾器), -
列名過濾器:
QualifierFilter:用于列名(Qualifier)過濾, -
行鍵過濾器:效率較高,行鍵前綴過濾效率較高
RowFilter:行鍵過濾器,一般來講,執行 Scan 使用 startRow/stopRow 方式比較好,而 RowFilter 過濾器也可以完成對某一行的過濾,
Bloom Filter 布隆過濾器
??Bloom Filter(布隆過濾器)是1970年由布隆提出的,它實際上是一個很長的二進制向量和一系列隨機映射函式,布隆過濾器可以用于檢索一個元素是否在一個集合中,它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和洗掉困難,
??在計算機科學中,我們常常會碰到時間換空間或者空間換時間的情況,即為了達到某一個方面的最優而犧牲另一個方面,Bloom Filter在時間空間這兩個因素之外又引入了另一個因素:錯誤率,在使用Bloom Filter判斷一個元素是否屬于某個集合時,會有一定的錯誤率,也就是說,有可能把不屬于這個集合的元素誤認為屬于這個集合(False Positive),但不會把屬于這個集合的元素誤認為不屬于這個集合(False Negative),在增加了錯誤率這個因素之后,Bloom Filter通過允許少量的錯誤來節省大量的存盤空間,
??它的用法其實是很容易理解的,我們拿個HBase中應用的例子來說下,我們已經知道rowKey存放在HFile中,那么為了從一系列的HFile中查詢某個rowkey,我們就可以通過 Bloom Filter 快速判斷 rowkey 是否在這個HFile中,從而過濾掉大部分的HFile,減少需要掃描的Block,
Bloom Filter 作業原理
??BloomFilter對于HBase的隨機讀性能至關重要,對于get操作以及部分scan操作可以剔除掉不會用到的HFile檔案,減少實際IO次數,提高隨機讀性能,在此簡單地介紹一下Bloom Filter的作業原理,Bloom Filter使用位陣列來實作過濾,初始狀態下位陣列每一位都為0,如下圖所示:
??假如此時有一個集合S = {x1, x2, … xn},Bloom Filter使用k個獨立的hash函式,分別將集合中的每一個元素映射到{1,…,m}的范圍,對于任何一個元素,被映射到的數字作為對應的位陣列的索引,該位會被置為1,比如元素x1被hash函式映射到數字8,那么位陣列的第8位就會被置為1,下圖中集合S只有兩個元素x和y,分別被3個hash函式進行映射,映射到的位置分別為(0,3,6)和(4,7,10),對應的位會被置為1:
??現在假如要判斷另一個元素是否是在此集合中,只需要被這3個hash函式進行映射,查看對應的位置是否有0存在,如果有的話,表示此元素肯定不存在于這個集合,否則有可能存在,下圖所示就表示z肯定不在集合{x,y}中:
- 從上面的內容我們可以得知,Bloom Filter有兩個很重要的引數:
- 哈希函式個數
- 位陣列的大小
Bloom Filter 在HBase中的應用
??HFile 中和 Bloom Filter 相關的Block;
Scanned Block Section(掃描HFile時被讀取):Bloom Block;
Load-on-open-section(regionServer啟動時加載到記憶體):BloomFilter Meta Block、 Bloom Index Block;
- Bloom Block:Bloom資料塊,存盤Bloom的位陣列
- Bloom Index Block:Bloom資料塊的索引
- BloomFilter Meta Block:從HFile角度看bloom資料塊的一些元資料資訊,大小個數等等
??HBase中每個HFile都有對應的位陣列,KeyValue在寫入HFile時會先經過幾個hash函式的映射,映射后將對應的陣列位改為1,get請求進來之后再進行hash映射,如果在對應陣列位上存在0,說明該get請求查詢的資料不在該HFile中,
??HFile中的Bloom Block中存盤的就是上面說得位陣列,當HFile很大時,Data Block 就會很多,同時KeyValue也會很多,需要映射入位陣列的rowKey也會很多,所以為了保證準確率,位陣列就會相應越大,那Bloom Block也會越大,為了解決這個問題就出現了Bloom Index Block,一個HFile中有多個Bloom Block(位陣列),根據rowKey拆分,一部分連續的Key使用一個位陣列,這樣查詢rowKey就要先經過Bloom Index Block(在記憶體中)定位到Bloom Block,再把Bloom Block加載到記憶體,進行過濾,
HBase 過濾器作用
作用:
- 過濾器的作用是在服務端判斷資料是否滿足條件,然后只將滿足條件的資料回傳給客戶端
- 過濾器的型別很多,但是可以分為兩大類:
- 比較過濾器:可應用于rowkey、列簇、列、列值過濾器
- 專用過濾器:只能適用于特定的過濾器
比較過濾器
比較運算子
-
LESS <
-
LESS_OR_EQUAL <=
-
EQUAL =
-
NOT_EQUAL <>
-
GREATER_OR_EQUAL >=
-
GREATER >
-
NO_OP 排除所有
常見的六大比較過濾器
BinaryComparator
按位元組索引順序比較指定位元組陣列,采用Bytes.compareTo(byte[])
BinaryPrefixComparator
通BinaryComparator,只是比較左端前綴的資料是否相同
NullComparator
判斷給定的是否為空
BitComparator
按位比較
RegexStringComparator
提供一個正則的比較器,僅支持 EQUAL 和非EQUAL
SubstringComparator
判斷提供的子串是否出現在中
示例代碼
rowKey過濾器:RowFilter
通過RowFilter與BinaryComparator過濾比rowKey 1500100010小的所有值出來
@Test
// 通過RowFilter過濾比rowKey 1500100010 小的所有值出來
public void BinaryComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
BinaryComparator binaryComparator = new BinaryComparator(Bytes.toBytes(1500100010));
RowFilter rowFilter = new RowFilter(CompareFilter.CompareOp.LESS, binaryComparator);
Scan scan = new Scan();
scan.setFilter(rowFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
列簇過濾器:FamilyFilter
通過FamilyFilter與SubstringComparator查詢列簇名包含in的所有列簇下面的資料
@Test
// 通過FamilyFilter查詢列簇名包含in的所有列簇下面的資料
public void SubstringComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
SubstringComparator substringComparator = new SubstringComparator("in");
FamilyFilter familyFilter = new FamilyFilter(CompareFilter.CompareOp.EQUAL, substringComparator);
Scan scan = new Scan();
scan.setFilter(familyFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
通過FamilyFilter與 BinaryPrefixComparator 過濾出列簇以info開頭的列簇下的所有資料
// 通過FamilyFilter與 BinaryPrefixComparator 過濾出列簇以info開頭的所有列簇下的所有資料
@Test
public void BinaryPrefixComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
// 二進制前綴比較器
BinaryPrefixComparator binaryPrefixComparator = new BinaryPrefixComparator("info".getBytes());
// FamilyFilter 作用于列簇的過濾器
FamilyFilter familyFilter = new FamilyFilter(CompareFilter.CompareOp.EQUAL, binaryPrefixComparator);
Scan scan = new Scan();
scan.withStartRow("1500100001".getBytes());
scan.withStopRow("1500100011".getBytes());
// 通過setFilter方法設定過濾器
scan.setFilter(familyFilter);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
列過濾器:QualifierFilter
通過QualifierFilter與SubstringComparator查詢列名包含in的列的值
public void printRS(ResultScanner scanner) throws IOException {
for (Result rs : scanner) {
String rowkey = Bytes.toString(rs.getRow());
System.out.println("當前行的rowkey為:" + rowkey);
for (Cell cell : rs.listCells()) {
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
byte[] bytes = CellUtil.cloneValue(cell);
if ("age".equals(qualifier)) {
int value = Bytes.toInt(bytes);
System.out.println(family + ":" + qualifier + "的值為" + value);
} else {
String value = Bytes.toString(bytes);
System.out.println(family + ":" + qualifier + "的值為" + value);
}
}
}
}
@Test
// 通過FamilyFilter查詢列簇名包含in的所有列簇下面的資料
public void SubstringComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
SubstringComparator substringComparator = new SubstringComparator("in");
FamilyFilter familyFilter = new FamilyFilter(CompareFilter.CompareOp.EQUAL, substringComparator);
Scan scan = new Scan();
scan.setFilter(familyFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
過濾出 列的名字 中 包含 “am” 所有的列 及列的值
// 過濾出 列的名字 中 包含 "am" 所有的列 及列的值
@Test
public void SubstringComparatorQualifierFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
SubstringComparator substringComparator = new SubstringComparator("am");
// 作用在列名上的過濾器
QualifierFilter qualifierFilter = new QualifierFilter(CompareFilter.CompareOp.EQUAL, substringComparator);
Scan scan = new Scan();
scan.withStartRow("1500100001".getBytes());
scan.withStopRow("1500100011".getBytes());
// 通過setFilter方法設定過濾器
scan.setFilter(qualifierFilter);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
列值過濾器:ValueFilter
通過ValueFilter與BinaryPrefixComparator過濾出所有的cell中值以 “張” 開頭的學生
@Test
// 通過ValueFilter與BinaryPrefixComparator過濾出所有的cell中值以 "張" 開頭的學生
public void BinaryPrefixComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
BinaryPrefixComparator binaryPrefixComparator = new BinaryPrefixComparator("張".getBytes());
ValueFilter valueFilter = new ValueFilter(CompareFilter.CompareOp.EQUAL, binaryPrefixComparator);
Scan scan = new Scan();
scan.setFilter(valueFilter);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
過濾出文科的學生,只會回傳clazz列,其他列的資料不符合條件,不會回傳
// 過濾出文科的學生
// 只會回傳clazz列,其他列的資料不符合條件,不會回傳
@Test
public void RegexStringComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
// 使用正則運算式比較器
RegexStringComparator regexStringComparator = new RegexStringComparator("^文科.*");
// ValueFilter 會回傳符合條件的cell,并不會回傳整條資料
ValueFilter valueFilter = new ValueFilter(CompareFilter.CompareOp.EQUAL, regexStringComparator);
Scan scan = new Scan();
scan.withStartRow("1500100001".getBytes());
scan.withStopRow("1500100011".getBytes());
// 通過setFilter方法設定過濾器
scan.setFilter(valueFilter);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
專用過濾器
單列值過濾器:SingleColumnValueFilter
SingleColumnValueFilter會回傳滿足條件的cell所在行的所有cell的值(即會回傳一行資料)
通過SingleColumnValueFilter與查詢文科班所有學生資訊
@Test
// 通過SingleColumnValueFilter與查詢文科班所有學生資訊
public void RegexStringComparatorFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(
"info".getBytes(),
"clazz".getBytes(),
CompareFilter.CompareOp.EQUAL,
new RegexStringComparator("^文科.*")
);
Scan scan = new Scan();
scan.setFilter(singleColumnValueFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
列值排除過濾器:SingleColumnValueExcludeFilter
與SingleColumnValueFilter相反,會排除掉指定的列,其他的列全部回傳
通過SingleColumnValueExcludeFilter與BinaryComparator查詢文科一班所有學生資訊,最終不回傳clazz列
@Test
// 通過SingleColumnValueExcludeFilter與BinaryComparator查詢文科一班所有學生資訊,最終不回傳clazz列
public void RegexStringComparatorExcludeFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
SingleColumnValueExcludeFilter singleColumnValueExcludeFilter = new SingleColumnValueExcludeFilter(
"info".getBytes(),
"clazz".getBytes(),
CompareFilter.CompareOp.EQUAL,
new BinaryComparator("文科一班".getBytes())
);
Scan scan = new Scan();
scan.setFilter(singleColumnValueExcludeFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
// clazz列為空
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
rowkey前綴過濾器:PrefixFilter
通過PrefixFilter查詢以150010008開頭的所有前綴的rowkey
@Test
// 通過PrefixFilter查詢以150010008開頭的所有前綴的rowkey
public void PrefixFilterFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
PrefixFilter prefixFilter = new PrefixFilter("150010008".getBytes());
Scan scan = new Scan();
scan.setFilter(prefixFilter);
ResultScanner scanner = students.getScanner(scan);
Result rs = scanner.next();
while (rs != null) {
String id = Bytes.toString(rs.getRow());
String name = Bytes.toString(rs.getValue("info".getBytes(), "name".getBytes()));
int age = Bytes.toInt(rs.getValue("info".getBytes(), "age".getBytes()));
String gender = Bytes.toString(rs.getValue("info".getBytes(), "gender".getBytes()));
// clazz列為空
String clazz = Bytes.toString(rs.getValue("info".getBytes(), "clazz".getBytes()));
System.out.println(id + "\t" + name + "\t" + age + "\t" + gender + "\t" + clazz + "\t");
rs = scanner.next();
}
}
分頁過濾器PageFilter
通過PageFilter查詢第三頁的資料,每頁10條
使用PageFilter分頁效率比較低,每次都需要掃描前面的資料,直到掃描到所需要查的資料
可設計一個合理的rowkey來實作分頁需求
@Test
// 通過PageFilter查詢第三頁的資料,每頁10條
public void PageFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
int PageNum = 3;
int PageSize = 10;
Scan scan = new Scan();
if (PageNum == 1) {
scan.withStartRow("".getBytes());
//使用分頁過濾器,實作資料的分頁
PageFilter pageFilter = new PageFilter(PageSize);
scan.setFilter(pageFilter);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
} else {
String current_page_start_rows = "";
int scanDatas = (PageNum - 1) * PageSize + 1;
PageFilter pageFilter = new PageFilter(scanDatas);
scan.setFilter(pageFilter);
ResultScanner scanner = students.getScanner(scan);
for (Result rs : scanner) {
current_page_start_rows = Bytes.toString(rs.getRow());
}
scan.withStartRow(current_page_start_rows.getBytes());
PageFilter pageFilter1 = new PageFilter(PageSize);
scan.setFilter(pageFilter1);
ResultScanner scanner1 = students.getScanner(scan);
printRS(scanner1);
}
}
通過合理的設定rowkey來實作分頁功能
@Test
// 通過合理的設定rowkey來實作分頁功能,提高效率
public void PageFilterTest2() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
int PageSize = 10;
int PageNum = 3;
int baseId = 1500100000;
int start_row = baseId + (PageNum - 1) * PageSize + 1;
int end_row = start_row + PageSize;
Scan scan = new Scan();
scan.withStartRow(String.valueOf(start_row).getBytes());
scan.withStopRow(String.valueOf(end_row).getBytes());
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
多過濾器綜合查詢
查詢文科班中的學生中學號以150010008開頭并且年齡小于23的學生資訊
@Test
// 查詢文科班中的學生中學號以150010008開頭并且年齡小于23的學生資訊
public void FilterListFilter() throws IOException {
Table students = conn.getTable(TableName.valueOf("students"));
Scan scan = new Scan();
SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(
"info".getBytes()
, "clazz".getBytes()
, CompareFilter.CompareOp.EQUAL
, new RegexStringComparator("^文科.*"));
PrefixFilter prefixFilter = new PrefixFilter("150010008".getBytes());
SingleColumnValueFilter singleColumnValueFilter1 = new SingleColumnValueFilter(
"info".getBytes()
, "age".getBytes()
, CompareFilter.CompareOp.LESS
, new BinaryComparator(Bytes.toBytes(23)));
FilterList filterList = new FilterList();
filterList.addFilter(singleColumnValueFilter);
filterList.addFilter(prefixFilter);
filterList.addFilter(singleColumnValueFilter1);
scan.setFilter(filterList);
ResultScanner scanner = students.getScanner(scan);
printRS(scanner);
}
到底啦!關注靚仔學習更多的大資料知識!😊
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/436406.html
標籤:其他
