Python爬虫实战: 多进程爬取百度百科页面超链接

Python爬虫实战: 多进程爬取百度百科页面超链接

最近因为需要,爬取了实体知识库里每个实体在百度百科页面下的所有超链接内容,这部分工作结束后,想着既是总结也是分享,把这部分工作和代码记录下来,对于刚想学爬虫的可以了解爬虫过程跑一下代码github,代码并不复杂只是requests的页面爬取,写得不好,也欢迎各位大佬指正和讨论。

  1. 抓取思路、流程
  2. 代码分析

确定爬取需求

写一个爬虫的话,首先是要明确自己的需求,即打开一个页面,知道自己想要抓取下来哪些内容,然后再去观察网页的源码,来分析如何通过html/xml的标签来捕获对应的内容。具体的爬虫套路也可以看我之前写的其它博客内容,整个流程记录的比较详细。

本次爬取的话,目的就是获取到某一实体再百度百科检索页面下的所有超链接,比如我这里随便举得一个例子,在百度百科页面中输入 “我的前半生”,页面里蓝色的字体其实代表的就是超链接的部分了。那我的目的就是获取该页面下所有超链接的地址(url), 以及它的title:
在这里插入图片描述

找到对应的标签

知道要爬取什么之后,就是怎么来爬取的问题了。首先当然还是利用浏览器的开发者选项,我这里用的是Chrome浏览器,所以在蓝色部分的位置右键,点击“检查",就会弹出来开发者功能选项:
在这里插入图片描述
可以看到右侧标灰的部分就是“沈严”在网页源码里的位置了,通过div标签,及其属性class,我们就可以找到包含该超链接的父节点,然后再遍历里面的a标签,及其href属性和文本,我们就可以获取到所需要的该超链接的地址及其标题。看起来对该页面似乎很简单,的确,但是当你需要检索的实体数量很多时,你就会发现百度百科里一些新的东西。

解决出现的多义词页面

刚刚举例的词条,因为其没有歧义,所以检索就只会出现一个界面,而当我们输入一些有歧义的词时,百度百科页面就会呈现出不一样的页面,比如下面这些情况:
在这里插入图片描述
在这里插入图片描述
对于第一种情况可以看出,百度百科返回了完全不一样的页面,由于“花都艳舞”是一个歧义词,所以该页面给出了“花都艳舞”实体可能链接到的两种不同的页面。而对于第二种情况,所以返回的页面布局跟最初的相似,但是可以看到,最上面出现了一个包含多义词的框,总共有203个可能的实体对应到“张健”这个人。

对于这些情况,单单只是简单访问页面就不行了,因为可能它的页面布局根本就不一样(比如第一种),或者百度百科返回的并不是你真正想检索的页面(比如第二种情况)。其实本次爬虫写的过程中,主要解决的也就是这两种问题。即当输入一个实体时,需要对返回的页面进行判断,到底返回的是上面哪一种情况的页面。

对返回的页面进行判断

判断的方法即是:对以上不同情况,去找出每个页面下独有的标签,以此来作为区分返回页面的依据。比如对于花都艳舞的页面,通过观察页面布局的不同,我们可以尝试找一下“花都艳舞”大标题的xml标签:
在这里插入图片描述
我们可以复制这里dd标签里的class内容,然后在其它情况下页面源代码里进行查找,发现查找不到该值,就说明该标签的内容可以作为我们区分返回这种页面情况的依据:

    if soup.find('div', attrs={'class': 'lemmaWgt-subLemmaListTitle'}):
        # 说明页面进入到多义词列表的页面,需要从多义词列表中找到匹配待检索实体的义项描述的链接
        li_label = soup.find_all('li', attrs={'class': 'list-dot list-dot-paddingleft'})

这里if就是作为判断返回的页面,下面的li标签,则是获取到页面左侧列出来的对应超链接的内容。接下来就是根据你自己输入实体的其它一些消歧信息,选择正确对应的超链接页面,然后再按照最先开始的单页面情况进行爬取就可以了。
在这里插入图片描述
接下来就是对第二种页面最上方有多义词框的情况进行判断。同理,仍然可以根据页面布局的不同,找到该情况下唯一的xml标签,按照刚刚讲到的步骤,我们可以发现:
在这里插入图片描述
ul标签里的class属性polysemantList-wrapper cmn-clearfix可以作为判断返回该页面情况的依据。同时可以看到,ul标签下的子标签li包含的内容即是该多义词框内所有可能的多义词,因此同样的,我们仍然利用实体的其它消歧信息,来从中选取出与我们输入实体所正确对应的实体页面。比如这里我的输入实体是重庆市潘中区政协副主席的张健,那么就遍历多义词框,然后选择与该信息相匹配的那个超链接即可。找到后,就可以仍然按照最初单页面的情况进行处理了。

所以总结一下就是,由于中文多义词等情况,百度百科的返回页面也会根据我们所输入的实体而有所区别。因此在爬取前,我们需要去判断返回页面的情况,然后再去进行爬取。这样才能让我们的程序不至于因为返回页面的不同,而中止。

加入多进程

通过以上的分析,我们已经能够对某一给定的实体,首先判断其返回页面的类型,然后得到与其正确对应页面下的所有超链接内容。如果只是单进程下的抓取,对于实体数量少的话还可以,但是对于实体数量比较多的情况,比如我这里共有快4w的实体数量,如果是用单进程,可能所花的时间就太久了。所以,为了要进行提速,我们就需要利用到多进程的方式来进行并行地爬取。

首先构思你希望怎样来使你的爬取过程并行地执行。本次我的构想是,首先是准备好一个包含所有实体的列表,也就相当于是一个实体池,然后每个进程随机的从里面选取出一个实体,然后再将该实体从实体池中删除(当然实体池内的每个实体都需要互不相同,所以我先将每个实体转成了对应的id值来表示)。因为涉及到不同进程对同一内容的修改操作,自然想到需要引入进程锁(Lock) 的内容,来使不同进程之间的修改互不影响。同时该实体池也要对每个进程都可见,相当于是一个全局变量,所以需要引入多进程中的Manger,而每个进程爬取到的结果的数据保存则采用了队列(Queue) 的方式。相关内容有兴趣的可以再去多了解一些,文末我也会贴出一些我查找过程中的一些链接。

本次多进程采用的是Python中的multiprocessing模块中的Process类。根据上面讲到的内容,因为涉及到我们自己需要设计的部分,所以多进程部分的编写采用了自定义多进程类,来继承Process类,同时重写里面的run()函数,这样我们就可以在run()函数里来实现我们刚刚提到的那些逻辑部分。
多进程部分的代码如下所示:

class CrawlerProcess(Process):
    def __init__(self, id_list, q, lock, id2subject, describe_dict, request_headers):
        """
        :param id_list: 包含实体id的实体池
        :param q: 每个进程保存爬取结果的队列
        :param lock: 进程锁
        :param id2subject: 实体id与实体名间的映射dict
        :param describe_dict: 包含义项描述(即实体消歧信息)的dict
        :param request_headers: requests访问请求头
        """
        Process.__init__(self)
        self.id_list = id_list
        self.q = q
        self.lock = lock
        self.id2subject = id2subject
        self.describe_dict = describe_dict
        self.request_headers = request_headers

    def run(self):
        # 每一个进程不断从实体池中取实体,直到实体池为空
        while len(self.id_list) != 0:
            # 加锁
            self.lock.acquire()
            if len(self.id_list) == 0:
                # 额外的一个退出判断,防止出现只有最后一个实体,但有多个进程进入了while循环的情况
                self.lock.release()
                break
            # 从实体池中随机选取一个实体
            choice_id = random.choice(self.id_list)
            # 选完后删除
            self.id_list.remove(choice_id)
            # 解锁
            self.lock.release()

            # 由实体id转换为对应的实体名
            subject = self.id2subject[choice_id]
            # 这里的义项描述,则表示额外的消歧信息,来帮助获取到正确的对应页面
            yixiang = self.describe_dict[choice_id]
            # 根据实体名,构造百度百科访问地址
            url = construct_url(keyword=subject)
            # 对于每个subject,获取符合其义项描述的对应页面下的所有超链接
            link_data = main_crawler(url, yixiang, self.request_headers)
            entity_link_dict = dict()
            if link_data is None or len(link_data) == 0:
                # 可能有页面没有超链接的情况
                entity_link_dict[choice_id] = "Null"
            else:
                entity_link_dict[choice_id] = link_data

            print(os.getpid(), choice_id, self.q.qsize(), entity_link_dict)

            # 将抓取到的数据放到队列中保存
            self.q.put(entity_link_dict)

            # 判断队列是否满队
            if self.q.full():
                # 释放队列内容,直到队列为空
                while not self.q.empty():
                    link_dict = self.q.get()
                    # 因为需要对本地文件进行写入,所以也需要加入锁,防止不同进程之间的写入混乱
                    self.lock.acquire()
                    with open('./multi_link_data/subject_hyperlinks.json', 'a', encoding='utf-8') as fin:
                        json.dump(link_dict, fin)
                        fin.write('\n')
                    self.lock.release()

判断队列是否满队,因为每个进程所能保存的容量大小应该是有限的,不可能在运行中保存所有的内容,万一中途内存不够了呢。因此需要设定队列的大小,然后在运行过程中,判断队列的容量是否已经到达上限,如果到达,则需要先将其释放,这里即是保存到本地的json文件中。通过以上的多进程代码,我们即可实现我们所需要实现的目标。

构造请求头

当然爬虫都需要我们将程序来进行伪装,这样才能不被对方的反爬机制所识别。不同网站的反爬手段不同,所需要伪装的程度也不同。本次百度百科页面的爬取,伪装还是比较容易的,只需要伪装成浏览器进行访问就可以了。方法在我之前的博客里也有详细写到,这里简单说明其实就是利用浏览器的开发者选项功能,找到请求页面的Request Headers,本次在我浏览器(Chrome)下的内容如图所示:
在这里插入图片描述
把里面的内容直接复制到代码里,然后作为dict添加到requests的请求里就可以了。

    request_headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed- xchange;v=b3;q=0.9',
        'Accept - Encoding': 'gzip, deflate, br',
        'Accept - Language': 'zh-CN,zh;q=0.9',
        'Cache - Control': 'max-age=0',
        'Connection': 'keep-alive',
        'Cookie': 'BAIDUID=003D94039A5FB16CE650EBCF5E72A45E:FG=1; BIDUPSID=003D94039A5FB16CE650EBCF5E72A45E; PSTM=1561885291; BDUSS=WVXUDRZSHdKeVJhaERyOXh2TTNoT3lPN3p4VE04SXhKSlVUWTd4S2JMMmtLMEZkSVFBQUFBJCQAAAAAAAAAAAEAAABh2fss1OfJz7XEtrm9rNPNzPUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKSeGV2knhldQ; H_PS_PSSID=; delPer=0; BD_CK_SAM=1; PSINO=2; BDRCVFR[BCzcNGRrF63]=mk3SLVN4HKm; BD_HOME=1; BD_UPN=12314753; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; COOKIE_SESSION=2691823_0_9_0_66_16_0_2_9_5_115_2_3625418_0_2_0_1588747660_0_1588747658%7 C9%23191_140_1583480922%7C9; H_PS_645EC=b082T2nk%2FHreRxzRLh%2F4Lvy%2FrJ0eUckomxoWqhlovZkh4zkgCdpy%2FXI9AIKSPp5b9I1IZ3c; BDSVRTM=193',
        'Host': 'baike.baidu.com',
        'Sec-Fetch-Dest': 'document',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-Site': 'same-origin',
        'Sec-Fetch-User': '?1',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0(Windows NT 10.0; Win64; x64) AppleWebKit / 537.36(KHTML, like Gecko) Chrome/81.0.4044.129 Safari / 537.36'
    }
req_text = requests.get(url, headers=request_headers).text

代码里的While True:则是为了防止在请求的过程中发生HTTPError的连接请求等问题,如果遇到这种问题,则可以等在10s,然后再重新发送请求。一般这样可以避免一些请求的问题。

    while True:
        try:
            response = requests.get(url, headers=request_headers)
            break
        except:
            print("#####Please wait 10 seconds#####")
            time.sleep(10)

总结

本次爬虫的任务其实并不难,各种标签的获取比较清晰明确,只是需要去针对不同的返回页面进行一定的判断,以及为了加快爬取效率,采用了多进程的方式进行抓取。最终的代码放在了github 上。当然,每个人也都可以根据自己的需求,对代码的某些部分进行修改,以获取到自己任务所需要的标签内容,提供的也仅仅是一个爬取思路。

辛苦码字,还希望大家觉得不错的话点个赞👍再走,也欢迎各位大佬讨论和改进,共同进步。

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JermeryBesian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值