詳解pandas的read_csv方法


楔子

使用pandas做數據處理的第一步就是讀取數據,數據源可以來自於各種地方,csv文件便是其中之一。而讀取csv文件,pandas也提供了非常強力的支持,參數有四五十個。這些參數中,有的很容易被忽略,但是在實際工作中卻用處很大。比如:

  • 文件讀取時設置某些列為時間類型
  • 導入文件, 含有重復列
  • 過濾某些列
  • 每次迭代指定的行數
  • 值替換

pandas在讀取csv文件是通過read_csv這個函數讀取的,下面就來看看這個函數都支持哪些不同的參數,看看它們都生得一副什么模樣,是三頭六臂,還是烈焰紅唇。

read_csv中的參數

下面都是read_csv中的參數,但是根據功能我們划分為不同的類別。

以下代碼都在jupyter notebook上運行,Python版本為3.8.2。

基本參數

filepath_or_buffer

數據輸入的路徑:可以是文件路徑、可以是URL,也可以是實現read方法的任意對象。這個參數,就是我們輸入的第一個參數。

import pandas as pd
pd.read_csv("girl.csv")

還可以是一個URL,如果訪問該URL會返回一個文件的話,那么pandas的read_csv函數會自動將該文件進行讀取。比如:我們用fastapi寫一個服務,將剛才的文件返回。

pd.read_csv("http://localhost/girl.csv")

里面還可以是一個 _io.TextIOWrapper,比如:

f = open("girl.csv", encoding="utf-8")
pd.read_csv(f)

甚至還可以是一個臨時文件:

import tempfile
import pandas as pd 

tmp_file = tempfile.TemporaryFile("r+")
tmp_file.write(open("girl.csv", encoding="utf-8").read())
tmp_file.seek(0)

pd.read_csv(tmp_file)

支持的格式非常齊全,但是一般情況下,我們還是讀取實際的csv文件比較多。

sep

讀取csv文件時指定的分隔符,默認為逗號。注意:"csv文件的分隔符" 和 "我們讀取csv文件時指定的分隔符" 一定要一致。

比如:上面的girl.csv,我們將其分隔符從逗號改成"\t",如果這個時候還是用默認的逗號分隔符,那么數據讀取之后便混為一體。

pd.read_csv("girl.csv")

由於指定的分隔符 和 csv文件采用的分隔符 不一致,因此多個列之間沒有分開,而是連在一起了。

所以,我們需要將分隔符設置成"\t"才可以。

pd.read_csv('girl.csv', sep='\t')

delimiter

分隔符的另一個名字,與 sep 功能相似。

delim_whitespace

0.18 版本后新加參數,默認為 False,設置為 True 時,表示分割符為空白字符,可以是空格、"\t"等等。比如:girl.csv的分隔符是"\t",如果設置delim_whitespace為True的話:

pd.read_csv('girl.csv',delim_whitespace=True)

不管分隔符是什么,只要是空白字符,那么可以通過delim_whitespace=True進行讀取。

header

設置導入 DataFrame 的列名稱,默認為 "infer",注意它與下面介紹的 names 參數的微妙關系。

names

  • 當names沒被賦值時,header會變成0,即選取數據文件的第一行作為列名。
  • 當 names 被賦值,header 沒被賦值時,那么header會變成None。如果都賦值,就會實現兩個參數的組合功能。

我們舉例說明:

  • 1) names 沒有被賦值,header 也沒賦值:
pd.read_csv('girl.csv',delim_whitespace=True)
# 我們說這種情況下,header為變成0,即選取文件的第一行作為表頭

  • 2) names 沒有被賦值,header 被賦值:
pd.read_csv('girl.csv',delim_whitespace=True, header=1)
# 不指定names,指定header為1,則選取第二行當做表頭,第二行下面的是數據

  • 3) names 被賦值,header 沒有被賦值:
pd.read_csv('girl.csv', delim_whitespace=True, names=["編號", "姓名", "地址", "日期"])

我們看到names適用於沒有表頭的情況,指定names沒有指定header,那么header相當於None。一般來說,讀取文件會有一個表頭的,一般是第一行,但是有的文件只是數據而沒有表頭,那么這個時候我們就可以通過names手動指定、或者生成表頭,而文件里面的數據則全部是內容。所以這里id、name、address、date也當成是一條記錄了,本來它是表頭的,但是我們指定了names,所以它就變成數據了,表頭是我們在names里面指定的

4) names和header都被賦值:

pd.read_csv('girl.csv', delim_whitespace=True, names=["編號", "姓名", "地址", "日期"], header=0)

這個相當於先不看names,只看header,我們說header等於0代表什么呢?顯然是把第一行當做表頭,下面的當成數據,好了,然后再把表頭用names給替換掉。

再舉個栗子:

pd.read_csv('girl.csv', delim_whitespace=True, names=["編號", "姓名", "地址", "日期"], header=3)
# header=3,表示第四行當做表頭,第四行下面當成數據
# 然后再把表頭用names給替換掉,得到如下結果

所以names和header的使用場景主要如下:

  • 1. csv文件有表頭並且是第一行,那么names和header都無需指定;
  • 2. csv文件有表頭、但表頭不是第一行,可能從下面幾行開始才是真正的表頭和數據,這個時候指定header即可;
  • 3. csv文件沒有表頭,全部是純數據,那么我們可以通過names手動生成表頭;
  • 4. csv文件有表頭、但是這個表頭你不想用,這個時候同時指定names和header。先用header選出表頭和數據,然后再用names將表頭替換掉,其實就等價於將數據讀取進來之后再對列名進行rename;

index_col

我們在讀取文件之后,生成的 DataFrame 的索引默認是0 1 2 3...,我們當然可以 set_index,但是也可以在讀取的時候就指定某個列為索引。

pd.read_csv('girl.csv', delim_whitespace=True, index_col="name")

這里指定 "name" 作為索引,另外除了指定單個列,還可以指定多個列,比如 ["id", "name"]。並且我們除了可以輸入列的名字之外,還可以輸入對應的索引。比如:"id"、"name"、"address"、"date" 對應的索引就分別是0、1、2、3。

usecols

如果列有很多,而我們不想要全部的列、而是只要指定的列就可以使用這個參數。

pd.read_csv('girl.csv', delim_whitespace=True, usecols=["name", "address"])

同 index_col 一樣,除了指定列名,也可以通過索引來選擇想要的列,比如:usecols=[1, 2] 也會選擇 "name" 和 "address" 兩列,因為 "name" 這一列對應的索引是 1、"address" 對應的索引是 2。

此外 use_cols 還有一個比較好玩的用法,就是接收一個函數,會依次將列名作為參數傳遞到函數中進行調用,如果返回值為真,則選擇該列,不為真,則不選擇。

# 選擇列名的長度大於 4 的列,顯然此時只會選擇 address 這一列
pd.read_csv('girl.csv', delim_whitespace=True, usecols=lambda x: len(x) > 4)

mangle_dupe_cols

實際生產用的數據會很復雜,有時導入的數據會含有重名的列。參數 mangle_dupe_cols 默認為 True,重名的列導入后面多一個 .1。如果設置為 False,會拋出不支持的異常:

# ValueError: Setting mangle_dupe_cols=False is not supported yet

prefix

prefix 參數,當導入的數據沒有 header 時,設置此參數會自動加一個前綴。比如:

pd.read_csv('girl.csv', delim_whitespace=True, header=None)

我們看到在不指定names的時候,header默認為0,表示以第一行為表頭。但如果不指定names、還顯式地將header指定為None,那么會自動生成表頭0 1 2 3...,因為DataFrame肯定是要有列名(表頭)的。那么prefix參數干什么用的呢?

pd.read_csv('girl.csv', delim_whitespace=True, header=None, prefix="夏色祭")

所以prefix就是給這樣的列名增加前綴的,個人感覺好像不是很常用,至少本人在工作中從未用過這個參數。

squeeze

感覺又是一個沒啥卵用的參數,首先我們讀取csv文件得到的是一個DataFrame,如果這個文件只有一列、或者我們只獲取一列的話,那么得到的還是一個DataFrame。

pd.read_csv('girl.csv', delim_whitespace=True, usecols=["name"])
# 這里只選擇一列

如果指定了squeeze參數為True的話,在只有一列的情況下,那么得到就是一個Series。

pd.read_csv('girl.csv', delim_whitespace=True, usecols=["name"], squeeze=True)

squeeze默認是False,當然如果是多列,即使指定squeeze為True,得到的依舊是DataFrame。如果只有一列,那么本來默認還是DataFrame,但是可以通過指定這個參數為True,將其變成Series。話說你們覺得這個參數有用嗎?反正我個人覺得用處不大。

通用解析參數

dtype

筆者就曾遇到一件比較尷尬的事情,就是處理地鐵人員數據的。工作人員的id都是以0開頭的,比如0100012521,這是一個字符串。但是在讀取的時候解析成整型了,結果把開頭的0給丟了。這個時候我們就可以通過dtype來指定某個列的類型,就是告訴pandas:你在解析的時候不要自以為是,直接按照老子指定的類型進行解析就可以了,我不要你覺得,我要我覺得。

df = pd.read_csv('girl.csv', delim_whitespace=True)
df["id"] = df["id"] * 3
df

比如這里的id,默認解析的是整型,如果我們希望它是個字符串呢?

df = pd.read_csv('girl.csv', delim_whitespace=True, dtype={"id": str})
df["id"] = df["id"] * 3
df

我們看到id變成了字符串類型。

engine

pandas解析數據時用的引擎,pandas 目前的解析引擎提供兩種:c、python,默認為 c,因為 c 引擎解析速度更快,但是特性沒有 python 引擎全。如果使用 c 引擎沒有的特性時,會自動退化為 python 引擎。

比如使用分隔符進行解析,如果指定分隔符不是單個字符、或者"\s+",那么c引擎就無法解析了。我們知道如果分隔符為空白字符的話,那么可以指定delim_whitespace=True,但是也可以指定sep=r"\s+"。

pd.read_csv('girl.csv', sep=r"\s+")

如果sep是單個字符,或者字符串\s+,那么C是可以解決的。但如果我們指定的sep比較復雜,這時候引擎就會退化。

# 我們指定的\s{0}相當於沒指定,\s+\s{0}在結果上等同於\s+。
# 但是它不是單個字符,也不是\s+,因此此時的C引擎就無法解決了,而是會退化為python引擎
pd.read_csv('girl.csv', sep=r"\s+\s{0}", encoding="utf-8")

我們看到雖然自動退化,但是彈出了警告,這個時候需要手動的指定engine="python"來避免警告。這里面還用到了encoding參數,這個后面會說,因為引擎一旦退化,在Windows上不指定會讀出亂碼。這里我們看到sep是可以支持正則的,但是說實話sep這個參數都會設置成單個字符,原因是讀取的csv文件的分隔符是單個字符。

converters

可以在讀取的時候對列數據進行變換:

pd.read_csv('girl.csv', sep="\t", converters={"id": lambda x: int(x) + 10})

將id增加10,但是注意 int(x),在使用converters參數時,解析器默認所有列的類型為 str,所以需要顯式類型轉換。

true_values和false_value

指定哪些值應該被清洗為True,哪些值被清洗為False。

pd.read_csv('girl.csv', sep="\t")

這里增加一個字段result。

pd.read_csv('girl.csv', sep="\t", true_values=["對"], false_values=["錯"])

注意這里的替換規則,只有當某一列的數據全部出現在true_values + false_values里面,才會被替換。

pd.read_csv('girl.csv', sep="\t", false_values=["錯"])

我們看到"錯"並沒有被替換成False,原因就是只有一個字段中所有的值都在true_values + false_values中,它們才會被替換,而"對"並沒有出現。

pd.read_csv('girl.csv', sep="\t",  false_values=["錯", "對"])

此時就全部被替換成了False。

skiprows

skiprows 表示過濾行,想過濾掉哪些行,就寫在一個列表里面傳遞給skiprows即可。注意的是:這里是先過濾,然后再確定表頭,比如:

pd.read_csv('girl.csv', sep="\t", skiprows=[0])

我們把第一行過濾掉了,但是第一行是表頭,所以過濾掉之后,第二行就變成表頭了。如果過濾掉第二行,那么只相當於少了一行數據,但是表頭還是原來的第一行:id、name、address、date、result。

當然里面除了傳入具體的數值,來表明要過濾掉哪些行,還可以傳入一個函數。

pd.read_csv('girl.csv', sep="\t", skiprows=lambda x: x > 0 and x % 2 == 0)

由於索引從0開始,凡是索引大於0、並且%2等於0的記錄都過濾掉。索引大於0,是為了保證表頭不被過濾掉。

skipfooter

從文件末尾過濾行,解析引擎退化為 Python。這是因為 C 解析引擎沒有這個特性。

pd.read_csv('girl.csv', sep="\t", skipfooter=3, encoding="utf-8", engine="python")

skipfooter接收整型,表示從結尾往上過濾掉指定數量的行,因為引擎退化為python,那么要手動指定engine="python",不然會警告。另外需要指定encoding="utf-8",因為csv存在編碼問題,當引擎退化為python的時候,在Windows上讀取會亂碼。

nrows

nrows 參數設置一次性讀入的文件行數,它在讀入大文件時很有用,比如 16G 內存的PC無法容納幾百 G 的大文件。

pd.read_csv('girl.csv', sep="\t", nrows=1)

很多時候我們只是想看看大文件內部的字段長什么樣子,所以這里通過nrows指定讀取的行數。

low_memory

這個看起來是和內存有關的,但更准確的說,其實它是和數據類型相關的。在解釋這個原因之前,我們還要先從DataFrame的數據類型說起。

我們知道DataFrame的每一列都是有類型的,那么在讀取csv的時候,pandas也是要根據數據來判斷每一列的類型的。但pandas主要是靠"猜"的方法,因為在讀取csv的時候是分塊讀取的,每讀取一塊的時候,會根據數據來判斷每一列是什么類型;然后再讀取下一塊,會再對類型進行一個判斷,得到每一列的類型,如果得到的結果和上一個塊得到結果不一樣,那么就會發出警告,提示有以下的列存在多種數據類型:

DtypeWarning: Columns (1,5,8,......) have mixed types. Specify dtype option on import or set low_memory=False.

而為了保證正常讀取,那么會把類型像大的方向兼容,比如第一個塊的user_id被解析成整型,但是在解析第二個塊發現user_id有的值無法解析成整型,那么類型整體就會變成字符串,於是pandas提示該列存在混合類型。而一旦設置low_memory=False,那么pandas在讀取csv的時候就不分塊讀了,而是直接將文件全部讀取到內存里面,這樣只需要對整體進行一次判斷,就能得到每一列的類型。但是這種方式也有缺陷,一旦csv過大,就會內存溢出。

不過從數據庫讀取就不用擔心了,因為數據庫是規定了每一列的類型的。如果是從數據庫讀取得到的DataFrame,那么每一列的數據類型和數據庫表中的類型是一致的。還有,我們在上面介紹了dtype,這個是我們手動規定類型,那么pandas就會按照我們規定的類型去解析指定的列,但是一旦無法解析就會報錯。

memory_map

如果你知道python的一個模塊mmap,那么你肯定很好理解。如果使用的數據在內存里,那么直接進行映射即可,不會再次進行IO操作,默認為False。這個參數比較底層,我們一般用不到。

空值處理相關參數

na_values

na_values 參數可以配置哪些值需要處理成 NaN,這個是非常常用的,但是用的人不多。

pd.read_csv('girl.csv', sep="\t", na_values=["對", "古明地覺"])

我們看到將"對"和"古明地覺"設置成了NaN,當然我們這里不同的列,里面包含的值都是不相同的。但如果兩個列中包含相同的值,而我們只想將其中一個列的值換成NaN該怎么做呢?

pd.read_csv('girl.csv', sep="\t", na_values={"name": ["古明地覺", "博麗靈夢"], "result": ["對"]})

通過字典實現只對指定的列進行替換。

keep_default_na

我們知道,通過 na_values 參數可以讓 pandas 在讀取 CSV 的時候將一些指定的值替換成空值,但除了 na_values 指定的值之外,還有一些默認的值也會在讀取的時候被替換成空值,這些值有: "-1.#IND"、"1.#QNAN"、"1.#IND"、"-1.#QNAN"、"#N/A N/A"、"#N/A"、"N/A"、"NA"、"#NA"、"NULL"、"NaN"、"-NaN"、"nan"、"-nan"、"" 。盡管這些值在 CSV 中的表現形式是字符串,但是 pandas 在讀取的時候會替換成空值(真正意義上的 NaN)。不過有些時候我們不希望這么做,比如有一個具有業務含義的字符串恰好就叫 "NA",那么再將它替換成空值就不對了。

這個時候就可以將 keep_default_na 指定為 False,默認為 True,如果指定為 False,那么 pandas 在讀取時就不會擅自將那些默認的值轉成空值了,它們在 CSV 中長什么樣,pandas 讀取出來之后就還長什么樣,即使單元格中啥也沒有,那么得到的也是一個空字符串。但是注意,我們上面介紹的 na_values 參數則不受此影響,也就是說即便 keep_default_na 為 False,na_values 參數指定的值也依舊會被替換成空值。舉個栗子,假設某個 CSV 中存在 "NULL"、"NA"、以及空字符串,那么默認情況下,它們都會被替換成空值。但 "NA" 是具有業務含義的,我們希望保留原樣,而 "NULL" 和空字符串,我們還是希望 pandas 在讀取的時候能夠替換成空值,那么此時就可以在指定 keep_default_na 為 False 的同時,再指定 na_values 為 ["NULL", ""]

na_filter

是否進行空值檢測,默認為 True,如果指定為 False,那么 pandas 在讀取 CSV 的時候不會進行任何空值的判斷和檢測,所有的值都會保留原樣。因此,如果你能確保一個 CSV 肯定沒有空值,則不妨指定 na_filter 為 False,因為避免了空值檢測,可以提高大型文件的讀取速度。另外,該參數會屏蔽 keep_default_na 和 na_values,也就是說,當 na_filter 為 False 的時候,這兩個參數會失效。

從效果上來說,na_filter 為 False 等價於:不指定 na_values、以及將 keep_default_na 設為 False。

skip_blank_lines

skip_blank_lines 默認為 True,表示過濾掉空行,如為 False 則解析為 NaN。

a,b,c
1,2,3
2,3,4

3,4,5
4,5,6

其中 a、b、c 是表頭,下面是數據,但我們看到有空行。那么默認情況下,pandas 在讀取之后,除了表頭,會得到 4 行數據,也就是空行會被過濾掉;而如果將 skip_blank_lines 指定為 False,那么除了表頭,會得到 5 行數據,並且第 3 行全部是 NaN,也就是空行會被保留,但該行的所有值都為 NaN(如果指定了 keep_default_na 為 False,那么就是空字符串)。

但如果是使用 Office、WPS 等軟件手動編輯 CSV 文件的話,那么很少會出現像上面那樣的空行。我舉個栗子,我們手動錄入一個 CSV 文件:

此時讀取的時候,無論 skip_blank_lines 是否為 True,圖中索引為 4 的數據都不會被過濾掉,原因就在於雖然每個單元格都為空,但這樣一整行卻並不為空,我們可以用 notepad++ 打開看一下,CSV 就是一個純文本。

a,b,c
1,2,3
2,3,4
,,
3,4,5
4,5,6

我們看到即使每個單元格都是空(CSV 中的空,本質上就是個空字符串),但這一行卻並不為空,原因就是自動添加了分隔符。因此讀取之后該行永遠不會被過濾掉,而是將其所有值都變成 NaN,因為 pandas 讀取的時候默認會將空字符串解析成 NaN,當然,我們依舊可以指定 keep_default_na 為 False 來改變這一點。

verbose

打印一些額外信息

時間處理相關參數

parse_dates

指定某些列為時間類型,這個參數一般搭配下面的date_parser使用。

date_parser

是用來配合parse_dates參數的,因為有的列雖然是日期,但沒辦法直接轉化,需要我們指定一個解析格式:

from datetime import datetime
pd.read_csv('girl.csv', sep="\t", parse_dates=["date"], date_parser=lambda x: datetime.strptime(x, "%Y年%m月%d日"))

infer_datetime_format

infer_datetime_format 參數默認為 False。如果設定為 True 並且 parse_dates 可用,那么 pandas 將嘗試轉換為日期類型,如果可以轉換,轉換方法並解析,在某些情況下會快 5~10 倍。

分塊讀入相關參數

分塊讀入內存,尤其單機處理大文件時會很有用。

iterator

iterator 為 bool類型,默認為False。如果為True,那么返回一個 TextFileReader 對象,以便逐塊處理文件。這個在文件很大、內存無法容納所有數據文件時,可以分批讀入,依次處理。

chunk = pd.read_csv('girl.csv', sep="\t", iterator=True)
print(chunk)  # <pandas.io.parsers.TextFileReader object at 0x000002550189C0A0>

print(chunk.get_chunk(1))
"""
   id  name        address       date         result
0   1  古明地覺     地靈殿    1999年3月8日      對
"""

print(chunk.get_chunk(2))
"""
   id  name         address       date         result
1   2  博麗靈夢     博麗神社    1999年3月8日      錯
2   3  芙蘭朵露     紅魔館      1999年3月8日      錯
"""

# 文件還剩下三行,但是我們指定讀取100,那么也不會報錯,不夠指定的行數,那么有多少返回多少
print(chunk.get_chunk(100))
"""
   id    name          address    date         result
3   4  西行寺幽幽子    白玉樓   1999年3月8日      對
4   5   霧雨魔理沙    魔法森林  1999年3月8日      對
5   6    八意永琳     永遠亭    1999年3月8日      對
"""

try:
    # 但是在讀取完畢之后,再讀的話就會報錯了
    chunk.get_chunk(5)
except StopIteration as e:
    print("讀取完畢")
# 讀取完畢    

chunksize

chunksize 整型,默認為 None,設置文件塊的大小。

chunk = pd.read_csv('girl.csv', sep="\t", chunksize=2)
# 還是返回一個類似於迭代器的對象
print(chunk)  # <pandas.io.parsers.TextFileReader object at 0x0000025501143AF0>


# 調用get_chunk,如果不指定行數,那么就是默認的chunksize
print(chunk.get_chunk())
"""
   id    name      address          date        result
0   1  古明地覺     地靈殿       1999年3月8日      對
1   2  博麗靈夢    博麗神社      1999年3月8日      錯
"""

# 但也可以指定
print(chunk.get_chunk(100))
"""
   id      name       address       date         result
2   3    芙蘭朵露     紅魔館     1999年3月8日        錯 
3   4  西行寺幽幽子   白玉樓     1999年3月8日        對
4   5   霧雨魔理沙    魔法森林   1999年3月8日        對
5   6    八意永琳     永遠亭     1999年3月8日        對
"""

try:
    chunk.get_chunk(5)
except StopIteration as e:
    print("讀取完畢")
# 讀取完畢    

格式和壓縮相關參數

compression

compression 參數取值為 {'infer', 'gzip', 'bz2', 'zip', 'xz', None},默認 'infer',這個參數直接支持我們使用磁盤上的壓縮文件。

# 直接將上面的girl.csv添加到壓縮文件,打包成girl.zip
pd.read_csv('girl.zip', sep="\t", compression="zip")

thousands

千分位分割符,如 , 或者 .,默認為None。

encoding

encoding 指定字符集類型,通常指定為 'utf-8'。根據情況也可能是'ISO-8859-1'

error_bad_lines和warn_bad_lines

如果一行包含過多的列,假設csv的數據有5列,但是某一行卻有6個數據,顯然數據有問題。那么默認情況下不會返回DataFrame,而是會報錯。

# ParserError: Error tokenizing data. C error: Expected 5 fields in line 5, saw 6

我們在某一行中多加了一個數據,結果顯示錯誤。因為girl.csv里面有5列,但是有一行卻有6個數據,所以報錯。

在小樣本讀取時,這個錯誤很快就能發現。但是如果樣本比較大、並且由於數據集不可能那么干凈,會很容易出現這種情況,那么該怎么辦呢?而且這種情況下,Excel基本上是打不開這么大的文件的。這個時候我們就可以將error_bad_lines設置為False(默認為True),意思是遇到這種情況,直接把這一行給我扔掉。同時會設置 warn_bad_lines 設置為True,打印剔除的這行。

pd.read_csv('girl.csv', sep="\t", error_bad_lines=False, warn_bad_lines=True)

以上兩參數只能在C解析引擎下使用。

總結

以上便是pandas的read_csv函數中絕大部分參數了,而且其中的部分參數也適用於讀取其它類型的文件。其實在讀取csv文件時所使用的參數就那么幾個,很多參數平常都不會用,但至少要了解一下,因為在某些特定的場景下它們是可以很方便地幫我們解決一些問題的。

當然,read_csv函數中的參數還不止我們上面說的那些,有幾個我們還沒有介紹到,感興趣可以自己看一下。但是個人覺得,掌握上面的那些參數的用法的話,其實已經完全夠用了。


免責聲明!

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



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