鱼C论坛

 找回密码
 立即注册
查看: 959|回复: 2

多线程爬取豆瓣top250的源码奉上,有个疑点请教各位大佬

[复制链接]
发表于 2019-5-26 22:34:46 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
最近学习多进程和多线程爬虫以提升效率。多进程基本搞懂了,多线程这块也差不多,不过在用多线程爬取豆瓣电影排行榜的时候,开了三个解析线程,每个线程解析第一页的速度都远远快于后面的页,百思不得其解,忘高手指点。源码奉上,基本的注释也都加了,数据存储在一个excel表格中,表格如图(这里添加不了图片?算了,表格内容就是A-O列第一行分别为排名,片名,年份,国家,导演,编剧,主演,类型,片长,又名,评分,评分人数,主题,详情页,图片链接)。

代码如下,可以复制过去直接运行。看出问题所在,可以微信加我,10块钱红包

  1. #多线程爬取豆瓣top250
  2. import re
  3. import  requests
  4. import  time
  5. from bs4 import  BeautifulSoup
  6. from openpyxl import load_workbook as lw
  7. import threading
  8. from queue import  Queue

  9. #采集线程类
  10. class CrawlThread(threading.Thread):
  11.     def __init__(self,thread_name,page_queue,html_queue):
  12.         super(CrawlThread, self).__init__()
  13.         self.thread_name = thread_name
  14.         self.page_queue = page_queue
  15.         self.html_queue = html_queue
  16.     def run(self):
  17.         print('———%s开始工作———' % self.thread_name)
  18.         while not CRAWL_EXIT:
  19.             page = self.page_queue.get()
  20.             print('—%s开始爬取第%s页-' % (self.thread_name,page))
  21.             url = 'https://movie.douban.com/top250?start=' + str((page-1) * 25) + '&filter='
  22.             html = requests.get(url).text
  23.             self.html_queue.put((page,html))#页面队列中,将html页面和页码组成元祖存入页面队列中
  24.             print('—%s爬取第%s页完毕—' % (self.thread_name,page))
  25.         print('———%s工作完毕———' % self.thread_name)

  26. #解析线程类
  27. class ParseThread(threading.Thread):
  28.     def __init__(self,thread_name,html_queue,sheet):
  29.         super(ParseThread, self).__init__()
  30.         self.thread_name = thread_name
  31.         self.html_queue = html_queue
  32.         self.sheet = sheet
  33.     def run(self):
  34.         print('******%s开始工作******' % self.thread_name)
  35.         while not PARSE_EXIT:
  36.             item = self.html_queue.get()#页面队列中取出页码和页面数据
  37.             page,html = item[0],item[1]
  38.             print('【%s开始解析第%s页】' % (self.thread_name,page))
  39.             time1 = time.time()
  40.             result = self.parse(html)
  41.             for each in result:  # 填表及保存图片
  42.                 # 爬取的每条数据根据排名存入对应的行,因此多线程并发写入该文件不会引起混乱,该操作不需要加锁
  43.                 row = int(each[0]) + 1
  44.                 self.sheet['A%s' % row].value = str(each[0])
  45.                 # sheet['A%s' % row].value = each[0]#这行为什么报错呢?匪夷所思!!!
  46.                 self.sheet['B%s' % row].value = each[1]
  47.                 self.sheet['C%s' % row].value = each[2]
  48.                 self.sheet['D%s' % row].value = each[3]
  49.                 self.sheet['E%s' % row].value = each[4]
  50.                 self.sheet['F%s' % row].value = each[5]
  51.                 self.sheet['G%s' % row].value = each[6]
  52.                 self.sheet['H%s' % row].value = each[7]
  53.                 self.sheet['I%s' % row].value = each[8]
  54.                 self.sheet['J%s' % row].value = each[9]
  55.                 self.sheet['K%s' % row].value = each[10]
  56.                 self.sheet['L%s' % row].value = each[11]
  57.                 self.sheet['M%s' % row].value = each[12]
  58.                 self.sheet['N%s' % row].value = each[13]
  59.                 self.sheet['O%s' % row].value = each[14]
  60.             time2 = time.time()
  61.             print('【%s解决掉第%s页,耗时%s秒】' % (self.thread_name,page,int(time2-time1)))
  62.         print('******%s工作完毕******' % self.thread_name)
  63.     def parse(self,html):
  64.         soup = BeautifulSoup(html, 'lxml')
  65.         # 以下定义存放各类数据的空列表
  66.         ranks = []
  67.         names = []
  68.         years = []
  69.         countrys = []
  70.         directors = []
  71.         scriptwriters = []
  72.         stars = []
  73.         types = []
  74.         runtimes = []
  75.         themes = []
  76.         detail_links = []
  77.         pic_srcs = []
  78.         other_names = []
  79.         scores = []
  80.         num_of_rating_person = []
  81.         other_name_pattern = re.compile(r'<span class="pl">又名:</span>(.+?)<br/>', re.S)  # 影片又名用正则提取
  82.         # 年份,国家,类型用正则提取。这里由于是从bs解析返回的网页文档中获取数据,因此其格式与requests直接获取的网页内容
  83.         # 有区别, 因此正则表达式与豆瓣top250_bs4_1中该处的正则有所区别
  84.         year_country_type_pattern = re.compile(r'<br/>\s*?(.*?)\s*?</p>')
  85.         for li in soup.ol.find_all('li'):
  86.             ranks.append(li.find('em').string)  # 提取排名
  87.             names.append(li.find('img')['alt'])  # 提取片名,从图片链接的属性中提取
  88.             try:  # 提取主题
  89.                 themes.append(li.find('span', attrs={'class': 'inq'}).text)
  90.             except AttributeError:
  91.                 themes.append('无')
  92.             scores.append(li.find('span', attrs={'class': 'rating_num'}).text)  # 提取评分
  93.             div = li.find('div', attrs={'class': 'star'})  # 提取评分人数
  94.             num_of_rating_person.append(div.find_all('span', attrs={'class': ''})[1].text)
  95.             year_country_type = re.search(year_country_type_pattern, li.prettify()).group(1).split('/')  # 提取年份,国家,类型
  96.             # 因为67条,上映年份列了好几个,都是用‘/’来分隔,因此年份只取第一个(0),而国家和类型则用倒序索引提取(-2,-1)
  97.             years.append(year_country_type[0].strip())
  98.             countrys.append(year_country_type[-2].strip())
  99.             types.append(year_country_type[-1].strip())

  100.             detail_link = li.find('a')['href']  # 提取详情页链接
  101.             detail_links.append(detail_link)
  102.             detail_html = requests.get(detail_link).text  # 进入详情页提取其他内容
  103.             soup2 = BeautifulSoup(detail_html, 'lxml')
  104.             pic_srcs.append(soup2.find('img', attrs={'title': '点击看更多海报'})['src'])  # 提取图片链接
  105.             # 提取导演0、编剧1、主演2;纪录片可能没有编剧和主演,因此加判断
  106.             director_scriptwriter = soup2.find_all('span', attrs={'class': 'attrs'})
  107.             if len(director_scriptwriter) == 3:
  108.                 directors.append(director_scriptwriter[0].text)
  109.                 scriptwriters.append(director_scriptwriter[1].text)
  110.                 stars.append(director_scriptwriter[2].text)
  111.             else:
  112.                 directors.append(director_scriptwriter[0].text)
  113.                 scriptwriters.append('无')
  114.                 stars.append('无')

  115.             runtimes.append(soup2.find('span', attrs={'property': 'v:runtime'}).text)  # 提取片长

  116.             try:  # 提取又名。又名内容不在标签里,故借助正则提取。有些影片没有给出又名,故需加一个异常处理。
  117.                 other_name = re.search(other_name_pattern, detail_html).group(1)
  118.                 other_names.append(other_name)
  119.             except AttributeError:
  120.                 other_names.append('无')
  121.             # 通过zip返回结果
  122.         return zip(ranks, names, years, countrys, directors, scriptwriters, \
  123.                    stars, types, runtimes, other_names, scores, num_of_rating_person, themes, detail_links, pic_srcs)
  124.         # for each in zip(ranks,names,years,countrys,directors,scriptwriters,\
  125.         #                 stars,types,runtimes,other_names,scores,num_of_rating_person,themes,detail_links,pic_srcs):
  126.         #     print(each)

  127. # 采集线程退出信号
  128. CRAWL_EXIT = False
  129. # 解析线程退出信号
  130. PARSE_EXIT = False
  131. def main():
  132.     page_queue = Queue(10)#页码队列,供采集线程爬取页面
  133.     html_queue = Queue()#爬取的页面放入页面队列,供解析线程解析页面并处理数据
  134.     for page in range(1,7):#例如爬取1-6页
  135.         page_queue.put(page)
  136.     crawl_thread_names = ['采集线程%s号' % i for i in range(1,4)]#定义3个采集线程的名字
  137.     parse_thread_names = ['解析线程%s号' % i for i in range(1,4)]#定义3个解析线程的名字
  138.     crawl_list = []#实例化后的采集线程存入该列表
  139.     parse_list = []#实例化后的解析线程存入该列表

  140.     wb = lw('豆瓣top250_2.xlsx')#爬取的数据存入此Excel表格
  141.     sheet = wb['Sheet1']#该sheet对象作为参数传给解析线程,以供解析线程写入数据

  142.     #启动采集线程
  143.     for thread_name in crawl_thread_names:
  144.         crawl_thread = CrawlThread(thread_name,page_queue,html_queue)
  145.         crawl_thread.start()
  146.         crawl_list.append(crawl_thread)
  147.     #启动解析线程
  148.     for thread_name in parse_thread_names:
  149.         parse_thread = ParseThread(thread_name,html_queue,sheet)
  150.         parse_thread.start()
  151.         parse_list.append(parse_thread)

  152.     #等待page_queue队列为空,等待采集线程工作完毕后全部退出工作(join)。
  153.     while not page_queue.empty():
  154.         pass
  155.     print('\npage_queue为空\n')
  156.     global  CRAWL_EXIT
  157.     CRAWL_EXIT = True
  158.     for crawl_thread in crawl_list:
  159.         crawl_thread.join()
  160.     print('——————采集线程全部结束工作——————')

  161.     # 等待page_queue队列为空,等待解析线程工作完毕后全部退出工作(join)。
  162.     while not html_queue.empty():
  163.         pass
  164.     print('\nhtml_queue为空\n')
  165.     global PARSE_EXIT
  166.     PARSE_EXIT = True
  167.     for parse_thread in parse_list:
  168.         parse_thread.join()
  169.     print('——————解析线程全部结束工作——————')

  170.     #保存excel
  171.     wb.save('豆瓣top250_2.xlsx')

  172. if __name__ == '__main__':
  173.     time_start = time.time()
  174.     main()
  175.     time_end = time.time()
  176.     print('总耗时%s秒' % (time_start - time_end))
复制代码
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2019-5-26 22:35:20 | 显示全部楼层
微信1324726119可加我,可以多交流。
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2019-5-26 23:43:16 | 显示全部楼层
完整测试了下,刚才那个表述有问题。应该是,每个线程最后一个页面耗时很短。比如爬取10个页面,开5个线程,情况如下图。
C:\Users\念羁情\Desktop\截图00.png
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2026-1-16 01:44

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表