Python多模态爬虫实战:文本、图片、PDF抓取与解析全攻略
2026/9/9 19:16:48 网站建设 项目流程

做爬虫这行干久了,你会发现一个特别扎心的现实:绝大多数人爬到今天,爬的还是纯文本。标题、正文、列表页,抓下来存进数据库就完事。可真正到了业务侧,老板要的是图片素材库,同事要的是PDF里的表格数据,客户要的是扫描件里的关键字段——文本爬虫在这时候连个屁都帮不上。

所以这次我打算把思路彻底打开,带你完整过一遍Python多模态爬虫的实战链路:文本、图片、PDF三种内容的抓取与智能解析一锅端。这篇内容适合已经写过简单爬虫、想往深走一步的Python开发者,也适合被“爬到的数据还得人工二次处理”折磨过的业务人员。我会把每一步的选型理由、避坑点、实操代码全部摊开讲,保证你能照着抄。

1. 为什么说“文本爬虫”不够用:多模态爬虫的真实需求

1.1 从一次失败的需求说起:只爬文本的坑

去年有个朋友找我,说他们团队要做竞品资料库,需求写得挺简单:把几个行业网站的新闻、报告、配图全部抓下来,按专题归档。我当时一听就问他:配图怎么存?URL还是二进制?报告——也就是PDF——里面的数据怎么提?他愣了半天,说以为爬虫就是拿到HTML解析一下文本就够了。

后来他们自己折腾了两周,文本倒是抓下来了,但图片全是外链,过几天源站一删全变死图;PDF更是只拿到了文件名,里面内容一个字没动。这个案例特别典型:很多人把“爬虫”和“抓HTML”画等号,忽略了目标数据本身的多样性。真实业务场景里,图片、PDF、表格、甚至音视频都是高价值信息载体,只抓文本等于把金矿挖出来又扔了一半。

多模态爬虫本质上不是三个爬虫,而是一条流水线:先识别页面里有什么类型的数据,再分别走文本解析、图片下载、文档解析这三条链路,最后统一归档、清洗、入库。这个思路一旦建立,后面遇到任何“奇怪”的数据源,你都不会慌。

1.2 多模态爬虫的架构认知:文本、图片、PDF三条链路

我习惯把多模态爬虫拆成四个模块:采集层、解析层、清洗层、存储层。采集层负责把HTML、图片二进制、PDF文件下载到本地;解析层针对不同内容用不同工具——BeautifulSoup/lxml处理HTML、pdfplumber/PyMuPDF处理PDF、OCR引擎处理扫描件;清洗层做去重、格式转换、内容校验;存储层决定最终落盘形式,可以是本地文件系统、数据库或对象存储。

这里有个关键认知:三条链路不是并列关系,而是依赖关系。比如PDF解析链路,第一步是爬虫先去把PDF文件下载下来,这本身就是一种“爬”。再比如图片链路,往往依赖文本链路先解析出页面里的img标签或懒加载数据。所以多模态爬虫不是三个独立脚本,而是用一套调度逻辑把采集、解析、清洗串起来。

选型上,我的原则是“够用就好”。requests加BeautifulSoup能解决的,不上Scrapy;pdfplumber能解决的,先不上复杂的OCR方案。很多新手一上来就堆一堆框架,结果排查问题的时候连日志都不知道看哪个,这完全没必要。后面每个部分我会给出具体的工具组合和理由。

2. 文本抓取:稳定高效的基础设施

2.1 请求与解析:requests加BeautifulSoup的黄金组合

文本抓取是整个多模态爬虫的地基。图片和PDF的入口往往藏在HTML里,你连HTML都拿不稳,后面全是空中楼阁。我的标准组合就是requests负责请求,BeautifulSoup加lxml解析器负责提取,简单直接,社区生态成熟,遇到问题随便搜都有答案。

先看一个最基础但也最容易出错的点:请求头。很多初学者写requests.get(url)一把梭,结果被对方服务器识别成爬虫,返回403或者一堆乱码。正确做法是带上完整的User-Agent,最好再带Referer和Accept-Language:

import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://example.com/", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } resp = requests.get("https://example.com/news", headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding # 解决乱码问题的关键

这里resp.encoding这行很多人会漏掉。服务器返回的Content-Type头里charset经常是错的,尤其国内很多站点,明明页面是UTF-8编码,却写gb2312,你按声明解码必然乱码。用apparent_encoding让requests根据字节内容自动推断编码,大多数情况下能直接解决乱码问题。

拿到HTML后,BeautifulSoup解析文本就一句话:

from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") title = soup.select_one("h1.article-title").get_text(strip=True) content = soup.select_one("div.article-content").get_text("\n", strip=True)

select_one支持CSS选择器,定位元素非常方便。这里有个实操技巧:目标站点如果结构经常微调,选择器不要写太死,用尽量短的路径,比如用class而不是用完整层级,因为前端一改样式,完整层级选择器就废了。

2.2 反爬处理与请求头伪装实战

文本抓取最烦的就是反爬。业内常见的反爬手段无非三种:请求头校验、频率限制、行为验证。请求头校验前面已经说了,频率限制则需要你在代码层面做限速。我个人习惯在每次请求之间加个随机延时:

import time import random time.sleep(random.uniform(1, 3))

这个延时的意义不只是“礼貌”,更重要的是让你的请求节奏接近真人浏览习惯。爬虫被识别很大程度上不是因为你发了多少请求,而是因为你请求的时间间隔太规律了——固定2秒一次,比人类还精准,一看就是机器。

会话保持也很重要。如果一个网站需要登录才能看到正文,或者登录后能拿到更完整的排版,那就用requests.Session():

session = requests.Session() login_data = {"username": "your_account", "password": "your_password"} session.post("https://example.com/login", data=login_data) resp = session.get("https://example.com/protected-page", headers=headers)

Session对象会自动保存Cookie,后续请求都会带上,这相当于你把“登录态”带在了身上。很多新手用两个独立的requests.get,发现登录后能访问的页面依旧403,就是因为Cookie没保持住。

再深一层,如果目标页面是JS动态渲染的,requests拿不到渲染后的HTML。我的优先级是:先找接口,再看能不能用Selenium或Playwright。很多网站就算页面是动态渲染的,它的数据也是通过XHR接口返回JSON,直接用requests请求那个接口,比开浏览器快十倍。怎么看接口?浏览器F12打开开发者工具,切到Network面板,刷新页面,找XHR类型的请求,看响应内容就知道了。

2.3 动态页面文本的抓取思路

如果实在找不到接口,只能上浏览器自动化。我用Playwright比Selenium多,因为 Playwright内置了等待机制、自动截图、还能拦截请求,性能也更好。核心用法很简单:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example.com", timeout=30000) page.wait_for_selector("div.article-content", timeout=10000) html = page.content() browser.close()

wait_for_selector是关键,它确保你要的元素已经渲染出来了才取HTML,避免拿到空白页。这里我踩过一个坑:headless模式(无头模式)下有些网站会检测WebDriver特征,返回验证码。解决办法是启动参数加上--disable-blink-features=AutomationControlled,去掉自动化标志。别问我为什么知道,都是被验证码逼出来的。

3. 图片采集:从URL到本地文件的完整链路

3.1 图片URL提取的三种方式

图片爬取的第一步是拿到图片的URL。最常见的当然是img标签的src属性,但真实页面远没这么老实。我总结下来有三种情况,对应三种提取手法。

第一种,常规img标签,直接抓src:

imgs = soup.select("img") urls = [img.get("src") for img in imgs if img.get("src")]

第二种,懒加载。现代网站为了省流量,图片的真实地址往往不放在src里,而是放在data-src、data-original、data-lazy之类的属性里,src放的是一张占位图。你直接抓src,下载下来全是1x1的透明像素。所以要从这些自定义属性里找:

urls = [] for img in soup.select("img"): url = img.get("data-src") or img.get("data-original") or img.get("src") if url: urls.append(url)

第三种,srcset响应式图片。现在很多高清图站会按屏幕宽度给不同尺寸的图,img标签里有srcset属性,里面是一堆“URL 宽度描述符”。这种情况我一般取最后一张最大的,因为爬虫要的就是高清原图:

import re def parse_srcset(srcset_value): candidates = re.findall(r"(\S+)\s+(\d+)w", srcset_value) if not candidates: return None candidates.sort(key=lambda x: int(x[1]), reverse=True) return candidates[0][0]

另外还有一个坑:HTML里的图片URL很多是相对路径,比如"/uploads/image.jpg",不是完整的http/https地址。这时需要用urljoin拼接:

from urllib.parse import urljoin full_url = urljoin(base_url, relative_url)

base_url一般取当前页面的URL,或者页面里link[rel=canonical]标签的值。

3.2 图片下载与去重、命名策略

拿到图片URL列表后,下载本身很简单,但有几个细节决定了你这批图片能不能真正“用起来”。

第一,下载要用stream模式,尤其是大图:

resp = requests.get(img_url, headers=headers, stream=True, timeout=15) resp.raise_for_status() with open(save_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk)

stream=True加iter_content分块写入的好处是,即使图片有几十MB,也不会一下子占满内存;而且下载到一半断了,已写入的部分还在,可以断点续传。

第二,命名不能拍脑袋。用原始文件名容易重名覆盖,用时间戳又丢失了来源信息。我推荐“来源站点+日期+哈希”的组合命名:

import hashlib from datetime import datetime # 内容哈希,用于去重 content_hash = hashlib.md5(resp.content).hexdigest() date_str = datetime.now().strftime("%Y%m%d") file_name = f"{site_name}_{date_str}_{content_hash[:12]}.jpg"

这里用内容哈希而不是URL哈希去重,原因很现实:同一个图片可能在不同页面有不同URL,但内容完全一样。内容哈希一旦重复,就说明这个文件已经下过了,直接跳过,能省下大量重复下载的带宽和时间。

第三,下载前最好先校验Content-Type或Content-Length,防止拿到错误页面或者空文件:

content_type = resp.headers.get("Content-Type", "") if "image" not in content_type: print(f"非图片内容,跳过: {img_url}") continue

3.3 图片质量校验与格式处理

下载完图片不等于拿到可用图片。我见过太多爬虫项目死在“图片打不开”上。原因是有些服务器返回的明明是HTML错误页,却用了200状态码,或者下载下来的文件扩展名是.jpg,实际是WebP格式。

所以下载后一定要做文件头校验。每种图片格式的二进制文件头都是固定的“魔数”,比如JPEG以FF D8开头,PNG是89 50 4E 47,GIF是47 49 46 38。用Pillow打开文件验证是最省事的方式:

from PIL import Image try: with Image.open(save_path) as img: img.verify() # 校验文件是否完整 except Exception as e: print(f"图片损坏: {save_path}, 错误: {e}") # 删除损坏文件 os.remove(save_path)

如果图片格式需要统一——比如业务要求一律转成RGB的JPEG——用Pillow转换即可:

from PIL import Image with Image.open(save_path) as img: rgb_img = img.convert("RGB") rgb_img.save(new_path, "JPEG", quality=90)

这里有个经验之谈:PNG图片如果有透明通道,直接存JPEG会变黑底,因为JPEG不支持Alpha通道。解决方法是先创建一个白色背景,再把图片贴上去:

from PIL import Image with Image.open(save_path) as img: if img.mode in ("RGBA", "LA", "P"): background = Image.new("RGB", img.size, (255, 255, 255)) background.paste(img, mask=img.split()[-1]) # 用Alpha通道作蒙版 background.save(new_path, "JPEG", quality=90)

这个细节很容易被忽略,但一旦遇到带透明底的产品图,效果天差地别。

4. PDF智能解析:文本、表格、内嵌图片一网打尽

4.1 PDF下载与文件校验

PDF爬取和图片爬取在“下载”这个环节逻辑差不多,但有一个独有的大坑:很多PDF资源不是直接以.pdf结尾的URL,而是通过一个下载接口返回,或者带着rid/token之类的参数。你在浏览器里点了一下能下载,直接用requests请求却得到一堆JSON。

解决办法是看两个东西:一是响应头里的Content-Type,真正的PDF下载响应头一般是application/pdf;二是Content-Disposition,它往往携带原始文件名:

resp = requests.get(pdf_url, headers=headers, stream=True, timeout=30) content_type = resp.headers.get("Content-Type", "") if "application/pdf" not in content_type: print(f"非PDF响应,实际类型: {content_type}") # 可能是JSON或HTML,需要进一步处理 print(resp.text[:500]) return # 从Content-Disposition提取文件名 import re disposition = resp.headers.get("Content-Disposition", "") filename_match = re.search(r"filename\*?=(?:UTF-8''|\")?([^\";]+)", disposition, re.IGNORECASE) if filename_match: filename = filename_match.group(1).strip("\"")

下载完同样要做文件头校验,PDF文件的魔数是%PDF(十六进制25 50 44 46)。这一步能挡掉很多“挂着PDF名头、实际是HTML错误页”的假文件,我处理过十几个PDF类的爬取需求,几乎每一家都有这种脏数据。

4.2 用pdfplumber提取文本与表格

PDF解析工具我首推pdfplumber。它比PyPDF2提取文本的质量更高,处理复杂排版的能力更强,而且表格提取是它的拿手好戏。安装也简单,pip install pdfplumber就行。

文本提取是最基本的操作:

import pdfplumber with pdfplumber.open("report.pdf") as pdf: for page in pdf.pages: text = page.extract_text() if text: print(text)

注意page.extract_text()有时会返回None,比如这一页全是图或者扫描件。这种情况可以先记录页码,后面统一走OCR,不要硬处理。

表格提取是pdfplumber的核心优势。它的原理是分析页面里的线条坐标,把被线条分隔的区域识别成单元格。用起来很简单:

with pdfplumber.open("report.pdf") as pdf: page = pdf.pages[0] tables = page.extract_tables() for table in tables: for row in table: print(row)

但实际效果和PDF本身的“良心程度”强相关。如果原PDF表格没有明确的线条,或者是不规则合并单元格,extract_table()出来可能是一堆错位数据。遇到这种情况我的处理方案是:先用extract_words()获取页面所有文字的坐标,再按x坐标聚类成列,自己拼表格。这招虽然麻烦,但对不规则表格特别管用。

4.3 提取PDF内嵌图片

很多PDF里的图片是分析报告里的图表、产品图、或者扫描版的插图,这些图片你光靠爬网页是拿不到的,得从PDF内部挖。这里我推荐用PyMuPDF(fitz),它提取PDF图片的速度和成功率都比pdfplumber好。pdfplumber虽然也能取图片,但效率和格式支持上都差点意思。

用PyMuPDF提取图片的核心代码:

import fitz doc = fitz.open("report.pdf") for page_num in range(len(doc)): page = doc[page_num] images = page.get_images(full=True) for img_index, img_info in enumerate(images): xref = img_info[0] base_image = doc.extract_image(xref) image_bytes = base_image["image"] ext = base_image["ext"] # jpeg, png, etc. file_name = f"pdf_p{page_num+1}_img{img_index+1}.{ext}" with open(file_name, "wb") as f: f.write(image_bytes)

这里的xref是PDF内部对象的引用编号,extract_image(xref)会拿到这个对象的原始二进制数据。这方法能把PDF里内嵌的图片完整抽出来,包括那些在页面上看不清、但实际是高分辨率的大图。

有一个隐蔽的坑:PDF里同一张图片可能被多个页面引用,你按页遍历会重复提取同一张图。所以提取后最好也做一次内容哈希去重,和前面图片爬取的思路一样。

4.4 OCR识别扫描版PDF

到这里还没完。现实里大量PDF是扫描件,就是每页都是一张图,虽然有PDF的外壳,但完全复制不出文字。你要能把“印刷体”变成“可检索文本”,就得靠OCR(光学字符识别)。

我常用的组合是pdf2image把PDF页面转成图片,再用pytesseract调用Tesseract引擎识别文字。完整的流程如下:

from pdf2image import convert_from_path import pytesseract from PIL import Image images = convert_from_path("scanned.pdf", dpi=300) for i, image in enumerate(images): text = pytesseract.image_to_string(image, lang="chi_sim+eng") print(f"第{i+1}页识别结果:\n{text}")

dpi参数很关键,我实测下来300dpi是性价比最高的档位。太低(150以下)识别率明显下降,特别是中文小字;太高(600以上)识别率提升有限,但内存和耗时成倍增长。

OCR识别的准入门槛是Tesseract的语言包。只装引擎不装中文语言包,识别中文就是一堆乱码。Debian/Ubuntu系统用apt install tesseract-ocr tesseract-ocr-chi-sim安装,Windows用户需要在安装时手动勾选中文语言。pytesseract的lang参数传"chi_sim+eng"表示中英文混合识别,这个顺序有讲究,中文在前英文在后,实测混合文档识别效果最好。

OCR效果优化的另一个重点是对图片做预处理。扫描件经常偏暗、有噪点、或者页面倾斜。我的经验是先用灰度化加二值化把前景背景分离,再用OpenCV做倾斜校正:

import cv2 import numpy as np # 转灰度 gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY) # 二值化,突出文字 _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 用PIL把处理后的数组转回Image再送给pytesseract processed_image = Image.fromarray(binary) text = pytesseract.image_to_string(processed_image, lang="chi_sim+eng")

OTSU大津法会自动计算二值化阈值,不用你手调,适合大部分扫描文档。这一套组合拳打下来,扫描版PDF的识别率能从“惨不忍睹”提升到“基本能读”,至少能用于检索和关键词提取。

5. 实战案例:一个综合爬虫脚本的完整实现

5.1 目标与设计思路

说了这么多理论,我们来跑一个完整的综合案例。假设目标是某个行业资讯站,页面列表里有新闻标题、正文、配图,偶尔还会有PDF格式的研究报告附件。我们的需求是:批量抓取最新20条资讯的文本内容、下载所有配图、解析PDF附件并将文本和表格内容存入本地数据库。

这个案例正好覆盖了三条链路,而且没有脱离真实业务。设计上我采用“队列驱动”的思路:第一步先抓列表页,把详情页URL存进队列;第二步逐个抓详情页,解析文本、图片、PDF链接;第三步分别下载图片和PDF;第四步对下载到的PDF进行文本和表格提取。

用队列驱动的优势是模块之间解耦。列表页挂了不影响详情页的处理,图片下载失败也不会导致PDF解析中断。你可以在不同环节分别加日志、加断点、加重试,整个脚本的可维护性完全不一样。

5.2 分步实现:文本、图片、PDF三条链路串起来

第一步,抓列表页,提取详情页链接:

import requests from bs4 import BeautifulSoup from urllib.parse import urljoin headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", } resp = requests.get("https://example-news.com/list", headers=headers, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") detail_urls = [] for a_tag in soup.select("h2.entry-title a"): href = a_tag.get("href") if href: detail_urls.append(urljoin("https://example-news.com", href))

第二步,抓详情页,同时提取文本、图片、PDF三种数据:

article_data = [] for url in detail_urls[:20]: item = {"url": url, "text": "", "images": [], "pdfs": []} resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") # 文本 title = soup.select_one("h1.entry-title") content = soup.select_one("div.entry-content") if title and content: item["text"] = f"{title.get_text(strip=True)}\n{content.get_text(chr(10), strip=True)}" # 图片 for img in content.select("img") if content else []: src = img.get("data-src") or img.get("src") if src: item["images"].append(urljoin(url, src)) # PDF链接 for a in soup.select("a[href$='.pdf'], a[href*='file=download']"): href = a.get("href") if href: item["pdfs"].append(urljoin(url, href)) article_data.append(item)

注意这里图片和PDF的提取都做了三种“兼容”:data-src和src都给到,PDF链接同时匹配了.pdf后缀和下载参数链接。这种写法看着啰嗦,但在真实站点上就是能多救回来一批数据。

第三步,下载图片和PDF:

import os import hashlib os.makedirs("downloads/images", exist_ok=True) os.makedirs("downloads/pdfs", exist_ok=True) seen_hashes = set() for item in article_data: for img_url in item["images"]: try: resp = requests.get(img_url, headers=headers, stream=True, timeout=15) resp.raise_for_status() if "image" not in resp.headers.get("Content-Type", ""): continue content = resp.content content_hash = hashlib.md5(content).hexdigest() if content_hash in seen_hashes: continue seen_hashes.add(content_hash) ext = "jpg" content_type = resp.headers.get("Content-Type", "") if "png" in content_type: ext = "png" filename = f"img_{content_hash[:16]}.{ext}" with open(f"downloads/images/{filename}", "wb") as f: f.write(content) except Exception as e: print(f"图片下载失败 {img_url}: {e}") for pdf_url in item["pdfs"]: try: resp = requests.get(pdf_url, headers=headers, stream=True, timeout=30) if "application/pdf" in resp.headers.get("Content-Type", ""): filename = f"doc_{hashlib.md5(pdf_url.encode()).hexdigest()[:16]}.pdf" with open(f"downloads/pdfs/{filename}", "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) except Exception as e: print(f"PDF下载失败 {pdf_url}: {e}")

第四步,对下载的PDF做智能解析:

import pdfplumber import csv for pdf_file in os.listdir("downloads/pdfs"): pdf_path = os.path.join("downloads/pdfs", pdf_file) with pdfplumber.open(pdf_path) as pdf: all_text = [] for page in pdf.pages: text = page.extract_text() if text: all_text.append(text) # 提取表格并保存为CSV for i, page in enumerate(pdf.pages[:3]): tables = page.extract_tables() for j, table in enumerate(tables): csv_name = f"table_{pdf_file}_{i+1}_{j+1}.csv" with open(csv_name, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerows(table)

5.3 主流程串联与工程化建议

实际项目里我不会把这些代码揉在一个脚本里。更合理的工程化做法是抽成几个函数或类:fetch负责请求,parse负责解析,download负责下载,pdf_parser负责PDF解析。每个模块独立日志、独立异常处理。主流程只需要调度它们:

1. fetch_list() 获取详情页URL列表 2. fetch_detail() 获取详情页并解析出文本、图片URL、PDF URL 3. download_images() 批量下载图片并去重 4. download_pdfs() 批量下载PDF 5. parse_pdfs() 解析PDF文本与表格 6. 汇总结果写入数据库或文件

这个架构的最大好处是容错性。图片下载失败不会阻塞PDF下载,PDF解析报错也不会影响已经存好的文本数据。每跑一次,日志里就能看到哪一步失败了多少条,下次可以针对性地补爬。

6. 常见问题与排查技巧实录

6.1 乱码与编码问题

抓文本遇到乱码,90%的情况是编码推断错误。我的排查顺序是:先打印resp.encoding和resp.apparent_encoding,看两者是否一致。不一致,以apparent_encoding为准。仍然乱码,就手动试几个常见编码:utf-8、gbk、gb2312、big5。

写文件时也容易出编码问题。Windows下open函数默认编码可能是gbk,写中文会报UnicodeEncodeError。所有文件读写都显式指定encoding="utf-8",这是最省心的做法。

6.2 图片或PDF下载失败

下载失败最常见的是超时。建议所有requests请求都设置timeout参数,并针对下载类请求开启重试。我实际操作中会写一个简单的重试装饰器:

import functools import time def retry(max_retries=3, delay=2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt == max_retries - 1: raise print(f"第{attempt+1}次失败,重试中... 错误: {e}") time.sleep(delay * (attempt + 1)) return None return wrapper return decorator @retry(max_retries=3) def download_file(url, save_path): resp = requests.get(url, headers=headers, stream=True, timeout=15) resp.raise_for_status() with open(save_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk)

对4xx状态码不要重试,那是请求本身有问题,重试一万次也没用,浪费带宽和时间。5xx和网络超时可以重试,因为服务器大概率只是临时抖动。

6.3 PDF解析异常

pdfplumber打开PDF偶尔会报错,常见的有两类:一是文件本身加密,二是文件损坏。加密PDF需要用PyPDF2先解密再交给pdfplumber:

from pypdf import PdfReader reader = PdfReader("encrypted.pdf") if reader.is_encrypted: reader.decrypt("")

空字符串密码表示尝试用空密码解密,很多“限制复制”的PDF实际上没有设置密码,只是加了权限限制,空密码就能解开。

另一个高频坑是pdfplumber内存占用过高。遇到几百页的大PDF,逐页处理完记得释放对象。用with语句管理文件句柄是基本操作,处理完一页就把page变量置空,避免引用堆积。

6.4 反爬升级后的应对思路

网站反爬是动态博弈,今天能爬的站点可能明天就加上了WAF。我的应对思路有三层:第一层是控制频率、伪装请求头、使用Session,这是基础;第二层是引入代理池和用户代理池,让请求来源看起来分散;第三层是采用浏览器自动化来处理验证码和复杂行为验证。

但更重要的是业务层面的策略:关键数据尽早持久化。我在爬虫脚本里每完成一条数据的解析,立刻写入本地文件或数据库,绝不攒在内存里最后统一写。因为一旦跑到一半被封IP或断网,内存里攒的数据全没了,只有你的心脏能感受到那种痛。

6.5 多模态数据的统一管理

最后提一个很多人忽略的点:多模态爬虫的数据管理。文本、图片、PDF各有各的存储形式,如果你不做统一管理,后续要用的时候会想骂人。

我的方案是建一个简单的清单文件,格式用JSON或SQLite都可以:

{ "id": "20250607001", "source_url": "https://example-news.com/article/123", "title": "行业报告:2025年市场趋势", "text_preview": "报告指出...", "text_file": "downloads/text/20250607001.txt", "image_files": ["downloads/images/img_ab12....jpg"], "pdf_file": "downloads/pdfs/doc_cdef....pdf", "pdf_text_file": "downloads/pdf_text/20250607001.txt", "pdf_tables": ["table_xxx_1_1.csv"] }

这样每条资讯就是一个完整的“数据包”,文本、图片、PDF解析结果都指到了具体文件。后续无论你是要训练AI模型、做资料库检索,还是直接给人看,都能快速定位到所有相关资源。这一步刚开始做的时候觉得多此一举,等数据量到几千条的时候你会庆幸当初多写了这一个JSON。

就我个人经验来说,多模态爬虫真正难的从来不是单个技术点,而是把所有环节串起来时那些意想不到的边界情况:图片URL是相对路径、PDF是被加密的扫描件、页面编码声明是错的、下载文件是假PDF……每一个都足以让脚本在凌晨三点悄悄崩溃。把上面这些坑都填平之后,你再回头跑批量任务,会发现省下的全是自己的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询