Python 萌新 - 實(shí)現(xiàn) Markdown 圖片下載器

圖片來自網(wǎng)絡(luò)

簡書支持打包下載所有文章功能,可以方便作者轉(zhuǎn)移或保存槐秧。但是圖片不支持自動下載儒拂,最近在學(xué)Python,便寫了一個md圖片下載器色鸳。

目標(biāo)

本人 Python 新手社痛,歡迎大佬指點(diǎn)。本文主要是對源碼進(jìn)行解讀命雀,期望實(shí)現(xiàn)以下目標(biāo):

  1. 一鍵下載所有Markdown文件中的圖片蒜哀,并保存到本地。
  2. 圖片根據(jù)文章分類
  3. 簡單易用吏砂。

先上最終效果:

下載過程
下載完成

實(shí)現(xiàn)步驟

  1. 搜索指定文件夾撵儿,找出文件夾及子文件包含的md文件。
  2. 匹配出md文件中所有的圖片狐血。
  3. 所有圖片異步下載淀歇。
  4. 下載報告與GUI。
  5. Python 打包工具匈织。
準(zhǔn)備開工

1. 搜索文件夾中md文件

首先我們要根據(jù)用戶指定的文件夾浪默,搜索出該文件夾及其子文件夾中包含的md文件,并將這些文件的絕對路徑加到數(shù)組當(dāng)中缀匕。

class Directory(object):

    @classmethod
    def find_sub_path(cls, path):
        # 初始化一個空的文章列表
        article_list = []
        # 獲取該文件夾下的所以子文件
        temp_files = os.listdir(path)
        # 遍歷子文件
        for temp_file in temp_files:
            # 拼接該文件絕對路徑
            full_path = os.path.join(path, temp_file)

            # 匹配.md文件 
            if os.path.isfile(full_path) and os.path.splitext(full_path)[1] == ".md":
                # 如果是.md文件 加入文章列表
                article = Article(full_path)
                article_list.append(article)
            # 如果是文件夾 進(jìn)行遞歸繼續(xù)搜索
            elif os.path.isdir(full_path):
                # 將子文件夾中的文章列表拼接到上級目錄的文章列表中
                article_list.extend(cls.find_sub_path(full_path))

        return article_list

2. 匹配出md文件中的圖片

為每一篇文章新建一個存放該文章中圖片的文件夾纳决,然后利用正則匹配出該文章中的所以圖片,并保存到圖片數(shù)組中乡小。

# 文章類

class Article(object):

    def __init__(self, path):
        # 文章的絕對路徑
        self.article_path = path
        # 拼接文章圖片下載后保存的路徑
        self.article_pic_dir = path.replace(".md", "_Image")
        # 新建保存文章圖片的文件夾
        self.mkdir_image_dir()
        # 開始搜索圖片
        self.pic_list = self.find_pics(self.article_path)

    # 查找圖片
    def find_pics(self, article_path):
        # 打開md文件
        f = open(article_path, 'r')
        content = f.read()
        pics = []

        # 匹配正則 match ![]()
        results = re.findall(r"!\[(.+?)\)", content)

        index = 0
        for result in results:
            temp_pic = result.split("](")
            # 將圖片加入到圖片數(shù)組當(dāng)中
            if len(temp_pic) == 2:
                pic = Picture(temp_pic[0], temp_pic[1], self.article_pic_dir, index)
                pics.append(pic)
            index += 1
        f.close()
        return pics

    # 新建圖片的保存文件夾
    def mkdir_image_dir(self):
        # 如果該文件夾不存在 則新建一個
        if not os.path.exists(self.article_pic_dir):
            os.mkdir(self.article_pic_dir)

3. 下載圖片

簡單地判斷了圖片是否有類型阔加,拼接圖片的保存路徑與命名。檢查圖片是否重復(fù)下載满钟,檢查圖片鏈接是否合法胜榔,下載并且保存圖片,同時做了下載失敗的處理和保存失敗的處理湃番,保存了下載失敗的原因夭织,以便后面生成報告。

# 圖片類
class Picture(object):

    def __init__(self, name, url, dir_path, index):
        # 該圖片順序下標(biāo)  用于設(shè)置默認(rèn)圖片名字
        self.index = index
        # 圖片名
        self.name = name
        # 圖片鏈接
        self.url = url
        # 圖片保存路徑
        self.dir_path = dir_path
        # 圖片下載失敗原因
        self.error_reason = None

    # 開始下載
    def start_download_pic(self, download_pic_callback):
        pic_path = self.build_pic_name()

        # 已存在則不重復(fù)下載
        if os.path.exists(pic_path):
            print ('pic has existed:' + self.url)
            self.error_reason = "pic has existed:"
            download_pic_callback(self)
            return

        # 圖片鏈接前綴不包含http
        if not self.url.startswith("http"):
            print ('pic has invalid url:' + self.url)
            self.error_reason = "pic has invalid url"
            download_pic_callback(self)
            return

        header = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36',
            'Cookie': 'AspxAutoDetectCookieSupport=1',
        }

        # 下載圖片
        request = urllib2.Request(self.url, None, header)
        try:
            response = urllib2.urlopen(request, timeout=10)
        # 下載失敗
        except Exception, error:
            print ('pic cannot download:' + self.url)
            self.error_reason = str(error)
            download_pic_callback(self)
            return

        # 保存圖片
        try:
            fp = open(pic_path, 'wb')
            fp.write(response.read())
            fp.close()
        # 保存失敗
        except IOError, error:
            print(error)
            self.error_reason = str(error)
            download_pic_callback(self)
            return
        
        # 下載完成回調(diào)
        download_pic_callback(self)

    # 組裝圖片保存命名
    def build_pic_name(self):
        # 剪去圖片鏈接后的參數(shù)
        pic_url = self.url.split("?")[0]
        
        # 獲取圖片格式后綴 如果沒有 默認(rèn)jpg
        urls = pic_url.split(".")
        if len(urls) > 1:
            pic_type = urls[len(urls)-1]
        else:
            pic_type = "jpg"

        # 組裝圖片命名
        if self.name is not None and len(self.name) > 0:
            pic_name = self.name + "." + pic_type
        else:
            pic_name = "no_name_" + str(self.index) + "." + pic_type
        
        pic_path = os.path.join(self.dir_path, pic_name)
        return pic_path

4. 下載報告與GUI

a. 本來 XTImageDownloaderGUI 應(yīng)該分開牵辣,但是對 Tkinter 理解還有限摔癣,所以就直接寫,方便調(diào)用纬向。

這部分代碼主要是 選擇文件夾開始搜索并下載 的 UI 及邏輯處理择浊。

選擇文件夾
class XTImageDownloader(object):

    def __init__(self):
        # 數(shù)據(jù)
        self.download_error_list = []
        self.all_pic_count = 0
        self.current_pic_index = 0
        self.thread_lock = threading.Lock()
        self.search_button = None
        # 圖形界面相關(guān)
        self.root = Tk()
        self.root.title("XTImageDownloader")
        self.path = StringVar()
        self.title = StringVar()
        self.title.set("請選擇Markdown文件所在文件夾")
        self.list_box = None
        Label(self.root, textvariable=self.title).grid(row=0, column=1)
        Label(self.root, text="文件夾路徑:").grid(row=1, column=0)
        Entry(self.root, textvariable=self.path).grid(row=1, column=1)
        Button(self.root, text="選擇路徑", command=self.select_path).grid(row=1, column=2)
        self.root.mainloop()

    # 選擇文件夾
    def select_path(self):
        self.path.set(tkFileDialog.askdirectory())
        # 用戶選中文件夾之后 顯示下載按鈕
        if self.path.get() != "":
            self.search_button = Button(self.root, text="開始搜索并下載", command=self.start_search_dir)
            self.search_button.grid(row=2, column=1)
            return self.path

    # 開始搜索文件夾 并且下載
    def start_search_dir(self):
        self.search_button['state'] = DISABLED
        self.search_button['text'] = "正在下載..."
        
        self.all_pic_count = 0
        self.current_pic_index = 0
        self.download_error_list = []
        # 獲取Markdown文件列表
        article_list = Directory.find_sub_path(self.path.get())

        # 更新搜索進(jìn)度 刷新UI
        for article in article_list:
            self.all_pic_count += len(article.pic_list)
        self.change_title(self.all_pic_count, self.current_pic_index)

        # 開始下載圖片
        for article in article_list:
            for pic in article.pic_list:
                # 開啟異步線程下載圖片 并且傳入下載完成的回調(diào)
                thread = threading.Thread(target=pic.start_download_pic, args=(self.download_pic_callback,))
                thread.start()

b. 開啟異步線程下載圖片,回調(diào)函數(shù)作為入?yún)魅搿?/p>

下載圖片
  // 異步下載
  thread = threading.Thread(target=pic.start_download_pic,   args=(self.download_pic_callback,))
  thread.start()

回調(diào)函數(shù)更新 UI 告知下載進(jìn)度逾条,由于開啟了多線程琢岩,這里需要加鎖,防止資源競爭师脂。全部下載成功后生成報告担孔。

    # 下載圖片完成后的回調(diào)函數(shù)
    def download_pic_callback(self, pic):
        # 獲取線程鎖
        self.thread_lock.acquire()
        # 如果下載失敗 則保存到失敗列表
        if pic.error_reason is not None and len(pic.error_reason) > 0:
            self.download_error_list.append(pic)

        self.current_pic_index += 1

        # 更新下載進(jìn)度 刷新UI
        print('finish:' + str(self.current_pic_index) + '/' + str(self.all_pic_count))
        self.change_title(self.all_pic_count, self.current_pic_index)

        # 全部下載成功 刷新UI 生成失敗報告
        if self.all_pic_count == self.current_pic_index:
            self.search_button['text'] = "下載完成"
            self.print_error(self.download_error_list)

        # 釋放鎖
        self.thread_lock.release()

    # 更新下載進(jìn)度 刷新UI
    def change_title(self, total_num, current_num):
        self.title.set("已完成" + str(current_num) + "/" + str(total_num))

c. 生成失敗報告,利用 list_box 來展示錯誤列表吃警,并且添加了滑塊糕篇,可以滑動瀏覽。

    # 生成失敗列表
    def print_error(self, download_error_list):
        # python log
        print("-----------------------------------")
        print("some pic download failure:")
        for pic in download_error_list:
            print("")
            print("name:" + pic.name)
            print("url:" + pic.url)
            print("error_reason:" + pic.error_reason)

        Label(self.root, text="部分圖片下載失敗:").grid(row=4, column=1)

        # GUI
        # 新建listbox
        self.list_box = Listbox(self.root)
        for pic in download_error_list:
            self.list_box.insert(END, pic.url + " -> " + pic.error_reason)
        self.list_box.grid(row=5, column=0, columnspan=3, sticky=W+E+N+S)

        # 垂直 scrollbar
        scr1 = Scrollbar(self.root)
        self.list_box.configure(yscrollcommand=scr1.set)
        scr1['command'] = self.list_box.yview
        scr1.grid(row=5, column=4, sticky=W+E+N+S)
        # 水平 scrollbar
        scr2 = Scrollbar(self.root, orient='horizontal')
        self.list_box.configure(xscrollcommand=scr2.set)
        scr2['command'] = self.list_box.xview
        scr2.grid(row=6, column=0, columnspan=3, sticky=W+E+N+S)

d. 最后貼上引用到的標(biāo)準(zhǔn)庫(Python 2.7):

  1. #!/usr/bin/env python# -*- coding:utf-8 -*-這兩行是為了支持中文酌心。
  2. osos.path用于系統(tǒng)及文件查找拌消。
  3. re 正則匹配。
  4. urllib2網(wǎng)絡(luò)庫安券,用于下載圖片墩崩。
  5. TkintertkFileDialog是 GUI 庫。
  6. threading多線程庫侯勉。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import os.path
import re
import urllib2
from Tkinter import *
import tkFileDialog
import threading

5. Python 打包工具

最終效果

這部分主要是講如何把 Python 腳本打包打包成可以在 Mac 上運(yùn)行的應(yīng)用鹦筹。

PyInstaller

[圖片上傳失敗...(image-fd72fe-1516382674300)]

PyInstaller 是一個打包工具,可以幫助你打包 Python 腳本址貌,生成應(yīng)用铐拐。

安裝 PyInstaller

$ pip install pyinstaller

打包后在dist文件夾中可找到可執(zhí)行文件

$ pyinstaller yourprogram.py

生成 app

$ pyinstaller --onedir -y  main.spec

py2app

py2app 也是打包工具,這里只是簡單介紹一下练对,想要深入了解詳細(xì)內(nèi)容可以自行搜索余舶。

切換到你的工程目錄下

$ cd ~/Desktop/yourprogram

生成 setup.py 文件

$ py2applet --make-setup yourprogram.py

生成你的應(yīng)用

$ python setup.py py2app

DMG Canvas

DMGCanvas

DMG Canvas 可以將 Mac 應(yīng)用打包成 DMG 鏡像文件,并且傻瓜式操作锹淌。

總結(jié)

剛開始只是寫了很簡單的一部分功能匿值,后來慢慢完善,逐漸才有了現(xiàn)在的樣子赂摆,在這個過程中學(xué)到了很多東西挟憔,包括 Python 中的 GUI 和多線程操作,如何生成應(yīng)用程序烟号。希望能對一部分人有所幫助绊谭。

最后貼上Demo,本人 Python 2.7 環(huán)境下運(yùn)行的汪拥, Python 3以上是無法運(yùn)行的达传。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宪赶,更是在濱河造成了極大的恐慌宗弯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搂妻,死亡現(xiàn)場離奇詭異蒙保,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)欲主,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門邓厕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扁瓢,你說我怎么就攤上這事详恼。” “怎么了引几?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵昧互,是天一觀的道長。 經(jīng)常有香客問我她紫,道長硅堆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任贿讹,我火速辦了婚禮渐逃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘民褂。我一直安慰自己茄菊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布赊堪。 她就那樣靜靜地躺著面殖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哭廉。 梳的紋絲不亂的頭發(fā)上脊僚,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音遵绰,去河邊找鬼辽幌。 笑死,一個胖子當(dāng)著我的面吹牛椿访,可吹牛的內(nèi)容都是我干的乌企。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼成玫,長吁一口氣:“原來是場噩夢啊……” “哼加酵!你這毒婦竟也來了拳喻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤猪腕,失蹤者是張志新(化名)和其女友劉穎冗澈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體码撰,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渗柿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年个盆,在試婚紗的時候發(fā)現(xiàn)自己被綠了脖岛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡颊亮,死狀恐怖柴梆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情终惑,我是刑警寧澤绍在,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站雹有,受9級特大地震影響偿渡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜霸奕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一溜宽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧质帅,春花似錦适揉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至魄揉,卻和暖如春剪侮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背洛退。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工瓣俯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人不狮。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓降铸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親摇零。 傳聞我的和親對象是個殘疾皇子推掸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345