从零开始写一只爬虫 · 提速外挂

据我观察,在那个招聘网站上注册的公司大概有十万家。如果要是让前面那只虫子一个公司一个公司进行爬取的话,咱就按照 2 秒钟一个公司吧,20W 秒……55 小时。

咱们开多线程玩吧!

Python 的多线程也是蛮神奇的:和 Java 的多线程不同,Python 的多线程不需要继承什么东西、实现什么东西,不需要写特定的方法和类。真的是 “一句话实现多线程”。

self.THREADS = []
# 生成线程
for i in range(self.thread):
    self.THREADS.append(threading.Thread(target=self.run_thread))
# 开启线程
for i in range(self.thread):
    self.THREADS[i].start()
# 等待线程完成
for i in range(self.thread):
    self.THREADS[i].join()

简单暴力是吧?

线程安全

然而我们不得不考虑一个问题:线程安全,也就是同步啊什么的。

你看啊,根据我们以前的代码,如果队列不空,就取出队头,进行操作。那好,队列中现在只有一个元素,两个线程跑并发,同时发现了队列不空,先后取队头——瞬间感觉有什么不对了吧?

所以呢,我们只能对队列进行加锁,保证每一次只有一个线程来操作队列。

# 互斥锁
self.queueLock = threading.Lock()

于是我们的代码长度再次加长了:

import csv
import threading
import requests
from bs4 import BeautifulSoup


class CsvSaver:
    def __init__(self, path='./', filename='result.txt', method='w+'):
        self.path = path
        self.filename = filename
        self.method = method
        # 打开的文件
        self.FILE = None
        self.WRITER = None

    def open(self):
        self.FILE = open(self.path + self.filename, self.method)
        self.WRITER = csv.writer(self.FILE, quoting=csv.QUOTE_ALL)

    def write(self, data):
        '参数是一个 list'
        self.WRITER.writerow(data)
        self.FILE.flush()

    def close(self):
        self.FILE.close()


class Spider:
    def __init__(self, dataSaver, failSaver=None, thread=5):
        self.thread = thread
        self.queue = []
        # 爬行数据记录
        self.dataSaver = dataSaver
        # 失败 Log 记录
        self.failSaver = failSaver
        # 线程们
        self.THREADS = []
        # 互斥锁
        self.queueLock = threading.Lock()

    def addUrlBlock(self, start=100, end=200):
        for index in range(start, end):
            url = 'http://www.lagou.com/gongsi/' + str(index) + '.html'
            self.addUrl(url)

    def addUrl(self, url):
        # 得到锁
        self.queueLock.acquire()
        # 添加 URL
        self.queue.append(url)
        # 释放锁
        self.queueLock.release()

    def getUrl(self):
        while self.queue:
            url = None
            # 得到锁
            self.queueLock.acquire()
            # 获得 URL
            if self.queue:
                url = self.queue.pop(0)
            # 释放锁
            self.queueLock.release()
            yield url
        raise StopIteration

    def runThread(self):
        for url in self.getUrl():
            try:
                response = requests.get(url, timeout=5, allow_redirects=False)
                data = BeautifulSoup(response.text)
                name = data.h1.a.text.strip()
                location = data.find('ul', 'info_list_with_icon').find(attrs={'class': 'location'}).span.text
                self.dataSaver.write([name, location])
                print("Success %s" % url)
            except:
                self.failSaver.write(["Fail", url])

    def run(self):
        self.dataSaver.open()
        if self.failSaver:
            self.failSaver.open()

        self.THREADS = []
        # 生成线程
        for i in range(self.thread):
            self.THREADS.append(threading.Thread(target=self.runThread))
        # 开启线程
        for i in range(self.thread):
            self.THREADS[i].start()
        # 等待线程完成
        for i in range(self.thread):
            self.THREADS[i].join()

        self.dataSaver.close()
        if self.failSaver:
            self.failSaver.close()


if __name__ == '__main__':
    dataStore = CsvSaver(filename='result.csv')
    logStore = CsvSaver(filename='log.csv')
    spider = Spider(dataSaver=dataStore, failSaver=logStore)
    spider.addUrlBlock(100, 200)
    spider.run()
    print('全部完成')

我不知道这样写会不会发生 “读-写” 或者 “写-写” 死锁或者异常。但是就我的测试来看,好象是没有问题的。

(因为到现在还没有 “动态地向队列里面添加 url”)

实测结果

加入多线程后,这个可怜的招聘网站能在两个小时之内被我们爬取完毕。

爬虫的CPU、内存和网络消耗

但是!!Python 的多线程很诡异!不是你开几个线程,它就给你跑几个线程的。不像 Java 那样能把四个 CPU 使用率都跑到 100%。也就是说,Python 在这一点上输得很厉害。这个可怜的招聘网站可以用 Java 拿 200 个线程来刷,大约半个小时就能全部跑下来了。

留下评论