上一篇的简单爬取github下载链接并没有考虑到遇到文件夹的情况,这次就针对文件夹的情况来对之前的代码进行一次更新。
一般情况下,要下载各个文件并不算困难,只需要判断一下这个是文件还是文件夹;而加入了文件夹后就牵涉到了嵌套的关系,所以会稍微有些麻烦。
一.日志
log是我根据python提供的logging来调用了几个语句。简单地说,就是把警告及以上的写入到文件,把INFO以及以上的输出到控制,加入日志的原因则是因为下载可能会出错,所以加上日志便于纠错和从断点开始。代码大致如下:
log.py
import logging
# 日志格式化输出
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s"
# 日期格式
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p"
# 仅仅把警告以上的写入日志文件
fp = logging.FileHandler("a.txt", "w", encoding="utf-8")
fp.setLevel(logging.WARNING)
# 日志信息全部输出到控制台
fs = logging.StreamHandler()
fs.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT, handlers=[fp, fs])
由于logging是官方提供的库,所以用途也比较广泛,但是logging本身并不支持多线程,在使用的时候需要注意。
crawl_github.py
import requests
from pyquery import PyQuery as pq
from urllib.parse import urljoin
from multiprocessing.pool import Pool
import threading
import os
import logging
import log
# 加锁,避免创建文件夹出错
lock = threading.Lock()
二.文件 or 文件夹的确定
简单的分析下github下的文件夹和文件的区别,以下面的链接为例子https://github.com/sky94520/Farm,
接着列出Classes和CMakeLists.txt的链接:
https://github.com/sky94520/Farm/tree/master/Classes
https://github.com/sky94520/Farm/blob/master/CMakeLists.txt
从上面的链接中,可以简单认为:当存在tree的时候,就认为它是文件夹;当存在blob的时候,就认为它是文件。
接着还需要修改之前的get_items_from_url函数,让它返回的数据项中多添加一个是文件还是文件夹的属性。
def get_items_from_url(url):
"""
从url中获取html文本,解析后返回dict
@param url 要解析的链接
@return dict {'name' : '文件名', 'url' : '下载链接', "type": }
"""
headers = {
'Host': 'github.com',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36',
}
response = requests.get(url=url, headers=headers)
base_url = 'https://github.com'
# 响应失败,则直接返回
if response.status_code != 200:
print('网页加载错误')
return
# 开始解析标签
doc = pq(response.text)
items = doc.find('tr.js-navigation-item').items()
for item in items:
a = item.find('.content span a')
# 获取链接的文本
name = a.text()
if len(name) == 0:
continue
# 获取链接
url = urljoin(base_url, a.attr('href'))
# 是路径
if url.find("blob") == -1:
is_file = 0
else:
is_file = 1
url = url.replace("blob", "raw")
yield {
'name': name,
'url': url,
'type': is_file
}
接着就需要对得到的数据项进行不同的操作:
- 对于文件,把它放在groups数组内,等待之后下载;
- 对于文件夹,把它放在队列中,等待之后再调用get_items_from_url()函数。
def start_download(start_url, base_dir):
"""
根据开始链接级联获取所有文件,根据base_dir确定根目录
:param start_url:
:param base_dir:
:return:
"""
# 解析完成的项列表
groups = []
# 等待跟进队列
queue = [start_url]
while len(queue) > 0:
url = queue.pop()
print("尝试爬取链接", url)
# 尝试爬取链接
for item in get_items_from_url(url):
# 是路径,则放入队列中,等待跟进
if item["type"] == 0:
queue.append(item["url"])
# 是文件,放入下载队列中
elif item["type"] == 1:
item["base_dir"] = base_dir
groups.append(item)
print('parse data success!!!')
# 多线程下载
pool = Pool()
pool.map(download_file, groups)
pool.close()
pool.join()
注意,start_download会先获取到所有的文件的链接,然后再多线程下载。start_download有两个参数,第一个是要下载的链接,第二个则是根目录名称。
这里使用到了python的多线程来下载文件。
def get_path_from_url(url, base_dir):
"""
从url中根据base_dir获取之后的路径
:param url:
:param base_dir:
:return:
"""
# 根据根目录和链接来确定目录的层级
index = url.find(base_dir)
path = url[index:]
path = os.path.split(path)[0]
return path
def download_file(dic):
"""
下载文件
@:param dic {'name' : '文件名', 'url' : '链接'}
"""
name, url, base_dir = dic["name"], dic["url"], dic["base_dir"]
# 保证文件夹存在
path = get_path_from_url(url, base_dir)
if not os.path.exists(path):
lock.acquire()
try:
print("尝试创建目录", path)
os.makedirs(path)
finally:
lock.release()
print('Ready download %s' % name)
# 开始下载
try:
response = requests.get(url)
file_path = os.path.join(path, name)
if not os.path.exists(file_path):
with open(file_path, 'wb') as f:
f.write(response.content)
print('Successfully download %s' % name)
else:
print('%s already downloaded' % name)
except requests.ConnectionError:
logging.warning("Failed download:%s" % url)
download_file和之前类似,只不过这次的数据项相对于之前增加了一个base_dir属性,用来标识根目录;然后就是当前会牵涉到文件和文件夹之间的嵌套问题,所有还单独拉出来一个函数get_path_from_url()来获取路径。
这里使用到了多线程下载链接,可以有效地加快下载速度。但是目前还是存在一个问题,那就是牵涉到多线程读取文件的问题,由于这里的loggin在输出warnning以及以上的输出时,会把输出同时写入到文件中,而文件在多线程下写入是需要加锁的,但是由于目前并没有有效利用到这个日志文件,所以暂时不考虑这个问题。(以后可能会删除这个日志功能,而改用其他的方法)
接着就是调用上面的函数了:
if __name__ == '__main__':
start_url = 'https://github.com/sky94520/SDL_Net/tree/master/single_tcp'
base_dir = "single_tcp"
start_download(start_url, base_dir)
不过当前的文件目录还是存在一些问题的,比如如果我以SDL_Net为根目录,那么项目的目录结构还会包含tree/master/。这个问题倒是无伤大雅。
github链接:https://github.com/sky94520/tools