一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Python如何通过URL链接判断文件类型

时间:2026-08-07 10:26:51 编辑:袖梨 来源:一聚教程网

在爬虫开发中经常遇到一类场景:拿到一批URL,有的是普通HTML网页需要解析文本,有的是PDF、Excel、压缩包等下载文件需要保存二进制。

Python如何通过URL链接判断文件类型

仅仅依靠URL后缀判断并不靠谱,很多动态下载接口没有真实文件后缀;直接全部请求完整内容又浪费带宽。本文分享一套兼顾性能与准确度的实现方案。

业务场景

爬虫抓取列表页拿到大量链接,我们需要做分流:

  1. 如果是HTML网页:请求页面,提取网页文本内容;
  2. 如果是文件资源(PDF/Word/Excel/ZIP等):直接下载保存二进制;
  3. 识别失败:标记为unknown,做兜底处理。

    同时文件类型字符串可以直接存入数据库,方便后续业务使用。

判断思路总览

一共有两种判断手段,组合使用兼顾性能与准确率:

1. URL后缀本地快速判断(无网络请求)

解析URL路径,提取末尾文件扩展名。

  1. 优点:零网络开销,速度极快
  2. 缺点:不可靠。动态下载接口/download?id=123没有后缀;也存在伪装后缀,例如xxx.pdf实际返回HTML页面。

适合做前置过滤,能识别就直接返回,减少不必要的HTTP请求。

2. HTTP响应头探测(权威判断)

优先发送HEAD请求,只获取响应头,不下载响应体

  1. Content-Type:资源MIME类型,用来区分是text/html网页还是application/pdf等二进制文件;
  2. Content‑Disposition:如果头部包含attachment,代表浏览器触发下载,里面还可以拿到服务器下发的真实文件名,这对无后缀动态下载接口最为关键。

部分服务器拒绝HEAD请求,此时降级为GET + stream=True,依然只读取响应头,不会拉取完整文件内容。

优先级顺序:Content‑Disposition中的真实文件名后缀 > MIME类型映射 > URL路径后缀兜底

完整可运行代码

import requestsimport refrom urllib.parse import urlparse, unquote# 需要识别的下载后缀集合(带点小写)DOWNLOAD_EXT = {    ".pdf", ".zip", ".rar", ".7z", ".doc", ".docx", ".xls", ".xlsx",    ".ppt", ".pptx", ".txt", ".csv", ".jpg", ".jpeg", ".png", ".gif",    ".bmp", ".mp4", ".mp3", ".tar", ".gz"}# 网页后缀集合HTML_EXT = {".html", ".htm", ".php", ".asp", ".aspx", ".jsp"}# MIME类型映射为文件后缀MIME_MAP = {    "application/pdf": "pdf",    "application/zip": "zip",    "application/x-zip-compressed": "zip",    "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",    "application/msword": "doc",    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",    "application/vnd.ms-excel": "xls",    "text/csv": "csv",    "image/jpeg": "jpg",    "image/png": "png",    "text/plain": "txt"}HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"}def check_resource_type(url: str, timeout=8) -> str:    """    判断URL资源类型    :param url: 待检测链接    :param timeout: 请求超时时间    :return: "html" | "pdf"/"docx"/"zip"... | "unknown"    """    # 第一步:本地解析URL后缀快速判断,不走网络    parse_result = urlparse(url)    path = parse_result.path.lower()    dot_index = path.rfind(".")    if dot_index != -1:        ext_with_dot = path[dot_index:]        ext_raw = path[dot_index + 1:]        if ext_with_dot in HTML_EXT:            return "html"        if ext_with_dot in DOWNLOAD_EXT:            return ext_raw    # 本地无法判断,发起HTTP头探测    try:        resp = requests.head(url, headers=HEADERS, timeout=timeout, allow_redirects=True)    except Exception:        # HEAD被拒绝,降级GET,stream=True不下载body        try:            resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True, stream=True)        except Exception:            return "unknown"    content_type = resp.headers.get("Content-Type", "").lower().split(";")[0].strip()    disposition = resp.headers.get("Content-Disposition", "")    # 判断是否HTML网页    if "text/html" in content_type:        return "html"    # 优先从Content‑Disposition提取服务器返回的文件名后缀(动态下载接口核心)    fn_match = re.search(r'filename*?=(?:["']?)([^"';n]+)', disposition)    if fn_match:        filename = fn_match.group(1)        # 处理RFC5987编码文件名 filename*=utf-8''xxx.pdf        if filename.startswith("utf-8''"):            filename = unquote(filename[7:])        filename = filename.lower()        fd = filename.rfind(".")        if fd != -1:            return filename[fd + 1:]    # MIME映射获取后缀    if content_type in MIME_MAP:        return MIME_MAP[content_type]    # 兜底再次使用URL后缀    if dot_index != -1:        return path[dot_index + 1:]    return "unknown"if __name__ == "__main__":    test_urls = [        "https://example.com/detail.html",        "https://example.com/report.pdf",        "https://example.com/api/download?id=100",        "https://example.com/no_suffix_page"    ]    for test_url in test_urls:        t = check_resource_type(test_url)        print(f"{test_url} --> {t}")

业务调用示例

返回的字符串可以直接存入数据库varchar字段。

url = "https://xxx.com/xxx"res_type = check_resource_type(url)if res_type == "html":    # 网页,请求获取文本    passelse:    # 文件,res_type为后缀,unknown时兜底为bin    suffix = res_type if res_type != "unknown" else "bin"    save_filename = f"output.{suffix}"    # 执行文件保存逻辑

返回值行为示例

URL示例返回结果业务动作
xxx/detail.htmlhtml解析网页文本
xxx/file.pdfpdf保存pdf文件
xxx/api/download?id=123,响应头携带attachment;filename="data.xlsx"xlsx保存xlsx文件
无后缀链接返回HTML页面html解析网页文本
二进制资源全部识别失败unknown保存为.bin

注意坑点

  1. 必须开启allow_redirects=True:很多下载链接会302重定向到真实资源地址,不跟随重定向拿到的是错误头信息。
  2. HEAD请求会被部分服务器405拒绝,代码已经做降级GET+stream=True,不会下载完整body,只会读取headers。
  3. 反爬:请求一定要带上User‑Agent,否则部分网站直接返回403。
  4. 网络异常:超时、连接失败直接返回unknown,不会抛出异常中断爬虫主流程。
  5. 局限性:极少数网站内容类型错误配置,会造成误判;生产环境建议做好异常捕获与日志记录。

性能优化提示

大量URL循环调用的时候,大部分普通HTML页面会在本地URL后缀阶段直接返回,不会产生网络请求

只有没有后缀的模糊链接,才会发送HTTP探测请求,大幅减少爬虫网络开销。

完整函数对外只有一个入口,返回简单字符串,方便数据库存储与后续分流处理。

知识扩展

通过 URL 判断文件类型,通常有三种策略,按准确度从低到高排列:① 扩展名② HTTP 响应头 Content-Type③ 文件内容魔数(Magic Number)。实际项目中,建议组合使用 ② 和 ③ 以获得最佳结果。

下面给出完整的实现方案。

准备工作:安装依赖

pip install requests python-magic

requests 用于发起 HTTP 请求,python-magic 用于读取文件魔数(底层依赖 libmagic,Linux/macOS 通常已自带,Windows 需额外安装)。

仅通过 URL 扩展名(快速,但不可靠)

import osfrom urllib.parse import urlparsedef get_type_by_extension(url: str) -> str:    """根据 URL 路径的扩展名猜测 MIME 类型"""    ext_to_mime = {        '.html': 'text/html',        '.htm': 'text/html',        '.txt': 'text/plain',        '.pdf': 'application/pdf',        '.jpg': 'image/jpeg',        '.jpeg': 'image/jpeg',        '.png': 'image/png',        '.gif': 'image/gif',        '.json': 'application/json',        '.xml': 'application/xml',        '.zip': 'application/zip',        '.mp4': 'video/mp4',        '.mp3': 'audio/mpeg',        # ... 更多映射    }    path = urlparse(url).path    ext = os.path.splitext(path)[1].lower()    return ext_to_mime.get(ext, 'application/octet-stream')

局限:无扩展名或动态 URL(如 /api/file?id=123)无法判断,且扩展名可伪造。

通过 HTTP 响应头 Content-Type(推荐,最常用)

import requestsdef get_type_by_content_type(url: str, timeout: int = 10) -> str:    """    发起 HEAD 请求(或 GET with stream)获取 Content-Type    """    try:        # 使用 HEAD 请求获取头部,不下载实际内容        resp = requests.head(url, timeout=timeout, allow_redirects=True)        content_type = resp.headers.get('Content-Type')        if content_type:            # 去掉 charset 等参数,只取主类型            return content_type.split(';')[0].strip().lower()        else:            # 若 HEAD 未返回 Content-Type,可改用 GET 只读前几个字节            resp = requests.get(url, timeout=timeout, stream=True)            content_type = resp.headers.get('Content-Type')            if content_type:                return content_type.split(';')[0].strip().lower()            return 'application/octet-stream'    except Exception as e:        return f'Error: {e}'

注意:部分服务器可能不支持 HEAD,或 HEAD 返回的 Content-Type 与 GET 不一致。若不可靠,可改用 stream=True 的 GET 请求,只读取头部分。

通过文件内容魔数(最准确,但需下载少量数据)

import requestsimport magicdef get_type_by_magic(url: str, timeout: int = 10, bytes_to_read: int = 2048) -> str:    """    下载前 N 个字节,用 libmagic 识别 MIME 类型    """    try:        resp = requests.get(url, timeout=timeout, stream=True)        # 只读取前 N 个字节        chunk = resp.raw.read(bytes_to_read)        # 使用 magic 检测        mime_type = magic.from_buffer(chunk, mime=True)        return mime_type or 'application/octet-stream'    except Exception as e:        return f'Error: {e}'

优点:即使 Content-Type 缺失或被伪造,依然能通过文件头真实识别,例如 PDF 以 %PDF 开头,JPEG 以 FF D8 开头。

综合方案:优先级策略

实际生产环境中,推荐按以下顺序判断:

  1. 优先使用响应头中的 Content-Type(服务端明确指定)。
  2. 若 Content-Type 为 application/octet-stream或缺失,则下载少量字节进行魔数检测
  3. 若魔数也无法识别,则回退到扩展名猜测
import osfrom urllib.parse import urlparseimport requestsimport magicdef guess_file_type(url: str, timeout: int = 10) -> str:    # 1. 尝试获取 Content-Type    try:        resp_head = requests.head(url, timeout=timeout, allow_redirects=True)        content_type = resp_head.headers.get('Content-Type')        if content_type:            main_type = content_type.split(';')[0].strip().lower()            if main_type != 'application/octet-stream':                return main_type    except:        pass    # 2. 若 Content-Type 不可靠或为 octet-stream,使用魔数    try:        resp_get = requests.get(url, timeout=timeout, stream=True)        chunk = resp_get.raw.read(2048)        mime = magic.from_buffer(chunk, mime=True)        if mime and mime != 'application/octet-stream':            return mime    except:        pass    # 3. 最后回退到扩展名    ext = os.path.splitext(urlparse(url).path)[1].lower()    ext_map = {        '.html': 'text/html', '.htm': 'text/html', '.txt': 'text/plain',        '.pdf': 'application/pdf', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',        '.png': 'image/png', '.gif': 'image/gif', '.json': 'application/json',        '.xml': 'application/xml', '.zip': 'application/zip',        '.mp4': 'video/mp4', '.mp3': 'audio/mpeg',    }    return ext_map.get(ext, 'application/octet-stream')# 示例print(guess_file_type('https://example.com/photo.jpg'))          # image/jpegprint(guess_file_type('https://example.com/download?file=123'))   # 实际会通过魔数识别

热门栏目