案例_(多线程)爬取糗事百科

讲解都写在代码注释中了,直接上代码

# 使用了线程库
import threading
# 队列
from queue import Queue
# 解析库
from lxml import etree
# 请求处理
import requests
# json处理
import time


class ThreadCrawl(threading.Thread):
    def __init__(self, thread_name, page_queue, data_queue):
        # threading.Thread.__init__(self)
        # 调用父类初始化方法
        super(ThreadCrawl, self).__init__()
        # 线程名
        self.thread_name = thread_name
        # 页码队列
        self.page_queue = page_queue
        # 数据队列
        self.data_queue = data_queue
        # 请求报头
        self.headers = {"User-Agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;"}

    def run(self):
        print("启动 " + self.thread_name)
        while not CRAWL_EXIT:
            try:
                # 取出一个数字,先进先出
                # 可选参数block,默认值为True
                # 1. 如果对列为空,block为True的话,不会结束,会进入阻塞状态,直到队列有新的数据
                # 2. 如果队列为空,block为False的话,就弹出一个Queue.empty()异常,
                page = self.page_queue.get(False)
                url = "http://www.qiushibaike.com/8hr/page/" + str(page) + "/"
                # print url
                content = requests.get(url, headers=self.headers).text
                time.sleep(1)
                self.data_queue.put(content)
                # print len(content)
            except:
                pass
        print("结束 " + self.thread_name)


class ThreadParse(threading.Thread):
    def __init__(self, thread_name, data_queue):
        super(ThreadParse, self).__init__()
        # 线程名
        self.thread_name = thread_name
        # 数据队列
        self.data_queue = data_queue

    def run(self):
        print("启动" + self.thread_name)
        while not PARSE_EXIT:
            try:
                html = self.data_queue.get(False)
                self.parse(html)
            except:
                pass
        print("退出" + self.thread_name)

    def parse(self, html):
        selector = etree.HTML(html)

        # 返回所有段子的节点位置,contant()模糊查询方法,第一个参数是要匹配的标签,第二个参数是这个标签的部分内容
        # 每个节点包括一条完整的段子(用户名,段子内容,点赞,评论等)
        node_list = selector.xpath('//div[contains(@id,"qiushi_tag_")]')

        for node in node_list:
            # 爬取所有用户名信息
            # 取出标签里的内容,使用.text方法
            user_name = node.xpath('./div[@class="author clearfix"]//h2')[0].text

            # 爬取段子内容,匹配规则必须加点  不然还是会从整个页面开始匹配
            # 注意:如果span标签中有br 在插件中没问题,在代码中会把br也弄进来
            duanzi_info = node.xpath('.//div[@class="content"]/span')[0].text.strip()

            # 爬取段子的点赞数
            vote_num = node.xpath('.//span[@class="stats-vote"]/i')[0].text

            # 爬取评论数
            comment_num = node.xpath('.//span[@class="stats-comments"]//i')[0].text

            # 爬取图片链接
            # 属性src的值,所以不需要.text
            img_url = node.xpath('.//div[@class="thumb"]//@src')
            if len(img_url) > 0:
                img_url = img_url[0]
            else:
                img_url = "无图片"

            self.save_info(user_name, duanzi_info, vote_num, comment_num, img_url)

    def save_info(self, user_name, duanzi_info, vote_num, comment_num, img_url):
        """把每条段子的相关信息写进字典"""
        item = {
            "username": user_name,
            "content": duanzi_info,
            "zan": vote_num,
            "comment": comment_num,
            "image_url": img_url
        }

        print(item)


CRAWL_EXIT = False
PARSE_EXIT = False


def main():
    # 页码的队列,表示20个页面
    pageQueue = Queue(20)
    # 放入1~10的数字,先进先出
    for i in range(1, 21):
        pageQueue.put(i)

    # 采集结果(每页的HTML源码)的数据队列,参数为空表示不限制
    dataQueue = Queue()

    filename = open("duanzi.json", "a")
    # 创建锁
    lock = threading.Lock()

    # 三个采集线程的名字
    crawlList = ["采集线程1号", "采集线程2号", "采集线程3号"]
    # 存储三个采集线程的列表集合
    thread_crawl = []
    for threadName in crawlList:
        thread = ThreadCrawl(threadName, pageQueue, dataQueue)
        thread.start()
        thread_crawl.append(thread)

    # 三个解析线程的名字
    parseList = ["解析线程1号", "解析线程2号", "解析线程3号"]
    # 存储三个解析线程
    thread_parse = []
    for threadName in parseList:
        thread = ThreadParse(threadName, dataQueue)
        thread.start()
        thread_parse.append(thread)

    # 等待pageQueue队列为空,也就是等待之前的操作执行完毕
    while not pageQueue.empty():
        pass

    # 如果pageQueue为空,采集线程退出循环
    global CRAWL_EXIT
    CRAWL_EXIT = True

    print("pageQueue为空")

    for thread in thread_crawl:
        thread.join()

    while not dataQueue.empty():
        pass

    global PARSE_EXIT
    PARSE_EXIT = True

    for thread in thread_parse:
        thread.join()


if __name__ == "__main__":
    main()

爬取结果如下:

 

如果你和我有共同爱好,我们可以加个好友一起交流!

### 创建 `news_spyder` 模块 在 Thonny 中,创建一个名为 `news_spyder.py` 的文件,定义要爬取的页面列表 `urls` 和使用 `requests` 库爬取单个 `url` 地址的函数 `craw(url)`。 ```python import requests # 定义要爬取的页面列表 urls = [ 'https://www.example.com/page1', 'https://www.example.com/page2', 'https://www.example.com/page3' ] # 爬取单个 url 地址的函数 def craw(url): try: response = requests.get(url) if response.status_code == 200: return response.text else: return None except requests.RequestException: return None ``` ### 创建 `multi_thread_craw.py` 文件 创建一个名为 `multi_thread_craw.py` 的文件,定义单线程爬取函数 `single_thread` 和多线程爬取函数 `multi_threads` 来爬取 `urls` 中的页面,对比两个函数运行时间。 ```python import threading import time from news_spyder import urls, craw # 单线程爬取函数 def single_thread(): start_time = time.time() for url in urls: craw(url) end_time = time.time() return end_time - start_time # 多线程爬取函数 def multi_threads(): start_time = time.time() threads = [] for url in urls: th = threading.Thread(target=craw, args=(url,)) threads.append(th) th.start() for th in threads: th.join() end_time = time.time() return end_time - start_time if __name__ == "__main__": single_time = single_thread() multi_time = multi_threads() print(f"单线程爬取时间: {single_time} 秒") print(f"多线程爬取时间: {multi_time} 秒") ``` ### 解释全局解释器锁(GIL)的概念 全局解释器锁(GIL)是 Python 解释器(如 CPython)中的一个机制,它确保在同一时刻只有一个线程可以执行 Python 字节码。加入 GIL 主要的原因是为了降低程序开发的复杂度,例如 Python 解释器会自动定期进行内存回收,若没有 GIL,程序里的线程和 Python 解释器的垃圾回收线程并发运行时,可能会出现数据不一致的问题,为了解决类似问题,Python 解释器加了锁,即当一个线程运行时,其他线程都不能动,这是 Python 早期版本的遗留问题[^1]。 ### 阐述 GIL 对 Python 多线程的影响 由于 GIL 的存在,同一时刻只有一个线程能执行 Python 字节码,这意味着在 CPU 密集型任务中,Python 的多线程并不能真正实现并行计算,无法充分利用多核 CPU 的优势。例如在进行大量的数值计算时,多线程的性能可能和单线程相差不大,甚至由于线程切换的开销,性能还会有所下降。 ### 说明有 GIL 限制时多线程爬取程序比单线程爬取程序性能仍有较大提升的原因 在网络爬虫这种 I/O 密集型任务中,线程大部分时间都在等待网络请求的响应,而不是执行 Python 字节码。当一个线程发起网络请求后,会进入阻塞状态,此时 GIL 会释放,其他线程可以获得 GIL 并执行。因此,多线程可以在等待网络响应的时间里,让其他线程继续发起新的请求,从而提高整体的爬取效率,比单线程依次等待每个请求的响应要快得多。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值