如何从响应头的Content-Disposition中获取文件名
我正在用Mechanize下载一个文件,响应头里面有一串字符:
Content-Disposition: attachment; filename=myfilename.txt
有没有什么简单的标准方法可以获取那个文件名的值?我现在想到的是这个:
filename = f[1]['Content-Disposition'].split('; ')[1].replace('filename=', '')
不过这看起来像是个快速而不太干净的解决方案。
4 个回答
在顶层回答中提到的 cgi
模块将在 Python 3.13 中被弃用。可以查看 PEP594 了解更多信息。
官方推荐的替代方案(详见引用的 PEP)是使用 email.message.Message
,这是一个标准库。
下面是一个示例,展示如何从 content-disposition
头中获取 filename
的值:
>>> from email.message import Message
>>> content_disposition_header = 'attachment; filename=myfilename.txt'
>>> msg = Message()
>>> msg['content-disposition'] = content_disposition_header
>>> msg.get_filename()
'myfilename.txt'
注意:上面的代码在所有版本的 Python >= 2.2
中应该都能正常工作。email
包是在 Python 2.2 中引入的,那时 Message
类的 API 与现在的形式是一样的,特别是在 __setitem__
和 get_filename
方法方面。
这些正则表达式是根据RFC 6266的语法制定的,不过做了一些修改,以便可以处理没有“处置类型”的头信息,比如说“Content-Disposition: filename=example.html”。
简单来说,就是可以有一个可选的“处置类型”,后面跟着一个或多个“处置参数”。
它可以处理带引号和不带引号的文件名参数,还能把引号里的内容去掉,比如说“filename=\"foo\\\"bar\"”会变成“foo"bar”。
它还可以处理“filename*”这种扩展参数,并且如果同时有“filename*”和“filename”这两个参数,它会优先使用“filename*”,不管它们在头信息中出现的顺序如何。
它会去掉文件夹名称的信息,比如说“/etc/passwd”会变成“passwd”。如果没有提供文件名参数(或者头信息,或者参数值是空字符串),它会默认使用URL路径中的基本名称。
这里面的“token”和“qdtext”正则表达式是根据RFC 2616的语法来的,而“mimeCharset”和“valueChars”正则表达式是根据RFC 5987的语法来的,还有“language”正则表达式是根据RFC 5646的语法来的。
import re, urllib
from os import path
from urlparse import urlparse
# content-disposition = "Content-Disposition" ":"
# disposition-type *( ";" disposition-parm )
# disposition-type = "inline" | "attachment" | disp-ext-type
# ; case-insensitive
# disp-ext-type = token
# disposition-parm = filename-parm | disp-ext-parm
# filename-parm = "filename" "=" value
# | "filename*" "=" ext-value
# disp-ext-parm = token "=" value
# | ext-token "=" ext-value
# ext-token = <the characters in token, followed by "*">
token = '[-!#-\'*+.\dA-Z^-z|~]+'
qdtext='[]-~\t !#-[]'
mimeCharset='[-!#-&+\dA-Z^-z]+'
language='(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}(?:-[A-Za-z]{3}){,2})?|[A-Za-z]{4,8})(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\d{3}))(?:-(?:[\dA-Za-z]{5,8}|\d[\dA-Za-z]{3}))*(?:-[\dA-WY-Za-wy-z](?:-[\dA-Za-z]{2,8})+)*(?:-[Xx](?:-[\dA-Za-z]{1,8})+)?|[Xx](?:-[\dA-Za-z]{1,8})+|[Ee][Nn]-[Gg][Bb]-[Oo][Ee][Dd]|[Ii]-[Aa][Mm][Ii]|[Ii]-[Bb][Nn][Nn]|[Ii]-[Dd][Ee][Ff][Aa][Uu][Ll][Tt]|[Ii]-[Ee][Nn][Oo][Cc][Hh][Ii][Aa][Nn]|[Ii]-[Hh][Aa][Kk]|[Ii]-[Kk][Ll][Ii][Nn][Gg][Oo][Nn]|[Ii]-[Ll][Uu][Xx]|[Ii]-[Mm][Ii][Nn][Gg][Oo]|[Ii]-[Nn][Aa][Vv][Aa][Jj][Oo]|[Ii]-[Pp][Ww][Nn]|[Ii]-[Tt][Aa][Oo]|[Ii]-[Tt][Aa][Yy]|[Ii]-[Tt][Ss][Uu]|[Ss][Gg][Nn]-[Bb][Ee]-[Ff][Rr]|[Ss][Gg][Nn]-[Bb][Ee]-[Nn][Ll]|[Ss][Gg][Nn]-[Cc][Hh]-[Dd][Ee]'
valueChars = '(?:%[\dA-F][\dA-F]|[-!#$&+.\dA-Z^-z|~])*'
dispositionParm = '[Ff][Ii][Ll][Ee][Nn][Aa][Mm][Ee]\s*=\s*(?:({token})|"((?:{qdtext}|\\\\[\t !-~])*)")|[Ff][Ii][Ll][Ee][Nn][Aa][Mm][Ee]\*\s*=\s*({mimeCharset})\'(?:{language})?\'({valueChars})|{token}\s*=\s*(?:{token}|"(?:{qdtext}|\\\\[\t !-~])*")|{token}\*\s*=\s*{mimeCharset}\'(?:{language})?\'{valueChars}'.format(**locals())
try:
m = re.match('(?:{token}\s*;\s*)?(?:{dispositionParm})(?:\s*;\s*(?:{dispositionParm}))*|{token}'.format(**locals()), result.headers['Content-Disposition'])
except KeyError:
name = path.basename(urllib.unquote(urlparse(url).path))
else:
if not m:
name = path.basename(urllib.unquote(urlparse(url).path))
# Many user agent implementations predating this specification do not
# understand the "filename*" parameter. Therefore, when both "filename"
# and "filename*" are present in a single header field value, recipients
# SHOULD pick "filename*" and ignore "filename"
elif m.group(8) is not None:
name = urllib.unquote(m.group(8)).decode(m.group(7))
elif m.group(4) is not None:
name = urllib.unquote(m.group(4)).decode(m.group(3))
elif m.group(6) is not None:
name = re.sub('\\\\(.)', '\1', m.group(6))
elif m.group(5) is not None:
name = m.group(5)
elif m.group(2) is not None:
name = re.sub('\\\\(.)', '\1', m.group(2))
else:
name = m.group(1)
# Recipients MUST NOT be able to write into any location other than one to
# which they are specifically entitled
if name:
name = path.basename(name)
else:
name = path.basename(urllib.unquote(urlparse(url).path))
首先,使用 mechanize 来获取头部的值,然后用 Python 自带的 cgi 模块来解析这个头部。
为了演示:
>>> import mechanize
>>> browser = mechanize.Browser()
>>> response = browser.open('http://example.com/your/url')
>>> info = response.info()
>>> header = info.getheader('Content-Disposition')
>>> header
'attachment; filename=myfilename.txt'
然后可以解析头部的值:
>>> import cgi
>>> value, params = cgi.parse_header(header)
>>> value
'attachment'
>>> params
{'filename': 'myfilename.txt'}
params
是一个简单的字典,所以你只需要用 params['filename']
来获取你需要的内容。无论文件名是否用引号包裹,都没关系。