如何使用Python requests获取PDF文件名?

67

我正在使用Python requests 库从网络上获取PDF文件。这个工作很好,但是我现在也想要原始文件名。如果我在Firefox中访问PDF文件并单击下载,它已经定义了要保存PDF的文件名。我该如何获取这个文件名呢?

例如:

import requests
r = requests.get('http://www.researchgate.net/profile/M_Gotic/publication/260197848_Mater_Sci_Eng_B47_%281997%29_33/links/0c9605301e48beda0f000000.pdf')
print r.headers['content-type']  # prints 'application/pdf'

我检查了r.headers里面有没有什么有趣的东西,但是里面没有文件名。实际上我希望有像r.filename这样的东西。

有人知道如何使用requests库获取已下载PDF文件的文件名吗?


有趣的是,我本来想说,“显然是0c9605301e48beda0f000000.pdf”(因为这是请求中的文件名),但幸运的是我决定先测试一下。而且FireFox想把它保存为“Mater Sci Eng B47(1997)33.pdf”。 - Jongware
1
你是如何检查头部的?文件名确实存在,content-disposition : inline; filename="Mater Sci Eng B47 (1997) 33.pdf"。顺便提一句,许多PDF文档中都嵌入了标题,但并非所有文档都有,并且如果PDF文件以二进制形式存在,则可能很难访问。 - PM 2Ring
9个回答

99

在HTTP头中指定了content-disposition。因此,要提取名称,您需要执行以下操作:

import re
d = r.headers['content-disposition']
fname = re.findall("filename=(.+)", d)[0]

通过正则表达式(re模块)从字符串中提取的名称。


1
如果文件名编码为utf8,则此方法将无法正常工作。有什么建议吗? - Tony Abou-Assaleh
7
findall返回匹配的结果列表。你需要像这样使用索引来获取其中的一个值:fname = re.findall("filename=(.+)", d)[0] - Nilpo
5
尝试使用 "filename=\"(.+)\"" 来去除引号。 - sheunglaili
1
有时候在头部信息中并没有提供预期的文件名,特别是在社交媒体CDN链接中。这只是一个小问题,你可以自己构建基本名称(也许解析URL以获取你想要使用的根文件名),然后通过类似于 resp.headers['Content-Type'].split('/')[-1] 的方式确定正确的扩展名作为后缀。 - weezilla
1
你可以使用 cgi.parse_headeremail.header.decode_header 来正确解析文件名。 - sshilovsky
显示剩余3条评论

21

在其他答案的基础上,这是我如何处理的。如果没有Content-Disposition头部,我会从下载链接中解析它:

建立在其他答案的基础上,这是我的做法。如果不包含Content-Disposition头部,我会从下载URL中解析它:

import re
import requests
from requests.exceptions import RequestException


url = 'http://www.example.com/downloads/sample.pdf'

try:
    with requests.get(url) as r:

        fname = ''
        if "Content-Disposition" in r.headers.keys():
            fname = re.findall("filename=(.+)", r.headers["Content-Disposition"])[0]
        else:
            fname = url.split("/")[-1]

        print(fname)
except RequestException as e:
    print(e)

可能有更好的解析URL字符串的方法,但为了简单起见,我不想涉及更多的库。


2
我建议在else子句中调用urllib.parse.unquote,这样您就不会在文件名中得到%20 - Noumenon

11

显然,对于这个特定的资源它在:

r.headers['content-disposition']

虽然我不确定是否总是这样。


并非所有响应都包含“content-disposition”头,但根据其中一条评论,似乎在这种情况下它们是可用的。 - Abhinav Sood

9

获取Content-Disposition中的文件名的Python3简单实现:

import requests
response = requests.get(<your-url>)
print(response.headers.get("Content-Disposition").split("filename=")[1])

注意,如果没有“Content-Disposition”头,请小心! - Cyril N.
1
可以使用类似 response.headers.get("Content-Disposition","filename=output.bin") 的方式来处理缺失的头部信息。 - ThisGuyCantEven

5

这是最强大的选项,因为它删除了可选的引号。 - moi

3

使用urllib.request代替requests,因为这样你可以执行urllib.request.urlopen(...).headers.get_filename(), 这比其他一些答案更安全,原因如下:

如果[Content-Disposition]头部没有filename参数,则此方法会回退到在Content-Type头部上查找name参数。

之后,更安全的做法是额外回退到URL中的文件名,就像另一个答案所做的那样。


2
根据文档,既不需要使用 Content-Disposition 也不需要其 filename 属性。而且,我在互联网上检查了许多链接,没有找到带有 Content-Disposition 头的响应。所以,在大多数情况下,我不会太依赖它,只是从请求 URL 中提取这个信息(注意:我从 req.url 获取它,因为可能存在重定向,我们想要获取“真实”的文件名)。我使用 werkzeug,因为它看起来更强大,并且可以处理带引号和不带引号的文件名。最终,我得出了这个解决方案(适用于 Python 3.8 及以上版本):
from urllib.parse import urlparse

import requests
import werkzeug


def get_filename(url: str):
    try:
        with requests.get(url) as req:
            if content_disposition := req.headers.get("Content-Disposition"):
                param, options = werkzeug.http.parse_options_header(content_disposition)
                if param == 'attachment' and (filename := options.get('filename')):
                    return filename

            path = urlparse(req.url).path
            name = path[path.rfind('/') + 1:]
            return name
    except requests.exceptions.RequestException as e:
        raise e

我使用 pytestrequests_mock 编写了一些测试:

import pytest
import requests
import requests_mock

from main import get_filename

TEST_URL = 'https://pwrk.us/report.pdf'


@pytest.mark.parametrize(
    'headers,expected_filename',
    [
        (
                {'Content-Disposition': 'attachment; filename="filename.pdf"'},
                "filename.pdf"
        ),
        (
                # The string following filename should always be put into quotes;
                # but, for compatibility reasons, many browsers try to parse unquoted names that contain spaces.
                # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives
                {'Content-Disposition': 'attachment; filename=filename with spaces.pdf'},
                "filename with spaces.pdf"
        ),
        (
                {'Content-Disposition': 'attachment;'},
                "report.pdf"
        ),
        (
                {'Content-Disposition': 'inline;'},
                "report.pdf"
        ),
        (
                {},
                "report.pdf"
        )
    ]
)
def test_get_filename(headers, expected_filename):
    with requests_mock.Mocker() as m:
        m.get(TEST_URL, text='resp', headers=headers)
        assert get_filename(TEST_URL) == expected_filename


def test_get_filename_exception():
    with requests_mock.Mocker() as m:
        m.get(TEST_URL, exc=requests.exceptions.RequestException)
        with pytest.raises(requests.exceptions.RequestException):
            get_filename(TEST_URL)

0
使用Python的标准库:
from email.message import EmailMessage

msg = EmailMessage()
msg["Content-Disposition"] = response.headers.get("Content-Disposition")
filename = msg.get_filename()

像其他人说的那样,文件名在"Content-Disposition"头部中。
以前,解析文件名的方法是使用"cgi"标准库模块,但自从"py311"以后,它已经被弃用了。
目前推荐的解析方法是使用"email"模块,它也是标准库的一部分。
参考资料:

0
这是一个有趣的挑战,因为它提出了更多新问题而非答案。 这是我在火狐浏览器中看到的原始链接,明显是一个“PDF”文件。如果我接受给定的名称,它会自动保存为“MaterSciEngB47199733.pdf”。

enter image description here

FireFox使用的名称可能与Chrome不同,因此对于给定的示例,我们使用Edge进行了相同链接的测试,并获得了非常相似的响应。
下载的文件来自历史链接 http://www.researchgate.net/profile/M_Gotic/publication/260197848_Mater_Sci_Eng_B47_%281997%29_33/links/0c9605301e48beda0f000000.pdf。 该链接目前已更新和更正为https,并重定向至https://www.researchgate.net/profile/Marijan-Gotic/publication/260197848_Mater_Sci_Eng_B47_1997_33/links/0c9605301e48beda0f000000/Mater-Sci-Eng-B47-1997-33.pdf
然而,无论是FireFox还是MS Edge都会显示带有标签的PII:S0921-5107(96)02041-7,并且不提供保存其已知的"文件名" Mater-Sci-Eng-B47-1997-33.pdf,而是一个更短的MaterSciEngB47199733.pdf。
由于用户想要"真实名称",他们可以随意手动编辑回到Mater-Sci-Eng-B47-1997-33.pdf或Mater Sci Eng B47 (1997) 33.pdf,因为无论叫什么名字,Curl都一样好。

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