我是否正确设置了我的多线程网络抓取工具?

Am I setting my multithreading web scraper properly?

我正在努力提高我的网络抓取工具的速度,我有数千个网站需要从中获取信息。我正在尝试获取来自 Facebook 和 Yelp 的 Google 搜索网页中网站的评级和评级数量。我通常只使用 API,但因为我有大量要搜索的站点列表,而且时间很紧迫,Facebook 每小时的小请求限制使得使用他们的图表 API 不可行(我试过了...)。我的网站都在 Google 个搜索页面中。到目前为止我所拥有的(我提供了 8 个示例站点以实现可重复性):

from multiprocessing.dummy import Pool
import requests
from bs4 import BeautifulSoup

pools = Pool(8) #My computer has 8 cores
proxies = MY_PROXIES

#How I set up my urls for requests on Google searches. 
#Since each item has a "+" in between in a Google search, I have to format 
#my urls to copy it.

site_list = ['Golden Gate Bridge', 'Statue of Liberty', 'Empire State Building', 'Millennium Park', 'Gum Wall', 'The Alamo', 'National Art Gallery', 'The Bellagio Hotel']

urls = list(map(lambda x: "+".join(x.split(" ")), site_list)

def scrape_google(url_list):

    info = []

    for i in url_list:

        reviews = {'FB Rating': None,
                   'FB Reviews': None,
                   'Yelp Rating': None,
                   'Yelp Reviews': None}   

        request = requests.get(i, proxies=proxies, verify=False).text

        search = BeautifulSoup(search, 'lxml')
        results = search.find_all('div', {'class': 's'}) #Where the ratings roughly are

        for j in results: 

            if 'Rating' in str(j.findChildren()) and 'yelp' in str(j.findChildren()[1]):
                reviews['Yelp Rating'] = str(j.findChildren()).partition('Rating')[2].split()[1] #Had to brute-force get the ratings this way.
                reviews['Yelp Reviews'] = str(j.findChildren()).partition('Rating')[2].split()[3]

            elif 'Rating' in str(j.findChildren()) and 'facebook' in str(j.findChildren()[1]):
                reviews['FB Rating'] = str(j.findChildren()).partition('Rating')[2].split()[1]
                reviews['FB Reviews'] = str(j.findChildren()).partition('Rating')[2].split()[3]

    info.append(reviews)

return info

results = pools.map(scrape_google, urls)

我尝试了类似的方法,但我认为我得到了太多重复的结果。多线程会使 运行 更快吗?我对我的代码进行了诊断,以查看哪些部分占用的时间最多,到目前为止,获得请求是速率限制因素。

编辑:我刚刚试了一下,但出现以下错误:

Invalid URL 'h': No schema supplied. Perhaps you meant http://h?

我不明白问题出在哪里,因为如果我在没有多线程的情况下尝试我的 scrape_google 函数,它工作得很好(尽管非常非常慢),所以 url 有效性不应该是一个问题。

是的,多线程可能会使 运行 更快。

作为一个非常粗略的经验法则,您通常可以并行发出大约 8-64 个请求,只要其中不超过 2-12 个是发往同一主机的请求即可。因此,一种非常简单的应用方法是将您的所有请求扔到一个 concurrent.futures.ThreadPoolExecutor 中,比如说,有 8 个工人。

事实上,那是 the main example for ThreadPoolExecutor in the docs

(顺便说一句,您的计算机有 8 个内核这一事实与此无关。您的代码不受 CPU 约束,而是 I/O 约束。如果您并行执行 12 个请求,甚至其中的 500 个,在任何给定时刻,几乎所有线程都在等待 socket.recv 或某处类似的调用,阻塞直到服务器响应,因此它们不会使用您的 CPU。 )


但是:

I think I'm getting way too many duplicated result

解决这个问题可能比多线程更有帮助。当然,你可以两者都做。

根据您提供的有限信息,我不知道您的问题是什么,但是有一个非常明显的解决方法:保留一组您目前看到的所有内容。每当你得到一个新的URL,如果它已经在集合中,就把它扔掉而不是排队一个新的请求。


最后:

I would just use an API normally, but because I have a huge list of sites to search for and time is of the essence, Facebook's small request limits per hour make this not feasible

如果您试图绕过主要网站的速率限制,(a) 您可能违反了他们的条款和条件,并且 (b) 您几乎肯定会触发某种检测并获得你自己被屏蔽了。1


在你编辑过的问题中,你试图用 multiprocessing.dummy.Pool.map 来做到这一点,这很好——但你的论点是错误的。

您的函数接受一个 url 列表并对其进行循环:

def scrape_google(url_list):
    # ...
    for i in url_list:

但是你一次用一个 URL 调用它:

results = pools.map(scrape_google, urls)

这类似于使用内置 map 或列表理解:

results = map(scrape_google, urls)
results = [scrape_google(url) for url in urls]

如果您得到一个 URL 而不是它们的列表,但尝试将其用作列表,会发生什么情况?一个字符串是它的字符序列,所以你一个一个地遍历 URL 的字符,尝试下载每个字符,就好像它是 URL.

所以,你想改变你的功能,像这样:

def scrape_google(url):
    reviews = # …
    request = requests.get(url, proxies=proxies, verify=False).text
    # …
    return reviews

现在它需要一个 URL 和 returns 一组 URL 的评论。 pools.map 会用每个 URL 调用它,并返回一个可迭代的评论,每个 URL.

一个。

1.或者更有创意的东西。几年前有人在 SO 上发布了一个问题,该网站显然发送了损坏的响应,这些响应似乎是专门为典型的刮板正则表达式浪费指数 CPU...