给出一些通用的爬虫模板,可以快速干活
通用爬虫模板-支持动态页面和静态页面爬取
Python
"""
智能爬虫 - 自动判断页面类型并选择合适的爬取方式
静态页面:使用 requests + BeautifulSoup
动态页面:使用 Playwright,支持手动登录
"""
import os
import re
import time
import logging
from typing import Optional, List, Dict, Tuple
from dataclasses import dataclass
import requests
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
# ============== 配置 ==============
OUTPUT_DIR = "output"
@dataclass
class CrawlerConfig:
"""爬虫配置"""
timeout: int = 10
max_retries: int = 3
retry_delay: float = 1.0
request_delay: float = 1.0
user_agents: tuple = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
)
# ============== 日志 ==============
def setup_logging():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
# ============== 页面类型检测 ==============
class PageTypeDetector:
"""页面类型检测器"""
FRAMEWORK_SIGNATURES = {
"React": [r'data-reactroot', r'react-dom', r'__REACT_DEVTOOLS'],
"Vue": [r'data-v-[a-f0-9]+', r'__vue__', r'Vue\.'],
"Angular": [r'ng-version', r'ng-app', r'angular\.module'],
"Next.js": [r'__NEXT_DATA__', r'_next/static'],
"SPA": [r'window\.__INITIAL_STATE__', r'bundle\..*\.js', r'main\..*\.js'],
}
DYNAMIC_SIGNATURES = [
r'document\.write', r'innerHTML', r'fetch\(',
r'axios\.', r'XMLHttpRequest', r'\.ajax\(',
]
# 登录页面特征
LOGIN_SIGNATURES = [
r'login', r'signin', r'sign-in', r'登录', r'登陆',
r'auth', r'authenticate', r'password', r'密码',
r'<form.*action.*login', r'type="password"',
r'请登录', r'请先登录', r'需要登录',
]
def __init__(self, timeout: int = 10):
self.timeout = timeout
def fetch_html(self, url: str) -> Optional[str]:
"""获取原始HTML"""
try:
headers = {"User-Agent": "Mozilla/5.0 Chrome/120.0.0.0"}
response = requests.get(url, headers=headers, timeout=self.timeout)
return response.text
except Exception:
return None
def check_login_page(self, html: str) -> Tuple[bool, List[str]]:
"""检测是否为登录页面"""
soup = BeautifulSoup(html, "html.parser")
reasons = []
text_content = html.lower()
# 检查登录关键词
for pattern in self.LOGIN_SIGNATURES:
if re.search(pattern, text_content, re.IGNORECASE):
reasons.append(f"发现登录特征: {pattern}")
# 检查是否有密码输入框
password_inputs = soup.find_all("input", type="password")
if password_inputs:
reasons.append("存在密码输入框")
# 检查标题包含登录关键词
title = soup.find("title")
if title:
title_text = title.text.lower()
if any(kw in title_text for kw in ["login", "登录", "signin", "sign in", "auth"]):
reasons.append(f"标题包含登录关键词: {title.text}")
# 检查登录表单
login_forms = soup.find_all("form")
for form in login_forms:
form_text = str(form).lower()
if any(kw in form_text for kw in ["login", "登录", "signin", "password", "auth"]):
reasons.append("存在登录表单")
break
is_login = len(reasons) >= 2
return is_login, reasons
def detect(self, url: str) -> Tuple[bool, float, Optional[str], bool, List[str]]:
"""
检测页面类型
Returns:
(is_dynamic, confidence, framework, is_login_page, login_reasons)
"""
html = self.fetch_html(url)
if not html:
return False, 0.0, None, False, ["无法获取页面"]
confidence = 0.0
framework = None
# 检查是否为登录页面
is_login, login_reasons = self.check_login_page(html)
# 检查JS框架
for fw, patterns in self.FRAMEWORK_SIGNATURES.items():
for pattern in patterns:
if re.search(pattern, html, re.IGNORECASE):
framework = fw
confidence += 0.7
break
# 检查动态脚本
for pattern in self.DYNAMIC_SIGNATURES:
if re.search(pattern, html):
confidence += 0.2
# 检查内容结构
soup = BeautifulSoup(html, "html.parser")
body = soup.find("body")
if body:
body_text = body.get_text(strip=True)
if len(body_text) < 50:
confidence += 0.8
# 检查空div比例
divs = soup.find_all("div")
empty_divs = [d for d in divs if not d.get_text(strip=True)]
if divs and len(empty_divs) / len(divs) > 0.5:
confidence += 0.5
is_dynamic = confidence >= 0.5
return is_dynamic, min(confidence, 1.0), framework, is_login, login_reasons
# ============== 静态页面爬虫 ==============
class StaticCrawler:
"""静态网页爬虫"""
def __init__(self, config: Optional[CrawlerConfig] = None):
self.config = config or CrawlerConfig()
self.session = requests.Session()
self.logger = logging.getLogger(self.__class__.__name__)
def _get_headers(self) -> Dict[str, str]:
import random
return {
"User-Agent": random.choice(self.config.user_agents),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
def fetch(self, url: str) -> Optional[requests.Response]:
"""发送HTTP请求"""
import random
for attempt in range(1, self.config.max_retries + 1):
try:
self.logger.info(f"请求: {url} (尝试 {attempt})")
response = self.session.get(
url,
headers=self._get_headers(),
timeout=self.config.timeout
)
response.raise_for_status()
self.logger.info(f"成功: {url}")
return response
except requests.RequestException as e:
self.logger.warning(f"失败: {e}")
if attempt < self.config.max_retries:
time.sleep(self.config.retry_delay * attempt)
return None
def crawl(self, url: str) -> Tuple[Optional[str], Optional[str]]:
"""爬取并返回(html, title)"""
response = self.fetch(url)
if response:
soup = BeautifulSoup(response.text, "html.parser")
title = soup.find("title")
title_text = title.text.strip() if title else ""
return response.text, title_text
return None, None
# ============== 动态页面爬虫 ==============
class DynamicCrawler:
"""动态网页爬虫(Playwright)"""
def crawl(self, url: str, headless: bool = False) -> Tuple[Optional[str], Optional[str], Optional[str]]:
"""
爬取动态页面
Args:
url: 目标URL
headless: 是否无头模式
Returns:
(html, text, title)
"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=headless)
context = browser.new_context()
page = context.new_page()
try:
print(f"正在访问: {url}")
page.goto(url, wait_until="networkidle")
if not headless:
# 有头模式:等待用户确认
print("\n" + "=" * 50)
print("浏览器已打开:")
print(" - 如果需要登录,请在浏览器中手动登录")
print(" - 登录完成后,按回车继续爬取")
print("=" * 50)
input("\n按回车键继续...")
print("\n正在爬取页面内容,请稍候...")
page.goto(url, wait_until="networkidle")
time.sleep(2)
print("页面加载完成,提取数据...")
else:
print("等待页面渲染...")
time.sleep(3)
print("提取数据...")
html = page.content()
text = page.evaluate("() => document.body.innerText")
title = page.title()
print("数据提取完成!")
return html, text, title
except Exception as e:
print(f"爬取失败: {e}")
return None, None, None
finally:
browser.close()
# ============== 文件保存 ==============
def sanitize_filename(name: str) -> str:
"""清理文件名"""
name = re.sub(r'[<>:"/\\|?*\n\r\t]', '', name)
name = name.strip().replace(' ', '_')
if len(name) > 100:
name = name[:100]
return name or f"page_{int(time.time())}"
def ensure_output_dir():
"""确保输出目录存在"""
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
def save_static_result(html: str, title: str, url: str):
"""保存静态页面结果"""
ensure_output_dir()
filename = sanitize_filename(title)
# 保存HTML
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
with open(html_path, "w", encoding="utf-8") as f:
f.write(f"<!-- 来源: {url} -->\n")
f.write(html)
print(f"HTML已保存: {html_path}")
# 保存TXT
soup = BeautifulSoup(html, "html.parser")
text_content = []
text_content.append(f"来源URL: {url}")
text_content.append(f"页面标题: {title}")
text_content.append("=" * 50)
for h in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
if h.text.strip():
text_content.append(f"\n[{h.name}] {h.text.strip()}")
text_content.append("\n【正文】")
for p in soup.find_all("p"):
if p.text.strip():
text_content.append(p.text.strip())
links = soup.find_all("a", href=True)
if links:
text_content.append("\n【链接】")
for link in links[:20]:
text_content.append(f"{link.text.strip()[:30]} -> {link['href']}")
txt_path = os.path.join(OUTPUT_DIR, f"{filename}.txt")
with open(txt_path, "w", encoding="utf-8") as f:
f.write("\n".join(text_content))
print(f"TXT已保存: {txt_path}")
def save_dynamic_result(html: str, text: str, title: str, url: str):
"""保存动态页面结果"""
ensure_output_dir()
filename = sanitize_filename(title)
# 保存HTML
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
with open(html_path, "w", encoding="utf-8") as f:
f.write(f"<!-- 来源: {url} -->\n")
f.write(html)
print(f"HTML已保存: {html_path}")
# 保存TXT
txt_path = os.path.join(OUTPUT_DIR, f"{filename}.txt")
with open(txt_path, "w", encoding="utf-8") as f:
f.write(f"来源URL: {url}\n")
f.write(f"页面标题: {title}\n")
f.write("=" * 50 + "\n")
f.write(text)
print(f"TXT已保存: {txt_path}")
# ============== 主程序 ==============
def get_urls_input() -> List[str]:
"""获取用户输入的URL列表"""
print("\n请输入要爬取的网址(每行一个,空行开始爬取,q退出):")
lines = []
while True:
line = input().strip()
if line.lower() == "q":
return None
if line == "":
break
lines.append(line)
urls = []
for line in lines:
if not line.startswith(("http://", "https://")):
line = "https://" + line
urls.append(line)
return urls
def get_headless_mode() -> bool:
"""获取运行模式"""
print("\n选择运行模式:")
print(" 1 - 有头模式(显示浏览器,可手动登录)")
print(" 2 - 无头模式(后台运行)")
choice = input("请选择 (1/2,默认1): ").strip()
return choice == "2"
def get_crawler_choice(is_dynamic: bool, is_login: bool, login_reasons: List[str]) -> str:
"""
让用户确认或选择爬取方式
Returns:
'static', 'dynamic', 或 'skip'
"""
print("\n请选择爬取方式:")
print(" 1 - 静态爬取")
print(" 2 - 动态爬取")
print(" 3 - 跳过此页面")
# 自动检测建议
if is_login:
print(f"\n检测到登录页面特征:")
for reason in login_reasons[:3]:
print(f" - {reason}")
print(" 建议: 选择动态爬取,手动登录")
default = "2"
elif is_dynamic:
print(" 建议: 检测为动态页面,建议动态爬取")
default = "2"
else:
print(" 建议: 检测为静态页面,建议静态爬取")
default = "1"
choice = input(f"\n请选择 (1/2/3,默认{default}): ").strip()
if choice == "3":
return "skip"
elif choice == "2":
return "dynamic"
elif choice == "1":
return "static"
else:
return "dynamic" if default == "2" else "static"
def main():
"""主程序"""
setup_logging()
detector = PageTypeDetector(timeout=15)
static_crawler = StaticCrawler(CrawlerConfig(timeout=15))
dynamic_crawler = DynamicCrawler()
headless = None # 运行模式,首次动态爬取时询问
print("=" * 50)
print("智能爬虫 - 自动判断页面类型")
print("=" * 50)
while True:
urls = get_urls_input()
if urls is None:
print("再见!")
break
if not urls:
print("未输入有效网址")
continue
print(f"\n共 {len(urls)} 个网址,开始处理...")
for i, url in enumerate(urls, 1):
print(f"\n{'=' * 50}")
print(f"[{i}/{len(urls)}] {url}")
# 检测页面类型
print("\n检测页面特征...")
is_dynamic, confidence, framework, is_login, login_reasons = detector.detect(url)
# 显示检测结果
if is_login:
print("检测到登录页面特征!")
else:
page_type = "动态页面" if is_dynamic else "静态页面"
print(f"检测结果: {page_type} (置信度 {confidence:.0%}%)")
if framework:
print(f"框架: {framework}")
# 让用户确认或手动选择
crawler_type = get_crawler_choice(is_dynamic, is_login, login_reasons)
if crawler_type == "skip":
print("跳过此页面")
continue
elif crawler_type == "dynamic":
# 首次使用动态爬虫时询问运行模式
if headless is None:
headless = get_headless_mode()
# 使用动态爬虫
html, text, title = dynamic_crawler.crawl(url, headless=headless)
if html:
save_dynamic_result(html, text, title or "", url)
# 检查结果是否为登录页面
if "登录" in text or "login" in text.lower() or "请先登录" in text or "signin" in text.lower():
print("\n爬取结果仍包含登录提示!")
print(" 1 - 重新爬取(手动登录)")
print(" 2 - 保存当前结果并继续")
retry_choice = input("请选择 (1/2,默认1): ").strip()
if retry_choice != "2":
# 重新爬取,强制有头模式手动登录
print("\n重新打开浏览器,请在浏览器中手动登录...")
html, text, title = dynamic_crawler.crawl(url, headless=False)
if html:
save_dynamic_result(html, text, title or "", url)
else:
print("重新爬取失败")
else:
print("爬取失败")
elif crawler_type == "static":
# 使用静态爬虫
html, title = static_crawler.crawl(url)
if html:
save_static_result(html, title or "", url)
else:
print("爬取失败")
# 延迟
if i < len(urls):
time.sleep(1)
print(f"\n全部完成!结果保存在 {OUTPUT_DIR}/ 目录")
if __name__ == "__main__":
main()动态渲染页面爬虫模板
- 基于 playwright 方式
- 通用cookie 配置
json
[
{
"name": "Cookie",
"value": "cookie value, copy from browser",
"domain": "example.com",
"path": "/"
}
]- 通用脚本
Python
"""
动态网页爬取脚本 - 使用 Playwright
支持交互式输入多个 URL 进行爬取
使用前请先配置 cookies.json
"""
from playwright.sync_api import sync_playwright
import json
import time
from pathlib import Path
import re
def parse_cookie_string(cookie_str: str, domain: str) -> list:
"""
解析 HTTP Cookie 字符串格式,转换为 Playwright 所需的 cookie 列表
支持格式: "name1=value1; name2=value2; name3=value3"
"""
cookies = []
for item in cookie_str.split(";"):
item = item.strip()
if "=" in item:
name, value = item.split("=", 1)
cookies.append({
"name": name.strip(),
"value": value.strip(),
"domain": domain,
"path": "/"
})
return cookies
def load_cookies(cookie_file: str = "cookies.json", default_domain: str = None) -> list:
"""
加载 cookies 文件,支持两种格式:
1. 标准 Playwright 格式:[{name, value, domain, path}, ...]
2. Cookie 字符串格式:[{name: "Cookie", value: "name1=val1; name2=val2", ...}]
"""
with open(cookie_file, "r") as f:
data = json.load(f)
cookies = []
for item in data:
item_domain = item.get("domain", default_domain)
# 如果是 Cookie 字符串格式(name="Cookie",value 是拼接的字符串)
if item.get("name") == "Cookie" and item.get("value"):
if item_domain:
parsed = parse_cookie_string(item["value"], item_domain)
cookies.extend(parsed)
else:
# 标准 Playwright 格式,直接使用
cookie = {
"name": item.get("name"),
"value": item.get("value"),
"domain": item_domain,
"path": item.get("path", "/")
}
if cookie["name"] and cookie["value"] and cookie["domain"]:
cookies.append(cookie)
return cookies
def sanitize_filename(name: str) -> str:
"""清理文件名,移除不安全字符"""
# 移除或替换不安全字符
name = re.sub(r'[<>:"/\\|?*\n\r\t]', '_', name)
# 移除前后空格
name = name.strip()
# 限制长度
if len(name) > 100:
name = name[:100]
# 如果为空,返回默认值
return name or "untitled"
def get_page_title(page) -> str:
"""从页面提取标题"""
# 优先尝试 <title> 标签
title = page.title()
if title:
return sanitize_filename(title)
# 尝试 <h1> 标签
try:
h1 = page.locator("h1").first.text_content()
if h1:
return sanitize_filename(h1)
except:
pass
# 尝试 meta og:title
try:
og_title = page.locator('meta[property="og:title"]').get_attribute("content")
if og_title:
return sanitize_filename(og_title)
except:
pass
return "untitled"
def get_filename_from_url(url: str) -> str:
"""从 URL 生成安全的文件名(作为备选)"""
parsed = urlparse(url)
path = parsed.path.strip("/").replace("/", "_")
if not path:
path = parsed.netloc.replace(".", "_")
return sanitize_filename(path or "page")
def scrape_page(page, url: str, wait_time: int = 3) -> tuple:
"""
爬取单个页面内容
Args:
page: Playwright page 对象
url: 目标页面 URL
wait_time: 等待动态内容的时间(秒)
Returns:
(html, text, title) 页面 HTML、纯文本内容和标题
"""
print(f"正在访问: {url}")
page.goto(url, wait_until="networkidle")
print("等待页面渲染...")
time.sleep(wait_time)
try:
page.wait_for_selector("body", timeout=10000)
except Exception as e:
print(f"等待超时: {e}")
print("提取页面内容...")
html = page.content()
text = page.evaluate("() => document.body.innerText")
title = get_page_title(page)
print(f"页面标题: {title}")
return html, text, title
def save_content(html: str, text: str, filename: str, output_dir: str = "output"):
"""保存爬取的内容"""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# 保存 HTML
html_file = output_path / f"{filename}.html"
with open(html_file, "w", encoding="utf-8") as f:
f.write(html)
print(f"HTML 已保存到: {html_file}")
# 保存纯文本
text_file = output_path / f"{filename}.txt"
with open(text_file, "w", encoding="utf-8") as f:
f.write(text)
print(f"文本已保存到: {text_file}")
def input_urls() -> list:
"""交互式输入 URL"""
print("\n请输入要爬取的 URL(每行一个,输入空行结束):")
urls = []
while True:
url = input().strip()
if not url:
break
if not url.startswith("http"):
url = "https://" + url
urls.append(url)
return urls
def main():
COOKIE_FILE = "cookies.json"
# 检查 cookies 文件
if not Path(COOKIE_FILE).exists():
print(f"提示: 未找到 {COOKIE_FILE},将无 cookie 模式运行")
cookies = []
else:
try:
cookies = load_cookies(COOKIE_FILE)
print(f"已加载 {len(cookies)} 个 cookies")
except Exception as e:
print(f"加载 cookies 失败: {e}")
cookies = []
# 交互式输入 URL
urls = input_urls()
if not urls:
print("未输入任何 URL,退出")
return
# 选择模式
print("\n选择运行模式:")
print(" 1. 有头模式(显示浏览器,方便调试)")
print(" 2. 无头模式(后台运行,速度快)")
mode = input("请输入选项 (1/2,默认 1): ").strip()
headless = mode == "2"
# 爬取多个页面
print(f"\n开始爬取 {len(urls)} 个页面...")
with sync_playwright() as p:
browser = p.chromium.launch(headless=headless)
context = browser.new_context()
if cookies:
context.add_cookies(cookies)
page = context.new_page()
for i, url in enumerate(urls, 1):
print(f"\n[{i}/{len(urls)}] 处理: {url}")
try:
html, text, title = scrape_page(page, url)
# 使用标题作为文件名,如果标题为空则用 URL
filename = title if title != "untitled" else get_filename_from_url(url)
save_content(html, text, filename)
print(f"文本预览: {text[:200]}...")
except Exception as e:
print(f"爬取失败: {e}")
browser.close()
print(f"\n全部完成!结果保存在 output/ 目录")
if __name__ == "__main__":
main()静态页面爬虫模板
Python
"""
静态网页爬虫模板
支持:重试机制、代理、User-Agent轮换、请求延迟、日志记录
"""
import time
import random
import logging
import os
import re
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
import requests
from bs4 import BeautifulSoup
# ============== 输出目录 ==============
OUTPUT_DIR = "output"
# ============== 配置 ==============
@dataclass
class CrawlerConfig:
"""爬虫配置"""
base_url: str = ""
timeout: int = 10
max_retries: int = 3
retry_delay: float = 1.0
request_delay: float = 1.0 # 请求间隔,避免被封
use_proxy: bool = False
proxies: Optional[List[str]] = None
# User-Agent池
user_agents: tuple = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
)
# ============== 日志配置 ==============
def setup_logging(log_file: Optional[str] = None, level: int = logging.INFO):
"""配置日志"""
log_format = "%(asctime)s - %(levelname)s - %(message)s"
handlers = [logging.StreamHandler()]
if log_file:
handlers.append(logging.FileHandler(log_file, encoding="utf-8"))
logging.basicConfig(
level=level,
format=log_format,
handlers=handlers
)
# ============== 爬虫核心 ==============
class StaticCrawler:
"""静态网页爬虫"""
def __init__(self, config: Optional[CrawlerConfig] = None):
self.config = config or CrawlerConfig()
self.session = requests.Session()
self.logger = logging.getLogger(self.__class__.__name__)
def _get_headers(self) -> Dict[str, str]:
"""生成请求头"""
return {
"User-Agent": random.choice(self.config.user_agents),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
def _get_proxies(self) -> Optional[Dict[str, str]]:
"""获取代理配置"""
if not self.config.use_proxy or not self.config.proxies:
return None
proxy = random.choice(self.config.proxies)
return {
"http": proxy,
"https": proxy,
}
def fetch(self, url: str, **kwargs) -> Optional[requests.Response]:
"""
发送HTTP请求
Args:
url: 目标URL
**kwargs: requests额外参数
Returns:
Response对象或None
"""
headers = self._get_headers()
headers.update(kwargs.pop("headers", {}))
proxies = self._get_proxies()
for attempt in range(1, self.config.max_retries + 1):
try:
self.logger.info(f"请求: {url} (尝试 {attempt}/{self.config.max_retries})")
response = self.session.get(
url,
headers=headers,
proxies=proxies,
timeout=self.config.timeout,
**kwargs
)
response.raise_for_status()
self.logger.info(f"成功: {url} - 状态码 {response.status_code}")
return response
except requests.RequestException as e:
self.logger.warning(f"失败: {url} - {e}")
if attempt < self.config.max_retries:
time.sleep(self.config.retry_delay * attempt)
self.logger.error(f"最终失败: {url}")
return None
def parse(self, html: str, parser: str = "html.parser") -> BeautifulSoup:
"""
解析HTML
Args:
html: HTML文本
parser: 解析器 (lxml, html.parser, html5lib)
Returns:
BeautifulSoup对象
"""
return BeautifulSoup(html, parser)
def crawl(self, url: str) -> Optional[BeautifulSoup]:
"""
爬取并解析页面(便捷方法)
Args:
url: 目标URL
Returns:
BeautifulSoup对象或None
"""
response = self.fetch(url)
if response:
return self.parse(response.text)
return None
def sanitize_filename(name: str) -> str:
"""清理文件名,移除非法字符"""
# 移除非法字符
name = re.sub(r'[<>:"/\\|?*\n\r\t]', '', name)
# 替换空格为下划线
name = name.strip().replace(' ', '_')
# 限制长度
if len(name) > 100:
name = name[:100]
# 如果为空,用时间戳
if not name:
name = f"page_{int(time.time())}"
return name
def save_results(soup: BeautifulSoup, html_content: str, url: str):
"""
保存爬取结果
Args:
soup: BeautifulSoup对象
html_content: 原始HTML
url: 原始URL
"""
# 创建输出目录
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
# 获取标题作为文件名
title = soup.find("title")
title_text = title.text.strip() if title else ""
filename = sanitize_filename(title_text)
# 保存HTML文件
html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
with open(html_path, "w", encoding="utf-8") as f:
f.write(f"<!-- 来源: {url} -->\n")
f.write(f"<!-- 爬取时间: {time.strftime('%Y-%m-%d %H:%M:%S')} -->\n")
f.write(html_content)
print(f"HTML已保存: {html_path}")
# 提取文本内容并保存TXT
text_content = []
text_content.append(f"来源URL: {url}")
text_content.append(f"爬取时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
text_content.append(f"页面标题: {title_text}")
text_content.append("=" * 50)
# 提取标题
for h in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"]):
text = h.text.strip()
if text:
text_content.append(f"\n[{h.name}] {text}")
# 提取段落
text_content.append("\n【正文内容】")
text_content.append("-" * 30)
for p in soup.find_all("p"):
text = p.text.strip()
if text:
text_content.append(text)
# 提取链接列表
links = soup.find_all("a", href=True)
if links:
text_content.append("\n【链接列表】")
text_content.append("-" * 30)
for link in links:
text = link.text.strip() or "[无文本]"
href = link["href"]
text_content.append(f"{text} -> {href}")
# 保存TXT文件
txt_path = os.path.join(OUTPUT_DIR, f"{filename}.txt")
with open(txt_path, "w", encoding="utf-8") as f:
f.write("\n".join(text_content))
print(f"TXT已保存: {txt_path}")
def display_page_info(soup: BeautifulSoup):
"""展示页面信息"""
print("\n" + "=" * 50)
print("【页面信息】")
print("=" * 50)
# 标题
title = soup.find("title")
print(f"\n标题: {title.text.strip() if title else '无标题'}")
# meta描述
meta_desc = soup.find("meta", attrs={"name": "description"})
if meta_desc and meta_desc.get("content"):
print(f"描述: {meta_desc['content'][:100]}...")
# 链接统计
links = soup.find_all("a", href=True)
print(f"\n链接总数: {len(links)}")
# 图片统计
images = soup.find_all("img")
print(f"图片总数: {len(images)}")
def main():
"""主程序:交互式爬取,支持多个网址"""
setup_logging()
config = CrawlerConfig(timeout=15, max_retries=3)
crawler = StaticCrawler(config)
print("\n" + "=" * 50)
print("静态网页爬虫 - 支持批量爬取")
print("=" * 50)
while True:
print("\n请输入要爬取的网址(每行一个,输入空行开始爬取,单独输入 q 退出):")
lines = []
while True:
line = input().strip()
if line.lower() == "q":
print("再见!")
return
if line == "":
break
lines.append(line)
if not lines:
print("请输入有效的网址")
continue
# 解析多个网址
urls = [line for line in lines if line]
# 自动补全协议
urls = [
u if u.startswith(("http://", "https://")) else "https://" + u
for u in urls
]
print(f"\n共 {len(urls)} 个网址,开始爬取...")
for i, url in enumerate(urls, 1):
print(f"\n[{i}/{len(urls)}] 正在爬取: {url}")
response = crawler.fetch(url)
if response:
soup = crawler.parse(response.text)
display_page_info(soup)
save_results(soup, response.text, url)
else:
print(f"爬取失败: {url}")
# 多个网址时添加延迟
if i < len(urls):
time.sleep(config.request_delay)
print(f"\n全部完成!结果保存在 {OUTPUT_DIR} 目录")
# ============== 入口 ==============
if __name__ == "__main__":
main()