Linux文本處理三劍客之awk學習筆記03:讀取文件

讀取文件

讀取“每行”數據

博客的開頭我們說過,默認情況下awk讀取文件的每行數據並將其存入$0變量當中。其實,awk在讀取數據之前會根據其內部的預定義變量RS的值來分隔每條記錄(record)。RS的默認值是“\n”,即換行符,因此也就會有我們剛纔所說的默認情況。

所以,awk在讀取文件時,會根據其自定義變量RS(Record Separator,記錄分隔符)的值將文件分爲多條記錄來循環讀取,每讀取一條記錄就將其賦值給$0變量,賦值完畢後再執行main代碼塊。

如果文件是一個空文件,那麼就讀取不到記錄也就不會執行main代碼塊。

[root@c7-server ~]# touch x.log
[root@c7-server ~]# awk '{print "hello world"}' x.log
[root@c7-server ~]#

可以在BEGIN代碼塊中設置RS的值來改變awk分隔記錄的方式。

[root@c7-server ~]# awk 'BEGIN{RS="com"}{print "---";print $0;print "---"}' a.txt 
---
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.
---
---
     18023394012
2   Alice   female  24   def@gmail.
---
... ...

被分隔的每條記錄中不會包含RS的值本身,在上述示例中即每條記錄不會包含com字符串。

細心的朋友會留意到

~]# awk '{print $0}' a.txt

當我們不修改RS,上述指令在輸出的時候會使得每條記錄之間自動換行,看起來就好像輸出數據包含了換行符(RS的默認值)。這個特點留到我們講解另一個預定義變量ORS的時候再做解釋。

那麼爲什麼一般將RS設置在BEGIN代碼塊當中呢?

首先,一般來說一個文件的RS在我們使用awk處理文件之前就可以確定了,而且一般不會改動。其次,基於我們截止目前爲止介紹的awk特性,如果我們在main代碼塊中設置RS的話,awk讀取第一條記錄的時候依然會使用換行符(RS的默認值)來作爲分隔符,讀取完第一條記錄接下來纔是第一次進入main代碼塊,然後纔是設置RS的值,而且每次awk內部循環執行main時都要爲RS賦相同值也沒有必要(性能略微損失)。

RS爲單個字符時直接使用該字符作爲分隔符;RS爲多個字符時被awk識別爲正則表達式。

RS的特殊值

如果記錄分隔符不存在於讀入數據中的話,那麼我們便可以在一次內部循環的情況下讀取出所有的數據。

RS="\0"和RS="^$":這兩種方式均可以一次性讀取所有數據,區別在於部分文件可能包含\0字符。雖然在正則中^$表示空行,但是在文件中即使包含空行也不會將其作爲RS。

[root@c7-server ~]# awk 'BEGIN{RS="\0"} {print "---";print $0;print "---"}' a.txt 
---
ID  name    gender  age  email          phone
... ...
10  Bruce   female  27   [email protected]   13942943905

---
[root@c7-server ~]# awk 'BEGIN{RS="^$"} {print "---";print $0;print "---"}' a.txt 
---
ID  name    gender  age  email          phone
... ...
10  Bruce   female  27   [email protected]   13942943905

---

RS="":按段落讀取。當段落與段落之間均爲空行的時候,按照段落作爲分隔符。注意,空格和製表符雖然也是空白看不到的字符,但是不算空行而算空白符。網友們可以自行在a.txt中鍵入幾個空行查看效果。

~]# awk 'BEGIN{RS=""} {print "---";print $0;print "---"}' a.txt

RS="\n+":以至少一個換行符作爲分隔符。默認情況下每行數據都視爲1條記錄,而該情況下可以將多個連續的換行符作爲分隔符,使得空行不會被視爲記錄。

當我們使用正則作爲分隔符的時候,分隔符可以有多種情況。每次awk遇到滿足正則條件的分隔符時,都會將這次分隔符賦值給RT(Record Termination),我們可以通過查看該值來判斷到底這條記錄是以什麼作爲分隔符。

最後一個分隔符比較特殊。我猜測可能是因爲已經EOF了,就將EOF視爲分隔符,雖然它並不滿足於正則條件。

在我們使用正則進行匹配的時候如果想要忽略大小寫,可以使用預定義變量IGNORECASE。

~]# awk 'BEGIN{IGNORECASE=1} /alice|bob/{print $0}' a.txt 
1   Bob     male    28   [email protected]     18023394012
2   Alice   female  24   [email protected]  18084925203

記錄號

記錄號即“行號”,awk使用NF和FNR兩個預定義變量來保存記錄號,每讀取1條記錄,它們的值就會加1。第一條記錄號的值就是1,以此開始遞增。

~]# awk '{print NR,FNR}' a.txt a.txt

NR會一直遞增,即使數據的來源屬於不同的文件,而FNR在遇到新的文件的時候其值會重回1開始遞增。

至此我們瞭解到,awk每讀取1條記錄就會設置$0、NR、FNR和RT的值。

讀取每字段數據

awk讀取記錄以後,還會根據預定義變量FS(Field Separator,字段分隔符)將記錄劃分成多個字段。其值默認是一個空格(FS=" "),表示將一個至多個空白字符(空格、製表符和換行符)識別爲字段分隔符。將第一個字段賦值給$1,第二個字段賦值給$2,依次類推直至將最後一個字段賦值給$NF。預定義變量NF表示這條記錄的字段數量。大家可以自己試試。

awk '{print $1}' a.txt
awk '{print $6}' a.txt
awk '{print $NF}' a.txt

引用的字段如果超出最大字段數則反饋空字符串,如果是負數則報錯。

[root@c7-server ~]# awk '{print $7}' a.txt 

... ...

[root@c7-server ~]# awk '{print $-1}' a.txt 
awk: cmd. line:1: (FILENAME=a.txt FNR=1) fatal: attempt to access field -1

根據分隔符劃分字段

通過預定義變量FS和選項-F可以用來指定字段分隔符。選項-F和預定義變量FS大同小異,只不過指定的位置不同罷了。

awk 'BEGIN{FS=":"}{print $1}' /etc/passwd
awk -F ":" '{print $1}' /etc/passwd

字段分隔符的特性大多數在上面介紹FS時已經介紹過,補充幾點。

如果FS的值爲空字符串"",那麼會將記錄中的每個字符都識別爲字段。空格字符也是字符。

~]# echo "a c" | awk 'BEGIN{FS=""}{print $1;print $2;print $3}'
a
 
c

如果在記錄中無法找到字段分隔符則將整個記錄($0)賦值給第一個字段$1。

~]# awk 'BEGIN{FS="_"}{print $1;print $2}' a.txt

根據字段寬度劃分字段

根據分隔符劃分字段的前提條件是文件有合適的分隔符便於我們劃分字段。我們copy一份a.txt至b.txt,並且修改某幾行的某幾個字段,使用等量的空格符來替換。

[root@c7-server ~]# cat b.txt 
ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
2           female  24   [email protected]  18084925203
3   Tony    male    21   [email protected]    17048792503
4   Kevin           21   [email protected]    17023929033
5   Alex    male    18   [email protected]    18185904230
6   Andy    female       [email protected]    18923902352
7   Jerry   female  25                  18785234906
8   Peter   male    20   [email protected]     17729348758
9   Steven  female  23   [email protected]               
10  Bruce   female  27   [email protected]   13942943905

此時再以字段分隔符的方式來爲b.txt劃分字段就不合適了。

此時我們通過觀察發現:

  • 缺失的字段使用了等量的空格字符填充;
  • 爲每個字段設定一個最大字符數之後(第一個字段最大字符數是2,第二個字段最大字符數是6,以此類推),字段間距是可知的(字段間距剛好都是2個字符,即使不同也沒事,主要是可知的),每條記錄同字段間的間距相同(例如每條記錄的第一個和第二個字段的間距相同)。

此時我們即可使用預定義變量FIELDFIDTHS來根據字段的字符寬度劃分字段。

~]# echo "abbcccddd" | awk 'BEGIN{FIELDWIDTHS="1 2 3 4"}{print $1;print $2;print $3;print $4}'

支持跳躍字符指定字段寬度。

~]# echo "a bb  ccc   ddd" | awk 'BEGIN{FIELDWIDTHS="1 1:2 2:3 3:4"}{print $1;print $2;print $3;print $4}'

支持通配符*匹配剩餘所有字符。

~]# echo "abbcccddd" | awk 'BEGIN{FIELDWIDTHS="1 2 3 *"}{print $1;print $2;print $3;print $NF}'
~]# echo "a bb  ccc   ddd" | awk 'BEGIN{FIELDWIDTHS="1 1:2 2:3 3:*"}{print $1;print $2;print $3;print $NF}'

因此我們可以使用FIELDWIDTHS來處理b.txt了。注意觀察結果(結果沒有放入博文,請網友自行敲看看)。

~]# awk 'BEGIN{FIELDWIDTHS="2 2:6 2:6 2:3 2:13 2:*"} $1==2||$1==4||$1==6||$1==7||$1==9{print "----";print $1;print $2;print $3;print $4;print $5;print $6;print "----"}' b.txt

根據模式劃分字段

預定義變量FPAT的值是一個正則表達式,awk根據這個值去匹配$0,第一次匹配成功賦值給$1,以此類推直到匹配完整個$0。不會修改$0。

FPAT適用於當我們打算使用分隔符取字段時,字段值包含了分隔符的情況。例如如下csv文件。

~]# cat FPAT.csv
Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA

此時我們使用逗號作爲分隔符取出的結果就不是我們想要的,因爲第三個字段包含了分隔符。另外,這裏我們使用了for循環遍歷了所有字段,第一次見此用法的網友照着敲直到功能即可,後面會講解for循環的。

~]# awk 'BEGIN{FS=","}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv 
Robbins
Arnold
"1234 A Pretty Street
 NE"
MyTown
MyState
12345-6789
USA

這時可以採取FPAT。正則的第一部分指明“分隔符”逗號以外的多個字符識別爲字段;正則的第二部分指明當遇到兩個雙引號(在awk中需要使用轉義字符表示雙引號\")的時候,將其與其中包裹的任意字符識別爲字段。這樣就可以正確分隔這個示例的字段了。

~]# awk 'BEGIN{FPAT="[^,]+|\".*\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv 
Robbins
Arnold
"1234 A Pretty Street, NE"
MyTown
MyState
12345-6789
USA

由於正則的貪婪匹配機制,如果記錄中包含2個以上的雙引號就會出問題。

[root@c7-server ~]# cat FPAT.csv
Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,"12345-6789",USA
[root@c7-server ~]# awk 'BEGIN{FPAT="[^,]+|\".*\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv 
Robbins
Arnold
"1234 A Pretty Street, NE",MyTown,MyState,"12345-6789"
USA

結果並不是我們想要的,此時需要改寫正則。

~]# awk 'BEGIN{FPAT="[^,]+|\"[^\"]+\""}{for(i=1;i<=NF;i++){print $i}}' FPAT.csv 
Robbins
Arnold
"1234 A Pretty Street, NE"
MyTown
MyState
"12345-6789"
USA

patsplit()函數的功能與FPAT預定義變量的功能相同。

至此我們學習了3種劃分字段的方式:

  1. 根據字段分隔符(預定義變量FS和選項-F)劃分字段;
  2. 根據字段的寬度(預定義變量FIELDWIDTHS)劃分字段;
  3. 根據字段的模式(預定義變量FPAT)劃分字段。

這三種方式只能選擇一種,它們相互之間是衝突的。

檢查字段分隔的方式

數組變量PROCINFO["FS"]存儲了字段分隔的三種方式,其值分別是FS、FIELDWIDTHS和FPAT。

~]# cat test.awk
BEGIN {
    if(PROCINFO["FS"]=="FS"){
        print "FS"
    } else if(PROCINFO["FS"]=="FPAT") {
        print "FPAT"
    } else {
        print "FIELDWIDTHS"
    }
}
~]# awk -f test.awk
~]# awk -v FS=":" -f test.awk
~]# awk -v FIELDWIDTHS="3" -f test.awk
~]# awk -v FPAT="[[:alpha:]]+" -f test.awk

字段與記錄的重建

預定義變量FS的含義我們已經很瞭解。有一個十分類似的預定義變量叫做OFS(Output FS),它表示當$0(記錄)重新計算(可以理解爲重建)的時候使用OFS的值作爲輸出字段的分隔符。接下來我們來看幾個重新計算的情況。

1、當修改$0的時候,將使用FS(假定我們就使用FS不使用其他劃分字段的方式)重新計算各個字段以及NF值。

awk 'BEGIN{FS=":"}{$0="a:b:c";print NF;for(i=1;i<=NF;i++){print $i}}' a.txt

2、當修改具體的字段的時候,使用OFS重建記錄。注意,哪怕是自我賦值也屬於字段的修改。

awk 'BEGIN{OFS="-"}{$1=0;print $0}' a.txt
awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt

3、爲不存在的字段賦值,將新增字段併爲不存在的字段(若有)賦空字符串,使用OFS重建記錄。

awk 'BEGIN{OFS="-"}{$(NF+3)=5;print $0}' a.txt

4、增加NF,使用空字符串爲新記錄賦值;減少NF,截斷多餘記錄。均會使用OFS重建記錄。

# awk 'BEGIN{OFS="-"}{NF+=3;print $0}' a.txt
# awk 'BEGIN{OFS="-"}{NF-=3;print $0}' a.txt

awk讀取記錄以後將數據原原本本存放於$0當中,只要不會發生上述使用OFS重建記錄的事情,即便指定了OFS也無妨。

# awk 'BEGIN{OFS="-"}{print $0}' a.txt

OFS的默認值是1個空格。因此即便沒指定具體的值也會使用單個空格重建記錄。

awk '{$1=$1;print $0}' a.txt

一般我們會先設置OFS的值再重建記錄。所以將其放入BEGIN中。如果先重建再設置OFS,那麼第一行會按照默認OFS重建,後續行才按照新OFS值重建。

awk '{$1=$1;OFS="-";print $0}' a.txt
awk '{$1=$1;OFS="-";$1=$1;print $0}' a.txt
awk 'BEGIN{OFS="-"}{$1=$1;print $0}' a.txt
awk '{$1=$1;print $0}' OFS="-" a.txt

這裏如果看不懂的朋友,等後面學習了awk工作流程和變量以後就會明白awk執行的順序了。

根據這個特性我們可以壓縮連續的多個空格。

# echo " a  b   c  d    " | awk '{$1=$1;print $0}'
# echo " a  b   c  d    " | awk 'BEGIN{OFS="-"}{$1=$1;print $0}'

數據篩選

記錄篩選

1、根據行號(NR或者FNR)篩選記錄。

awk 'NR==2{print $0}' a.txt
awk 'NR>2{print}' a.txt
awk 'NR<2' a.txt
awk 'NR>=2' a.txt
awk 'NR<=2' a.txt

此前已經說過,省略{action}即表示{print}等價於{print $0}。

2、根據正則表達式篩選記錄。

正則匹配,默認使用$0來匹配,可以省略$0。

awk '/qq.com/' a.txt
awk '$0~/qq.com/' a.txt

匹配不包含@的記錄,即整條記錄均由非@字符構成。

awk '/^[^@]+$/' a.txt

awk支持取反,使用取反更易理解。

awk '!/@/' a.txt

3、根據字段篩選記錄。

# awk '$4>24' a.txt 
ID  name    gender  age  email          phone
1   Bob     male    28   [email protected]     18023394012
7   Jerry   female  25   [email protected]  18785234906
10  Bruce   female  27   [email protected]   13942943905

第一條記錄的$4是age,age是字符串,24是數字,其在進行比較時會有內部轉換機制,將24識別爲字符串,字符串比較根據ASCII編碼(maybe)按字符一一比較,字符a大於字符2。如果我們期望不篩選出age那條,可以將其+0從而轉換成數字。字符串+0等於數字0。

# awk '($4+0)>24' a.txt 
1   Bob     male    28   [email protected]     18023394012
7   Jerry   female  25   [email protected]  18785234906
10  Bruce   female  27   [email protected]   13942943905
awk '$5~/qq.com/' a.txt

4、組合篩選。

使用邏輯與和邏輯或運算符組合多個條件。

awk 'NR>=2&&NR<=4' a.txt
awk '($4+0)>=20||$3=="male"' a.txt

5、按照範圍篩選(flip-flop)。

awk 'NR==2,NR==4' a.txt
awk '$2=="Kevin",$5~/qq.com/' a.txt

字段處理

字段的篩選即print $X(X表示具體的字段)沒什麼好說的,因此講字段的處理。

# awk 'NR>1{$4+=4;print $0}' a.txt 
1 Bob male 32 [email protected] 18023394012
2 Alice female 28 [email protected] 18084925203
... ...

處理字段目前只接觸到賦值,修改了字段值會導致使用OFS重建$0。

要想使得輸出結果恢復重建前的效果,可以結合外部命令,例如該示例中的column。

# awk 'NR>1{$4+=4;print $0}' a.txt | column -t
1   Bob     male    32  [email protected]     18023394012
2   Alice   female  28  [email protected]  18084925203
... ...

或者在後續學會了字符串處理函數以後來實現。基本思路是取得$0重建前的$4的前後部分保留,然後修改$4的值,最後再將三部分組合。

awk 'NR>1{$6=$6"*";print $0}' a.txt
awk 'NR>1{$6=$6"*";print $0}' a.txt | column -t

數據篩選示例

該示例要求我們從ifconfig的輸出結果中取得ipv4地址(不包含環回地址lo),該示例同時也是常見的運維面試題。

# ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.152.100  netmask 255.255.255.0  broadcast 192.168.152.255
        inet6 fe80::7a4:5a06:46b4:9ce5  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:46:79:46  txqueuelen 1000  (Ethernet)
        RX packets 3151  bytes 258273 (252.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1606  bytes 166414 (162.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 72  bytes 8088 (7.8 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 72  bytes 8088 (7.8 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

virbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255
        ether 52:54:00:a6:3d:cf  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

有3種思路來取得地址。

思路一:ipv4地址位於包含“inet ”(注意有空格)的記錄,因此篩選出該記錄。同時我們要過濾掉換回地址,因此$2不以127打頭的記錄。將2個條件使用邏輯與連接。

# ifconfig | awk '/inet /&&!($2~/^127/)'
        inet 192.168.152.100  netmask 255.255.255.0  broadcast 192.168.152.255
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255

思路二:ifconfig輸出信息中包含了3段信息,每段表示1張網卡並以空行作爲記錄分隔符。因此結合我們在講解RS時提到的,這裏我們以段劃分記錄。記錄不包含lo,同時我們取得ip地址所在的字段(手工數一下可知是第6字段)。

# ifconfig | awk 'BEGIN{RS=""}!/lo/{print $6}'
192.168.152.100
192.168.122.1

思路三:基於思路二,假設ip地址所在的字段數比較靠後,那麼我們就需要數好幾個字段纔可以數到ipv4地址,我們來看一下下面這個輸出結果。

# ifconfig | awk 'BEGIN{RS=""}!/lo/{print "---"$0"---"}'
---ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.152.100  netmask 255.255.255.0  broadcast 192.168.152.255
        inet6 fe80::7a4:5a06:46b4:9ce5  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:46:79:46  txqueuelen 1000  (Ethernet)
        RX packets 3997  bytes 330636 (322.8 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2063  bytes 225016 (219.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0---
---virbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255
        ether 52:54:00:a6:3d:cf  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0---

按段劃分並篩選記錄以後,ipv4地址,從我們的視覺上來看,可以理解爲每條記錄的第2“行”。不過我們這裏因爲已經將整個網卡的信息(多行)理解爲了1條記錄(行)了,因此我們要將原本的第二行識別爲第2個字段,即修改FS的值。

# ifconfig | awk 'BEGIN{RS=""}!/lo/{FS="\n";print $2}'
flags=4163<UP,BROADCAST,RUNNING,MULTICAST>
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255

輸出結果中第一行並不是我們所期望的字段信息。這是因爲根據RS=""讀取記錄以後會賦值$0,並按照FS的值(沒有另外指定因此使用FS=" ")賦值$1...$NF各個位置參數。賦值完畢以後才執行main代碼塊賦值FS="\n",此時第一條記錄的各位置參數已經確定好了。因此從第二條記錄開始,$2纔是我們所想要的信息。

我們只要將FS設置在BEGIN中即可,這也是爲什麼大多數情況下如果要修改默認的FS和RS都在BEGIN中設置。

# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{print $2}'
        inet 192.168.152.100  netmask 255.255.255.0  broadcast 192.168.152.255
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255

接下來我們將每條記錄的$2賦值給記錄$0本身,設置FS並取第2個字段的信息。按照下面的命令明顯無法取正確。

ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";print $2}'
# 第一條空信息
# 第二條空信息,注意這兩條空信息所取的字段是不同的。

原因此前我們也說了,修改$0($0=$2)會重新劃分各字段,而FS=" "在修改$0之後出現,因此第一條記錄依然是按照FS="\n"劃分字段。

【第一條空信息】的$0是:inet 192.168.152.100  netmask 255.255.255.0  broadcast 192.168.152.255,根據FS,因此它也會是$1,因此$2爲空。

想讓【第一條空信息】取值正確的話,就要重新設置$0。

# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2}'
192.168.152.100
    # 第二條空信息

【第二條空信息】取值錯誤的原因是從第二條記錄開始,FS的值就一直是main中的" ",我們需要在main的結尾再將其設置回BEGIN中的值。

# ifconfig | awk 'BEGIN{RS="";FS="\n"}!/lo/{$0=$2;FS=" ";$0=$0;print $2;FS="\n"}'
192.168.152.100
192.168.122.1

在我們實際使用當中使用思路一和思路二取ipv4地址即可,思路三隻是利於我們理解awk的工作原理,看不懂的同學多看看上面的【字段與記錄的重建】。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章