python自動更新pom文件

語言: CN / TW / HK

前言

  • 項目越來越多,版本管理越來越麻煩,在項目上我使用 maven version 來進行版本管理。主要還是在分佈式項目中模塊眾多的場景中使用,畢竟各個模塊對外的版本需要保持統一。

  • 關於這個插件如何使用呢?也是非常的簡單。只需要在maven視圖中進行設置版本號即可將分模塊項目的版本進行升級了。

  • 除了idea插件外,maven本身也提供了一個版本管理工具 versions-maven-plugin 。 具體用法以後有機會在贅述。

自定義實現版本更新

  • 作為一個專業懶人,我還是覺得idea的插件不夠智能,確切的説還不夠自動化。之前我已經動手實現了防 jenkins 自動打包上傳啟動服務的腳本的功能了,難道提交合並代碼這種簡單的事情還需要我自己處理嗎。不得不承認代碼衝突了的確還是需要認為干涉的,但是在平時開發中有多少概率會發生代碼衝突呢?我們都是分工合作基本上代碼衝突概率很低。
  • 關於代碼提交,自己分支如何合併到dev, 如何保證自己分支代碼最新等等這些場景我們只需要通過腳本來進行自動化操作就行了。針對這些功能場景我大概寫了兩個腳本 batfhmerge.shbatchgrade.sh 就搞定了。
  • 然而我想説的是關於項目的版本如何升級。上面也提到了我們分工合作就必然涉及到別人使用你的jar的場景了。你可以使用 SNAPSHOT 版本來保證別人拉取到你最新的功能代碼,但是有些公司會要求使用非 SNAPSHOT 版本進行管理也就是正式版本,這樣做的好處就是容易找到之前的版本代碼功能。

SHELL 實現

  • 之前用SHELL 實現了自動更新置頂項目的版本號為最新日期後綴。雖然使用起來沒發現有什麼BUG, 但是感覺代碼實現上還是很弱智的。
# 該腳本主要用來升級發包期間修改各服務版本
FILEPATH=$1
GROUPID=$2
ARTIFACTID=$3
FILENAME=$4
while getopts ":f:g:a:" opt
do
    case $opt in
        f)
        FILENAME=$OPTARG
        echo "您輸入的文件配置:$FILENAME"
        ;;
        g)
        GROUPID=$OPTARG
        echo "您輸入的groupid配置:$GROUPID"
        ;;
        a)
        ARTIFACTID=$OPTARG
        echo "您輸入的artifactid配置:$ARTIFACTID"
        ;;
        ff)
        FILENAME=$OPTARG
        echo "您輸入的帶修改文件為:$FILENAME"
        ;;
        ?)
        echo "未知參數"
        exit 1;;
    esac
done
echo "開始修改版本號"
NEWCONTENT=1.2.5.$(date +%Y%m%d)
LINE=`cat ${FILENAME} | grep -n -A 1 '<groupId>'"${GROUPID}"'<\/groupId>'| grep -n '<artifactId>'"${ARTIFACTID}"'<\/artifactId>' | awk -F "[:-]+" '{print $2}'`
echo 具體行號:$LINE
if [[ -z $LINE  ]]
then
    echo 未匹配
    exit
fi
VERSIONOLDCONTENT=`sed -n ''"$((LINE+1))"'p' ${FILENAME}| grep '[0-9a-zA-Z\.-]+' -Eo | sed -n '2p'`
echo ${VERSIONOLDCONTENT}
#gsed -i  ''"$((LINE+1))"'c\'"${NEWCONTENT}"'' pom.xml
sed -i "" ''"$((LINE+1))"'s/'"${VERSIONOLDCONTENT}"'/'"${NEWCONTENT}"'/' ${FILENAME}
  • 其實邏輯很簡單,主要就是尋找 groupIdartifactId ,最後確定好 version 對應的行號將最新的日期後綴版本進行填充進去。
  • 填充呢肯定需要三劍客中的 SED 進行操作,那就需要先獲取到以前的舊版本,然後進行替換操作。

為什麼使用SHELL

  • shell腳本作為後端程序猿必備技能選擇他進行實現也是為了温故下shell的知識。基本上腳本離不開三劍客,換句話説會了三劍客你就可以做好半個運維工作了。
  • 有了這個腳本我每次在功能開發完成之後,會通過SHELL腳本進行版本升級以及自己分支合併到dev分支,這樣方便別人獲取到最新的代碼。

python實現

  • SHELL腳本定製度很高,很難做到自動的兼容功能。比如上面我們在定位包的時候是通過grep進行定位的,正常情況下應該是沒什麼問題的,但是當pom.xml 出現被註釋的同名座標時或者説名稱存在其他相似度的情況下很難保證SHELl腳本還能夠正確的解析出來。
  • 除此之外還有一個重要的原因就是SHELL腳本很難在windows 運行,為了能夠兼顧到windows電腦我決定用python 進行重新實現該功能。

文件思考

  • 與SHELL不同的是 python處理需要考慮文件格式的問題。SHELL中不管什麼格式都是通過三劍客進行定位處理,這是他的優點也是他的缺點。
  • 首先我們得知道 pom.xml 文件他是一個 XML 格式的文件, XML=e X tensible M arkup L anguage 。即是一種可擴展的標記語言。它與 JSON 一樣主要用來存儲和傳輸數據。在之前的Springboot章節中我們也實現瞭如何實現接口傳遞 XML 數據結構。

常見的 XML 編程接口有 DOM 和 SAX,這兩種接口處理 XML 文件的方式不同,當然使用場合也不同。

Python 有三種方法解析 XML,SAX,DOM,以及 ElementTree:

### 1.SAX (simple API for XML )

Python 標準庫包含 SAX 解析器,SAX 用事件驅動模型,通過在解析XML的過程中觸發一個個的事件並調用用户定義的回調函數來處理XML文件。

### 2.DOM(Document Object Model)

將 XML 數據在內存中解析成一個樹,通過對樹的操作來操作XML。

### 3.ElementTree(元素樹)
  • 而我所採用的就是最後一種方式 ElementTree

xml.etree.ElementTree

官網直通車

  • ElementTree 在 python3中已經作為標準庫存在了,所以這裏不需要我們額外的安裝。
基於事件和基於文檔的APID來解析XML,可以使用XPath表達式搜索已解析的文件,具有對文檔的增刪改查的功能,該方式需要注意大xml文件,因為是一次性加載到內存,所以如果是大xml文件,不推薦使用該模塊解析,應該使用sax方式
  • 不能説最好只能説他是合適的工具,因為 pom.xml 文件不會很大的。 ElementTree 通過 XPath 進行節點選擇,所以關於xml 節點查找我們可以參考 xpath 語法即可。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.zxhTom</groupId>
    <artifactId>bottom</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>bottom</name>
    <url>http://maven.apache.org</url>
    <description>最底層的繁瑣封裝</description>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <log4j2.version>2.10.0</log4j2.version>
    </properties>

    <dependencies>
        <!-- 20180927提供了針對stirng bean list 等判斷的操作。不用我們在詳細的判斷了 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <!-- 提供了針對list 等判斷的操作。不用我們在詳細的判斷了 -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <!-- jsonobeject jar包依賴 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>
        <!-- 日誌記錄 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j2.version}</version>
        </dependency>
        <!-- 通過反射獲取標有註解的類 -->
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>0.9.10</version>
        </dependency>
    </dependencies>   
</project>
  • 上面的 pom.xml 摘自於 com.github.zxhTom 的 bottom 項目中。裏面的恰好出現了註釋,方便我們後期測試。

解析xml

import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)

print(tree)

  • 實際有效的內容就是 ET.parse 即可解析出來 xml 。

查看pom.xml所有節點標籤名稱

import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
# 根據tree進行遍歷
for node in tree.iter():
    print(node.tag)

讀取xml中dependency

  • 我想看下所有的 dependency 標籤,只有這樣我才能夠匹配是否是我需要的那個maven座標。
import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
# tree遍歷
for node in tree.findall('.//dependency'):
    print(node.tag)
  • 上述代碼很明顯是錯誤的,因為我們執行腳本後沒有任何的輸出,至於為什麼是這樣呢?你可以翻到上一節就可以看到我們在打印所有節點標籤名稱的時候前面好像都多了一串地址。
  • 這個地址就是 pom.xml 中的命名空間,在跟節點 project 標籤中設置的 xmlns 屬性。至於為什麼需要這個呢?每個xml 標籤內容都是自定義的,比如你可以將dependency用來做版本號的作用,只要你自己解析的時候注意就行了。而maven中將dependency作為引入座標的概念,每個人的想法不一,所以引入命名空間,在指定的命名空間中標籤的作用是唯一的,這就是 xmlns 存在的意義。
import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
# tree遍歷
for node in tree.findall('.//{http://maven.apache.org/POM/4.0.0}dependency'):
    print(node.tag)

讀取com.alibaba.fastjson 的版本號

  • 通過上面的內容我們知道在定位節點時候需要加入命名空間。

  • 首先我們知道 com.alibaba.fastjson 的版本號是 1.2.28

import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
# tree遍歷
for node in tree.findall('.//{http://maven.apache.org/POM/4.0.0}dependency'):
    groupIdNode=node.find('.{http://maven.apache.org/POM/4.0.0}groupId')
    artifactNode=node.find('.{http://maven.apache.org/POM/4.0.0}artifactId')
    if(artifactNode.text=='fastjson' and groupIdNode.text=='com.alibaba'):
        print(node.find('.{http://maven.apache.org/POM/4.0.0}version').text)
  • 通過 python3 upgrade.py 即可打印出 1.2.28

保存xml

  • 説了這麼多,還記得我們一開始的任務嗎,沒錯就是修改掉pom.xml 中指定jar的版本號。這裏就將 com.alibaba.fastjson 的版本號升級為 1.2.29 吧。
import xml.etree.ElementTree as ET
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
# tree遍歷
for node in tree.findall('.//{http://maven.apache.org/POM/4.0.0}dependency'):
    groupIdNode=node.find('.{http://maven.apache.org/POM/4.0.0}groupId')
    artifactNode=node.find('.{http://maven.apache.org/POM/4.0.0}artifactId')
    if(artifactNode.text=='fastjson' and groupIdNode.text=='com.alibaba'):
        node.find('.{http://maven.apache.org/POM/4.0.0}version').text='1.2.29'
tree.write('pom.xml')
  • 修改還是很容易的,我們只需要將node.text進行重新賦值就行了。但是經過對比我發現保存後的 pom.xml 好像並不是只改了version標籤的內容。
    • 標籤多了ns0前綴。
    • 中文亂碼
    • 註釋丟失

API追蹤

def write(self, file_or_filename,
              encoding=None,
              xml_declaration=None,
              default_namespace=None,
              method=None, *,
              short_empty_elements=True):
  • 上述提到了三個問題都是很常見的問題,因為我們打開 /Lib/xml/etree/ElementTree.py 源碼就能夠看到在寫會 xml 文件的時候我們一共有7個參數可選,其中第一個self 沒啥好説的。
屬性 作用
file_or_filename 文件
encoding 輸出的編碼格式;默認US-ASCII
xml_declaration 將XML聲明添加到文件中: True添加;False不添加;None在非US-ASCII 或 UTF-8 或 Unicode時添加; 默認None
default_namespace 默認的命名空間
method xml、 html 、 text。默認 xml
short_empty_elements 空內容的標籤時的處理

修改xml後節點多了前綴ns0

  • 很明顯是我們沒有指定輸出的默認命名空間導致程序自動生成一個前綴。
tree.write('pom.xml',default_namespace='http://maven.apache.org/POM/4.0.0')
  • 當我們指定了命名空間,這個時候再查看下文件節點的前綴問題就解決了。

中文亂碼

  • 中文亂碼就是我們沒有指定編碼格式。大家都知道默認的 US-ASCII 都是人家老外的東西,我們國內想正常使用肯定還是需要 UTF-8 的。
tree.write('pom.xml',default_namespace='http://maven.apache.org/POM/4.0.0',encoding='UTF-8')
  • 這裏我也就不截圖了,筆者親測是可以解決中文亂碼的問題的。

標準化xml

  • 這不算個問題,不知道你有沒有發現上面我提供的 pom.xml 嚴格意義上來説不是一個標準的 xml 文件,就好比你上學不帶紅領巾就不是一個標準學生一樣,上面的pom.xml 確實了xml的標準開頭申明。不過沒關係我們在wirte的時候注意到有一個參數 xml_declaration 就是控制是否生成標準申明的。

修改xml後原來的註釋丟了

  • 關於註釋丟了這種問題,怎麼説呢?無關緊要吧但是往往註釋是進行解釋説明的,為了追求完美我還是希望能夠將註釋保留下來。關於註釋的問題我們還得簡單查看下源碼説明。
with open('pom.xml', 'tr', encoding='utf-8') as rf:
    tree = ET.parse(rf)
  • parse 對應的就是 ElementTree 的源碼中 parse 中。

  • 到了這裏我們應該不難看出,在渲染的時候主要是通過 TreeBuilder 進行渲染的,其中 CommentHandler 就是 TreeBuilder.comment 進行的。那麼我們繼續查看下 TreeBuilder 的源碼來查看為什麼默認的 parser 沒有保留下注釋。

_comment_factory
class CommentedTreeBuilder (ET.TreeBuilder):
    def comment(self,data):
        self.start(ET.Comment,{})
        self.data(data)
        self.end(ET.Comment)
  • 上述代碼類似於 Java中繼承了 TreeBuilder 並且重寫了 comment方法。主要的邏輯就是開頭一個註釋標籤屬性為空,結尾是一個註釋標籤結尾,中間的內容是我們註釋的內容,所以綜上就是保留下來註釋。

  • 重寫了類之後我們還需要在ET中指定我們自定義的TreeBuilder 。

parser = ET.XMLParser(target=CommentedTreeBuilder())
tree = ET.parse(xml_path,parser=parser)
  • 構建好tree之後其他就是一樣的操作,最終倒出來的文件就是我們保留註釋的xml 了。

獲取不到子節點

  • 就的版本是 getchildren , 但是在新版本中直接廢棄了這個方法。而在新版本中是通過 iter(tag) 進行創建迭代器的。但tag=None或者 * 表示所有子孫節點,其他的情況就只查找指定tag 名稱的集合。

優雅解析帶命名空間的xml

  • 還記得我們是如何解析獲取到 pom.xml 中的標籤的。
# tree遍歷
for node in tree.findall('.//{http://maven.apache.org/POM/4.0.0}dependency'):
    print(node.tag)
  • 這種方式我只想説NO , 每次在通過xpath 定位節點都需要添加前綴,這簡直是個噩夢。
ns = {'real_mvn': 'http://maven.apache.org/POM/4.0.0',
      'vistual': 'http://characters.zxhtom.com'}
# tree遍歷
for node in tree.findall('.//real_mvn:dependency',ns):
    print(node.tag)

總結

  • 本文主要是通過腳本實現pom.xml 的版本升級功能。SHELL通過三劍客定位到指定座標進行版本更新,缺點是可能不兼容註釋同內容的座標,優點是修改內容範圍小,不會造成大影響
  • 而python實現的xml文件修改。他的優點是能夠精準定位不會產生錯誤定位問題且支持註釋內容保留,缺點是不適合操作大文件。
文件 dependency數量
knife4j-spring-ui-1.9.6.pom 0
spring-context-support-1.0.6.pom 3
spring-core-5.1.7.RELEASE.pom 10
springfox-swagger-common-3.0.0.pom 11

其他説明