在爬虫开发中经常遇到一类场景:拿到一批url,有的是普通html网页需要解析文本,有的是pdf、excel、压缩包等下载文件需要保存二进制。
仅仅依靠url后缀判断并不靠谱,很多动态下载接口没有真实文件后缀;直接全部请求完整内容又浪费带宽。本文分享一套兼顾性能与准确度的实现方案。
业务场景
爬虫抓取列表页拿到大量链接,我们需要做分流:
- 如果是html网页:请求页面,提取网页文本内容;
- 如果是文件资源(pdf/word/excel/zip等):直接下载保存二进制;
- 识别失败:标记为unknown,做兜底处理。
同时文件类型字符串可以直接存入数据库,方便后续业务使用。
判断思路总览
一共有两种判断手段,组合使用兼顾性能与准确率:
1. url后缀本地快速判断(无网络请求)
解析url路径,提取末尾文件扩展名。
- 优点:零网络开销,速度极快。
- 缺点:不可靠。动态下载接口
/download?id=123没有后缀;也存在伪装后缀,例如xxx.pdf实际返回html页面。
适合做前置过滤,能识别就直接返回,减少不必要的http请求。
2. http响应头探测(权威判断)
优先发送head请求,只获取响应头,不下载响应体。
content-type:资源mime类型,用来区分是text/html网页还是application/pdf等二进制文件;content‑disposition:如果头部包含attachment,代表浏览器触发下载,里面还可以拿到服务器下发的真实文件名,这对无后缀动态下载接口最为关键。
部分服务器拒绝head请求,此时降级为get + stream=true,依然只读取响应头,不会拉取完整文件内容。
优先级顺序:content‑disposition中的真实文件名后缀 > mime类型映射 > url路径后缀兜底
完整可运行代码
import requests
import re
from 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":
# 网页,请求获取文本
pass
else:
# 文件,res_type为后缀,unknown时兜底为bin
suffix = res_type if res_type != "unknown" else "bin"
save_filename = f"output.{suffix}"
# 执行文件保存逻辑
返回值行为示例
| url示例 | 返回结果 | 业务动作 |
|---|---|---|
xxx/detail.html | html | 解析网页文本 |
xxx/file.pdf | pdf | 保存pdf文件 |
xxx/api/download?id=123,响应头携带attachment;filename="data.xlsx" | xlsx | 保存xlsx文件 |
| 无后缀链接返回html页面 | html | 解析网页文本 |
| 二进制资源全部识别失败 | unknown | 保存为.bin |
注意坑点
- 必须开启allow_redirects=true:很多下载链接会302重定向到真实资源地址,不跟随重定向拿到的是错误头信息。
- head请求会被部分服务器405拒绝,代码已经做降级get+stream=true,不会下载完整body,只会读取headers。
- 反爬:请求一定要带上user‑agent,否则部分网站直接返回403。
- 网络异常:超时、连接失败直接返回
unknown,不会抛出异常中断爬虫主流程。 - 局限性:极少数网站内容类型错误配置,会造成误判;生产环境建议做好异常捕获与日志记录。
性能优化提示
大量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 os
from urllib.parse import urlparse
def 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 requests
def 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 requests
import magic
def 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 开头。
综合方案:优先级策略
实际生产环境中,推荐按以下顺序判断:
- 优先使用响应头中的
content-type(服务端明确指定)。 - 若 content-type 为
application/octet-stream或缺失,则下载少量字节进行魔数检测。 - 若魔数也无法识别,则回退到扩展名猜测。
import os
from urllib.parse import urlparse
import requests
import magic
def 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/jpeg
print(guess_file_type('https://example.com/download?file=123')) # 实际会通过魔数识别到此这篇关于python如何通过url链接判断文件类型的文章就介绍到这了,更多相关python判断文件类型内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论