幾個月前,我完成了一次網(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)上的一些例子脆霎,也能比較容易地寫出來。最終界面如下所示:
當(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)行測試:
主要內(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)目的源代碼地址丈挟,歡迎交流和討論 ~~~