Python之xml文檔及配置文件處理(ElementTree模塊、ConfigParser模塊)


本節內容


  1. 前言
  2. XML處理模塊
  3. ConfigParser/configparser模塊
  4. 總結

一、前言


我們在 <<Python之數據序列化>> 中我們描述了Python數據持久化的大體概念和基本處理方式,通過這些知識點我們已經能夠處理大部分Python數據序列化/反序列化的需求。本節我們來介紹下另外兩個模塊,它們都有各自特殊的用途,且提供了功能更加強大的api:

模塊名稱 描述
xml.etree.ElementTree(簡稱ET) 一個簡單、輕量級的XML處理器,用於創建、解析、處理XML數據
ConfigParser(Python 3.x中已改名為configparser) 配置文件解析器, 用於創建、解析、處理類似Windows系統上的INI配置文件

二、xml處理模塊:xml.etree.ElementTree


1. XML簡介

XML,全稱eXtensible Markup Language,即可擴展標記語言。它可以用來標記數據、定義數據類型,是一種允許用戶對自己的標記語言進行定義的源語言。XML的結構與HMTL(超文本標記語言)結構很相似,但是HTML是用來顯示數據的,而XML主要用來傳輸和存儲數據。

來看一個XML實例,它表示的是一個植物列表:

<?xml version="1.0" encoding="utf-8"?>
<CATALOG>
    <PLANT id='001'>
        <NAME>Sanguinaria canadensis</NAME>
        <LIGHT>Mostly Shady</LIGHT>
    </PLANT>
    <PLANT id='002'>
        <NAME>Aquilegia canadensis</NAME>
        <LIGHT>Mostly Shady</LIGHT>
    </PLANT>
    <PLANT id='003'>
        <NAME>Phlox divaricata</B=NAME>
        <LIGHT>Sun or Shade</LIGHT>
    </PLANT>
</CATALOG>

由上面的XML實例,可以看出XML數據有以下幾個特征:

  • XML是由多個標簽對組成,如: <CATALOG></CATALOG>,<PLANT></PLANT>
  • 標簽是可以有屬性的,如: <PLANT id='001'></PLANT>中的id就是PLANT標簽的一個屬性
  • 標簽對應的數據放在標簽對中間,如: <NAME>Sanguinaria canadensis</NAME>
  • 標簽可以嵌套,如下所示:
<CATALOG>
    <PLANT>
        <NAME></NAME>
    </PLANT>
</CATALOG>

2. XML tree 和 elements的概念和相關類介紹

XML是一種層級化的數據格式,因此它最自然的表示方式是一個(倒置的)樹形結構。如上面表示植物列表的XML對應的樹形結構應該是這樣的:

在面向對象的編程語言中,我們需要定義幾個類來表示這個樹形結構的組成部分。分析這個樹形結構我們發現其實每個節點(指的是上圖中的每個圓形,對應XML中的每個標簽對)都可以抽象為一個共同的類,比如叫Leaf(葉子)、Node(節點)、Element都可以。但是,除了表示節點的類,我們還需要定義一個類來表示這個樹本身。ET(xml.etree.ElementTree)中就設計了以下幾個類:

  • ElementTree: 表示整個XML層級結構
  • Element: 表示樹形結構中所有的父節點
  • SubElement: 表示樹形結構中所有的子節點

有些節點既是父節點,又是子節點

下面來看下這兩個類的定義及其提供的函數:

Element類

class xml.etree.ElementTree.Element(tag, attrib={}, **extra)

Element類對應的是樹形結構中的每個節點,對應的是XML文檔中的標簽對。我們上面提到過XML標簽有以下幾個特征,除了子標簽外都有相應的屬性與之對應:

標簽特征 對應的類屬性 數據類型
標簽名稱 tag 字符串
屬性 attrib 字典,所有屬性和屬性值的集合
標簽值 text 通常是字符串
與下一個標簽之間的值 tail 通常是字符串
子標簽 標簽的父子關系是在SubElement中指定的

關於Element.text和Element.tail屬性的說明

  • 它們可被用於保存於該Element相關的額外數據;
  • 它們的值通常是字符串,但是也可以是任何應用特定的對象類型;
  • 如果Element對象是由一個XML創建而來的,那么text屬性保存的是這個element的開始標簽與它的第一個子標簽或結束標簽之間的文本或是None;而tail保存的是這個element的結束標簽與下一個標簽之間的文本或None,可以看下面的例子。
<a><b>1<c>2<d/>3</c></b>4</a>
  • element a 的text和tail屬性值都是None;
  • element b 的text屬性值是1,tail屬性值是4;
  • element c 的text屬性值是2, tail屬性值是None;
  • element d 的text屬性值是None, tail的屬性值是3;
Element類中用於操作標簽屬性attrib的方法(類似於字典的方法):
# 以一個(name, value)的序列的形式返回該element所有屬性,且屬性在序列是隨機的
items()

# 返回該element所有屬性的名稱列表,屬性名稱順序隨機
keys()

# 獲取該element指定屬性key的值
get(key, default=None)

# 設置該element指定屬性的值
set(key, value)

# 重置當前element:刪除所有的 subelemnts、清空所有屬性、設置text和tail屬性值為None
clear()
Element類中用於操作子標簽(subelement)的方法:
# 向最后添加一個子標簽
append(subelement)

# 向最后追加多個子標簽,subelements是一個Element序列;這是Python 3.2中新增的方法
extends(subelements)

# 像該element的指定位置插入一個subelement
insert(index, subelement)

# 返回第一個與match匹配的subelement或None,match可以是一個tag名稱(標簽名稱),也可以是一個path
find(match, namespaces=None)

# 返回所有與match匹配的subelement列表或None,match可以是一個tag名稱(標簽名稱),也可以是一個path
findall(match, namespaces=None)

# 返回第一個與match匹配的subelement的text屬性值,如果匹配到的element沒有text屬性則返回一個空字符串,如果沒有匹配到subelement則返回default參數定義的值
findtext(match, default=None, namespaces=None)

# Python 3.2開始已將該方法廢棄,請使用list(elem)或迭代
getchildren()

# Python 3.2開始已將該方法廢棄,改用Element.iter()
getiterator(tag=None)

# 這是Python 3.2中新增的方法。以當前element作為根創建一個tree迭代器,該迭代器會議深度優先的方式迭代這個element及其下面的所有elements。如果tag不是None或'*',則只有tag值等於tag參數所指定值的element才會被這個迭代器返回;如果樹形結構在迭代過程中被修改,則結果為undefined。
iter(tag=None)

# 這是Python 3.2中新增的方法。查找與match參數指定的tag名稱或path相匹配的所有subelements,返回一個以文檔順序產生所有匹配element的可迭代對象。
iterfind(match, namespaces=None)

# 這是Python 3.2中新增的方法。創建一個迭代器,這個迭代器以文檔順序循環當前element和所有subelements並返回所有內部文本。
itertext()

# 從當前element中移除指定的subelement
remove(subelement)

SubElement類

SubElement(parent, tag, attrib={}, **extra)

parent參數表示父節點(標簽對),它應該是一個Element類的實例。SubElement的其他屬性和函數與Element相同。

ElementTree類

ElementTree表示的是整個element層級關系,並且該類還添加了一些對標准XML序列化和反序列化的額外支持。

class xml.etree.ElementTree.ElementTree(element=None, file=None)

element: 是一個Element實例,表示root element;

file: 是一個XML文件文成,如果該參數被給出,則會以該文件的內容初始化樹形層次結構;

下面是ElementTree提供的方法:

# 以指定的element實例替換當前tree的root element,相當於把整個XML的內容替換掉了
_setroot(element)

# 返回當前樹形層級結構的root element
getroot()

# 與Element.find()功能相同,只是它是從樹形結構的root element開始查找
find(match, namespaces=None)

# 與Element.findall()功能相同,只是它是從樹形結構的root element開始查找
findall(match, namespaces=None)

# 與Element.findtext()功能相同,只是它是從樹形結構的root element開始查找
findtext(match, default=None, namespaces=None)

# Python 3.2開始已將該方法廢棄,改用ElementTree.iter()
getiterator(tag=None)

# Python 3.2新增的方法。為當前root element創建並返回一個樹迭代器,該迭代器將會按順序循環這個樹形結構的中的所有與tag匹配的elements,默認返回所有elements
iter(tag=None)

# Python 3.2新增的方法。與Element.iterfind()功能相同,只是它是從樹形結構的root element開始查找
iterfind(match, namespaces=None)

# 加載一個外部XML片斷到當前element樹並返回該XML片斷的root element。source是一個文件名稱或文件對象。parser是一個可選的parser實例,如果沒有給出該參數,將會使用標准的XMLParser解析器。
parse(source, parser=None)

# 將當前element tree以XML形式寫入一個文件中。
# file 是一個文件名稱或一個以寫模式打開的文件對象
# encoding 用於指定輸出編碼
# xml_declaration 用於控制師傅將一個XML聲明也添加到文件中(False表示添加、True表示不添加、None表示只有編碼不是"US-ASCII"或"UTF-8"或"Unicode"時才添加)
# default_namespace 設置默認的XML命名空間(“xmlns”)
# method 可取值為"xml"、"html"和"text",默認為"xml"
# short_empty_elements 是唯一一個關鍵字參數,是Python 3.4新增加的參數。它用於控制那些不包含任何內容的elements的格式,如果該參數值為Ture則這些標簽將會被輸出為一個單獨的自關閉標簽(如: <a/>),如果值為False則這些標簽將會被輸出為一個標簽對(如:<a></a>)
write(file, encoding="us-ascii", xml_declaration=None, default_namespace=None, method="xml", *, short_empty_elements=True)

注意:

write()方法的輸出可以是一個字符串(str),可以可以是二進制(bytes)。這是受encoding參數控制的:

  • 如果encoding參數值為"unicode",則輸出是一個字符串;
  • 否則,輸出時二進制字節。
    如果file是一個(以可寫模式)打開的文件對象,這有可能會發生沖突。因此,我們需要確定不會嘗試將一個字符串寫入一個二進制流,反之亦然。

3. xml.etree.ElementTree模塊提供的函數

xml.etree.ElementTree模塊也直接提供了一些函數便於我們直接對XML進行操作,下面來介紹幾個常用的函數:

# 解析包含XML數據的字符串,返回一個Element實例
xml.etree.ElementTree.fromstring(text)

# 生成並返回指定的Element實例對應的包含XML數據的字符串(encoding="unicode")或字節流
# 參數講解請參考上面的ElementTree類的write()方法
xml.etree.ElementTree.toString(element, encoding="us-ascii", method="xml", *, short_empty_elements=True)

# 解析包含XML數據的文件名或文件對象,返回一個ElementTree實例
xml.etree.ElementTree.parse(source, parser=None)

# 將XML數據以遞增的方式解析到元素樹中,並向用戶報告發生了什么(類似SAX的回調機制),最終返回一個提供(event, elem)對的迭代器(iterator)。
# source 是一個包含XML數據的文件名稱或文件對象
# events 是一個包含要報告的事件序列,這里支持的事件包括:"start"、"end"、"start-ns"、"end-ns"(“ns”事件用於獲取詳細的命名空間信息)。如果event參數被省略,則僅報告"end"事件。
# parser是一個可選的解析器實例,如果為給出則使用標准的XMLParser解析器
xml.etree.ElementTree.iterparse(source, events=None, parser=None)

關於xml.etree.ElementTree.iterparse()方法的說明:

  • 雖然它以遞增的方式構建元素樹,但是它仍然會鎖定對source的讀取操作。因此,它不適用於不能接受讀阻塞的應用。
  • 它只保證在發出一個“start”事件時,它已經看到了起始標簽(tag)的">"結束字符,因此此時它定義了atrrib屬性,但是text和tail屬性的內容在那一時刻是沒有被定義的(這同樣適用於子元素)。如果你需要一個完全填充的元素,請查找“end”事件。

4. 實例

實例1:生成XML數據

生成上面表示植物列表XML數據:

import xml.etree.ElementTree as ET

# 創建root element
catalog = ET.Element("CATALOG")

# 直接通過SubElement類為root element添加一個子元素
plant01 = ET.SubElement(catalog, "PLANT", attrib={"id": "001"})
name01 = ET.SubElement(plant01, "NAME")
name01.text = "Sanguinaria canadensis"
light01 = ET.SubElement(plant01, "LIGHT")
light01.text = "Mostly Shady"

# 通過Element.append()方法為root element添加一個子元素
plant02 = ET.Element("PLANT", id="002")
name02 = ET.Element("NAME")
name02.text = "Aquilegia canadensis"
light02 = ET.Element("LIGHT")
light02.text = "Mostly Shady"
plant02.append(name02)
plant02.append(light02)
catalog.append(plant02)

# 通過SubElement類和Element.append()方法為root element添加一個子元素
plant03 = ET.Element("PLANT", id="003")
name03 = ET.SubElement(plant03, "NAME")
name03.text = "Phlox divaricata"
light03 = ET.SubElement(plant03, "LIGHT")
light03.text = "Sun or Shade"
catalog.append(plant03)

# 以指定的root element創建一個ElementTree實例
et = ET.ElementTree(element=catalog)
# 將創建的ElementTree對應的XML數據寫入(序列化)到本地文件
et.write("plants.xml", encoding="utf-8", xml_declaration=True)

此時會在當前目錄生成一個名為"plants.xml"的文件,內容和上面表示植物列表的XML數據一致。另外,通過上面的實例可知,使用SubElement創建和添加子元素是最方便的。

<?xml version='1.0' encoding='utf-8'?>
<CATALOG>
    <PLANT id="001">
        <NAME>Sanguinaria canadensis</NAME>
        <LIGHT>Mostly Shady</LIGHT>
    </PLANT>
    <PLANT id="002">
        <NAME>Aquilegia canadensis</NAME>
        <LIGHT>Mostly Shady</LIGHT>
    </PLANT>
    <PLANT id="003">
        <NAME>Phlox divaricata</NAME>
        <LIGHT>Sun or Shade</LIGHT>
    </PLANT>
</CATALOG>

實例2:解析XML數據

解析上面生成的XML數據

import xml.etree.ElementTree as ET

# 由以下兩種方式可以從一個包含XML數據的文件創建一個ElementTree實例
# et = ET.ElementTree(file="plants.xml")
et = ET.parse("plants.xml")

# 獲取root element
elem01 = et.getroot()
print(elem01.tag)  # CATALOG

# 獲取第一個標簽為"PLANT"的“直接” subelement
elem02 = et.find("PLANT")
print(elem02.tag)
print(elem02.attrib)
print(elem02.items())
print(elem02.keys())
print(elem02.get("id"))

# 遍歷指定element的所有subelement
for e in elem02:
    ET.dump(e)

# 獲取所有標簽為"PLANT"的“直接” subelement
for e in et.findall("PLANT"):
    print(ET.tostring(e, encoding="unicode"))
    print(e.items())

# 遍歷XML中所有的element
for e in et.iter():
    print(ET.tostring(e))

輸出結果為:

CATALOG

PLANT
{'id': '001'}
[('id', '001')]
['id']
001

<NAME>Sanguinaria canadensis</NAME>
<LIGHT>Mostly Shady</LIGHT>

<PLANT id="001"><NAME>Sanguinaria canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>
[('id', '001')]
<PLANT id="002"><NAME>Aquilegia canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>
[('id', '002')]
<PLANT id="003"><NAME>Phlox divaricata</NAME><LIGHT>Sun or Shade</LIGHT></PLANT>
[('id', '003')]

<CATALOG><PLANT id="001"><NAME>Sanguinaria canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT><PLANT id="002"><NAME>Aquilegia canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT><PLANT id="003"><NAME>Phlox divaricata</NAME><LIGHT>Sun or Shade</LIGHT></PLANT></CATALOG>
<PLANT id="001"><NAME>Sanguinaria canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>
<NAME>Sanguinaria canadensis</NAME>
<LIGHT>Mostly Shady</LIGHT>
<PLANT id="002"><NAME>Aquilegia canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>
<NAME>Aquilegia canadensis</NAME>
<LIGHT>Mostly Shady</LIGHT>
<PLANT id="003"><NAME>Phlox divaricata</NAME><LIGHT>Sun or Shade</LIGHT></PLANT>
<NAME>Phlox divaricata</NAME>
<LIGHT>Sun or Shade</LIGHT>

實例3:修改與刪除XML數據

import xml.etree.ElementTree as ET

# 從plants.xml文件初始化一個ElementTree實例
et = ET.parse("plants.xml")

# 獲取第一個標簽為PLANT的element
elem = et.find("PLANT")
print(ET.tostring(elem, encoding="unicode"))

# 為這個element設置一個新的標簽屬性
elem.set("color", "red")
print(ET.tostring(elem, encoding="unicode"))

# 清空這個element的所有屬性、文本和 subelement
elem.clear()
print(ET.tostring(elem, encoding="unicode"))
print(ET.tostring(elem, encoding="unicode", short_empty_elements=False))


namelist = ET.Element("NameList")

name = ET.SubElement(namelist, "name", attrib={"name": "Tom"})
age = ET.SubElement(name, "age")
age.text = '22'
role = ET.SubElement(name, "role")
role.text = 'cat'

name = ET.SubElement(namelist, "name", attrib={"name": "Jerry"})
age = ET.SubElement(name, "age")
age.text = '20'
role = ET.SubElement(name, "role")
role.text = 'mouse'

# 替換整個XML屬性結構的內容為一個名字列表
et._setroot(namelist)
print(ET.tostring(namelist, encoding="unicode"))

# 將修改過的ElementTree實例以XML的形式序列化到新的文件中
et.write("name_list.xml", encoding="utf-8", xml_declaration=True)

輸出結果為:

<PLANT id="001"><NAME>Sanguinaria canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>

<PLANT color="red" id="001"><NAME>Sanguinaria canadensis</NAME><LIGHT>Mostly Shady</LIGHT></PLANT>

<PLANT />
<PLANT></PLANT>

<NameList><name name="Tom"><age>22</age><role>cat</role></name><name name="Jerry"><age>20</age><role>mouse</role></name></NameList>

同時,會在當前目錄下生成一個新的名為"name_list.xml"的文件:

<?xml version='1.0' encoding='utf-8'?>
<NameList>
    <name name="Tom">
        <age>22</age>
        <role>cat</role>
    </name>
    <name name="Jerry">
        <age>20</age>
        <role>mouse</role>
    </name>
</NameList>

5. 補充說明

需要說明的是,Python中用於處理和操作XML數據的模塊不僅僅是這里介紹的xml.etree.ElementTree模塊,下面這張圖是Python 3.5.2中xml包(package)下的所有模塊。

Python處理XML數據的4種方法

總體來講,這些模塊對應的是對XML進行操作的種中方法:

  • DOM: DOM解析器在進行任何處理之前,必須把XML文件生成的樹形結構數據一次性完全放到內存中,所以DOM解析器的內存使用量完全是由要處理的XML數據的大小決定的。
  • SAX: SAX是Simple API for XML的縮寫,它犧牲了便捷性來獲取內存占用量的降低。它是事件驅動的,並需要一次性讀入整個XML文檔,文檔的讀入過程就是SAX的解析過程。所謂事件驅動,是指一種基於回調機制的程序運行方法,通常我們需要提前寫好相應的處理函數等待被回調(比如,當讀取到一個開始標簽時、讀取到一個結束標簽時,這些都是一種事件)。
  • Expat: expat接口與SAX類似,也是基於事件回調機制,但是該接口並不是標准化的,只適用於expat庫。
  • ElementTree: ElementTree是一個輕量級的DOM實現,它提供了Pythonic的API,同時還有一個高效的C語言實現,即 xml.etree.cElementTree。但是從Python 3.3開始cElementTree模塊已經被廢棄了,從官方文檔上的說明來看,應該是與ElementTree合並了,解釋器會在盡可能多的情況下自動啟動更高效的處理方式。

以上幾種方式的對比

  • 與DOM相比,ET的速度更快,API使用也更直觀、便捷。
  • 與SAX相比,ET.iterparse()函數同樣提供了按需解析XML數據的功能,而可以不用一次性在內存中讀入整個文檔。ET的性能與SAX模塊大致相仿,但是它的API抽象層次更高,使用更加方便。

可見,ET與SAX和DOM相比更加有優勢,因此我們推薦使用ET處理XML數據。當然,如果在某種特定的情況下需要使用其它API也是可以的。

注意: 解析XML的這幾種API並不是Python獨創的,Python是通過借鑒其它語言或直接從其它語言引入進來的。例如expat就是一個用C語言開發的、用來解析XML文檔的開發庫;SAX最初是由DavidMegginson使用java語言開發的;而DOM可以以一種獨立於平台和語言的方式訪問和修改一個文檔的內容和結構,也就是說它可以應用於任何編程語言。

以上文字來自<<這里>>

三、ConfigParser/configparser模塊


1. 模塊初識

ConfigParser模塊在Python3中已經被重命名為configparser,從模塊名稱上就可以看出這是一個配置解析器模塊。那么這里喲兩個問題:

問題1:這個模塊用於處理什么格式的配置文件?

它用於處理(讀寫)與Windows上的INI文件結構相似的文件,其格式類似於這樣:

[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes

[bitbucket.org]
User = hg

[topsecret.server.com]
Port = 50022
# Enable X11 forward protocol or not?
ForwardX11 = no

可看出,則個文件由3個組成部分:

組成部分名稱 描述 實例
[section] 相當於一個分組 [DEFAULT]、 [bitbucket.ogrg]
options 每個分組下面包含0個多個的key = value形式的選項,也可以這樣寫key : value User = hg, Port = 50022
comment 注釋,說明性文字;默認以“#”號或“;”號開頭的行 # Enable X11 forward protocol or not?

說明: [DEFAULT]是一個特殊的section,它為其它section提供它所包含的option的默認值。也就是說其它section默認會繼承[DEFAULT]下所有option的值,但是各個section可以覆蓋這些默認值。換句話說,在獲取某個section下不存在但是在[DEFAULT]下存在的option的值時,獲取的就是[DEFAULT]下這個option的值。

搞運維的同學會說,這跟mysql和php的配置文件結構一樣。是的,這種格式的文件已經被廣泛使用。

問題2:Python開發的什么場景下會用到這個模塊?

當我們需要編寫一個允許用戶自定義某些功能參數的Python程序時就會用到這個模塊。這其實很好理解,想想mysql和php的配置文件的作用就明白了。php和mysql就相當於我們自己編寫的Python程序,它們允許我們這些用戶通過配置文件(my.cnf和php.ini)來自定義一些參數從而影響php和mysql的運行。顯然,php和mysql的實現程序中必然要有相應的代碼來解析這些配置文件中的參數值。而在編寫Python程序時,這個解析配置文件的工作就可以由ConfigParser/configparser模塊來完成。只是ConfigParser/configparser模塊不僅僅可以解析配置信息,還可以創建、修改和刪除配置信息。

2. 如果讓你來開發這個模塊,你會提供哪些功能?

這個配置文件很簡單,只有3個組成部分:section、options、comment,因此這個模塊提供的功能應該就是對這3個組成部分的增、刪、改、查操作。那么我們至少應該提供以下幾個函數:

功能 模擬函數名
添加一個section add_section(section_name)
在一個section下添加或修改一個option set_option(section_name, option_name, option_value)
刪除指定的section remove_section(section_name)
刪除一個section下的某個option remove_option(section_name, option_name)
返回所有section的列表 list_sections()
返回指定section下所有option名稱列表 list_option_keys(section_name)
返回包含指定setcion下所有option的字典 list_option_items(section_name)
獲取指定section下的指定option的值 get_option(section_name,option_name)

好的,思考結束,下面來看看大神們是怎么做的。

3. Python 2中的ConfigParser模塊

模塊概述

Python 2中的ConfigParser模塊提供了3個相關的類,這3各類是依次的繼承關系,下面每一層縮進都表示一層繼承關系:

class RawConfigParser
    class ConfigParser
        class SafeConfigParser

它們提供的功能是依次增強的:

類名 描述
RawConfigParser 提供配置文件讀寫相關功能、不支持格式字符串替換
ConfigParser 允許取當前section或DEFAULT下option的值進行格式字符串替換
SafeConfigParser 允許取其他section下option的值進行格式字符串替換

格式字符串替換 是指一個option的值中可以包含並動態引用其它option的值,如:

base_dir = /data/wwww
project_dir = %(base_dir)s/myproject

對於上面這個實例,以上3個類對於project_dir值中的base_dir變量的查找過程如下:

類名 格式字符串查找過程 project_dir的值
RawConfigParser 不進行格式字符串的查找 %(base_dir)s/myproject
ConfigParser 嘗試分別從 當前section -> [DEFAULT] 查找base_dir的值 /data/www/myproject
SafeConfigParser 嘗試分別從 當前section -> [DEFAULT] -> 其他section 查找base_dir的值 /data/www/myproject

各個類的構造方法:

class ConfigParser.RawConfigParser([defaults[, dict_type[, allow_no_value]]])

class ConfigParser.ConfigParser([defaults[, dict_type[, allow_no_value]]])

class ConfigParser.SafeConfigParser([defaults[, dict_type[, allow_no_value]]])

可見,這幾個類的構造方法中的參數都是可選參數:

  • defaults: 如果該參數被提供,它將被初始化為一個默認值的字典。
  • dict_type: 如果該參數被提供,她將被用於為section列表、一個section中的options以及默認選項創建一個該類型的字典對象。該參數於 Python 2.6被添加,Python 2.7時其默認值改為collections.OrderedDict。
  • allow_no_value: 表示是否允許option沒有值的情況發生(此時獲取該選項的值為None),默認值為False,表示不允許。該參數於Python 2.7被添加。

ConfigParser.RawConfigParser類

與 section 相關的方法:

# 添加一個section。
# 如果該section名稱已經存在,將會引發DuplicateSectionError;
# 如果name參數的值是`DEFAULT`或任何它的大小寫不敏感的變體(如:default、Default),將會引發ValueError
add_section(section)

# 刪除指定的section,如果該section存在則返回True, 否則返回False
remove_section(section)

# 返回一個所有可用section的列表,但是不包括`DEFAULT` 
sections()

# 判斷指定的section是否已經存在,返回True或False
has_section(section)

與 option 相關的方法:

# 添加或修改某個已存在的section下option的值,如果section不存在將會引發NoSectionError。
set(section, option, value)

# 獲取某個section下指定option的值, 返回值為str,如果需要其他數據類型需要自己進行轉換
get(section, option)

# 對get()方法的字符串結果強制轉換為整數並返回
getint(section, option)

# 對get()方法的字符串結果強制轉換為浮點數並返回
getfloat(section, option)

# 對get()方法的字符串結果轉換為布爾值並返回,但是這里並不是簡單的數據類型的轉換:
# "1", "yes", "true", "on" 將會返回True
# “0”, “no”, "false", "off" 將會返回False
getboolen(section, option)

# 刪除指定section下的某option
# 如果section不存在將會引發NoSectionError;如果存在將被刪除的option則返回Ture,否則返回False
remove_option(section, option)

# 返回一個包含默認值的字典--即`[DEFAULT]`下所有option
defaults()

# 返回一個指定section下所有可用options的key的列表
options(section)

# 返回一個由指定section中包含的每個option的(key, value)對組成的列表
items(section)

# 判斷某個指定的section下是否包含某個option
# 如果指定的section存在,並且包含這個option,則返回True,否則返回False
has_option(section, option)

# 將指定的option名字轉換為內部使用的格式,默認實現是轉換為小寫。可以通過子類和設置實例屬性這兩種方法來改變其默認行為,如`cfgparser.optionxform = str`則保持原樣。
optionxform(option)

配置文件讀寫相關方法:

# 讀取並解析文件對象中的配置數據到當前實例中,如果filename被忽略且fp有一個name屬性,則filename將會取這個屬性值
readfp(fp[, filename])

# 將當前實例中的配置數據寫入指定的文件對象中
write(fileobject)

# 嘗試讀取並解析一個文件列表,然后返回一個被成功解析的文件列表。filenames可以是一個字符串,也可以是一個文件名列表:
# 如果filenames是一個字符串或Unicode字符串,它將會被當做一個單數的文件;
# 如果filenames列表中的某個文件不能被打開,該文件將會被忽略;
# 該方法的設計目的在於讓用戶可以指定一個可能的配置文件位置列表,所有存在的配置文件都會被讀取,如果任何一個配置文件都不存在,則ConfigParser實例將會包含一個空的數據集。
# 官方文檔給出的建議是:如果一個應用需要從一個配置文件加載初始參數值,應該線使用readp()方法加載必要文件中的數據,然后再調用read()方法加載可選配置文件中的數據。
read(filenames)

總結: readp()用於讀取必要配置文件,read()用於讀取可選配置文件。

ConfigParser.ConfigParser類

首先,ConfigParser類是RawConfigParser的子類,所以它繼承了RawConfigParser的以上方法;然后,ConfigParser類相對於RawConfigParser類的增強功能在於,它支持option值的格式化替換。為了向后兼容,允許用戶關閉對option值的格式化替換功能,它重寫了以下兩個方法:

# 獲取指定section下某個otpion的值
get(section, option[, raw[, vars]])

# 返回一個由指定section下的每個option的(key, value)對組成的列表
items(section[, raw[, vars]])

可以看出,這兩個方法的參數列表與RawConfigParser類相相應方法相比多了兩個參數:

raw: 這是一個布爾值,主要是為了向后兼容。該參數默認值為False,所有的option值中包含的'%'格式字符串都會被替換為相應的值;如果該參數值為True,則不對這些格式字符串做處理,直接返回。
vars: 這是一個字典,可以用來臨時定義某個被格式字符串引用的option的值,此時被格式字符串應用的option的查找過程是: vars(如果被提供) -> 當前section -> DEFAULT

ConfigParser.SafeConfigParser類

SafeConfigParser類只是在ConfigParser類的基礎上對set()方法做了一些限制:

添加或修改某個已存在的section下option的值,如果section不存在將會引發NoSectionError。
set(section, option, value)

但是它要求option的value必須是一個字符串(str或unicode),否則將會引發TypeError。

實例

實例1:創建一個配置文件
import ConfigParser as CP

# 這里用RawConfigParser、ConfigParser和SafeConfigParser都是一樣的效果
config = CP.RawConfigParser({'BAZ': 'hard', 'Bar': 'Life'})

config.add_section('Section1')
config.set('Section1', 'an_int', '15')
config.set('Section1', 'a_float', '3.1415')
config.set('Section1', 'a_bool', 'true')

config.add_section('Section2')
config.set('Section2', 'baz', 'fun')
config.set('Section2', 'bar', 'Python')
config.set('Section2', 'foo', '%(bar)s is %(baz)s!')

with open('example.cfg', 'wb') as configfile:
    config.write(configfile)

生成一個名為 example.cfg的文件,內容如下:

[DEFAULT]
bar = Life
baz = hard

[Section1]
an_int = 15
a_float = 3.1415
a_bool = true

[Section2]
baz = fun
bar = Python
foo = %(bar)s is %(baz)s!

說明:

  • option的key被寫入文件時默認會被轉換成小寫形式。
  • [DEFAULT]這個section是不能、也不需要通過config.add_section('DEFAULT')的方式添加的,默認就存在。
實例2:讀取一個已存在的配置文件
>>> config = CP.RawConfigParser()
>>> config.read('example.cfg')
['example.cfg']
>>>
>>> config.sections()  # 結果是不包含'DEFAULT'的
['Section1', 'Section2']
>>>
>>> config.has_section('Section1')
True
>>> config.has_section('Section11')
False
>>> config.has_section('DEFAULT')  # 注意這里
False
>>> config.has_section('Default')
False
>>>
>>> config.defaults()
OrderedDict([('bar', 'Life'), ('baz', 'hard')])
>>>
>>> config.options('Section1')  # 包括[DEFAULT]下的option
['an_int', 'a_float', 'a_bool', 'bar', 'baz']
>>>
>>> config.items('Section1')
[('bar', 'Life'), ('baz', 'hard'), ('an_int', '15'), ('a_float', '3.1415'), ('a_bool', 'true')]
>>>
>>> config.has_option('Section1', 'an_int')
True
>>>
>>> config.has_option('Section1', 'two_int')
False
>>>
>>> config.get('Section1', 'an_int')
'15'
>>> config.getint('Section1', 'an_int')
15
>>> config.getfloat('Section1', 'a_float')
3.1415
>>>
>>> config.get('Section1', 'a_bool')
'true'
>>>
>>> config.getboolean('Section1', 'a_bool')
True
>>>
>>> config.get('Section2', 'Foo')  # 重點在這里
'%(bar)s is %(baz)s!'

要想支持格式化字符串替換,就只能使用ConfigParser或SafeConfigParser類:

>>> import ConfigParser as CP
>>>
>>> config = CP.ConfigParser()
>>> config.read('example.cfg')
['example.cfg']
>>>
>>> config.get('Section2', 'foo')  # %(bar)s和%(baz)s取[Section2]所包含的option的值
'Python is fun!'
>>>
>>> config.get('Section2', 'foo', raw=True)  # 不進行格式字符串替換
'%(bar)s is %(baz)s!'
>>>
>>> config.get('Section2', 'foo', vars={'baz': 'busy', 'bar': 'Job'})  # %(bar)s和%(baz)s取vars參數所包含的option的值
'Job is busy!'
>>>
>>> config.remove_option('Section2', 'baz')
True
>>> config.remove_option('Section2', 'bar')
True
>>> config.get('Section2', 'foo')  # %(bar)s和%(baz)s取[DEFAULT]所包含的option的值
'Life is hard!'

如果已存在的配置數據中包含沒有值的option,需要在創建解析器實例時指定allow_no_value=True

>>> import ConfigParser as CP
>>> import io
>>>
>>> sample_config = """
... [mysqld]
... user = mysql
... pid-file = /var/run/mysqld/mysqld.pid
... skip-external-locking
... old_passwords = 1
... skip-bdb
... skip-innodb
... """
>>> config = CP.RawConfigParser(allow_no_value=True)
>>> config.readfp(io.BytesIO(sample_config))
>>> config.get('mysqld', 'user')
'mysql'
>>>
>>> config.get('mysqld', 'skip-bdb')  # 返回None
>>>
>>> config.get('mysqld', 'does-not-exist')  # 獲取不存在的option會報錯
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python27\lib\ConfigParser.py", line 340, in get
    raise NoOptionError(option, section)
ConfigParser.NoOptionError: No option 'does-not-exist' in section: 'mysqld'
>>>
實例3:修改配置文件

貌似大神們沒有提供相應的方法來實現將一個section下的option移動到另外一個section下的功能,那么我們自己來實現它。

思路很簡單: 取出原section下要移動的option的值 --> 在目標section(如果不存在則添加該section)下添加該option --> 從原section中刪除移動的option

def move_option(config, section_from, section_to, option):
    try:
        config.set(section_to, option, config.get(section_from, option))
    except ConfigParser.NoSectionError:
        config.add_section(section_to)
        move_option(config, section_from, section_to, option)
    else:
        config.remove_option(section_from, option)

有人會問,如果原section不存在,或者要移動的option不存在怎么辦?個人覺得這些判斷在調用這個函數之前進行處理會更好,這個函數只處理移動操作:

>>> import ConfigParser
>>>
>>> config = ConfigParser.ConfigParser()
>>> config.read('example.cfg')
['example.cfg']

# 查看當前已存在的section以及各section下所包含的option
>>> config.sections()
['Section1', 'Section2']
>>> config.items('Section1')
[('bar', 'Life'), ('baz', 'hard'), ('an_int', '15'), ('a_float', '3.1415'), ('a_bool', 'true')]
>>> config.items('Section2')
[('bar', 'Python'), ('baz', 'fun'), ('foo', 'Python is fun!')]

# 將Section1下的an_int移動到已經存在的Section2下
>>> move_option(config, 'Section1', 'Section2', 'an_int')

# 將Section1下的a_float移動到不存在的Section3下
>>> move_option(config, 'Section1', 'Section3', 'a_float')

# 查看當前已存在的section以及各section下所包含的option
>>> config.sections()
['Section1', 'Section2', 'Section3']
>>> config.items('Section1')
[('bar', 'Life'), ('baz', 'hard'), ('a_bool', 'true')]
>>> config.items('Section2')
[('bar', 'Python'), ('baz', 'fun'), ('foo', 'Python is fun!'), ('an_int', '15')]
>>> config.items('Section3')
[('bar', 'Life'), ('baz', 'hard'), ('a_float', '3.1415')]


# 移除Section1
>>> config.remove_section('Section1')
True

# 將當前配置數據寫回配置文件
with open('example.cfg', 'wb') as configfile:
    config.write(configfile)

此時,配置文件的內容已被修改為:

[DEFAULT]
bar = Life
baz = hard

[Section2]
baz = fun
bar = Python
foo = %(bar)s is %(baz)s!
an_int = 15

[Section3]
a_float = 3.1415

4. Python 3中的configparser模塊

Python 3中不僅僅是將ConfigParser改名為configparse,還對該模塊做了一些改進:

改進1:允許我們以類似字典的方式來操作配置數據

生成一個配置文件:

import configparser

config = configparser.ConfigParser()
config['DEFAULT'] = {'BAR': 'Life',
                    'BAZ': 'hard'}

config['Section1'] = {}
config['Section1']['an_int'] = '15'
config['Section1']['a_float'] = '3.1415'
config['Section1']['a_bool'] = 'true'

config['Section2'] = {}
config['Section2']['baz'] = 'fun'
config['Section2']['bar'] = 'Python'
config['Section2']['fun'] = '%(bar)s is %(baz)s!'

with open('example.ini', 'w') as configfile:
    config.write(configfile)

讀取一個已存在的配置文件:

>>> import configparser
>>>
>>> config = configparser.ConfigParser()
>>> config.read('example.ini')
['example.ini']
>>>
>>> config['Section1']['an_int']
'15'
>>> config['Section2']['fun']
'Python is fun!'

改進2:所有的get*()方法都增加了一個fallback參數

Python 2的ConfigParser模塊中定義的解析器所提供的get*()方法在獲取一個不存在的option的值時會拋出ConfigParser.NoOptionError錯誤:

>>> import ConfigParser
>>> config = ConfigParser.ConfigParser()
>>> config.read('example.ini')
['example.ini']
>>>
>>> config.options('Section1')
['an_int', 'a_float', 'a_bool', 'bar', 'baz']
>>> config.get('Section1', 'not_exist_option')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python27\lib\ConfigParser.py", line 618, in get
    raise NoOptionError(option, section)
ConfigParser.NoOptionError: No option 'not_exist_option' in section: 'Section1'
>>>

因此,我們需要在獲取某個Section下的option時需要先判斷該option是否存在,或者需要處理相應的異常信息。而Python 3中的configparser模塊提供的所有的get*()方法都增加了一個fallback參數,這個fallback參數可以用於指定獲取一個不存在的option的時的默認值,這其實也是類似字典的操作。

需要注意的是: 這個fallback參數必須以關鍵詞參數的形式提供,如果不提供該參數還是會拋出異常(KeyError)。

>>> config = configparser.ConfigParser()
>>> config.read('example.ini')
['example.ini']
>>>
>>> config.options('Section1')
['an_int', 'a_float', 'a_bool', 'bar', 'baz']
>>> config.get('Section1', 'an_int')
'15'
>>> config.get('Section1', 'not_exist_option', fallback='None')
'None'
>>>

改進3:讀取配置信息的read*()方法

read()方法新加了一個encoding參數

之前讀取配置文件時,都使用open()函數的default encoding,Python 3.2新加了encoding參數允開發者修改打開文件的要使用的字符編碼:

read(filenames, encoding=None)
以read_file()方法替代readfp()方法

從Python 3.2開始以read_file()方法替代readp()方法

read_file(f, source=None)
新加了read_string()和read_dict()方法

Python 2中可以使用read()和readfp()方法很方便的從文件中讀取配置信息,如果要從一個字符串中讀取配置信息,需要這樣做:config.readfp(io.BytesIO(str_config_data)) ,上面有例子。且Python 2中沒有提供從一個Python字典讀取配置數據的方法。

Python 3中專門提供了read_string()和read_dict()方法來分別從字符串和Python字典中讀取配置數據:

read_string(string, source='<string>')

read_dict(dictionary, source='<dict>')

改進4:write()方法新加了一個space_around_delimiter參數

Python 3中的write()方法新加了一個space_around_delimiter參數,用於寫入文件的配置數據的option的key和option之間的分隔符前后是否保留空白字符。該參數默認值為True,則文件格式是這樣的:

[DEFAULT]
bar = Life
baz = hard

...

如果該參數值為False,則文件格式是這樣的:

[DEFAULT]
bar=Life
baz=hard

...

改進5:解析器類的構造函數增加了很多靈活的參數

class configparser.ConfigParser(defaults=None, dict_type=collections.OrderedDict, 
                                allow_no_value=False, delimiters=('=', ':'),
                                comment_prefixes=('#', ';'), inline_comment_prefixes=None,
                                strict=True, empty_lines_in_values=True,
                                default_section=configparser.DEFAULTSECT,
                                interpolation=BasicInterpolation(), converters={})

class configparser.RawConfigParser(defaults=None, dict_type=collections.OrderedDict,
                                allow_no_value=False, *, delimiters=('=', ':'), 
                                comment_prefixs=('#', ';'), inline_comment_prefixes=None, 
                                strict=True, empty_lines_in_values=True, 
                                default_section=configparser.DEFAULTSECT[, interpolation])

四、總結


本文對Python中的XML數據和應用配置文件的處理模塊進行了盡可能詳細的介紹並附帶了一些示例代碼。本文主要是對之前那篇《Python之數據序列化》的一個補充,但是在實際開發工作中也經常會用到這些內容,希望對大家有所幫助。

問題交流群:666948590


免責聲明!

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



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