对python有一定了解的朋友,可能会知道这个命令:

1
2
3
4
# py2.x
python2 -m SimpleHTTPServer
# py3.x
python3 -m http.server

它是将整个文件夹作为一个http服务器的根目录部署到8000端口。这样就能达到在一个局域网内通过http协议快速的传输文件的目的。在Win和Mac之间传输文件的时候极为有用。然而有一个问题是文件是一个个放在某处的,如果需要传输文件夹的时候要么一个个点,要么就先得打包。

于是就想能不能写个简单的脚本,然后下载的时候是直接按照文件夹来进行下载的呢?想到这东西的逻辑其实也不复杂,于是就试了一下,非常不熟练地花了一个多小时才折腾完^1

观察页面的源代码(F12),可以发现网页中这种图片都是简单的a标签。然后如果是文件夹就会在文字最后加上一个反斜杠。

image

image

我们需要些一些什么东西呢?

我们确定我们的程序有哪些是可以变的

  • host_url: 即主机的地址。这肯定可以是可变的参数,因为我懒得每回都进到程序里面改host的地址。
  • folder_path: 即下载下来的文件存放的地址。这个也可以固定,但是固定的话每次弄完还要重命名一下就显得太蠢。

我直接把上面这个两个东西设成了全局变量。因为我每次访问的时候都要使用主机地址,每次写入或存储的时候都要使用文件存放位置。

然后我们看看我们的函数逻辑:

首先是一个递归的函数:解析一个文件夹,如果在文件夹中发现文件夹,应当继续使用本函数对该文件夹进行解析。如果是文件则做另一套处理。然后我们需要标识这个文件夹与调用它的文件夹,就需要至少一个base_url的参数来表示文件夹所在的当前位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_folder(base_url):
response = urllib2.urlopen(host_url+base_url)
labels = re.findall(r"<a href=\".+?\"", str(response.read()), re.S)
for label in labels:
href = label[9:-1]
if href[-1] == '/':
# if href is a folder
# create folder for this url
try:
os.mkdir(base_url + href)
print("create folder: " + folder_path + base_url + href)
except:
print("folder create failed: " + folder_path + base_url + href)
get_folder(base_url + href)
else: # else href refer to a file
get_file(base_url + href)

我们使用urllib2模块[^2]进行对网页的请求,然后通过re来对网页进行解析,获得a标签的href属性的内容,并依次对每一个链接地址进行判断。

[^2]: 实际上这里的urllib2是import urllib.request as urlib2的。

如果最后一个字符是’/‘,说明指向的网址表示一个文件夹,那么新建一个文件夹,然后递归调用本函数,将新的URL传入该函数

如果最后一个字符不是’/‘,那么所指的内容应该是一个文件了,那就把参数传进去,交给处理file的函数进行处理。

解析文件的函数逻辑更简单,就是获取网页的内容然后存下来而已。代码如下:

1
2
3
4
5
def get_file(file_url): # url as filename
response = urllib2.urlopen(host_url + file_url)
with open(file_url, 'w') as fp:
fp.write(str(response.read()))
print('writing file: ' + folder_path + file_url)

为了我们的脚本更好用,我希望能用命令行参数来使用它:python有解析命令行参数的模块optparser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def parse_args():
global host_url
global folder_path
global verbose
# parse our args
parser = OptionParser(usage="usage: %prog [options] url")
parser.add_option("-v", "--verbose",
action="store_true", dest="verbose",
help="output process in the console")
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=False,
help="disable the output in the console[default]")
parser.add_option("-f", "--folder", metavar="FOLDER", default='folder/',
help="the path to store the download content"),
(options, args) = parser.parse_args()
if len(args) == 0:
parser.error("incorrect number of arguments")
host_url = args[0]
folder_path = options.folder
verbose = options.verbose

这个函数会帮我们处理好参数模块的。主机url是必须输入的,文件地址可以通过–folder参数传入,如果没有默认为’folder/‘。

-q 和 -v 的参数是让我们控制脚本的输出的,因为有时我们不希望脚本输出一大堆的东西,看起来太杂乱。上面做的处理是如果命令行参数中含有-v的话,就是希望详细的输出,否则就什么都不输出。

为了达到这个目的,我用下面这个函数代替了几乎所有的print

1
2
3
def verbose_output(output_string):
if verbose:
print(output_string)

其实到这里大致已经结束了。但是我还希望我输入的时候脚本不要那么蠢,希望他帮我补全url,以及url出错的时候提醒我。然后就有了下面这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def deal_arg():
# to parse host url
global host_url
global folder_path
url_match = r'^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&\/:~\+#]*[\w\-\@?^=%&\/~\+#])?$'
if not re.match(url_match, host_url):
seconnd_match = r'^[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&\/:~\+#]*[\w\-\@?^=%&\/~\+#])?$'
if re.match(seconnd_match, host_url):
host_url = 'http://' + host_url
else:
raise(ValueError("url invalid. Check it again."))
if host_url[-1] != '/':
host_url += '/'
print("You're get the folder under the url: " + host_url)

# to parse the folder
if folder_path[-1] != '/':
folder_path += '/'
if not os.path.exists(folder_path):
os.mkdir(folder_path)
os.chdir(folder_path)

url_match是匹配url的正则表达式。说明只支持http/https和ftp/ftps协议。

然后如果缺协议,脚本会自动补上http协议的声明;会自动补上最后一个没写的’/‘。或者url填写有误,会抛出提示你url错误的异常。

后面就是尝试新建文件夹,并将工作目录变更到你的存储文件夹。

最后主函数这样写就好:

1
2
3
4
if __name__ == "__main__":
parse_args()
deal_arg()
get_folder('')

其实蠢的地方挺多的,而且非常不熟练。然而总算是解决了。


可能出现的问题是在Windows和Mac间传输文件时,Windows的中文编码方式是mbcs,是一种比较特别的Windows命令行的字符编码。所以使用python2的朋友们自己小心注意吧……(我选择换用了py3