首頁 > php教程 > PHP开发 > 主體

python pickle模組

高洛峰
發布: 2016-12-16 11:36:37
原創
1112 人瀏覽過

持久性就是指保持對象,甚至在多次執行相同程序之間也保持對象。透過本文,您將對 Python物件的各種持久性機制(從關聯式資料庫到 Python 的 pickle以及其它機制)有一個總體認識。另外,也會讓您更深一步地了解Python 的物件序列化能力。

什麼是持久性?

持久性的基本思想很簡單。假定有一個 Python 程序,它可能是一個管理日常待辦事項的程序,您希望在多次執行這個程序之間可以保存應用程式物件(待辦事項)。換句話說,您希望將物件儲存在磁碟上,以便於以後檢索。這就是持久性。要達到這個目的,有幾種方法,每種方法都有其優缺點。

例如,可以將物件資料儲存在某種格式的文字檔案中,譬如 CSV 檔案。或可用關聯式資料庫,譬如 Gadfly、MySQL、PostgreSQL 或 DB2。這些檔案格式和資料庫都非常優秀,對於所有這些儲存機制,Python 都有健全的介面。

這些儲存機制都有一個共同點:儲存的資料是獨立於對這些資料進行操作的物件和程式。這樣做的好處是,數據可以作為共享的資源,供其它應用程式使用。缺點是,用這種方式,可以允許其它程式存取物件的數據,這違背了物件導向的封裝性原則 — 即物件的資料只能透過這個物件本身的公共(public)介面來存取。

另外,對於某些應用程序,關係資料庫方法可能不是很理想。尤其是,關係資料庫不理解對象。相反,關聯式資料庫會強行使用自己的型別系統和關聯式資料模型(表),每張表包含一組元組(行),每行包含具有固定數目的靜態型別欄位(列)。如果應用程式的物件模型不能夠方便地轉換到關係模型,那麼在將物件對應到元組以及將元組映射回物件方面,會碰到一定難度。這種困難常被稱為阻礙性不匹配(impedence-mismatch)問題。

對象持久性

如果希望透明地存儲Python 對象,而不丟失其身份和類型等信息,則需要某種形式的對象序列化:它是一個將任意複雜的對象轉成對象的文本或二進制表示的過程。同樣,必須能夠將物件經過序列化後的形式恢復到原有的物件。在Python 中,這個序列化過程稱為pickle,可以將物件pickle 成字串、磁碟上的檔案或任何類似檔案的對象,也可以將這些字串、檔案或任何類似檔案的物件unpickle 成原來的對象。我們將在本文後面詳細討論 pickle。

假定您喜歡將任何事物都保存成對象,而且希望避免將對象轉換成某種基於非對象存儲的開銷;那麼pickle 文件可以提供這些好處,但有時可能需要比這種簡單的pickle 文件更健壯以及更具有可伸縮性的事物。例如,只用 pickle 不能解決命名和尋找 pickle 檔案這樣的問題,另外,它也不能支援並發地存取持久性物件。如果需要這些方面的功能,則要求助類似於 ZODB(針對 Python 的 Z 物件資料庫)這類資料庫。 ZODB 是一個健全的、多用戶的和物件導向的資料庫系統,它能夠儲存和管理任意複雜的 Python 對象,並支援事務操作和並發控制。 (請參閱 參考資料,以下載 ZODB。)令人足夠感興趣的是,甚至 ZODB 也依靠 Python 的本機序列化能力,而且要有效地使用 ZODB,必須充分了解 pickle。

另一種令人感興趣的解決持久性問題的方法是 Prevayler,它最初是用 Java 實現的(有關 Prevaylor 方面的developerWorks 文章,請參閱 參考資料)。最近,一群 Python 程式設計師將 Prevayler 移植到了 Python 上,另起名為 PyPerSyst,由 SourceForge 託管(有關至 PyPerSyst 專案的鏈接,請參閱 參考資料)。 Prevayler/PyPerSyst 概念也是建立在 Java 和 Python 語言的本機序列化能力之上。 PyPerSyst 將整個物件系統保存在記憶體中,並透過不時地將系統快照 pickle 到磁碟以及維護一個命令日誌(透過此日誌可以重新應用最新的快照)來提供災難復原。所以,儘管使用PyPerSyst 的應用程式受到可用記憶體的限制,但好處是本機物件系統可以完全裝入到記憶體中,因而速度極快,而且實作起來要像ZODB 這樣的資料庫簡單,ZODB 允許物件的數目比同時在能記憶體中所保持的物件要多。

既然我們已經簡要討論了儲存持久物件的各種方法,那麼現在該詳細探討 pickle 流程了。雖然我們主要感興趣的是探索以各種方式來保存Python 對象,而不必將其轉換成某種其它格式,但我們仍然還有一些需要關注的地方,譬如:如何有效地pickle 和unpickle 簡單對像以及複雜對象,包括定制類別的實例;如何維護對象的引用,包括循環引用和遞歸引用;以及如何處理類別定義發生的變化,從而使用先前經過pickle 的實例時不會發生問題。我們將在隨後關於 Python 的 pickle 能力探討中涉及所有這些問題。

一些經過 pickle 的 Python

pickle 模組及其同類模組 cPickle 向 Python 提供了 pickle 支援。後者是用 C 編碼的,它具有更好的性能,對於大多數應用程序,建議使用該模組。我們將繼續討論 pickle ,但本文的範例實際上是利用了 cPickle 。由於其中大多數範例要用Python shell 來顯示,所以先展示如何導入cPickle ,並可以作為pickle 來引用它:

>>> import cPickle as pickle

現在已經導入了該模組,接下來讓我們導入了該模組,接下來讓我們看一下pickle 介面。 pickle 模組提供了以下函數對: dumps(object) 傳回一個字串,它包含一個pickle 格式的物件;loads(string) 傳回包含在pickle 字串中的物件; dump(object, file) 將物件寫入檔案,這個檔案可以是實際的實體文件,但也可以是任何類似文件的對象,這個物件具有write() 方法,可以接受單一的字串參數; load(file) 傳回包含在pickle 檔案中的物件。

缺省情況下, dumps() 和 dump() 使用可列印的 ASCII 表示來建立 pickle。兩者都有一個 final 參數(可選),如果為 True ,則該參數指定用更快以及更小的二進位表示來建立 pickle。 loads() 和 load() 函數自動偵測 pickle 是二進位格式還是文字格式。

清單1 顯示了一個互動式會話,這裡使用了剛才所描述的dumps() 和loads() 函數:

清單1. dumps() 和loads() 的示範

>>> import cPickle as pickle  
>>> t1 = ('this is a string', 42, [1, 2, 3], None)
登入後複製

>>> t1  ('this is a string', 42, [1, 2, 3], None)  

>>> p1 = pickle.dumps(t1)  🠎🠎. /nI42/n(lp1/nI1/naI2/naI3/naNtp2/n."  

>>> print p1  

(S'this is a string'  

(S'this is a string'  

(S'this is a string' 

I1  

aI2  

aI3  

aNtp2  

.  

>>> t2 = pickle.loads(p1)  

>> 2, 3], None)  

>>> p2 = pickle.dumps(t1, True)  

>>> p2  

'(U/x10this is a stringK*]q/x01(Kx2/x01(Kx2013x > t3 = pickle.loads(p2)  

>>> t3  

('this is a string', 42, [1, 2, 3], string', 42, [1, 2, 3], string)不解釋了。的效率。 ) 和load() ,它們使用檔案和類似檔案的物件。能一個接著一個地將幾個物件轉儲到同一個檔案。 load() 範例

>>> a1 = 'apple'  
>>> b1 = {1: 'One', 2: 'Two', 3: 'Three'}  
>>> c1 = ['fee', 'fie', 'foe', 'fum']  
>>> f1 = file('temp.pkl', 'wb')  
>>> pickle.dump(a1, f1, True)  
>>> pickle.dump(b1, f1, True)  
>>> pickle.dump(c1, f1, True)  
>>> f1.close()  
>>> f2 = file('temp.pkl', 'rb')  
>>> a2 = pickle.load(f2)  
>>> a2  
'apple'  
>>> b2 = pickle.load(f2)  
>>> b2  
{1: 'One', 2: 'Two', 3: 'Three'}  
>>> c2 = pickle.load(f2)  
>>> c2  
['fee', 'fie', 'foe', 'fum']  
>>> f2.close()
登入後複製

Pickle 的威力

到目前為止,我們講述了關於pickle 方面的基本知識。在這一節,將討論一些進階問題,當您開始 pickle 複雜物件時,會遇到這些問題,其中包括自訂類別的實例。幸運的是,Python 可以輕鬆處理這種情況。

可移植性

从空间和时间上说,Pickle 是可移植的。换句话说,pickle 文件格式独立于机器的体系结构,这意味着,例如,可以在 Linux 下创建一个 pickle,然后将它发送到在 Windows 或 Mac OS 下运行的 Python 程序。并且,当升级到更新版本的 Python 时,不必担心可能要废弃已有的 pickle。Python 开发人员已经保证 pickle 格式将可以向后兼容 Python 各个版本。事实上,在 pickle 模块中提供了有关目前以及所支持的格式方面的详细信息.

清单 3. 检索所支持的格式

>>> pickle.format_version  
'1.3'  
>>> pickle.compatible_formats  
['1.0', '1.1', '1.2']
登入後複製

多个引用,同一对象

在 Python 中,变量是对象的引用。同时,也可以用多个变量引用同一个对象。经证明,Python 在用经过 pickle 的对象维护这种行为方面丝毫没有困难,如清单 4 所示:

清单 4. 对象引用的维护

>>> a = [1, 2, 3]  
>>> b = a  
>>> a  
[1, 2, 3]  
>>> b  
[1, 2, 3]  
>>> a.append(4)  
>>> a  
[1, 2, 3, 4]  
>>> b  
[1, 2, 3, 4]  
>>> c = pickle.dumps((a, b))  
>>> d, e = pickle.loads(c)  
>>> d  
[1, 2, 3, 4]  
>>> e  
[1, 2, 3, 4]  
>>> d.append(5)  
>>> d  
[1, 2, 3, 4, 5]  
>>> e  
[1, 2, 3, 4, 5]
登入後複製

循环引用和递归引用

可以将刚才演示过的对象引用支持扩展到 循环引用(两个对象各自包含对对方的引用)和 递归引用(一个对象包含对其自身的引用)。下面两个清单着重显示这种能力。我们先看一下递归引用:

>清单 5. 递归引用

>>> l = [1, 2, 3]  
>>> l.append(l)  
>>> l  
[1, 2, 3, [...]]  
>>> l[3]  
[1, 2, 3, [...]]  
>>> l[3][3]  
[1, 2, 3, [...]]  
>>> p = pickle.dumps(l)  
>>> l2 = pickle.loads(p)  
>>> l2  
[1, 2, 3, [...]]  
>>> l2[3]  
[1, 2, 3, [...]]  
>>> l2[3][3]  
[1, 2, 3, [...]]
登入後複製

现在,看一个循环引用的示例:

清单 6. 循环引用

>>> a = [1, 2]  
>>> b = [3, 4]  
>>> a.append(b)  
>>> a  
[1, 2, [3, 4]]  
>>> b.append(a)  
>>> a  
[1, 2, [3, 4, [...]]]  
>>> b  
[3, 4, [1, 2, [...]]]  
>>> a[2]  
[3, 4, [1, 2, [...]]]  
>>> b[2]  
[1, 2, [3, 4, [...]]]  
>>> a[2] is b  
1  
>>> b[2] is a  
1  
>>> f = file('temp.pkl', 'w')  
>>> pickle.dump((a, b), f)  
>>> f.close()  
>>> f = file('temp.pkl', 'r')  
>>> c, d = pickle.load(f)  
>>> f.close()  
>>> c  
[1, 2, [3, 4, [...]]]  
>>> d  
[3, 4, [1, 2, [...]]]  
>>> c[2]  
[3, 4, [1, 2, [...]]]  
>>> d[2]  
[1, 2, [3, 4, [...]]]  
>>> c[2] is d  
1  
>>> d[2] is c  
1
登入後複製

注意,如果分别 pickle 每个对象,而不是在一个元组中一起 pickle 所有对象,会得到略微不同(但很重要)的结果,如清单 7 所示:

清单 7. 分别 pickle vs. 在一个元组中一起 pickle

>>> f = file('temp.pkl', 'w')  
>>> pickle.dump(a, f)  
>>> pickle.dump(b, f)  
>>> f.close()  
>>> f = file('temp.pkl', 'r')  
>>> c = pickle.load(f)  
>>> d = pickle.load(f)  
>>> f.close()  
>>> c  
[1, 2, [3, 4, [...]]]  
>>> d  
[3, 4, [1, 2, [...]]]  
>>> c[2]  
[3, 4, [1, 2, [...]]]  
>>> d[2]  
[1, 2, [3, 4, [...]]]  
>>> c[2] is d  
0  
>>> d[2] is c  
0
登入後複製

相等,但并不总是相同

正如在上一个示例所暗示的,只有在这些对象引用内存中同一个对象时,它们才是相同的。在 pickle 情形中,每个对象被恢复到一个与原来对象相等的对象,但不是同一个对象。换句话说,每个 pickle 都是原来对象的一个副本:

清单 8. 作为原来对象副本的被恢复的对象

>>> j = [1, 2, 3]  
>>> k = j  
>>> k is j  
1  
>>> x = pickle.dumps(k)  
>>> y = pickle.loads(x)  
>>> y  
[1, 2, 3]  
>>> y == k  
1  
>>> y is k  
0  
>>> y is j  
0  
>>> k is j  
1
登入後複製

同时,我们看到 Python 能够维护对象之间的引用,这些对象是作为一个单元进行 pickle 的。然而,我们还看到分别调用 dump() 会使 Python 无法维护对在该单元外部进行 pickle 的对象的引用。相反,Python 复制了被引用对象,并将副本和被 pickle 的对象存储在一起。对于 pickle 和恢复单个对象层次结构的应用程序,这是没有问题的。但要意识到还有其它情形。

值得指出的是,有一个选项确实允许分别 pickle 对象,并维护相互之间的引用,只要这些对象都是 pickle 到同一文件即可。 pickle 和 cPickle 模块提供了一个 Pickler (与此相对应是 Unpickler ),它能够跟踪已经被 pickle 的对象。通过使用这个 Pickler ,将会通过引用而不是通过值来 pickle 共享和循环引用:

清单 9. 维护分别 pickle 的对象间的引用

>>> f = file('temp.pkl', 'w')  
>>> pickler = pickle.Pickler(f)  
>>> pickler.dump(a)  
<cPickle.Pickler object at 0x89b0bb8>  
>>> pickler.dump(b)  
<cPickle.Pickler object at 0x89b0bb8>  
>>> f.close()  
>>> f = file(&#39;temp.pkl&#39;, &#39;r&#39;)  
>>> unpickler = pickle.Unpickler(f)  
>>> c = unpickler.load()  
>>> d = unpickler.load()  
>>> c[2]  
[3, 4, [1, 2, [...]]]  
>>> d[2]  
[1, 2, [3, 4, [...]]]  
>>> c[2] is d  
1  
>>> d[2] is c  
1
登入後複製

不可 pickle 的对象

一些对象类型是不可 pickle 的。例如,Python 不能 pickle 文件对象(或者任何带有对文件对象引用的对象),因为 Python 在 unpickle 时不能保证它可以重建该文件的状态(另一个示例比较难懂,在这类文章中不值得提出来)。试图 pickle 文件对象会导致以下错误:

清单 10. 试图 pickle 文件对象的结果

>>> f = file(&#39;temp.pkl&#39;, &#39;w&#39;)  
>>> p = pickle.dumps(f)  
Traceback (most recent call last):  
  File "<input>", line 1, in ?  
  File "/usr/lib/python2.2/copy_reg.py", line 57, in _reduce  
    raise TypeError, "can&#39;t pickle %s objects" % base.__name__  
TypeError: can&#39;t pickle file objects
登入後複製

类实例

与 pickle 简单对象类型相比,pickle 类实例要多加留意。这主要由于 Python 会 pickle 实例数据(通常是 _dict_ 属性)和类的名称,而不会 pickle 类的代码。当 Python unpickle 类的实例时,它会试图使用在 pickle 该实例时的确切的类名称和模块名称(包括任何包的路径前缀)导入包含该类定义的模块。另外要注意,类定义必须出现在模块的最顶层,这意味着它们不能是嵌套的类(在其它类或函数中定义的类)。

当 unpickle 类的实例时,通常不会再调用它们的 _init_() 方法。相反,Python 创建一个通用类实例,并应用已进行过 pickle 的实例属性,同时设置该实例的 _class_ 属性,使其指向原来的类。

对 Python 2.2 中引入的新型类进行 unpickle 的机制与原来的略有不同。虽然处理的结果实际上与对旧型类处理的结果相同,但 Python 使用 copy_reg 模块的 _reconstructor() 函数来恢复新型类的实例。

如果希望对新型或旧型类的实例修改缺省的 pickle 行为,则可以定义特殊的类的方法 _getstate_() 和 _setstate_() ,在保存和恢复类实例的状态信息期间,Python 会调用这些方法。在以下几节中,我们会看到一些示例利用了这些特殊的方法。

现在,我们看一个简单的类实例。首先,创建一个 persist.py 的 Python 模块,它包含以下新型类的定义:

清单 11. 新型类的定义

class Foo(object):  
    def __init__(self, value):  
        self.value = value
登入後複製

现在可以 pickle Foo 实例,并看一下它的表示:

清单 12. pickle Foo 实例

>>> import cPickle as pickle  
>>> from Orbtech.examples.persist import Foo  
>>> foo = Foo(&#39;What is a Foo?&#39;)  
>>> p = pickle.dumps(foo)  
>>> print p  
ccopy_reg  
_reconstructor  
p1  
(cOrbtech.examples.persist  
Foo  
p2  
c__builtin__  
object  
p3  
NtRp4  
(dp5  
S&#39;value&#39;  
p6  
S&#39;What is a Foo?&#39;  
sb.
登入後複製

可以看到这个类的名称 Foo 和全限定的模块名称 Orbtech.examples.persist 都存储在 pickle 中。如果将这个实例 pickle 成一个文件,稍后再 unpickle 它或在另一台机器上 unpickle,则 Python 会试图导入 Orbtech.examples.persist 模块,如果不能导入,则会抛出异常。如果重命名该类和该模块或者将该模块移到另一个目录,则也会发生类似的错误。

这里有一个 Python 发出错误消息的示例,当我们重命名 Foo 类,然后试图装入先前进行过 pickle 的 Foo 实例时会发生该错误:

清单 13. 试图装入一个被重命名的 Foo 类的经过 pickle 的实例

>>> import cPickle as pickle  
>>> f = file(&#39;temp.pkl&#39;, &#39;r&#39;)  
>>> foo = pickle.load(f)  
Traceback (most recent call last):  
  File "<input>", line 1, in ?  
AttributeError: &#39;module&#39; object has no attribute &#39;Foo&#39;
登入後複製

在重命名 persist.py 模块之后,也会发生类似的错误:

清单 14. 试图装入一个被重命名的 persist.py 模块的经过 pickle 的实例

>>> import cPickle as pickle  
>>> f = file(&#39;temp.pkl&#39;, &#39;r&#39;)  
>>> foo = pickle.load(f)  
Traceback (most recent call last):  
  File "<input>", line 1, in ?  
ImportError: No module named persist
登入後複製

我们会在下面 模式改进这一节提供一些技术来管理这类更改,而不会破坏现有的 pickle。

特殊的状态方法

前面提到对一些对象类型(譬如,文件对象)不能进行 pickle。处理这种不能 pickle 的对象的实例属性时可以使用特殊的方法( _getstate_() 和 _setstate_() )来修改类实例的状态。这里有一个 Foo 类的示例,我们已经对它进行了修改以处理文件对象属性:

清单 15. 处理不能 pickle 的实例属性

class Foo(object):  
    def __init__(self, value, filename):  
        self.value = value  
        self.logfile = file(filename, &#39;w&#39;)  
    def __getstate__(self):  
        """Return state values to be pickled."""  
        f = self.logfile  
        return (self.value, f.name, f.tell())  
    def __setstate__(self, state):  
        """Restore state from the unpickled state values."""  
        self.value, name, position = state  
        f = file(name, &#39;w&#39;)  
        f.seek(position)  
        self.logfile = f
登入後複製

pickle Foo 的实例时,Python 将只 pickle 当它调用该实例的 _getstate_() 方法时返回给它的值。类似的,在 unpickle 时,Python 将提供经过 unpickle 的值作为参数传递给实例的 _setstate_() 方法。在 _setstate_() 方法内,可以根据经过 pickle 的名称和位置信息来重建文件对象,并将该文件对象分配给这个实例的 logfile 属性。

模式改进

随着时间的推移,您会发现自己必须要更改类的定义。如果已经对某个类实例进行了 pickle,而现在又需要更改这个类,则您可能要检索和更新那些实例,以便它们能在新的类定义下继续正常工作。而我们已经看到在对类或模块进行某些更改时,会出现一些错误。幸运的是,pickle 和 unpickle 过程提供了一些 hook,我们可以用它们来支持这种模式改进的需要。

在这一节,我们将探讨一些方法来预测常见问题以及如何解决这些问题。由于不能 pickle 类实例代码,因此可以添加、更改和除去方法,而不会影响现有的经过 pickle 的实例。出于同样的原因,可以不必担心类的属性。您必须确保包含类定义的代码模块在 unpickle 环境中可用。同时还必须为这些可能导致 unpickle 问题的更改做好规划,这些更改包括:更改类名、添加或除去实例的属性以及改变类定义模块的名称或位置。

类名的更改

要更改类名,而不破坏先前经过 pickle 的实例,请遵循以下步骤。首先,确保原来的类的定义没有被更改,以便在 unpickle 现有实例时可以找到它。不要更改原来的名称,而是在与原来类定义所在的同一个模块中,创建该类定义的一个副本,同时给它一个新的类名。然后使用实际的新类名来替代 NewClassName ,将以下方法添加到原来类的定义中:

清单 16. 更改类名:添加到原来类定义的方法

def __setstate__(self, state):  
    self.__dict__.update(state)  
    self.__class__ = NewClassName
登入後複製

当 unpickle 现有实例时,Python 将查找原来类的定义,并调用实例的 _setstate_() 方法,同时将给新的类定义重新分配该实例的 _class_ 属性。一旦确定所有现有的实例都已经 unpickle、更新和重新 pickle 后,可以从源代码模块中除去旧的类定义。

属性的添加和删除

这些特殊的状态方法 _getstate_() 和 _setstate_() 再一次使我们能控制每个实例的状态,并使我们有机会处理实例属性中的更改。让我们看一个简单的类的定义,我们将向其添加和除去一些属性。这是是最初的定义:

清单 17. 最初的类定义

class Person(object):  
    def __init__(self, firstname, lastname):  
        self.firstname = firstname  
        self.lastname = lastname
登入後複製

假定已经创建并 pickle 了 Person 的实例,现在我们决定真的只想存储一个名称属性,而不是分别存储姓和名。这里有一种方式可以更改类的定义,它将先前经过 pickle 的实例迁移到新的定义:

清单 18. 新的类定义

class Person(object):  
    def __init__(self, fullname):  
        self.fullname = fullname  
    def __setstate__(self, state):  
        if &#39;fullname&#39; not in state:  
            first = &#39;&#39;  
            last = &#39;&#39;  
            if &#39;firstname&#39; in state:  
                first = state[&#39;firstname&#39;]  
                del state[&#39;firstname&#39;]  
            if &#39;lastname&#39; in state:  
                last = state[&#39;lastname&#39;]  
                del state[&#39;lastname&#39;]  
            self.fullname = " ".join([first, last]).strip()  
        self.__dict__.update(state)
登入後複製

在这个示例,我们添加了一个新的属性 fullname ,并除去了两个现有的属性 firstname 和 lastname 。当对先前进行过 pickle 的实例执行 unpickle 时,其先前进行过 pickle 的状态会作为字典传递给 _setstate_() ,它将包括 firstname 和 lastname 属性的值。接下来,将这两个值组合起来,并将它们分配给新属性 fullname 。在这个过程中,我们删除了状态字典中旧的属性。更新和重新 pickle 先前进行过 pickle 的所有实例之后,现在可以从类定义中除去 _setstate_() 方法。

模块的修改

在概念上,模块的名称或位置的改变类似于类名称的改变,但处理方式却完全不同。那是因为模块的信息存储在 pickle 中,而不是通过标准的 pickle 接口就可以修改的属性。事实上,改变模块信息的唯一办法是对实际的 pickle 文件本身执行查找和替换操作。至于如何确切地去做,这取决于具体的操作系统和可使用的工具。很显然,在这种情况下,您会想备份您的文件,以免发生错误。但这种改动应该非常简单,并且对二进制 pickle 格式进行更改与对文本 pickle 格式进行更改应该一样有效。


更多python pickle模块相关文章请关注PHP中文网!


相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門推薦
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板