R學習筆記(4): 使用外部數據


 

鑒於內存的非持久性和容量限制,一個有效的數據處理工具必須能夠使用外部數據:能夠從外部獲取大量的數據,也能夠將處理結果保存。R中提供了一系列的函數進行外部數據處理,從外部數據的類型可以分為文件、數據庫、網絡等;其中文件操作還可以區分為導入/導出操作和流式操作。

1 數據框

前面 僅僅提到:

列表(list)和數據框(data frame)分別是向量和矩陣的泛化——列表允許包含不同類型的元素,甚至可以把對象作為元素;數據框允許每列使用不同類型的元素。對於列表和數據框,其中的元素通常稱為分量(components)。

因為外部數據的處理涉及到數據框,這里對列表和數據框進行更詳細的說明。

1.1 列表

列表的分量可以是不同的類型,使用list()函數可以創建列表:

> x = list(name="Fred", wife="Mary", no.children=3, child.ages=c(4,7,9))
> x
$name
[1] "Fred"

$wife
[1] "Mary"

$no.children
[1] 3

$child.ages
[1] 4 7 9

列表元素可以通過幾種不同的索引進行訪問:

> x[[1]]
[1] "Fred"
> x[1][1]
$name
[1] "Fred"

> x["name"][1]
$name
[1] "Fred"
> x$name
[1] "Fred"

1.2 數據框

數據框是一種特殊的列表,是和矩陣類似的一種結構。在數據框中, 列可以是不同的對象。 可以把數據框看作是一個 行表示觀測個體並且(可能)同時擁有數值變量和 分類變量的 `數據矩陣' ,行和列可以通過矩陣的索引方式進行訪問。

下面是例子:

> L3 = LETTERS[1:3]
> L3
[1] "A" "B" "C"
> d = data.frame(cbind(x = 1, y = 1:10), fac = sample(L3, 10, replace = TRUE))
> d
   x  y fac
1  1  1   B
2  1  2   A
3  1  3   C
4  1  4   C
5  1  5   B
6  1  6   A
7  1  7   A
8  1  8   B
9  1  9   B
10 1 10   A
> d$x
 [1] 1 1 1 1 1 1 1 1 1 1
> d[[2]]
 [1]  1  2  3  4  5  6  7  8  9 10
> d[1][1]
   x
1  1
2  1
3  1
4  1
5  1
6  1
7  1
8  1
9  1
10 1

數據框是非常有用的工具。R中還提供了合並數據框的函數。對於兩個有相同列的數據框,可以用merge()函數進行合並,可以指定安裝哪一個列進行合並:

> x <- data.frame(k1 = c(NA,NA,3,4,5), k2 = c(1,NA,NA,4,5), data = 1:5)
> y <- data.frame(k1 = c(NA,2,NA,4,5), k2 = c(NA,NA,3,4,5), data = 1:5)
> x
  k1 k2 data
1 NA  1    1
2 NA NA    2
3  3 NA    3
4  4  4    4
5  5  5    5
> y
  k1 k2 data
1 NA NA    1
2  2 NA    2
3 NA  3    3
4  4  4    4
5  5  5    5

> merge(x,y)
  k1 k2 data
1  4  4    4
2  5  5    5
> merge(x,y,by='k1')
  k1 k2.x data.x k2.y data.y
1  4    4      4    4      4
2  5    5      5    5      5
3 NA    1      1   NA      1
4 NA    1      1    3      3
5 NA   NA      2   NA      1
6 NA   NA      2    3      3

> merge(x, y, by = c("k1","k2"))
  k1 k2 data.x data.y
1  4  4      4      4
2  5  5      5      5
3 NA NA      2      1

1.3 編輯數據框

前面提到data.entry()函數可以打開數據編輯器,但是不適用於數據框。如果用data.entry()修改了數據框,會轉換成列表(list)類型。

編輯數據框需要使用edit()函數:

> xnew = edit(xold)
edit()函數只是編輯,並不賦值。如果要直接修改數據框,需要使用如下的形式:
> x = edit(x)
> fix(x)        #等價於上面的形式

2 CSV文件的導入導出

R中處理文本文件主要是使用read.table()函數將數據讀入數據框;如果要對導入方式進行復雜的控制,開可以使用古老的scan()。 注:scan處理處理文件導入外,還可以直接接受鍵盤輸入。

在本系列的一開始,我們提到了工作空間,可以使用函數getwd()和setwd()來獲取/設置工作空間目錄;使用list.files()查看當前目錄下的文件。

對於工作空間中的文本文件,可以使用相對路徑操作,其他文件要使用絕對路徑。

2.1 文件格式

R支持豐富的文件格式,支持CSV、FIX、DIF、XML等文本格式和DBF、XLS、HDF5、netCDF等二進制格式。

對於CSV文件,R認為最理想的是如下的格式:

 
  Price Floor Area Rooms Age Cent.heat
01 52.00 111.0 830 5 6.2 no
02 54.75 128.0 710 5 7.5 no
03 57.50 101.0 1000 5 4.2 no
04 57.50 131.0 690 6 8.8 no
05 59.75 93.0 900 5 1.9 yes

即,第一行為數據框各分量的名字,隨后的每一行第一項為行標簽,其余為數據。

如果不符合這樣的默認格式,需要在導入函數中指定特定的參數。

2.2 read.table()和write.table()

最常用的方式是使用read.table()函數和write.table()處理CSV文件的導入導出。函數read()和write()只能處理矩陣或向量的特定列,而read.table()和write.table()可以處理包含行、列標簽的數據框。

read.talbe()函數讀取文件並返回一個數據框:

read.table(file, header = FALSE, sep = "", quote = "\"'",
           dec = ".", row.names, col.names,
           as.is = !stringsAsFactors,
           na.strings = "NA", colClasses = NA, nrows = -1,
           skip = 0, check.names = TRUE, fill = !blank.lines.skip,
           strip.white = FALSE, blank.lines.skip = TRUE,
           comment.char = "#",
           allowEscapes = FALSE, flush = FALSE,
           stringsAsFactors = default.stringsAsFactors(),
           fileEncoding = "", encoding = "unknown", text)

一些主要的參數:

  • file : 要處理的文件。可以用字符串指定文件名,也可以使用函數,如:file('file.dat',encoding='utf-8')
  • header:首行是否為字段名。如果不指定,read.table()會根據行標簽進行判斷,即如果首行比下面的行少一列,就是header行
  • col.names: 如果指定,則用指定的名稱替代首行中的列名稱
  • sep:指定分隔符。默認為空白符(空格,制表符,換行符等)。可以指定為' ', '\t'等
  • quote:指定字符串分隔符,如" 或 '
  • na.strings: 指定缺損值。默認為NA
  • fill :文件中是否忽略了行尾字段。如果有,必須指定為 TRUE
  • strip.white:是否去除字符串字段首尾的空白
  • blank.lines.skip:是否忽略空白行,默認為TRUE。如果要指定為FALSE,需要同時指定 fill = TRUE 才有效
  • colClasses:指定每個列的數據類型
  • comment.char : 注釋符。默認使用#作為注釋符號,如果文件中沒有注釋,指定comment.char = "" 會比較安全 (也可能讓速度比較快)

為了使用方便,read.table()函數還提供了一些變體,這些變體為read.table()的一些參數設定了默認值:

read.csv(file, header = TRUE, sep = ",", quote = "\"",
         dec = ".", fill = TRUE, comment.char = "", ...)

read.csv2(file, header = TRUE, sep = ";", quote = "\"",
          dec = ",", fill = TRUE, comment.char = "", ...)

read.delim(file, header = TRUE, sep = "\t", quote = "\"",
           dec = ".", fill = TRUE, comment.char = "", ...)

read.delim2(file, header = TRUE, sep = "\t", quote = "\"",
            dec = ",", fill = TRUE, comment.char = "", ...)

write.table()的參數要少一些:
write.table(x, file = "", append = FALSE, quote = TRUE, sep = " ",
            eol = "\n", na = "NA", dec = ".", row.names = TRUE,
            col.names = TRUE, qmethod = c("escape", "double"),
            fileEncoding = "")

一些參數的說明:

  • x 要寫入的對象的名稱
  • file 文件名(缺省時對象直接被“寫”在屏幕上)
  • append 是否為增量寫入
  • quote 一個邏輯型或者數值型向量:如果為TRUE,則字符型變量和因子寫在雙引 號""中;若quote是數值型向量則代表將欲寫在""中的那些列的列標。(兩種 情況下變量名都會被寫在""中;若quote = FALSE則變量名不包含在雙引號中)
  • sep 文件中的字段分隔符
  • eol 指定行尾符,默認為'\n'
  • na 表示缺失數據的字符
  • dec 用來表示小數點的字符
  • row.names 一個邏輯值,決定行名是否寫入文件;或指定要作為行名寫入文件的字符型 向量
  • col.names 一個邏輯值(決定列名是否寫入文件);或指定一個要作為列名寫入文件中 的字符型向量
  • qmethod 若quote=TRUE,則此參數用來指定字符型變量中的雙引號"如何處理: 若參數值為"escape" (或者"e",缺省)每個"都用\"替換;若值為"d"則每 個"用""替換

類似的,write.table()也提供了一些變體:

write.csv(…)

write.csv2(…)

示例: 將前面的例子保存為工作空間下的文件,然后執行命令:

> x = read.table('sample.csv',sep='\t')
> x
  V1    V2    V3   V4    V5  V6        V7
1 NA Price Floor Area Rooms Age Cent.heat
2  1 52.00 111.0  830     5 6.2        no
3  2 54.75 128.0  710     5 7.5        no
4  3 57.50 101.0 1000     5 4.2        no
5  4 57.50 131.0  690     6 8.8        no
6  5 59.75  93.0  900     5 1.9       yes

使用fix(x)編輯數據后,用write.table(x,'sample1.csv')保存。

2.3 scan()和cat()

read.table()很方便,但是處理大矩陣時的效率很低,比如你可以實驗一下一個不太大(200x2000)的矩陣操作:

>write.table(matrix(rnorm(200*2000), 200), "matrix.dat", row.names=F, col.names=F)
> A <- as.matrix(read.table("matrix.dat"))      #需要大概7秒

read.table()調用了scan()讀取文件,然后對結果進行處理。如果直接使用scan()讀取,效率會更高:

> A <- matrix(scan("matrix.dat", n = 200*2000), 200, 2000, byrow = TRUE)    #需要大概2秒

當矩陣的規模更大時,這種差異會更加突出。 scan()函數比read.table()要更加靈活,一個非常主要的區別是scan()可以指定變量的類型,避免類型校驗帶來的開銷:

scan(file = "", what = double(), nmax = -1, n = -1, sep = "",
     quote = if(identical(sep, "\n")) "" else "'\"", dec = ".",
     skip = 0, nlines = 0, na.strings = "NA",
     flush = FALSE, fill = FALSE, strip.white = FALSE,
     quiet = FALSE, blank.lines.skip = TRUE, multi.line = TRUE,
     comment.char = "", allowEscapes = FALSE,
     fileEncoding = "", encoding = "unknown", text)

同理,cat()函數也比write.table()靈活:

cat(... , file = "", sep = " ", fill = FALSE, labels = NULL,
    append = FALSE)

3 使用連接(connection)

R中的連接(Connections)提供了一組函數,實現靈活的指向類似文件對象的接口,以代替文件名的使用。 使用連接的基本步驟:

  1. 創建連接
  2. 打開連接
  3. 操作數據
  4. 關閉連接

R中通過函數 showConnections() 可以列出當前用戶打開的連接。 通過函數 showConnections(all = TRUE) 則可以查看所有連接的匯總信息,包括已經關閉或終止的連接。

3.1 連接的類型

R可以把很多種數據源都看做連接,包括:

  • 文件 file()函數創建一個文件連接,可以打開文本文件或二進制文件。對於gzip或bzip2壓縮的文件,可以使用gzfile()和bzfile()函數創建連接。
  • 標准I/O R中可以使用stdin()、stdout()、stderr()函數建立到標准I/O的連接。這些連接不需要打開就能直接使用,而且不能關閉。
  • 字符向量 R中甚至允許以一個字符向量作為輸入或輸出。使用textConnection()函數創建到字符向量的連接。
  • 管道(Pipes) UNIX中的管道有着非凡重要的意義,可以非常簡單的實現進程間通信。R函數pipe()可以創建管道連接。
  • URL

URL 類型的 http://,ftp:// 和 //localhost/ 可以通過函數 url 讀內容。為方便起見,file 也可以 接受這種文件規范和調用url。

  • socket

函數socketConnection()可以創建socket連接

3.2 輸出到連接

直接看例子:

zz <- file("ex.data", "w")  # 打開一個輸出文件連接
     cat("TITLE extra line", "2 3 5 7", "", "11 13 17",
         file = zz, sep = "\n")
     cat("One more line\n", file = zz)
     close(zz)

     ## 使用管道(Unix)在輸出中把小數點轉換成逗號
     ## R字符串和(可能)SHELL腳本中都需要把 \ 寫兩次
     zz <- pipe(paste("sed s/\\\\./,/ >", "outfile"), "w")
     cat(format(round(rnorm(100), 4)), sep = "\n", file = zz)
     close(zz)
     ## 現在查看輸出文件:
     file.show("outfile", delete.file = TRUE)

     ## 捕獲R輸出:使用 help(lm) 里面的例子
     zz <- textConnection("ex.lm.out", "w")
     sink(zz)
     example(lm, prompt.echo = "> ")
     sink()
     close(zz)
     ## 現在 `ex.lm.out' 含有需要進一步處理的輸出內容
     ## 查看里面的內容,如
     cat(ex.lm.out, sep = "\n")

3.3 從連接輸入

從連接讀入數據的基本函數是scan 和 readLines。這些函數有個以字符串作為輸入的參數,在 函數調用時會打開一個文件連接,但顯式地打開文件連接允許一個文件 可以連續地以不同格式讀入。 調用 scan 的其它函數也可以使用連接, 特別是 read.table。 一些簡單的例子如下:

## 讀入前面例子中創建的文件
readLines("ex.data")
unlink("ex.data")

## 讀入當前目錄的清單(Unix)
readLines(pipe("ls -1"))

# 從輸入文件中去掉拖尾的逗號。
# 假定我們有一個包含如下`數據'的文件
450, 390, 467, 654,  30, 542, 334, 432, 421,
357, 497, 493, 550, 549, 467, 575, 578, 342,
446, 547, 534, 495, 979, 479
# 然后通過如下命令讀入
scan(pipe("sed -e s/,$// data"), sep=",")

從連接輸入數據還可以使用壓棧操作。類似於C語言中的ungetc函數,R中的pushBack()函數可以把任意數據壓入給連接。壓入后的數據以堆棧方式存儲(FILO)。棧不為空時從棧中取數據,棧為空才從連接輸入數據。

例子如下:

> zz <- textConnection(LETTERS)
     > readLines(zz, 2)
     [1] "A" "B"
     > scan(zz, "", 4)
     Read 4 items
     [1] "C" "D" "E" "F"
     > pushBack(c("aa", "bb"), zz)
     > scan(zz, "", 4)
     Read 4 items
     [1] "aa" "bb" "G"  "H"
     > close(zz)

壓棧操作僅適用於文本輸入模式的連接。

3.4 二進制連接

在打開連接時用'b'設置二進制方式,如'rb','wb'等,則可以使用readBin()和writeBin()函數進行二進制方式的讀寫。函數說明如下:

readBin(con, what, n = 1, size = NA, endian = .Platform$endian)
writeBin(object, con, size = NA, endian = .Platform$endian)

其中:

  • con 要打開的連接。如果給定的是字符串,它會被假定是文件名字。
  • what 說明向量的類型/模式。比如 numeric,integer,logical,character, complex 或 raw 。可以用函數如integer()或字符串如'integer'作為參數。
  • n 要讀入的最大元素數量
  • size 指定字節數。比如,通過設定size可以讀寫16位的整數或單精度的實數。
  • object 要寫入的對象,必須是原子型向量對象,也就是沒有屬性的 numeric,integer,logical,character, complex 或 raw 模式的向量。默認情況下,這些以 和內存里面完全一樣字節流的寫入文件的。

4 一些特定的文件格式

DBF文件:使用read.dbf()和write.dbf()函數進行讀寫

XLS文件:最好轉換成csv再導入,如果一定要直接使用XLS,可以用RODBC操作,參考后面的數據庫部分;

FIX文件:使用read.fwf()、 read.fortran()導入;

DIF文件:使用read.DIF()導入;也可以使用read.DIF('clipboard')讀取剪貼板中的數據;

XML文件:包XML 提供了對xml文件的支持。

HDF5文件:使用包hdf5處理

netCDF文件:使用包RNetCDF處理

foreign包提供了一些函數,可以導入EpiInfo, Minitab, S-PLUS, SAS, SPSS, Stata, Systat、Octave等軟件的數據文件;可以導出Stata和SPSS的數據文件。

5 使用關系數據庫

R中提供了不同抽象層次上的連接數據庫的包,比如底層的DBI ,上層的RMySQL、 ROracle、 RSQlite、RODBC等。

此外還有一個把R嵌入PostgreSQL 的項目:http://www.joeconway.com/plr/ 。

5.1 包 DBI 和 RMySQL

MySQL是很常用的開源數據庫。CRAN的包RMySQL提供了對MySQL數據庫的訪問支持:

  • 使用dbDriver("MySQL")獲取數據庫連接管理對象。類似的,其他的包中也提供了dbDriver("Oracle") , dbDriver("SQLite")的方式。
  • 調用dbConnect打開一個數據庫連接
  • 使用dbSendQuery()或 dbGetQuery()發送查詢。其中dbGetQuery 傳送查詢語句, 把結果以數據框形式返回。dbSendQuery 傳送查詢,返回的結果是 繼承"DBIResult"的一個子類的對象。"DBIResult" 類 可用於取得結果,而且還可以通過調用 dbClearResult 清除結果。
  • 使用fetch()函數 獲得查詢結果的部分或全部行,並以列表返回。 函數 dbHasCompleted 確定是否所有行已經獲得了, 而 dbGetRowCount 返回結果中行的數目。
  • 函數dbReadTable 和 dbWriteTable 可以在R數據框和數據庫表之間傳遞數據,數據框的行名字映射到 MySQL 表的 rownames 字段。
  • dbDisconnect()用於關閉數據庫連接

下面是例子:

> library(RMySQL) # will load DBI as well
## 打開一個MySQL數據庫的連接
> con <- dbConnect(dbDriver("MySQL"), dbname = "test")
## 列出數據庫中表
> dbListTables(con)
## 把一個數據框導入到數據庫,刪除任何已經存在的拷貝
> data(USArrests)
> dbWriteTable(con, "arrests", USArrests, overwrite = TRUE)
TRUE
> dbListTables(con)
[1] "arrests"
## 獲得整個表
> dbReadTable(con, "arrests")
               Murder Assault UrbanPop Rape
Alabama          13.2     236       58 21.2
Alaska           10.0     263       48 44.5
Arizona           8.1     294       80 31.0
Arkansas          8.8     190       50 19.5
...
## 從導入的表中查詢
> dbGetQuery(con, paste("select row_names, Murder from arrests",
                        "where Rape > 30 order by Murder"))
   row_names Murder
1   Colorado    7.9
2    Arizona    8.1
3 California    9.0
4     Alaska   10.0
5 New Mexico   11.4
6   Michigan   12.1
7     Nevada   12.2
8    Florida   15.4
> dbRemoveTable(con, "arrests")
> dbDisconnect(con)

5.2 RODBC

CRAN 里面的包 RODBC 提供了 ODBC的訪問接口:

  • odbcConnect 或 odbcDriverConnect (在Windows圖形化界面下,可以通過對話框選擇數據庫) 可以打開一個連接,返回一個用於隨后數據庫訪問的控制(handle)。 打印一個連接會給出ODBC連接的一些細節,而調用 odbcGetInfo 會給出客戶端和服務器的一些細節信息。
  • 在一個連接中的表的細節信息可以通過函數 sqlTables 獲得。
  • 函數 sqlSave 會把 R 數據框復制到一個數據庫的表中, 而函數 sqlFetch 會把一個數據庫中的表拷貝到 一個 R 的數據框中。
  • 通過sqlQuery進行查詢,返回的結果是 R 的數據框。(sqlCopy把一個 查詢傳給數據庫,返回結果在數據庫中以表的方式保存。) 一種比較好的控制方式是首先調用 odbcQuery, 然后 用 sqlGetResults 取得結果。后者可用於一個循環中 每次獲得有限行,就如函數 sqlFetchMore 的功能。
  • 連接可以通過調用函數 close 或 odbcClose 來關閉。 沒有 R 對象對應或不在 R 會話后面的連接也可以調用這兩個函數來關閉, 但會有警告信息。

代碼示例:

> library(RODBC)
## 讓函數把名字映射成小寫
> channel <- odbcConnect("testdb", uid="ripley", case="tolower")
## 把一個數據框導入數據庫
> data(USArrests)
> sqlSave(channel, USArrests, rownames = "state", addPK = TRUE)
> rm(USArrests)
## 列出數據庫的表
> sqlTables(channel)
  TABLE_QUALIFIER TABLE_OWNER TABLE_NAME TABLE_TYPE REMARKS
1                              usarrests      TABLE
## 列出表格
> sqlFetch(channel, "USArrests", rownames = "state")
               murder assault urbanpop rape
Alabama          13.2     236       58 21.2
Alaska           10.0     263       48 44.5
    ...
## SQL查詢,原先是在一行的
> sqlQuery(channel, "select state, murder from USArrests
           where rape > 30 order by murder")
       state murder
1 Colorado      7.9
2 Arizona       8.1
3 California    9.0
4 Alaska       10.0
5 New Mexico   11.4
6 Michigan     12.1
7 Nevada       12.2
8 Florida      15.4
## 刪除表
> sqlDrop(channel, "USArrests")
## 關閉連接
> odbcClose(channel)

作為 Windows下面用 ODBC 連接 Excel電子表格的一個簡單例子, 我們可以如下讀取電子表格:

> library(RODBC)
> channel <- odbcConnectExcel("bdr.xls")
## 列出電子表格
> sqlTables(channel)
  TABLE_CAT TABLE_SCHEM        TABLE_NAME   TABLE_TYPE REMARKS
1 C:\\bdr            NA           Sheet1$ SYSTEM TABLE      NA
2 C:\\bdr            NA           Sheet2$ SYSTEM TABLE      NA
3 C:\\bdr            NA           Sheet3$ SYSTEM TABLE      NA
4 C:\\bdr            NA Sheet1$Print_Area        TABLE      NA
## 獲得表單1的內容,可以用下面任何一種方式
> sh1 <- sqlFetch(channel, "Sheet1")
> sh1 <- sqlQuery(channel, "select * from [Sheet1$]")

注意,數據庫表的規范和 sqlTables 返回的名字是不一樣的: sqlFetch 可以映射這種差異。

6 網絡接口及外部工具

R對於在網絡連接的底層水平上交換數據,提供的支持非常有限。但是也提供了一些支持:

函數 make.socket, read.socket,write.socket 和 close.socket 提供了socket支持;

函數 download.file 通過FTP或HTTP讀取來自網絡資源的文件,然后寫入到一個文件中;

函數 read.table 和 scan 都可以直接從一個URL讀取內容,它們要么顯式地用 url 打開一個連接,要么暗含地給 file 參數設定一個URL,不需要保存文件到本地;

CORBA包 提供了CORBA協議的支持;

此外對於DCOM協議也有第三方的支持。

按照UNIX哲學,我們不建議在R中直接使用這些接口,而是交給外部工具來做。這里舉一個外部工具的例子:

> files <- system("ls x*", intern=T) #一定要指定 intern

7 處理大數據

前面介紹了R使用外部數據的一些方法,通常這已經夠用了。但是從外部獲取的數據會被R放到內存中,在處理大數據時,就會遇到問題。在處理大數據時,可以采用一下的方法:

  1. 使用數據庫 每次從數據庫中讀取一部分數據進行處理。
  2. 使用連接 盡管文本文件不使用連接也可以操作,但是連接提供了“流”的方式:可以分批進行讀寫。
  3. 使用UNIX工具 可以使用Unix中 grep、awk 和 wc 等實用工具對文本文件進行預處理,然后在讀入到R中,比如:
> howmany <- as.numeric(system ("grep -c ',C,' file.dat"))
> totalrows <- as.numeric(strsplit(system("wc -l Week.txt", intern=T), split=" ")[[1]][1])
  1. 使用虛擬內存 如果大量數據不能拆分,必須一起處理,還可以使用“虛擬內存”。

    包filehash可以將變量存儲在磁盤上而不是內存中。

    還可以使用數據庫:將文件讀入數據庫,然后再把數據庫裝載為環境來代替將文件讀入內存的作法。用with()函數可以指定環境。

> dumpDF(read.table("large.txt", header=T), dbName="mydb") > myenv<-db2env(db="mydb")
> with(mydb, z<-y+x ) # 指定mydb為操作環境

Date: 2013-05-16 10:37:42 CST

Author: Holbrook

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM