爬取目录型整站URL的思路

目标

https://opendev.org/openstack/neutron 是Openstack 中的一个网络组件项目,这次示例目标是爬取这个项目中所有文件的URL,有了URL再去检索内容,简单的就只剩下写写正则啦。

页面分析

用元素选择器定到目标位置,通过观察发现整个目标内容都包裹在<tbody>标签中。内容分为两种元素:文件目录,它们都被包裹在<td class="name four wide">标签中。其中文件的<td>标签中包含了<span class="octicon-file-directory">标签,目录的<td>标签中包含了<span class="octicon-file-text">标签。如下表:

爷爷标签 父标签 子标签
tbody <td class="name four wide"> <span class="octicon-file-directory">
- - <span class="octicon-file-text">

文件是末梢,目录下包含了目录文件。点击文件超链接能进入内容呈现页面。点击目录的链接进入到它所包含内容的呈现页面。因此我们只需要一层层不断进入目录中,拿到该目录所包含的所有文件的URL,即算是完成目标。

tbody

<span class="octicon-file-directory">
<span class="octicon-file-text">

爬取思路

我们把爬取的目标抽象成树型结构,先假设这是一颗无环的生成树,不对爬到的URL进行重复检测。因此只需要借助队列,对树进行层次遍历即可。


目录树

工具

爬网页用的两库已经很熟悉了,除此之外我还需要用到python自带的队列库queue。我要用到 queue.Queue() 类中提供了三个方法,入队put()、出队get()和判队空empty()get()如果在空队列上操作,会一直阻塞等待新元素入队。

from bs4 import BeautifulSoup
import requests
from queue import Queue

我准备设置两个队列,一个是用于暂存URL的cache,一个用于输出爬取成果的result
处理流程如下:

处理流程

先来看看伪码的实现

def 爬取(url):
    html = 访问(url)
    for td in html:
        if 在<td>中找到了<span class="octicon octicon-file-directory">:
            cache.入队(td.url)
        elif 在<td>中找到了<span class="octicon octicon-file-text">:
            result.入队(td.url)

def 多线程处理result
    print(result.出队())

while 队列不为空:
    url = 首元素出队
    爬取(url)

完整代码:

from queue import Queue
from bs4 import BeautifulSoup
import requests
import threading


cache = Queue()
result = Queue()
link = 'https://opendev.org/openstack/neutron'
cache.put(link)

def find_links(url, queue1, queue2):
    try:
        html = requests.get(url, timeout=30)
        html.raise_for_status
        html.encoding = html.apparent_encoding
    except:
        html.text = ''
    soup = BeautifulSoup(html.text, "html.parser")
    tds = soup.find('tbody').find_all('td', class_='name four wide')
    for td in tds:
        if td.find('span', class_='octicon octicon-file-directory') is not None:
            # directory enqueue 'cache'
            href = 'https://opendev.org' + td.find('a')['href']
            queue1.put(href)
        elif td.find('span', class_='octicon octicon-file-text') is not None:
            # file enqueue 'result'
            href = 'https://opendev.org' + td.find('a')['href']
            queue2.put(href)

def process_result(queue):
    while True:
        url = queue.get()
        print('Result:{}'.format(url))

threading.Thread(target=process_result, args=(result,)).start()

while not cache.empty():
    url = cache.get_nowait()
    print('Accessing:{}'.format(url))
    find_links(url, cache, result)

处理result队列数据的函数是放在另外一个线程里跑的,面对这种异步的情况,我希望每当队列里的元素全部被取空之后,它就会停下来等待新元素入队。get()方法恰好就是为这种场景设计的,不过这里还有点小问题,因为它无法自己停下来,我放到后面完善这个地方。
处理cache队列的循环是同步的,即每次取一个URL拿给find_links去跑,跑完之后后再取下一个URL。因此它没有“停-等”的必要,get_nowait()方法恰好就是为这种场景设计的。

完善功能

前面的场景假设有点单纯,实际情况来看,网站中会存超链接指向闭环的情况(环路),还会有许多重复的链接。因此它们看起来更像一张由超连接组成的有向图。由于我们并不关注它是链接还是被链接(出入度),因此这张图可以化简成无向图。这样一来爬取整站的问题就变成了图中各节点的遍历。
防止环路爬取的有效办法是设置 visited 标识符、重复URL检测。我准备使用后一种方法,因为用Python实现这个很简单。
图的遍历有两种方法:深度优先广度优先。前者使用递归的方法实现,后者依靠队列。我认为深度优先在处理层级数未知的情况,函数反复递归可能会出现意想不到的情况,并且很耗费内存。广度优先就很有意思了,因为它依靠队列,因此无论是使用外部存储或是增加处理节点,都会变的很容易扩展,再者就实现原理来讲,它更容易理解。

有向图
无向图

处理流程

处理流程

前面使用的队列库没有元素重复检测方法,因此我需要重新封装一个队列,实质上它就是一个list。

先看看伪码的实现:

def 爬取(url):
    html = 访问(url)
    for td in html:
        if 在<td>中找到了<span class="octicon octicon-file-directory">:
            if cache.重复检测(url):
                cache.入队(td.url)
        elif 在<td>中找到了<span class="octicon octicon-file-text">:
            if result.重复检测(url):
                result.入队(td.url)

def 多线程处理result:
    print(result.出队())

while 队列不为空:
    url = 首元素出队
    爬取(url)

完整代码:

from bs4 import BeautifulSoup
import requests
import threading
import time

class LQueue:
    def __init__(self):
        self._queue = []
        self.mutex = threading.Lock()
        self.condition = threading.Condition(self.mutex)

    def put(self, item):
        with self.condition:
            self._queue.append(item)
            self.condition.notify()

    def get(self, timeout=60):
        with self.condition:
            endtime = time.time() + timeout
            while True:
                if not self.empty():
                    return self._queue.pop(0)
                else:
                    remaining = endtime - time.time()
                    if remaining <= 0.0:
                        # 等待超时后,返回 None
                        return None
                    self.condition.wait(remaining)

    def get_nowait(self):
        with self.condition:
            try:
                return self._queue.pop(0)
            except:
                IndexError('Empty queue')

    def has(self, item):
        if item in self._queue:
            return True
        else:
            return False

    def empty(self):
        if self._queue.__len__() == 0:
            return True
        else:
            return False

cache = LQueue()
result = LQueue()
link = 'https://opendev.org/openstack/neutron'
cache.put(link)
single = True

def find_links(url, queue1, queue2):
    print('Accessing:{}'.format(url))
    try:
        html = requests.get(url, timeout=30)
        html.raise_for_status
        html.encoding = html.apparent_encoding
    except:
        html.text = ''
    soup = BeautifulSoup(html.text, "html.parser")
    tds = soup.find('tbody').find_all('td', class_='name four wide')
    for td in tds:
        if td.find('span', class_='octicon octicon-file-directory') is not None:
            # directory enqueue 'cache'
            href = 'https://opendev.org' + td.find('a')['href']
            if not queue1.has(href):
                queue1.put(href)
        elif td.find('span', class_='octicon octicon-file-text') is not None:
            # file enqueue 'result'
            href = 'https://opendev.org' + td.find('a')['href']
            if not queue2.has(href):
                queue2.put(href)

def process_result(queue):
    while True:
        # get()方法等待35秒后就会超时,在超时前依然没有新元素进入 cache 
        # 队列,就说明当所有链接都爬完了。这时候 get() 会返回None,判断返回值
        # 如果是None就结束任务。在这设置35秒的超时时间是由于我设置访问URL的超时时间为30。
        url = queue.get(35)
        if url is None:
            break
        print('Result:{}'.format(url))

threading.Thread(target=process_result, args=(result,)).start()

while not cache.empty():
    url = cache.get_nowait()
    find_links(url, cache, result)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342