Python下载多个页面链接上的文件

6
我正在努力从这个 网站 下载所有的 PGN
我认为我必须使用 urlopen 打开每个 URL ,然后通过访问每场比赛底部附近的下载按钮,使用 urlretrieve 下载每个 PGN 。我是否需要为每个游戏创建一个新的 BeautifulSoup 对象?我也不确定 urlretrieve 是如何工作的。
import urllib
from urllib.request import urlopen, urlretrieve, quote
from bs4 import BeautifulSoup

url = 'http://www.chessgames.com/perl/chesscollection?cid=1014492'
u = urlopen(url)
html = u.read().decode('utf-8')

soup = BeautifulSoup(html, "html.parser")
for link in soup.find_all('a'):
    urlopen('http://chessgames.com'+link.get('href'))
2个回答

6
没有简短的答案来回答你的问题。我将展示给你一个完整的解决方案,并对这段代码进行注释。
首先,导入必要的模块:
from bs4 import BeautifulSoup
import requests
import re

接下来,获取索引页面并创建BeautifulSoup对象:
req = requests.get("http://www.chessgames.com/perl/chesscollection?cid=1014492")
soup = BeautifulSoup(req.text, "lxml")

我强烈建议使用lxml解析器,而不是常见的html.parser。之后,您应该准备游戏链接列表:

pages = soup.findAll('a', href=re.compile('.*chessgame\?.*'))

你可以通过搜索包含“chessgame”单词的链接来实现。 现在,你应该准备一个函数来为你下载文件:
def download_file(url):
    path = url.split('/')[-1].split('?')[0]
    r = requests.get(url, stream=True)
    if r.status_code == 200:
        with open(path, 'wb') as f:
            for chunk in r:
                f.write(chunk)

最后一个神奇的步骤,是重复所有前面的步骤,为文件下载器准备链接:

host = 'http://www.chessgames.com'
for page in pages:
    url = host + page.get('href')
    req = requests.get(url)
    soup = BeautifulSoup(req.text, "lxml")
    file_link = soup.find('a',text=re.compile('.*download.*'))
    file_url = host + file_link.get('href')
    download_file(file_url)

首先搜索链接描述中包含“下载”文本的链接,然后构建完整的URL - 连接主机名和路径,最后下载文件。
我希望您可以无需更改使用此代码!

顺便问一下,为什么我应该使用requests而不是urllib?你能用urllib做同样的事情吗? - Monty
当然,你可以像在问题中那样使用 urllib。但是使用 requests 是一个好的实践。你可以在这里获取更多信息。 - Roman Mindlin
这两行代码是做什么的?req = requests.get(url) soup = BeautifulSoup(req.text, "lxml") 我知道第一行发出请求,但不确定具体含义。在BS构造函数中,.text方法是用来做什么的?我了解它可以获取所有子字符串,但不确定在这种情况下有什么含义。 - Monty
req = requests.get(url)会返回一个类型为requests.models.Response的对象。它是一个类,包含http响应本身以及许多属性和方法来处理它。其中一个属性是text,它允许您获取接收到的网页的纯html代码。您应该将此html代码作为参数传递给BeautifuSoup对象的构造函数(soup = BeautifulSoup(req.text, "lxml")),以获取BeautifulSoup对象 - 一个类,其方法让您搜索特定的标记和其他内容。 - Roman Mindlin

6

被接受的答案非常棒,但任务令人尴尬地并行;没有必要一次一个地检索这些子页面和文件。这个答案展示了如何加快速度。

第一步是在向单个主机发送多个请求时使用requests.Session()。引用requests文档中的高级用法:会话对象

会话对象允许您跨请求保留某些参数。它还在从会话实例进行的所有请求之间保留cookie,并将使用urllib3的{{link1:连接池}}。因此,如果您对同一主机进行多个请求,则将重用基础TCP连接,这可能会导致显着的性能提高(请参见{{link2:HTTP持久连接}})。
接下来,可以使用asyncio、多进程或多线程来并行处理工作量。每种方法各有优缺点,应根据具体任务进行基准测试和分析以确定最合适的方法。{{link3:此页面}}提供了三种方法的很好的示例。
为了本文的目的,我将展示多线程。由于任务大多是I / O绑定的,即等待响应的请求,所以GIL的影响不应该太大成为瓶颈。当一个线程被I / O阻塞时,它可以让出CPU时间给另一个线程解析HTML或执行其他CPU绑定的工作。

这是代码:

import os
import re
import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor

def download_pgn(task):
    session, host, page, destination_path = task
    response = session.get(host + page)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "lxml")
    game_url = host + soup.find("a", text="download").get("href")
    filename = re.search(r"\w+\.pgn", game_url).group()
    path = os.path.join(destination_path, filename)
    response = session.get(game_url, stream=True)
    response.raise_for_status()

    with open(path, "wb") as f:
        for chunk in response.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)

def main():
    host = "http://www.chessgames.com"
    url_to_scrape = host + "/perl/chesscollection?cid=1014492"
    destination_path = "pgns"
    max_workers = 8

    if not os.path.exists(destination_path):
        os.makedirs(destination_path)

    with requests.Session() as session:
        session.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
        response = session.get(url_to_scrape)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "lxml")
        pages = soup.find_all("a", href=re.compile(r".*chessgame\?.*"))
        tasks = [
            (session, host, page.get("href"), destination_path) 
            for page in pages
        ]

        with ThreadPoolExecutor(max_workers=max_workers) as pool:
            pool.map(download_pgn, tasks)

if __name__ == "__main__":
    main()

我在这里使用了response.iter_content,虽然对于如此小的文本文件而言是不必要的,但这是一种通用方法,可以以内存友好的方式处理更大的文件。

粗略基准测试结果(第一个请求是瓶颈):

最大工作进程数 是否使用会话? 秒数
1 126
1 111
8 24
8 22
32 16

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接