用Python編寫短程序的一般格式

如果現在要寫一個將pdf文件的全部內容轉為一張png圖片的Python短程序(Python script),那麼最簡單的做法就是建立一個.py文件,然後在裏面直接將代碼按順序地寫出來。例如,我們現在使用PyMuPDF這個package去實現以上的功能,那先安裝PyMuPDF(以下命令行中的python和pip命令在不同操作系統可能有不同的寫法,本文略過不提):

$ pip install PyMuPDF

接著就可以編寫Python程序:

# 導入PyMuPDF包
import fitz

pdf_name = 'abc.pdf'
doc = fitz.open(pdf_name)

# ...
# 中間代碼暫時略過不寫

# 輸出成PNG文件
output_pix.writePNG('abc.png')

寫完後,就可以直接運行pdf_to_png.py程序,將abc.pdf轉為abc.png:

$ python pdf_to_png.py

這樣寫程序不是不行,但會有幾個問題。首先,如果你要在其他程序中將pdf文件轉為png圖片,難道要將這段代碼複製貼上到那個程序中嗎?如果有十個程序要用這個功能,難道又要將這段代碼複製貼上十次?就算可以這樣做,那以後要修改將pdf轉png這個程序,如發現有bug了,該怎麼辦?為了令代碼有重用性,可以將相關的程序改寫為一個函數:

import fitz

def convert_to_png(source, output, zoom=2.0):
    """
    這是docstring的部份
    """
    doc = fitz.open(source)
    # 略

這樣改寫後,將要轉換的pdf文件和輸出的png圖片路徑作為函數convert_to_png的參數sourceoutput。既然為了重用而寫成函數,那就不要將文件的路徑硬性地寫入函數中,而應該讓使用者自己設定。另外,函數中還有一個keyword參數zoom,用作調整輸出圖片的大小,預設值為2.0。

寫函數時,一般會在定義後,即函數的本體一開始加上名為「docstring」的文字,所以描述這個函數的用法和功能。加上docstring後,即可調用函數object的__doc__屬性或用help()函數顯示一個函數的docstring,作為一種查詢函數用法的做法:

>>> import pdf2png
>>> dir(pdf2png.convert_to_png)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', 
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> print(pdf2png.convert_to_png.__doc__)

    這是docstring的部份

>>> help(pdf2png.convert_to_png)
Help on function convert_to_png in module pdf2png:

convert_to_png(source, output, zoom=2.0)
    這是docstring的部份

上方先進入Python的interpreter,然後導入剛才寫的module pdf2png。由於導入的法方是import pdf2png,所以使用函數convert_to_png()時,要把module的名字也寫上,即pdf2png.convert_to_png(),這樣做就可以通過導入module的方法在其他程序中重用這個pdf轉png的函數。接下來我們用dir()函數顯示這個函數object的全部屬性和方法,見到__doc__屬性也在裏面,所以接下來就分別用__doc__help()顯示convert_to_png()的docstring。

要注意此時pdf2png這個module中只有一個函數convert_to_png(),並沒有任何代碼調用這個函數,那可不可以在函數convert_to_png()之後加入代碼去調用這個函數,以做到將abc.pdf轉為abc.png的效果呢?例如:

import fitz

def convert_to_png(source, output, zoom=2.0):
    """
    這是docstring的部份
    """
    doc = fitz.open(source)
    # ...

convert_to_png('abc.pdf', 'test_dir/abc.png')

這樣做時,每次在terminal中運行pdf2png.py,如:

$ python pdf2png.py

就會將abc.pdf轉為abc.png,並放入test_dir資料夾中(假設test_dir這個資料夾存在)。不過,這樣做又有問題出現:

  • 如何在不修改pdf2png.py代碼的情況下轉換其他的pdf文件?
  • 如果在其他程序中import pdf2png,又會有甚麼事情發生?

我們首先試試在其他程序中import pdf2png,看看會出現甚麼結果。為簡單,將pdf2png.py中的代碼改為只剩一行print函數輸出一些東西:

import fitz

def convert_to_png(source, output, zoom=2.0):
    # ...
    print(f'Converting {source} to {output}...')

convert_to_png('abc.pdf', 'test_dir/abc.png')

進入Python interpreter並導入pdf2png這個module:

$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdf2png
Converting abc.pdf to test_dir/abc.png...

從上面的結果可見,當執行了import pdf2png後,pdf2png.py中的最後一行convert_to_png('abc.pdf', 'test_dir/abc.png')亦被執行。這代表任何其他程序只要導入了這個module,都會執行一次convert_to_png('abc.pdf', 'test_dir/abc.png')這句代碼,將abc.pdf轉為abc.png並放在test_dir裏。這顯然不是我們想要的,我們只想導入pdf2png這個module後使用其中的convert_to_png()函數 ,並不希望執行裏面的其他內容。

有沒有辦法可以讓Python知道,如果將module導入時,就不執行某些代碼,而運行module時,就會執行其中的全部代碼,令到一個module既可以用於導入到其他程序,自己本身又可以運行?解決方法是使用Python中一個特別的變量:__name__。請看以下例子:

print(__name__)

這個程序將變量__name__顯示出來,那麼它的結果是甚麼呢?如果運行test_name.py時,有:

$ python test_name.py 
__main__

__name__等於__main__。當導入test_name時,又有:

$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test_name
test_name

__name__等於module的名字test_name。可見,__name__這個變量是會變化的,只要利用好這一點,就能夠讓Python只在module運行時才執行某些代碼。所以,pdf2png.py的寫法可改為:

import fitz

def convert_to_png(source, output, zoom=2.0):
    # ...
    print(f'Converting {source} to {output}...')

if __name__ == '__main__':
    convert_to_png('abc.pdf', 'test_dir/abc.png')

當導入這個module時,由於__name__等於module的名字pdf2png,所以上方第7行之後的代碼不會執行,但其中的函數卻可以使用:

$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdf2png
>>> pdf2png.convert_to_png('abc.pdf', 'test_dir/abc.png')
Converting abc.pdf to test_dir/abc.png...

解決導入的問題了,現在要解決另一個問題,即如何在不修改pdf2png.py代碼的情況下運行pdf2png.py來轉換其他的pdf文件。要做到這一點,就要給程序傳入至少兩個值:待轉換的pdf文件位置及輸出的png圖片位置。將pdf2png.py修改為:

import fitz

def convert_to_png(source, output, zoom=2.0):
    # ...
    print(f'Converting {source} to {output}...')

if __name__ == '__main__':
    import sys
    print(sys.argv)
    # convert_to_png('abc.pdf', 'test_dir/abc.png')

在terminal這樣運行pdf2png.py,並加入三個參數a、b和c:

$ python pdf2png.py a b c
['pdf2png.py', 'a', 'b', 'c']
Converting abc.pdf to test_dir/abc.png...

可見,sys module中的sys.argv是一個list,sys.argv[0]為Python程序的名字,後面是接下來的命令行參數。利用這一點,我們就可以為pdf2png.py傳入其他的pdf文件位置和設定png圖片的輸出位置。將pdf2png.py修改為:

import fitz

def convert_to_png(source, output, zoom=2.0):
    # ...
    print(f'Converting {source} to {output}...')

if __name__ == '__main__':
    import sys
    convert_to_png(sys.argv[1], sys.argv[2])

於是運行時加入適當的參數,就可以自定要轉換的文件位置啦:

$ python pdf2png.py xyz.pdf test_dir/xyz.png
Converting xyz.pdf to test_dir/xyz.png...

寫到這裏,我們總結一下Python短程序的一般格式:

  • 將程序的主要功能用函數包裝起來。
  • 為函數加入docstring。
  • if __name__ == '__main__'讓程序既可被導入,又可以直接運行。
  • 使用sys.argv取得命令行傳入的參數,讓程序在直接運行時使用。

知道Python短程序的一般格式後,剩下來就是將程序的功能加以實現:

import fitz


def convert_to_png(source, output, zoom=2.0):
    """
    This function converts a multipage pdf file to a single png image.

    Args:
        source: (str) path of the pdf file.
        output: (str) path of the output png file.
        zoom: (float) zoom factor, the default value is 2.0.
    Returns:
        None
    """
    doc = fitz.open(source)

    if not output.lower().endswith('.png'):
        print(f"{output} is not an image name.")
        return

    mat = fitz.Matrix(zoom, zoom)
    images = []
    for page in doc:
        pix = page.getPixmap(matrix=mat, alpha=False)
        images.append(pix)

    out_width = images[0].width
    out_height = images[0].height * doc.pageCount

    out_pix = fitz.Pixmap(images[0].colorspace,
                          (0, 0, out_width, out_height), images[0].alpha)

    for i in range(doc.pageCount):
        images[i].y = images[i].height * i
        out_pix.copyPixmap(images[i], images[i].irect)

    print(f"Converting {source} to {output}...")
    out_pix.writePNG(output)


if __name__ == '__main__':
    import sys
    if len(sys.argv) < 3:
        sys.exit(
            f"USAGE: python {sys.argv[0]} [pdf path] [output png path] [zoom level]")
    else:
        convert_to_png(sys.argv[1], sys.argv[2],
                       sys.argv[3] if len(sys.argv) > 3 else 2.0)

上述代碼主要應用PyMuPDF這個package,其中已經能夠檢測許多不同類型的錯誤,如輸入的檔案類型不是pdf、輸出位置的資料夾不存在等,所以我們在第47行就直接調用函數而沒有做其他錯誤檢測。這個程序還有許多未完善的地方,不過本文的主題不是關於如何將pdf文件轉為png圖片,只是借這個內容說明Python短程序的一般格式是甚麼,所以在此就不再寫得更詳細了。如對PyMuPDF有興趣,可看官方的文檔

他和Python有關的文章:Python中用os.walk()列出資料夾中的全部內容

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments