几个月前,我完成了一次网络综合实验的课设,内容是要设计并实现一个网站下载程序。感觉里面有几个地方挺有意思的,于是在此记录下自己的思路,与大家分享。
实验要求
网站下载程序可以按照要求下载整个网站的网页,其原理是分析每个页面中的所有链接,然后根据该链接下载单个文件,并保存下来,采用递归方式进行扫描下载,直到下载页数达到设定好的最大值或者下载层数达到了设定的最大层数才停止。
主要功能
(1) 设定站点名称; (2) 设定最大下载页; (3) 设定最大下载层; (4) 设定是否下载多媒体文件(图片); (5) 设定是否下载其他站点网页; (6) 图形化显示。
思路分析
实现的思路并不难,首先获取用户输入的网址的 html 文件,然后分析其中的链接和图片并保存到一个列表,接下来对该列表中的链接继续重复这个过程。在下载过程中维持两个计数器,第一个计数器指示着已下载的网页数目,第二个计数器指示着当前下载到了第几层。
这不就是一个树的层次遍历嘛!每个链接相当于一个子树的根节点,图片就相当于一个叶子节点。
树的层次遍历需要队列来实现。首先将用户输入的网址入队,然后将其取出,并把该网页中的链接入队(相当于子节点入队),然后取出队头的第一个子节点,将该节点对应的网页中的链接继续入队(相当于把这个子节点的子节点入队)。这时队头的节点就是根节点的第二个子节点了。将其取出,继续进行之前的操作... ...直到达到了两个计数器中的某个的要求。
接下来就是具体实现的方式了。因为要求图形化显示,所以需要写一个 GUI 界面,这里我在查阅了目前常用的 Python 图形界面库以后,最终选择了自带的 Tkinter 库。虽然之前没写过 Python 的界面,但是参照着官方文档的示例程序和网上的一些例子,也能比较容易地写出来。最终界面如下所示:
当点击开始下载的按钮以后,会触发判断方法,对用户的输入是否合法、网站是否能访问进行验证,如果验证无误才开始正式下载。这部分判断代码比较简单,就不放出来了,感兴趣的同学可以在这里查看源代码。
然后就是这个树的结构。每个节点都包含 value, children, layer, directory, number 一共五个属性,分别指的是对应的网址、子节点列表、所在的层次、下载后存放的目录,以及当前的序号。这里需要说明存放的目录,因为整个结构是一个树形结构,所以我将下载后的文件也以树形结构保存。定义树节点的具体代码如下:
class TreeNode(object):
# 通过__slots__来限制成员变量
__slots__ = ("value", "children", "layer", "directory", "number")
# 构造函数
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
# 返回节点的序号
def get_number(self):
return self.number
# 插入子节点
def insert_child(self, node):
self.children.append(node)
def pop_child(self):
return self.children.pop(0)
接下来是核心代码部分,首先将根节点入队,然后通过while 循环来进行子节点的入队。之所以需要while,是因为可能根节点和它的页面中的子节点的数目之和还没有达到用户输入的数量,所以需要while 循环让子节点依次出队,并将这些子节点的子节点再次入队。还有一些其他需要注意的地方,在代码的注释进行了说明。
# 当队列不为空时一直循环,直到入队的节点数目达到用户输入的数量时return 退出
while treenode_queue:
# 让队尾的节点出队,下载其 html 文件
temp = treenode_queue.pop(0)
re = requests.get(temp.get_value())
# 注意需要进行编码,否则下载的 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 标签
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"//":
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
# 层数的判断出口,当下载的网页层数等于用户输入的层数时,退出
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
# 接下来对于出队的节点使用 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>标签中 href 的属性前四个字符是“http”的记录,首先输出其信息,然后构造节点将其入队,并且将其加入上方出队节点的 children 列表
if each.has_attr("href") and each["href"][:4] == "http":
website_count = website_count + 1
print("第" + str(website_count) + "个网站/第" + 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)
# 发现下载数目够了的时候,调用函数下载队列中的所有节点的 html 文件,然后生成README文件,记录输出文件夹中的树形结构
with open(r"D:\Download_Website\README.txt",
"w+") as readme_file:
readme_file.write(get_dir_list(r"D:\Download_Website"))
return
核心代码中有以下几个点需要注意:
- 下载网页时需要进行编码,以避免中文乱码。这里统一编码成 utf-8。
- 注意下载图片时的处理,有的图片网址前面只有 \\ ,没有 https 头,需要我们手动添加。
- 注意函数出口的位置,先判断下载的层数是否已经达到,再进行下一步的操作。
- 注意对两个计数器的操作和判断。
其中还有两个函数,分别是 download_website_of_queue(),以及 get_dir_list()。前者是用来下载队列中剩下的节点的,内容和核心代码部分比较像。后者用递归的方式产生一个 README 文件,这个文件以树形图的方式呈现了下载后的文件夹结构。这个方法的代码参考了这里。它们的代码如下:
def download_website_of_queue(*args):
download_img = False
if (int(btn_Group1.get()) == 1):
download_img = True
# 函数的输入是一个列表,对于队列中的每个节点,下载其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:
# 以下为下载图片的代码,之前已经给出,此处不再赘述
# 产生README 说明文件的函数,输入为一个文件夹目录的字符串,输出为该目录下所有文件的树形结构
# 以字符串的形式输出。主要是通过递归调用来实现。
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 = ''
# 对于文件夹列表中的第一个到倒数第二个元素,添加一个branch,然后递归调用直到
# 最深的第一个子文件夹,里面只有文件,然后开始加入第一个到倒数第二个文件,
# 直到添加完该文件夹的最后一个文件,依次类推,直到根目录文件夹的最深的最后
# 一个子文件夹里的最后一个最后一个文件,递归才结束
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
接下来用某博客网站进行测试:
主要内容差不多就是这些了。还有一点就是,程序界面上第二个按钮是用来打开对应的文件夹的,这个功能的实现是用 os 模块完成的,只需要一行代码:os.system((r"start explorer D:\Download_Website"))
。本质上相当于是调用 Windows 控制台的指令来完成的。从这一点出发,也可以产生许多有意思的应用。
总的来说,这是一个很适合练手的小项目,整个代码加起来还不到 300 行,但可以考察到很多细节方面的东西 ~~~ 虽然我写出来了,但是里面的一些写法、命名之类的还是让人不怎么满意,可能还有一些隐藏的 bug ...但之后我会抽空进行更进一步的完善的!
最后,给出这个小项目的源代码地址,欢迎交流和讨论 ~~~