Python爬虫【四】爬取PC网页版“微博辟谣”账号内容(selenium多线程异步处理多页面)

专题系列导引

  爬虫课题描述可见:

Python爬虫【零】课题介绍 – 对“微博辟谣”账号的历史微博进行数据采集

  课题解决方法:

微博移动版爬虫

Python爬虫【一】爬取移动版“微博辟谣”账号内容(API接口)

微博PC网页版爬虫

Python爬虫【二】爬取PC网页版“微博辟谣”账号内容(selenium同步单线程)
Python爬虫【三】爬取PC网页版“微博辟谣”账号内容(selenium单页面内多线程爬取内容)
Python爬虫【四】爬取PC网页版“微博辟谣”账号内容(selenium多线程异步处理多页面)



前言

  前面专题文章【三】中,我们编写了微博PC网页版单页面内多线程爬取内容的爬虫工程。但因为翻页、下拉等操作仍为单线程执行,并且耗时较高,所以总体执行速度仍然不太可观。尤其是网络不理想的情形下,加载页面、翻页格外耗时,拖累了整体的效率。
  因此设想,是否可以异步启动多个driver驱动,数量为n;同时将微博辟谣240页数据分割为n份的piece,每个driver驱动负责分析爬取一部分piece,最后再汇总,写入表格,来实现整体异步并行爬取的逻辑,提升整个工程的效率。
  此文我们仍然是基于【二】【三】中的项目进行改造升级,让其实现selenium多线程异步处理多页面的功能。

一.改造多页面多线程

  对于整个设计流程,之前CrawlHandle串行方法内的逻辑没有问题,仍然是执行五个步骤。前面的启动driver、登录认证,后面的关闭driver、写入数据,都没有变化。
  唯一需要做改动的是步骤三,此时仅用Crawl类已无法满足要求,因为类中def crawler_all_wb_and_save_df()方法的设计,从一开始就是串行处理的。所以我们创建新的爬取类:BatchCrawl

1. 新爬取类:BatchCrawl
a. 初始化

  因此我们需要仿照Crawl类,写一个并行处理的BatchCrawl类,此类也有def crawler_all_wb_and_save_df()方法,只是改造成了并行启动多driver、并行爬取的方法。具体如下:

class BatchCrawler:

    def __init__(self, driver_num, all_page_num, async_flag=True):
        self.driver_num = driver_num
        self.all_page_num = all_page_num
        self.async_flag = async_flag
        # 定义空df,以装载处理完的数据
        self.excel_df = DataFrame(columns=EXCEL_COLUMNS)

        self.driver = None
        self.cookies = None

可以看到多了很多参数。因为要分piece并行处理,所以微博总共有多少页,分几片,每片包含多少页微博,这些参数需要预先传入。

b. crawler_all_wb_and_save_df()方法

  BatchCrawl类的def crawler_all_wb_and_save_df()方法,结合Crawl类的方法改造如下:

    def crawler_all_wb_and_save_df(self):
        """
        不断爬取所有微博内容数据,并存入excel_df最末端
        :param driver:
        :return:
        """

        try:
            # 初始化启动一批driver,每个driver负责若干页的爬取
            self.__init_b_driver_list()

            thread_list = []
            for b_driver in self.b_driver_list:
                # 多线程:
                thread = util.WBCrawlerThread(b_driver.crawler_batch_wb_and_save_df)
                thread_list.append(thread)
                thread.start()
            # 取结果
            for thread in thread_list:
                thread.join()
                self.excel_df = self.excel_df.append(thread.result)
            # 关闭批处理的driver
            self.__drivers_quit()

        except:
            print("爬虫爬取全部微博数据时出现问题,先返回数据:excel_df")
            traceback.print_exc()

步骤如下:

  1. 构造一批可以异步启动的driver浏览器
  2. 异步启动上面的driver
  3. 等待异步爬取完成后,取出每个driver内的结果df,组成大的结果df
c. __init_b_driver_list()方法

  def __init_b_driver_list()方法会启动一批driver,每个driver、编号数、对应piece的页面都封装入BatchDriver对象中,存入self.b_driver_list属性内;

    def __init_b_driver_list(self):
        """
        初始化启动一批driver,每个driver负责若干页的爬取
        :return:
        """

        b_driver_list = []
        page_size = math.ceil(self.all_page_num / self.driver_num)
        print("初始化启动一批driver:数量:%s,每个driver负责处理page数:%s" % (self.driver_num,page_size))

        for index in range(self.driver_num):
            start_page = page_size * index + 1
            end_page = page_size * ( index + 1 ) if index < (self.driver_num - 1) else self.all_page_num
            p_driver = None
            if index == 0:
                p_driver = BatchDriver(index + 1, start_page, end_page, self.cookies, self.driver)
            else:
                p_driver = BatchDriver(index + 1, start_page, end_page, self.cookies, None)
            b_driver_list.append(p_driver)

        self.b_driver_list = b_driver_list
2. BatchDriver

  BatchDriver是异步处理多页面功能的核心类。存储了本批次的driver、编号、driver负责的微博piece页面区间等属性;并且编写了爬取方法def crawler_batch_wb_and_save_df(),Thread框架正是多线程执行此方法,实现了异步爬取的功能。
定义如下:

class BatchDriver:
    """
    多线程分批次处理若干页的driver,每个driver处理start_page到end_page内的爬取
    """

    def __init__(self, index, start_page, end_page, cookies, driver=None, async_flag=True):

        # 定义空df,以装载处理完的数据
        self.excel_df = DataFrame(columns=EXCEL_COLUMNS)

        self.index = index
        self.start_page = start_page
        self.end_page = end_page
        self.async_flag = async_flag

        # 以下A、B两种创建driver方式二选一
        # A.创建没有chrome弹框的driver驱动;注意:此种情况有可能出现无法下拉页面、点击下一页等操作
        # # 创建chrome参数对象
        # opt = webdriver.ChromeOptions()
        # # 把chrome设置成无界面模式,不论windows还是linux都可以,自动适配对应参数
        # opt.set_headless()
        # driver = webdriver.Chrome(options=opt)
        if not driver:
            # B. 创建传统driver
            driver = webdriver.Chrome()
            # 要先打开URL,再添加cookie;但此链接可能会被跳转passport.weibo.com登录页,因此要检测
            driver.get(WB_PIYAO_URL_PAGE % self.start_page)
            time.sleep(0.5)

            # 必须要清除cookie再set,否则登录态不生效,无法翻页
            driver.delete_all_cookies()
            # print("cookie now:%s" % driver.get_cookies())
            for cookie in cookies:
                # print("driver%s 初始化cookie: %s" % (index, cookie))
                driver.add_cookie(cookie)

            driver.refresh()
            time.sleep(3)

            while not driver.current_url.startswith(WB_PIYAO_URL):
                print("URL不对,需要刷新 URL=%s" % driver.current_url)
                driver.get(WB_PIYAO_URL_PAGE % self.start_page)
                time.sleep(3)

            # 如果跳转到passport.weibo.com,必须再设一次cookies,才能真正登录态有效
            print("再设登录态")
            driver.delete_all_cookies()
            for cookie in cookies:
                driver.add_cookie(cookie)
            driver.refresh()

        print("=== driver%i[%i,%i] 启动成功" % (index, start_page, end_page))
        self.driver = driver


    def crawler_batch_wb_and_save_df(self):
        """
        不断爬取所有微博内容数据,并存入excel_df最末端
        :param driver:
        :return:
        """

        try:
            # 没到本批的最后一页,则一直循环翻页
            for page in range(self.start_page, self.end_page + 1):
                self.page = page
                # 1. 下拉3次至本页最底端,会出现分页按钮  需要拉到最底,以防selenium 出现 element not interactable 错误
                for i in range(2):
                    print("  分批爬虫:driver%i 下拉到最底端操作,第 %i 次 ..." % (self.index, i))
                    self.driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")
                    # 为防止下拉时,新页面短时间加载不出来,让程序睡眠几秒等待
                    time.sleep(2)

                # 补救措施:若3次下拉还不能到最底,还需再循环
                while not util.is_element_exist_by_css_selector(self.driver, "div[class='W_pages']"):
                    print("  分批爬虫:driver%i 没下拉到最底端,再次下拉..." % (self.index))
                    self.driver.execute_script("window.scrollTo(0,document.body.scrollHeight)")
                    # 为防止下拉时,新页面短时间加载不出来,让程序睡眠几秒等待
                    time.sleep(1)

                # 2. 下拉完毕,展示全部内容后,爬取此页微博数据,并添加入df中
                self.__crawler_page_and_save_df()

                # 有时候翻页会失败。在此做检查,看看微博页面中的页数是否为程序中的页数,如不一致则提示
                wb_page_num = self.driver.find_element_by_css_selector(".W_pages>span>a").text
                wb_page_num = wb_page_num[2:-2].strip()
                if str(self.page) != wb_page_num:
                    print("程序页面:%s 与微博页面:%s 不匹配,可能有翻页出错的情况,请检查!" % (self.page, wb_page_num))

                # 3. 检查是否有"下一页"按钮
                w_page = self.driver.find_element_by_class_name("W_pages")
                if "下一页" in w_page.text:
                    # 如果有“下一页”,则翻页至下一页
                    w_page_next = w_page.find_element_by_class_name("next")

                    # w_page_next.send_keys("\n")
                    # w_page_next.click()
                    # 要用如下写法先移动到button上,再点击,不然总是 ElementClickInterceptedException
                    webdriver.ActionChains(self.driver).move_to_element(w_page_next).click(w_page_next).perform()
                    # self.driver.execute_script("arguments[0].click();", w_page_next)
                    time.sleep(2)
                else:
                    # 如果没有,则说明到了最后一页,整个爬取完成
                    print("分批爬虫:driver%i 已经到最后一页 %i,爬取微博完成" % (self.index, self.page))
                    break
            else:
                print("分批爬虫:driver%i 循环到最后一页 %i,爬取微博完成" % (self.index, self.end_page))
        except:
            print("分批爬虫:driver%i 出现问题! 先返回数据excel_df,可能不全" % self.index)
            traceback.print_exc()

        return self.excel_df

    def __crawler_page_and_save_df(self):
        """
        使用selenium工具爬取当前微博页面信息
        :param page:
        :return:
        """
        wb_page_start_time = time.time()  # 用于计时

        wb_list = []
        # print("开始爬取第 %i 页数据..." % page)
        try:
            # 1. 找出微博内容框架list,也就是每个微博内容块的集合
            wb_cardwrap_list = self.driver.find_elements_by_class_name("WB_feed_type")

            if self.async_flag:
                # 多线程处理,每个线程解析一个微博内容框架,从中提取所需数据
                wb_list = self.__async_crawler_weibo_info(wb_cardwrap_list)
            else:
                # 单线程处理
                wb_list = self.__sync_crawler_weibo_info(wb_cardwrap_list)
        except:
            print("driver%i 爬取处理 第 %i 页html数据时出错! " % (self.index, self.page))
            traceback.print_exc()
        else:
            print("driver%i 成功爬取第 %i 页数据,爬取有效微博数:%s, 处理本页数据耗时:%s " % (
            self.index, self.page, len(wb_list), time.time() - wb_page_start_time))

        # 不为空则写入df中
        if wb_list:
            self.excel_df = self.excel_df.append(wb_list)

    def __async_crawler_weibo_info(self, wb_cardwrap_list):
        """
        用多线程方式异步并发爬取微博内容
        :param wb_cardwrap_list:
        :return:
        """
        wb_list = []  # 爬取到的微博信息整理后的储存list
        thread_list = []
        for wb_count in range(len(wb_cardwrap_list)):
            # 多线程:约18秒左右处理完45条数据,比单线程串行36秒左右减少一半时间。 Python多线程是伪多线程
            thread = util.WBCrawlerThread(util.crawler_weibo_info_func,
                                          (wb_cardwrap_list[wb_count], self.page, wb_count))
            thread_list.append(thread)
            thread.start()
        # 取结果
        for thread in thread_list:
            thread.join()
            # 去除None
            if thread.result:
                wb_list.append(thread.result)

        return wb_list

    def __sync_crawler_weibo_info(self, wb_cardwrap_list):
        """
        同步爬取微博数据
        :return:
        """
        wb_list = []  # 爬取到的微博信息整理后的储存list
        for wb_count in range(len(wb_cardwrap_list)):
            # 多线程:约16秒左右处理完45条数据,比单线程串行35秒左右减少一半时间。 Python多线程是伪多线程
            etl_json = util.crawler_weibo_info_func(wb_cardwrap_list[wb_count], self.page, wb_count)
            if etl_json:
                wb_list.append(etl_json)

        return wb_list

此类中的爬取逻辑与之前Crawl类中的爬取逻辑类似,只是多了piece前后页面的判断,而非像之前总会从第一页开始,一直爬取到最后一页了;
并且此多线程类中,也仍可实现再多线程爬取页面内的数据,通过参数async_flag来控制(但博主自己试async_flag为true时,并没有性能提升的效果,可能是多线程下的效率已经饱和,也跟python本文伪并发有关)。

3. main.py

入口创建的对象是BatchCrawler对象,如下

if __name__ == '__main__':
    # 网页爬取:分批处理,每批若干页
    b_crawler = batch_crawl.BatchCrawler(4, 240, True)
    crawl_handle.crawl_wb_and_write_excel(b_crawler)

以上即为selenium单页面内多线程爬取内容的改造

二. 程序执行

  执行过程中,前面登录与单线程没区别;但当用户登录成功,下拉两次后,开始爬取时,可以看到启动了另外4个chrome页面,每个页面都在独立的爬取。
在这里插入图片描述

后台不再是按微博页面顺序、内部上下顺序依次爬取,而是每个driver下都在爬取与打印,后台打印日志的顺序也交错输出,可能出现第1页与第100页的微博,同时在提取数据。
在这里插入图片描述

三. 问题总结

  1. selenium爬取网页是模拟人操作页面浏览的方式,进行信息提取。因此实际执行中发现,如果程序执行find_element_by_XXX()、click()等查询和点击操作时,如果driver弹出的浏览器,有不限于如下的一些情况 (被最小化隐藏、被其他程序页面覆盖浏览器、要操作的对象还在滚动条区域内,没页面中显示、被其他可以click的标签比如消息提醒button布局覆盖、driver窗口被拉太瘦以至于下拉最底后js无法展示“下一页”按钮…),则selenium的操作会无法生效,甚至报错can’t find element,导致程序异常。这个问题在电脑全屏打开其他窗口时尤其容易发生,例如在看pycharm后端日志、打开其他浏览器全屏搜索问题。

因此在程序运行时,请保持driver浏览器始终在最顶端,显示窗口足够大,并在中途不要操作,等待爬取完成;同时,driver浏览器窗口需要保持一定的大小,当触发登录点击按钮、下拉到最低端点击下一页按钮时,都需要在chrome浏览器内能肉眼观测到这个元素
对于多线程程序,最好只启动2个driver,或者用多块屏幕的电脑,将driver浏览器分散在多个屏幕中同时显示,保证每个driver浏览器不会全屏化,不会彼此覆盖,并在中途不要操作,等待爬取完成
在这里插入图片描述

  1. 同时开多个窗口可能会引起电脑性能不够,彼此难在窗口并排放置的情况。因此可以用如下方式让chrome窗口只在后台运行,不在前端展示,但此方法有一定概率让程序下拉和翻页失效,读者可以自行尝试决定是否启用
        # A.创建没有chrome弹框的driver驱动;注意:此种情况有可能出现无法下拉页面、点击下一页等操作
        # # 创建chrome参数对象
        opt = webdriver.ChromeOptions()
        # 把chrome设置成无界面模式,不论windows还是linux都可以,自动适配对应参数
        opt.set_headless()
        driver = webdriver.Chrome(options=opt)
  1. 博主笔记本为酷睿i5二核,测试时,分为4个drive进行多线程跑批。经测算,每个driver爬取时间为25s,结合下拉翻页耗时,每个页面耗时约33s。每个driver负责60页微博,则耗时约为1980s,折合0.55小时。因为每个driver并发,所以理论上每个driver耗时都为如此,整个项目耗时也差不多。但实际上因为机器性能有限、python伪多线程的问题,耗时会略有增加,基本需要0.7~0.9小时。
  2. 多线程程序涉及登录态cookie复制、多窗口多并发等问题,从实际执行结果来看稳定性并不如前面几种方法可靠。博主已尽量做到多种极端情况判断,提高了稳定性,但多线程程序仍以实验性质为主,读者尽量不采用此种方式来实际获取数据
  3. 本程序初稿编时间为2020年12月,整理发表时间为2021年3月20日,此时间点程序运行正常。但微博HTML页面会随时间而更新,因此有可能导致本程序selenium步骤执行失效。本程序旨在抛砖引玉,希望读者能从中获取灵感,开发出适合自己的版本

执行程序

项目工程编译了windows版本执行程序:微博数据采集python+selenium执行程序:WBCrawler.exe

  1. 执行项目前,需要下载selenium对应的浏览器驱动程序(driver.exe),并放在本机环境变量路径中,否则会报错。安装操作具体可见博客专题中的指导【二】

  2. 执行程序时,会在系统用户默认路径下,创建一个虚拟的python环境(我的路径是C:\Users\Albert\AppData\Local\Temp_MEI124882\),因此启动项目所需时间较长(约20秒后屏幕才有反应,打出提示),请耐心等待;也正因如此,执行电脑本身环境是可以无需安装python和selenium依赖包的;同时最后爬取保存的excel也在此文件夹下。

  3. 本项目采用cmd交互方式执行,因此等到屏幕显示:

     选择爬取方式:
     1. 移动版微博爬取
     2. PC网页版微博爬取(单线程)
     3. PC网页版微博爬取(页面内多线程)
     4. PC网页版微博爬取(多线程异步处理多页面)
    

后,用键盘输入1~4,敲回车执行

  1. 此exe编译时,工程代码内编写的最终excel记录保存地址为:相对工程根路径下的excel文件夹;因此当本exe执行到最后保存数据时,会因为此excel文件夹路径不存在而报错。若在工程中将保存地址改为绝对路径(例如D:\excel\),再编译生成exe执行,则最终爬取数据可以正确保存

项目工程

工程参见:微博数据采集python+selenium工程:WBCrawler.zip

本专题内对源码粘贴和分析已经比较全面和清楚了,可以满足读者基本的学习要求。源码资源为抛砖引玉,也只是多了配置文件和一些工具方法而已,仅为赶时间速成的同学提供完整的项目案例。大家按需选择

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Python爬虫中可以使用Selenium库来网页信息。Selenium可以模拟浏览器行为,能够JavaScript渲染后的网页信息。使用Selenium网页时,需要配合浏览器驱动(如ChromeDriver、FirefoxDriver)使用。 ### 回答2: Python是一种高级编程语言,吸引了大量编程人员和开发者使用Python进行Web开发、数据分析、机器学习和人工智能等领域的开发。爬虫技术正是其中的一项重要技术,用python编写爬虫程序通常更加便捷和灵活。而seleniumPython中主要的爬虫库之一,用于动态Web页面,可以模拟用户在浏览器中的行为,从而获大量数据。 使用selenium信息可以分为以下几个步骤: 1.安装和导入selenium和webdriver: 首先需要安装适合的本的selenium包,并导入selenium和webdriver模块: ```python from selenium import webdriver ``` 2.配置浏览器驱动: Selenium需要浏览器驱动(如Chrome,Firefox等)来与其进行交互,需要配置如下: ```python driver = webdriver.Chrome() ``` 其中,Chrome()表示使用Chrome浏览器驱动,如果使用Firefox,则需要改为Firefox()。 3.访问网页: 使用get()函数可以访问指定的网址: ```python driver.get("https://www.baidu.com/") ``` 4.查找元素: 使用selenium的查找元素功能,可以根据元素的ID、name、class、tag等属性进行查找: ```python element = driver.find_element_by_id("kw") # 根据ID查找 element = driver.find_element_by_name("wd") # 根据name查找 element = driver.find_element_by_class_name("s_ipt") # 根据class查找 element = driver.find_element_by_tag_name("input") # 根据tag查找 ``` 5.模拟用户输入/点击: 使用send_keys()函数模拟用户在搜索框中输入关键字,使用click()函数模拟用户在搜索按钮上点击: ```python element.send_keys("Python") element.click() ``` 6.解析数据: 使用webdriver的page_source属性可以获网页的源代码,然后使用正则表达式或BeautifulSoup库等解析数据。 以上就是使用selenium进行爬虫的主要步骤。实际应用中,需要根据不同的网站和需要的数据进行具体的配置和调整。在使用selenium过程中,需要了解一些常见的使用技巧和注意事项,例如模拟等待时间,处理弹窗、验证码等。同时,也需要遵循爬虫的法律和道德规范,不得进行非法、滥用等行为。 ### 回答3: selenium是一种自动化测试工具,它可以模拟浏览器行为,实现自动化操作。在Python爬虫中,selenium也可以用来需要模拟人工操作的网站数据。 使用selenium可以实现以下操作: 1.自动模拟浏览器打开网页,获网页源码。 2.模拟用户操作,如点击按钮、填写文本框、下拉选择框等。 3.通过获网页源码进行数据解析。 基本流程比较简单,首先需要准备好selenium的环境,这需要下载对应的webdriver,这里我使用Chrome浏览器,并且下载了对应本的chromedriver。 然后通过selenium启动浏览器,在浏览器中进行模拟操作,最后获网页源码进行数据解析。 具体实现可以参考以下代码: ```python from selenium import webdriver from bs4 import BeautifulSoup # 创建一个Chrome浏览器实例 browser = webdriver.Chrome() # 访问目标网页 browser.get('https://www.example.com') # 模拟点击按钮,等待加载完成 button = browser.find_element_by_xpath('//button[@class="btn"]') button.click() browser.implicitly_wait(5) # 获网页源码 html = browser.page_source soup = BeautifulSoup(html, 'html.parser') data = soup.find_all('div', class_='data') # 处理数据 for item in data: # do something # 关闭浏览器 browser.quit() ``` 总体来说,selenium是一个强大的爬虫工具,可以应对大部分需要模拟人工操作的场景,但也存在一些缺点,比如速度慢、占用资源高等。因此在具体应用中需要根据实际情况进行选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

郭Albert

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

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

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

打赏作者

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

抵扣说明:

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

余额充值