Python中的多线程/ BeautifulSoup抓取并没有加速

时间:2014-08-18 22:42:25

标签: multithreading python-2.7 parallel-processing web-scraping beautifulsoup

我有一个csv文件(" SomeSiteValidURLs.csv"),其中列出了我需要抓取的所有链接。代码正在运行,将通过csv中的url,抓取信息并记录/保存在另一个csv文件中(" Output.csv")。但是,由于我计划在网站的大部分内容(对于> 10,000,000页)进行此操作,因此速度非常重要。对于每个链接,爬行并将信息保存到csv大约需要1秒,这对于项目的大小来说太慢了。所以我已经整合了多线程模块,令我惊讶的是它根本没有加速,它仍然需要1个人链接。我做错什么了吗?还有其他方法可以加快处理速度吗?

没有多线程:

import urllib2
import csv
from bs4 import BeautifulSoup
import threading

def crawlToCSV(FileName):

    with open(FileName, "rb") as f:
        for URLrecords in f:

            OpenSomeSiteURL = urllib2.urlopen(URLrecords)
            Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
            OpenSomeSiteURL.close()

            tbodyTags = Soup_SomeSite.find("tbody")
            trTags = tbodyTags.find_all("tr", class_="result-item ")

            placeHolder = []

            for trTag in trTags:
                tdTags = trTag.find("td", class_="result-value")
                tdTags_string = tdTags.string
                placeHolder.append(tdTags_string)

            with open("Output.csv", "ab") as f:
                writeFile = csv.writer(f)
                writeFile.writerow(placeHolder)

crawltoCSV("SomeSiteValidURLs.csv")

使用多线程:

import urllib2
import csv
from bs4 import BeautifulSoup
import threading

def crawlToCSV(FileName):

    with open(FileName, "rb") as f:
        for URLrecords in f:

            OpenSomeSiteURL = urllib2.urlopen(URLrecords)
            Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
            OpenSomeSiteURL.close()

            tbodyTags = Soup_SomeSite.find("tbody")
            trTags = tbodyTags.find_all("tr", class_="result-item ")

            placeHolder = []

            for trTag in trTags:
                tdTags = trTag.find("td", class_="result-value")
                tdTags_string = tdTags.string
                placeHolder.append(tdTags_string)

            with open("Output.csv", "ab") as f:
                writeFile = csv.writer(f)
                writeFile.writerow(placeHolder)

fileName = "SomeSiteValidURLs.csv"

if __name__ == "__main__":
    t = threading.Thread(target=crawlToCSV, args=(fileName, ))
    t.start()
    t.join()

1 个答案:

答案 0 :(得分:11)

你没有正确地并行化这个。你真正想做的是让你的for循环中的工作在许多工人中同时发生。现在你将所有工作移动到一个后台线程中,它同步完成整个事情。这根本不会改善性能(实际上只会略微伤害它)。

这是一个使用ThreadPool来并行化网络操作和解析的示例。尝试一次跨多个线程写入csv文件是不安全的,所以我们将返回已经写回父级的数据,并让父级将所有结果写入文件末尾。

import urllib2
import csv
from bs4 import BeautifulSoup
from multiprocessing.dummy import Pool  # This is a thread-based Pool
from multiprocessing import cpu_count

def crawlToCSV(URLrecord):
    OpenSomeSiteURL = urllib2.urlopen(URLrecord)
    Soup_SomeSite = BeautifulSoup(OpenSomeSiteURL, "lxml")
    OpenSomeSiteURL.close()

    tbodyTags = Soup_SomeSite.find("tbody")
    trTags = tbodyTags.find_all("tr", class_="result-item ")

    placeHolder = []

    for trTag in trTags:
        tdTags = trTag.find("td", class_="result-value")
        tdTags_string = tdTags.string
        placeHolder.append(tdTags_string)

    return placeHolder


if __name__ == "__main__":
    fileName = "SomeSiteValidURLs.csv"
    pool = Pool(cpu_count() * 2)  # Creates a Pool with cpu_count * 2 threads.
    with open(fileName, "rb") as f:
        results = pool.map(crawlToCSV, f)  # results is a list of all the placeHolder lists returned from each call to crawlToCSV
    with open("Output.csv", "ab") as f:
        writeFile = csv.writer(f)
        for result in results:
            writeFile.writerow(result)

请注意,在Python中,线程实际上只会加速I / O操作 - 由于GIL,CPU绑定操作(如解析/搜索BeautifulSoup正在执行)实际上无法并行执行线程,因为一次只有一个线程可以执行基于CPU的操作。因此,您仍然可能看不到您希望采用这种方法的速度。当您需要在Python中加速CPU绑定操作时,您需要使用多个进程而不是线程。幸运的是,您可以轻松地看到此脚本如何使用多个进程而不是多个线程执行;只需将from multiprocessing.dummy import Pool更改为from multiprocessing import Pool即可。不需要进行其他更改。

修改

如果您需要将其扩展为包含10,000,000行的文件,则需要稍微调整此代码 - Pool.map将传递给它的可迭代转换为列表,然后再将其发送出去对于你的工人来说,这显然不会很好地与10,000,000个参赛名单一起工作;在记忆中拥有整件事可能会让你的系统陷入困境。将所有结果存储在列表中的问题相同。相反,您应该使用Pool.imap

  

imap(func,iterable [,chunksize])

     

map()的lazier版本。

     

chunksize参数与map()使用的参数相同   方法。对于使用较大值的chunksize可以进行很长时间的迭代   使用默认值1来快速完成工作。

if __name__ == "__main__":
    fileName = "SomeSiteValidURLs.csv"
    FILE_LINES = 10000000
    NUM_WORKERS = cpu_count() * 2
    chunksize = FILE_LINES // NUM_WORKERS * 4   # Try to get a good chunksize. You're probably going to have to tweak this, though. Try smaller and lower values and see how performance changes.
    pool = Pool(NUM_WORKERS)

    with open(fileName, "rb") as f:
        result_iter = pool.imap(crawlToCSV, f)
    with open("Output.csv", "ab") as f:
        writeFile = csv.writer(f)
        for result in result_iter:  # lazily iterate over results.
            writeFile.writerow(result)

使用imap,我们永远不会将所有f同时放入内存,也不会立即将所有结果存储在内存中。我们记忆中最多的是chunksizef,这应该更容易管理。