【问题标题】:Unable to boost the performance while parsing links from landing pages解析着陆页中的链接时无法提高性能
【发布时间】:2021-03-05 07:43:09
【问题描述】:

我正在尝试使用concurrent.futures 在以下脚本中实现多处理。问题是即使我使用concurrent.futures,性能仍然相同。它似乎对执行过程没有任何影响,这意味着它无法提高性能。

我知道如果我创建另一个函数并将从get_titles() 填充的链接传递给该函数以便从它们的内页中刮取标题,我可以使这个concurrent.futures 工作。但是,我希望使用我在下面创建的功能从登录页面获取标题。

我使用迭代方法而不是递归只是因为如果我选择后者,当调用超过 1000 次时,该函数将抛出递归错误。

这是我迄今为止尝试过的方式 (the site link that I've used within the script is a placeholder):

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import concurrent.futures as futures

base = 'https://stackoverflow.com'
link = 'https://stackoverflow.com/questions/tagged/web-scraping'

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36',
}

def get_titles(link):
    while True:
        res = requests.get(link,headers=headers)
        soup = BeautifulSoup(res.text,"html.parser")
        for item in soup.select(".summary > h3"):
            post_title = item.select_one("a.question-hyperlink").get("href")
            print(urljoin(base,post_title))

        next_page = soup.select_one(".pager > a[rel='next']")

        if not next_page: return
        link = urljoin(base,next_page.get("href"))

if __name__ == '__main__':
    with futures.ThreadPoolExecutor(max_workers=5) as executor:
        future_to_url = {executor.submit(get_titles,url): url for url in [link]}
        futures.as_completed(future_to_url)

问题:

如何在解析着陆页链接时提高性能?

编辑: 我知道我可以按照以下路线实现相同的目标,但 这不是我最初尝试的样子

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import concurrent.futures as futures

base = 'https://stackoverflow.com'
links = ['https://stackoverflow.com/questions/tagged/web-scraping?tab=newest&page={}&pagesize=30'.format(i) for i in range(1,5)]

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36',
}

def get_titles(link):
    res = requests.get(link,headers=headers)
    soup = BeautifulSoup(res.text,"html.parser")
    for item in soup.select(".summary > h3"):
        post_title = item.select_one("a.question-hyperlink").get("href")
        print(urljoin(base,post_title))

if __name__ == '__main__':
    with futures.ThreadPoolExecutor(max_workers=5) as executor:
        future_to_url = {executor.submit(get_titles,url): url for url in links}
        futures.as_completed(future_to_url)

【问题讨论】:

  • 请注意,您永远不会跳出while True: 循环,尝试向寻呼机的最后一页发出无限数量的请求。当没有next_page 时,您可能需要break
  • 我忘了包括这一行 if not next_page: return。我已经编辑了脚本。感谢@MatsLindh 的指点。
  • html.parser 更改为 lxml :) 你会看到性能提升

标签: python python-3.x web-scraping concurrent.futures


【解决方案1】:

既然您的爬虫使用线程,为什么不“派生”更多的工作人员来处理来自着陆页的后续 URL?

例如:

import concurrent.futures as futures
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

base = "https://stackoverflow.com"
links = [
    f"{base}/questions/tagged/web-scraping?tab=newest&page={i}&pagesize=30"
    for i in range(1, 5)
]

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36",
}


def threader(function, target, workers=5):
    with futures.ThreadPoolExecutor(max_workers=workers) as executor:
        jobs = {executor.submit(function, item): item for item in target}
        futures.as_completed(jobs)


def make_soup(page_url: str) -> BeautifulSoup:
    return BeautifulSoup(requests.get(page_url).text, "html.parser")


def process_page(page: str):
    s = make_soup(page).find("div", class_="grid--cell ws-nowrap mb8")
    views = s.getText() if s is not None else "Missing data"
    print(f"{page}\n{' '.join(views.split())}")


def make_pages(soup_of_pages: BeautifulSoup) -> list:
    return [
        urljoin(base, item.select_one("a.question-hyperlink").get("href"))
        for item in soup_of_pages.select(".summary > h3")
    ]


def crawler(link):
    while True:
        soup = make_soup(link)
        threader(process_page, make_pages(soup), workers=10)
        next_page = soup.select_one(".pager > a[rel='next']")
        if not next_page:
            return
        link = urljoin(base, next_page.get("href"))


if __name__ == '__main__':
    threader(crawler, links)

示例运行输出:

https://stackoverflow.com/questions/66463025/exporting-several-scraped-tables-into-a-single-csv-file
Viewed 19 times
https://stackoverflow.com/questions/66464511/can-you-find-the-parent-of-the-soup-in-beautifulsoup
Viewed 32 times
https://stackoverflow.com/questions/66464583/r-subscript-out-of-bounds-for-reading-an-html-link
Viewed 22 times

and more ...

理由:

本质上,您在初始方法中所做的是派生工作人员从搜索页面获取问题 URL。您不处理以下 URL。

我的建议是产生额外的工人来处理爬行工人收集的东西。

在你提到的问题中:

我希望从登录页面获取标题

这就是您的初始方法的调整版本试图通过利用 threader() 函数来实现的目标,该函数基本上是 ThreadPool() 的包装器。

【讨论】:

  • 这个答案与我在第二次尝试中显示的有什么不同?
【解决方案2】:
  1. 我希望您实际上没有像示例中所示产生单个线程:)
future_to_url = {executor.submit(get_titles,url): url for url in [link]}
  1. 您可以通过简单地使用 Session(这意味着重用连接,明确声明您可以再次查询此站点,直到会话对象似乎已明确完成或垃圾收集)而不是普通的requests.get() 调用。
  2. 确实,由于 GIL,python 线程并不擅长 CPU 密集型任务(如解析 HTML),您可能应该使用ProcessPoolExecutor 进行解析,并留给ThreadPoolExecutor(甚至单个thread) 仅处理 HTTP 请求。
  3. 毕竟,我强烈建议看一下aiohttp 作为requests 的非阻塞继承者(实际上是urllib,但是nvm) - 它建立在asyncio 之上,这样它就可以忘记线程安全问题,隐式锁等等......
    In [3]: import aiohttp, asyncio, time
       ...: 
       ...: t0 = time.monotonic()
       ...: 
       ...: 
       ...: async def do_the_request(session):
       ...:     async with session.get("http://www.google.com") as resp:
       ...:         content = await resp.read()
       ...: 
       ...: 
       ...: async def main():
       ...:     async with aiohttp.ClientSession() as session:
       ...:         tasks = {asyncio.create_task(do_the_request(session)) for _ in r
       ...: ange(100)}
       ...:         await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
       ...: 
       ...: 
       ...: t0 = time.monotonic()
       ...: asyncio.run(main())
       ...: t1 = time.monotonic()
       ...: print(f"Time elapsed: {t1 - t0:.3f}")
       Time elapsed: 0.571
    
       In [4]: 
    
  4. 最后,如果您在运行进程时达到 CPU 内核的限制(我敢打赌,您会提前耗尽带宽) - 只需使用 ProcessPoolExecutor 启动另一个进程即可。

【讨论】:

    【解决方案3】:

    Python 不能很好地处理并发,您可以通过让 Python 脚本处理单个链接来绕过它,然后使用 Bash 来实现并发。这是一个例子:

    python代码,姑且称之为crawlLink.py

    import requests
    from bs4 import BeautifulSoup
    from urllib.parse import urljoin
    import sys
    
    base = 'https://stackoverflow.com'
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36',
    }
    
    def get_titles(link):
        res = requests.get(link,headers=headers)
        soup = BeautifulSoup(res.text,"html.parser")
        for item in soup.select(".summary > h3"):
            post_title = item.select_one("a.question-hyperlink").get("href")
            print(urljoin(base,post_title))
    
    link = sys.argv[1]
    get_titles(link)
    

    bash 脚本:

    #! /bin/bash
    
    links=""
    
    for page in {1..5}
    do
        links="${links} https://stackoverflow.com/questions/tagged/web-scraping?tab=newest&page=${page}&pagesize=30"
    done
    
    echo "${links}" | xargs  -i --max-procs=5 bash -c '/usr/bin/python3 crawlLink.py "{}"'
    

    【讨论】:

      猜你喜欢
      • 2011-06-22
      • 1970-01-01
      • 2017-11-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-13
      • 1970-01-01
      • 2023-02-13
      相关资源
      最近更新 更多