用 Python 實(shí)現(xiàn)一個網(wǎng)頁下載工具

幾個月前,我完成了一次網(wǎng)絡(luò)綜合實(shí)驗(yàn)的課設(shè)徐勃,內(nèi)容是要設(shè)計(jì)并實(shí)現(xiàn)一個網(wǎng)站下載程序。感覺里面有幾個地方挺有意思的早像,于是在此記錄下自己的思路,與大家分享肖爵。


實(shí)驗(yàn)要求

網(wǎng)站下載程序可以按照要求下載整個網(wǎng)站的網(wǎng)頁卢鹦,其原理是分析每個頁面中的所有鏈接,然后根據(jù)該鏈接下載單個文件劝堪,并保存下來冀自,采用遞歸方式進(jìn)行掃描下載,直到下載頁數(shù)達(dá)到設(shè)定好的最大值或者下載層數(shù)達(dá)到了設(shè)定的最大層數(shù)才停止秒啦。

主要功能

(1) 設(shè)定站點(diǎn)名稱熬粗; (2) 設(shè)定最大下載頁; (3) 設(shè)定最大下載層余境; (4) 設(shè)定是否下載多媒體文件(圖片)驻呐; (5) 設(shè)定是否下載其他站點(diǎn)網(wǎng)頁; (6) 圖形化顯示芳来。

思路分析

實(shí)現(xiàn)的思路并不難含末,首先獲取用戶輸入的網(wǎng)址的 html 文件,然后分析其中的鏈接和圖片并保存到一個列表即舌,接下來對該列表中的鏈接繼續(xù)重復(fù)這個過程佣盒。在下載過程中維持兩個計(jì)數(shù)器,第一個計(jì)數(shù)器指示著已下載的網(wǎng)頁數(shù)目顽聂,第二個計(jì)數(shù)器指示著當(dāng)前下載到了第幾層肥惭。

這不就是一個樹的層次遍歷嘛盯仪!每個鏈接相當(dāng)于一個子樹的根節(jié)點(diǎn),圖片就相當(dāng)于一個葉子節(jié)點(diǎn)蜜葱。

樹的層次遍歷需要隊(duì)列來實(shí)現(xiàn)全景。首先將用戶輸入的網(wǎng)址入隊(duì),然后將其取出笼沥,并把該網(wǎng)頁中的鏈接入隊(duì)(相當(dāng)于子節(jié)點(diǎn)入隊(duì))蚪燕,然后取出隊(duì)頭的第一個子節(jié)點(diǎn),將該節(jié)點(diǎn)對應(yīng)的網(wǎng)頁中的鏈接繼續(xù)入隊(duì)(相當(dāng)于把這個子節(jié)點(diǎn)的子節(jié)點(diǎn)入隊(duì))奔浅。這時(shí)隊(duì)頭的節(jié)點(diǎn)就是根節(jié)點(diǎn)的第二個子節(jié)點(diǎn)了馆纳。將其取出,繼續(xù)進(jìn)行之前的操作... ...直到達(dá)到了兩個計(jì)數(shù)器中的某個的要求汹桦。


接下來就是具體實(shí)現(xiàn)的方式了鲁驶。因?yàn)橐髨D形化顯示,所以需要寫一個 GUI 界面舞骆,這里我在查閱了目前常用的 Python 圖形界面庫以后钥弯,最終選擇了自帶的 Tkinter 庫。雖然之前沒寫過 Python 的界面督禽,但是參照著官方文檔的示例程序和網(wǎng)上的一些例子脆霎,也能比較容易地寫出來。最終界面如下所示:

image.png

當(dāng)點(diǎn)擊開始下載的按鈕以后狈惫,會觸發(fā)判斷方法睛蛛,對用戶的輸入是否合法、網(wǎng)站是否能訪問進(jìn)行驗(yàn)證胧谈,如果驗(yàn)證無誤才開始正式下載忆肾。這部分判斷代碼比較簡單,就不放出來了菱肖,感興趣的同學(xué)可以在這里查看源代碼客冈。

然后就是這個樹的結(jié)構(gòu)。每個節(jié)點(diǎn)都包含 value, children, layer, directory, number 一共五個屬性稳强,分別指的是對應(yīng)的網(wǎng)址场仲、子節(jié)點(diǎn)列表、所在的層次键袱、下載后存放的目錄燎窘,以及當(dāng)前的序號。這里需要說明存放的目錄蹄咖,因?yàn)檎麄€結(jié)構(gòu)是一個樹形結(jié)構(gòu)褐健,所以我將下載后的文件也以樹形結(jié)構(gòu)保存。定義樹節(jié)點(diǎn)的具體代碼如下:

class TreeNode(object):
    # 通過__slots__來限制成員變量
    __slots__ = ("value", "children", "layer", "directory", "number")
    # 構(gòu)造函數(shù)
    def __init__(self, value, layer, directory, number):
        self.value = value
        self.children = []
        self.layer = layer
        self.directory = directory
        self.number = number

    def get_value(self):
        return self.value

    def get_layer(self):
        return self.layer

    def get_directory(self):
        return self.directory
    # 返回節(jié)點(diǎn)的序號
    def get_number(self):
        return self.number
    # 插入子節(jié)點(diǎn)
    def insert_child(self, node):
        self.children.append(node)

    def pop_child(self):
        return self.children.pop(0)

接下來是核心代碼部分,首先將根節(jié)點(diǎn)入隊(duì)蚜迅,然后通過while 循環(huán)來進(jìn)行子節(jié)點(diǎn)的入隊(duì)舵匾。之所以需要while,是因?yàn)榭赡芨?jié)點(diǎn)和它的頁面中的子節(jié)點(diǎn)的數(shù)目之和還沒有達(dá)到用戶輸入的數(shù)量谁不,所以需要while 循環(huán)讓子節(jié)點(diǎn)依次出隊(duì)坐梯,并將這些子節(jié)點(diǎn)的子節(jié)點(diǎn)再次入隊(duì)。還有一些其他需要注意的地方刹帕,在代碼的注釋進(jìn)行了說明吵血。

# 當(dāng)隊(duì)列不為空時(shí)一直循環(huán),直到入隊(duì)的節(jié)點(diǎn)數(shù)目達(dá)到用戶輸入的數(shù)量時(shí)return 退出
while treenode_queue:
    # 讓隊(duì)尾的節(jié)點(diǎn)出隊(duì)偷溺,下載其 html 文件
    temp = treenode_queue.pop(0)
    re = requests.get(temp.get_value())
    # 注意需要進(jìn)行編碼蹋辅,否則下載的 html 文件里的中文會亂碼
    re.encoding = "utf-8"
    with open(os.path.join(temp.directory, str(temp.number)) + ".html", "w+", encoding="utf-8") as html_file:
        html_file.write(re.text)
    # 判斷用戶是否勾選下載多媒體文件,主要使用urllib 的urlretrieve()來下載
    if download_img:
        # 用SoupStrainer 類來過濾html 文件里的img 標(biāo)簽
        soup = BeautifulSoup(re.text, "html.parser", parse_only=SoupStrainer('img'))
        count = 1
        print("正在下載", temp.value, "的圖片... ...")
        for img in soup:
            if not (img["src"] == ""):
                if str(img["src"][:2]) == r"http://":
                    img["src"] = "https:" + img["src"]
                img_dir = os.path.join(temp.directory, str(count))
                if img["src"][-3:] == "png":
                    urllib.request.urlretrieve(img["src"], img_dir + ".png")
                elif img["src"][-3:] == "gif":
                    urllib.request.urlretrieve(img["src"], img_dir + ".gif")
                elif img["src"][-3:] == "jpg":
                    urllib.request.urlretrieve(img["src"], img_dir + ".jpg")
                else:  # 有些圖片不帶后綴挫掏,目前還無法下載下來侦另,比如博客配圖
                    print("Failed :", img["src"])
                count = count + 1
    # 層數(shù)的判斷出口,當(dāng)下載的網(wǎng)頁層數(shù)等于用戶輸入的層數(shù)時(shí)尉共,退出
    if layer_count >= int(input_max_layers.get()) + 1:
        download_website_of_queue(*treenode_queue)
        with open(r"D:\Download_Website\README.txt", "w+") as readme_file:
            readme_file.write(get_dir_list(r"D:\Download_Website"))
        return
    # 接下來對于出隊(duì)的節(jié)點(diǎn)使用 BeautifulSoup 和 SoupStrainer 來獲取鏈接
    soup = BeautifulSoup(re.text, "html.parser", parse_only=SoupStrainer('a'))
    layer_count = layer_count + 1
    print("----------第" + str(layer_count) + "層----------")
    for each in soup:
        # 對于每個<a>標(biāo)簽中 href 的屬性前四個字符是“http”的記錄褒傅,首先輸出其信息,然后構(gòu)造節(jié)點(diǎn)將其入隊(duì)袄友,并且將其加入上方出隊(duì)節(jié)點(diǎn)的 children 列表
        if each.has_attr("href") and each["href"][:4] == "http":
            website_count = website_count + 1
            print("第" + str(website_count) + "個網(wǎng)站/第" + str(layer_count) +
                  "層:" + each["href"])
            anode = TreeNode(each["href"], layer_count, os.path.join(temp.directory, str(website_count)), website_count)
            temp.insert_child(anode)
            treenode_queue.append(anode)
            if website_count >= int(input_max_pages.get()):
                download_website_of_queue(*treenode_queue)
                # 發(fā)現(xiàn)下載數(shù)目夠了的時(shí)候殿托,調(diào)用函數(shù)下載隊(duì)列中的所有節(jié)點(diǎn)的 html 文件,然后生成README文件剧蚣,記錄輸出文件夾中的樹形結(jié)構(gòu)
                with open(r"D:\Download_Website\README.txt",
                          "w+") as readme_file:
                    readme_file.write(get_dir_list(r"D:\Download_Website"))
                return

核心代碼中有以下幾個點(diǎn)需要注意:

  • 下載網(wǎng)頁時(shí)需要進(jìn)行編碼碌尔,以避免中文亂碼。這里統(tǒng)一編碼成 utf-8券敌。
  • 注意下載圖片時(shí)的處理,有的圖片網(wǎng)址前面只有 \\ 柳洋,沒有 https 頭待诅,需要我們手動添加。
  • 注意函數(shù)出口的位置熊镣,先判斷下載的層數(shù)是否已經(jīng)達(dá)到卑雁,再進(jìn)行下一步的操作。
  • 注意對兩個計(jì)數(shù)器的操作和判斷绪囱。

其中還有兩個函數(shù)测蹲,分別是 download_website_of_queue(),以及 get_dir_list()。前者是用來下載隊(duì)列中剩下的節(jié)點(diǎn)的鬼吵,內(nèi)容和核心代碼部分比較像扣甲。后者用遞歸的方式產(chǎn)生一個 README 文件,這個文件以樹形圖的方式呈現(xiàn)了下載后的文件夾結(jié)構(gòu)。這個方法的代碼參考了這里琉挖。它們的代碼如下:

def download_website_of_queue(*args):
    download_img = False
    if (int(btn_Group1.get()) == 1):
        download_img = True
    # 函數(shù)的輸入是一個列表启泣,對于隊(duì)列中的每個節(jié)點(diǎn),下載其html 文件
    for temp in args:
        re = requests.get(temp.get_value())
        re.encoding = "utf-8"
        if not os.path.exists(temp.directory):
            os.makedirs(temp.directory)
        with open(
                os.path.join(temp.directory, str(temp.number)) + ".html",
                "w+",
                encoding="utf-8") as html_file:
            html_file.write(re.text)

        if download_img:
            # 以下為下載圖片的代碼示辈,之前已經(jīng)給出寥茫,此處不再贅述



# 產(chǎn)生README 說明文件的函數(shù),輸入為一個文件夾目錄的字符串矾麻,輸出為該目錄下所有文件的樹形結(jié)構(gòu)
# 以字符串的形式輸出险耀。主要是通過遞歸調(diào)用來實(shí)現(xiàn)。
BRANCH = '├─'
LAST_BRANCH = '└─'
TAB = '│  '
EMPTY_TAB = '   '

def get_dir_list(path, placeholder=''):
    # 列出輸入目錄下的文件/文件夾,如果是文件夾,則加入folder_list 列表
    folder_list = [
        folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))
    ]
    # 列出輸入目錄下的文件/文件夾,如果是文件匀油,則加入file_list 列表
    file_list = [
        file for file in os.listdir(path) if os.path.isfile(os.path.join(path, file))
    ]
    result = ''
    # 對于文件夾列表中的第一個到倒數(shù)第二個元素窝爪,添加一個branch,然后遞歸調(diào)用直到
    # 最深的第一個子文件夾,里面只有文件刁品,然后開始加入第一個到倒數(shù)第二個文件勒叠,
    # 直到添加完該文件夾的最后一個文件拌汇,依次類推与倡,直到根目錄文件夾的最深的最后
    # 一個子文件夾里的最后一個最后一個文件溉潭,遞歸才結(jié)束
    for folder in folder_list[:-1]:
        result += placeholder + BRANCH + folder + '\n'
        result += get_dir_list(os.path.join(path, folder), placeholder + TAB)
    if folder_list:
        result += placeholder + (BRANCH if file_list else
                                 LAST_BRANCH) + folder_list[-1] + '\n'
        result += get_dir_list(
            os.path.join(path, folder_list[-1]),
            placeholder + (TAB if file_list else EMPTY_TAB))
    for file in file_list[:-1]:
        result += placeholder + BRANCH + file + '\n'
    if file_list:
        result += placeholder + LAST_BRANCH + file_list[-1] + '\n'
    return result

接下來用某博客網(wǎng)站進(jìn)行測試:

存放下載結(jié)果的文件夾.png

README 文件和 html 文件.png

主要內(nèi)容差不多就是這些了。還有一點(diǎn)就是喳瓣,程序界面上第二個按鈕是用來打開對應(yīng)的文件夾的馋贤,這個功能的實(shí)現(xiàn)是用 os 模塊完成的,只需要一行代碼:os.system((r"start explorer D:\Download_Website"))畏陕。本質(zhì)上相當(dāng)于是調(diào)用 Windows 控制臺的指令來完成的掸掸。從這一點(diǎn)出發(fā),也可以產(chǎn)生許多有意思的應(yīng)用蹭秋。

總的來說,這是一個很適合練手的小項(xiàng)目堤撵,整個代碼加起來還不到 300 行仁讨,但可以考察到很多細(xì)節(jié)方面的東西 ~~~ 雖然我寫出來了,但是里面的一些寫法实昨、命名之類的還是讓人不怎么滿意洞豁,可能還有一些隱藏的 bug ...但之后我會抽空進(jìn)行更進(jìn)一步的完善的!

最后,給出這個小項(xiàng)目的源代碼地址丈挟,歡迎交流和討論 ~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刁卜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子曙咽,更是在濱河造成了極大的恐慌蛔趴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件例朱,死亡現(xiàn)場離奇詭異孝情,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)洒嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門箫荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人渔隶,你說我怎么就攤上這事羔挡。” “怎么了间唉?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵绞灼,是天一觀的道長。 經(jīng)常有香客問我终吼,道長镀赌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任际跪,我火速辦了婚禮商佛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘姆打。我一直安慰自己良姆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布幔戏。 她就那樣靜靜地躺著玛追,像睡著了一般。 火紅的嫁衣襯著肌膚如雪闲延。 梳的紋絲不亂的頭發(fā)上痊剖,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機(jī)與錄音垒玲,去河邊找鬼陆馁。 笑死,一個胖子當(dāng)著我的面吹牛合愈,可吹牛的內(nèi)容都是我干的叮贩。 我是一名探鬼主播击狮,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼益老!你這毒婦竟也來了彪蓬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捺萌,失蹤者是張志新(化名)和其女友劉穎档冬,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體互婿,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捣郊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年诱鞠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玷室。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蘑险。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡汤纸,死狀恐怖属百,靈堂內(nèi)的尸體忽然破棺而出每窖,到底是詐尸還是另有隱情肺素,我是刑警寧澤粤策,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布壮锻,位于F島的核電站琐旁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏猜绣。R本人自食惡果不足惜灰殴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掰邢。 院中可真熱鬧牺陶,春花似錦、人聲如沸辣之。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怀估。三九已至狮鸭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間多搀,已是汗流浹背歧蕉。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留康铭,地道東北人廊谓。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像麻削,于是被迫代替她去往敵國和親蒸痹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,105評論 1 32
  • feisky云計(jì)算呛哟、虛擬化與Linux技術(shù)筆記posts - 1014, comments - 298, trac...
    不排版閱讀 3,855評論 0 5
  • 昨天晚上去跑步叠荠,還是在那條路上,一圈大概兩三公里的樣子扫责,因?yàn)槭峭砩祥欢Γ夷菞l路很少有人路過,又沒有路燈鳖孤,其...
    時(shí)間煮魚片閱讀 405評論 0 2
  • 昨晚發(fā)生了一件十分囧的事。 因?yàn)橹皝韻s港我換光了所有人民幣和港幣平匈。然后買買買太隨意框沟,用著用著就發(fā)現(xiàn)沒有現(xiàn)金了!增炭?...
    向太白閱讀 414評論 0 0
  • 3. 怎樣學(xué)會使用這個詞忍燥? 1)翻譯下面的句子: 要在兩個月之內(nèi)把雅思從 6 分提到 8 分對任何人來說都是一件非...
    江貼心閱讀 145評論 0 0