赞
踩
爬虫课题描述可见:
课题解决方法:
微博移动版爬虫
微博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
因此我们需要仿照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并行处理,所以微博总共有多少页,分几片,每片包含多少页微博,这些参数需要预先传入。
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()
步骤如下:
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
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本文伪并发有关)。
入口创建的对象是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页的微博,同时在提取数据。

因此在程序运行时,请保持driver浏览器始终在最顶端,显示窗口足够大,并在中途不要操作,等待爬取完成;同时,driver浏览器窗口需要保持一定的大小,当触发登录点击按钮、下拉到最低端点击下一页按钮时,都需要在chrome浏览器内能肉眼观测到这个元素
对于多线程程序,最好只启动2个driver,或者用多块屏幕的电脑,将driver浏览器分散在多个屏幕中同时显示,保证每个driver浏览器不会全屏化,不会彼此覆盖,并在中途不要操作,等待爬取完成
# A.创建没有chrome弹框的driver驱动;注意:此种情况有可能出现无法下拉页面、点击下一页等操作
# # 创建chrome参数对象
opt = webdriver.ChromeOptions()
# 把chrome设置成无界面模式,不论windows还是linux都可以,自动适配对应参数
opt.set_headless()
driver = webdriver.Chrome(options=opt)
项目工程编译了windows版本执行程序:微博数据采集python+selenium执行程序:WBCrawler.exe
执行项目前,需要下载selenium对应的浏览器驱动程序(driver.exe),并放在本机环境变量路径中,否则会报错。安装操作具体可见博客专题中的指导【二】
执行程序时,会在系统用户默认路径下,创建一个虚拟的python环境(我的路径是C:\Users\Albert\AppData\Local\Temp_MEI124882\),因此启动项目所需时间较长(约20秒后屏幕才有反应,打出提示),请耐心等待;也正因如此,执行电脑本身环境是可以无需安装python和selenium依赖包的;同时最后爬取保存的excel也在此文件夹下。
本项目采用cmd交互方式执行,因此等到屏幕显示:
选择爬取方式:
1. 移动版微博爬取
2. PC网页版微博爬取(单线程)
3. PC网页版微博爬取(页面内多线程)
4. PC网页版微博爬取(多线程异步处理多页面)
后,用键盘输入1~4,敲回车执行
工程参见:微博数据采集python+selenium工程:WBCrawler.zip
本专题内对源码粘贴和分析已经比较全面和清楚了,可以满足读者基本的学习要求。源码资源为抛砖引玉,也只是多了配置文件和一些工具方法而已,仅为赶时间速成的同学提供完整的项目案例。大家按需选择
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。