简单爬取github下载链接2:按照文件夹层级关系下载文件

上一篇的简单爬取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
        }

接着就需要对得到的数据项进行不同的操作:

  1. 对于文件,把它放在groups数组内,等待之后下载;
  2. 对于文件夹,把它放在队列中,等待之后再调用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值