在实际项目中我们经常需要从公开网站获取结构化数据用于分析、训练或集成。传统爬虫面临反爬策略、页面结构变动和内容解析复杂等问题。而一些网站开始提供对自动化工具更友好的数据接口例如以纯净的 Markdown 格式提供内容这为数据获取带来了新的思路。然而这类接口往往并非完全无私可能会嵌入推广信息或要求遵守特定的使用规范。本文将围绕如何识别、请求并处理这类特殊的数据源展开。我们将以一个假设的场景为例一个内容网站为标识为特定User-Agent如ClaudeBot的请求返回带有内嵌广告或推广链接的 Markdown 格式页面而非标准的 HTML。我们将从原理分析、环境准备、代码实现、数据处理到合规性探讨完成一个完整的、可复现的技术流程。通过本文你将掌握如何针对此类 API 友好的数据源编写稳健的爬虫并理解其中的技术细节与边界。1. 理解“为 AI 爬虫提供 Markdown”背后的机制在深入代码之前我们需要厘清几个核心概念为什么网站要区分User-AgentMarkdown 格式对爬虫意味着什么以及广告是如何被“嵌入”的。1.1 User-Agent 的作用与识别User-Agent是 HTTP 请求头中的一个字段用于告知服务器客户端的软件类型、版本、操作系统等信息。网站服务器可以通过解析这个字段来提供差异化的内容。对普通浏览器返回完整的、渲染好的 HTML 页面包含样式、脚本和复杂的交互元素。对搜索引擎爬虫返回利于索引的简化 HTML 或特定格式如sitemap.xml。对声明为特定 AI 爬虫的客户端可能返回更纯净、结构化的数据格式如 Markdown、JSON 或特定 XML。这是一种“白名单”机制旨在为合作或认可的自动化工具提供便利同时减少对主站资源的无意义爬取压力。在我们的场景中服务器会检查User-Agent是否包含ClaudeBot等关键词以此决定响应内容。1.2 Markdown 作为数据接口的优势与解析 HTML 相比直接获取 Markdown 有显著优点结构更清晰内容以标题、列表、代码块、链接等语义化标记呈现无需处理复杂的div嵌套和 CSS 类名。信息密度高去除了导航栏、侧边栏、页脚、推荐阅读等无关的页面“噪音”直接获取核心正文。解析成本低Markdown 语法规则简单使用正则表达式或专门的 Markdown 解析库可以轻松提取纯文本、链接和图片地址。便于后续处理获取的 Markdown 可以直接用于生成文档、导入笔记软件或作为大语言模型LLM的输入语料。1.3 “带广告的 Markdown”的实现方式网站提供便利的同时也可能有商业或推广需求。广告或推广信息可能以以下几种方式嵌入返回的 Markdown 内容中文内推广段落在文章开头、中间或结尾插入固定的推广文本例如 “本文由 XX 赞助提供……”。替换或添加链接将正文中的某些关键词或原始链接替换为带有推广追踪参数的链接。添加脚注或头部信息在 Markdown 内容的顶部或底部添加包含广告信息的区块。使用特殊标记使用如[AD]、!-- sponsored --等自定义标记来标识广告内容便于爬虫后期过滤。理解这些机制有助于我们在编写爬虫时设计相应的策略来识别和处理这些非核心内容。2. 环境准备与核心工具选择我们将使用 Python 作为实现语言因为它拥有强大且成熟的网络请求和文本处理生态。2.1 Python 环境与依赖库确保你已安装 Python 3.7 及以上版本。我们将使用pip安装以下核心库requests: 用于发送 HTTP 请求是网络爬虫的基石。beautifulsoup4(可选但推荐): 虽然我们主要处理 Markdown但初始探测或处理混合内容时可能用到。markdown(或mistune,markdown-it-py): 用于将 Markdown 文本转换为 HTML以便更精细地操作或验证。这里我们选择 Python-Markdown。re(Python 内置): 正则表达式库用于基于模式的文本查找与替换。通过以下命令安装必要库pip install requests beautifulsoup4 markdown2.2 项目结构与思路规划创建一个新的项目目录例如markdown_crawler。我们的代码将遵循以下逻辑流程探测与请求构造带有特定User-Agent的 HTTP 请求获取原始响应。内容识别与提取判断响应内容是 HTML 还是 Markdown并提取目标部分。广告识别与过滤根据预设规则如关键词、固定位置、特殊标记识别并移除广告内容。数据清洗与存储将净化后的 Markdown 内容转换为所需格式如纯文本、结构化 JSON并保存。异常处理与日志确保爬虫在遇到网络错误、内容变更时能优雅处理并记录。我们将逐步实现这些模块。3. 构建基础爬虫请求与内容获取首先我们实现最核心的请求部分模拟一个 AI 爬虫向目标网站发起请求。3.1 设置请求头与 User-Agent创建一个名为crawler.py的文件。我们需要精心构造请求头其中User-Agent是关键。import requests from typing import Optional, Dict class MarkdownCrawler: def __init__(self, user_agent: str “ClaudeBot”): 初始化爬虫 :param user_agent: 用于标识爬虫的 User-Agent 字符串 self.user_agent user_agent self.session requests.Session() # 设置默认请求头模拟一个“友好”的爬虫 self.session.headers.update({ ‘User-Agent’: self.user_agent, ‘Accept’: ‘text/markdown, text/html; q0.9, application/json; q0.8’, ‘Accept-Language’: ‘en-US,en;q0.5’, ‘Accept-Encoding’: ‘gzip, deflate’, ‘Connection’: ‘keep-alive’, }) def fetch_content(self, url: str) - Optional[str]: 从指定 URL 获取内容 :param url: 目标网页地址 :return: 返回响应文本内容失败则返回 None try: response self.session.get(url, timeout10) response.raise_for_status() # 检查 HTTP 状态码是否为 200 # 检查 Content-Type判断是否是 Markdown content_type response.headers.get(‘Content-Type’, ‘’).lower() if ‘markdown’ in content_type or ‘text/plain’ in content_type: print(f“成功获取 Markdown 格式内容 from {url}”) elif ‘html’ in content_type: print(f“警告获取到 HTML 格式内容 from {url}可能 User-Agent 未被识别”) else: print(f“信息获取到未知格式({content_type})内容 from {url}”) return response.text except requests.exceptions.RequestException as e: print(f“请求 {url} 时发生错误: {e}”) return None # 示例用法 if __name__ “__main__”: crawler MarkdownCrawler(user_agent“ClaudeBot”) # 注意此处为示例URL实际使用时需替换为目标网站地址 test_url “https://example.com/article/123” content crawler.fetch_content(test_url) if content: print(“获取到的前500个字符”) print(content[:500])关键点解释Accept头中我们优先请求text/markdown表明客户端偏好 Markdown 格式。使用requests.Session()可以保持连接和 cookies提高效率。response.raise_for_status()会在状态码非 2xx 时抛出异常便于错误处理。通过检查Content-Type响应头我们可以初步判断服务器是否如我们所愿返回了 Markdown。3.2 处理可能的反爬策略即使提供了“友好”的User-Agent网站仍可能有基础防护。我们需要让爬虫行为更接近人类或合规爬虫。def __init__(self, user_agent: str “ClaudeBot”): # ... 之前的初始化代码 ... self.session.headers.update({ ‘User-Agent’: self.user_agent, ‘Accept’: ‘text/markdown, text/html; q0.9, application/json; q0.8’, ‘Accept-Language’: ‘en-US,en;q0.5’, ‘Accept-Encoding’: ‘gzip, deflate’, ‘Connection’: ‘keep-alive’, ‘Referer’: ‘https://www.google.com/‘, # 模拟从搜索引擎跳转而来 }) # 添加请求延迟避免过快请求导致 IP 被封 self.request_delay 2 def fetch_content(self, url: str) - Optional[str]: import time time.sleep(self.request_delay) # 每次请求前延迟 try: response self.session.get(url, timeout10) # ... 后续处理 ...4. 解析与清洗从原始 Markdown 到纯净内容获取到原始文本后我们需要识别并移除嵌入的广告内容。这里我们假设广告以固定的模式出现。4.1 识别常见的广告嵌入模式根据之前的分析我们定义几种常见的广告模式并使用正则表达式进行匹配。import re class ContentCleaner: def __init__(self): # 定义需要过滤的广告模式正则表达式 self.ad_patterns [ # 模式1以“赞助”、“广告”、“推广”开头的段落 r‘^[\\s]*[\\*\\-]?[\\s]*(赞助|广告|推广|Sponsored|Ad)[\\s]*[:]?.*$’, # 模式2包含特定推广链接的文本行 r‘.*(点击领取|限时优惠|注册送|购买链接).*’, # 模式3被特定标记包围的内容如 [AD]...[/AD] r‘\\[AD\\][\\s\\S]*?\\[/AD\\]’, # 模式4文章末尾的固定推广区块假设以“---”分隔 r‘\\n-{3,}\\n[\\s\\S]*$’, # 匹配最后一个“---”之后的所有内容 ] def clean_markdown(self, raw_text: str) - str: 清洗 Markdown 文本移除广告内容 :param raw_text: 原始的、可能包含广告的 Markdown 文本 :return: 清洗后的 Markdown 文本 cleaned_text raw_text for pattern in self.ad_patterns: cleaned_text re.sub(pattern, ‘’, cleaned_text, flagsre.MULTILINE) # 移除因删除广告而产生的多余空行 cleaned_text re.sub(r‘\\n{3,}’, ‘\\n\\n’, cleaned_text) cleaned_text cleaned_text.strip() return cleaned_text def extract_links(self, markdown_text: str) - list: 从 Markdown 文本中提取所有链接 :param markdown_text: Markdown 文本 :return: 链接URL列表 # Markdown链接格式: [文字](URL) link_pattern r‘\\[([^\\[\\]]*)\\]\\(([^\\s\\)])(?:\\s“[^”]*“)?\\)’ links re.findall(link_pattern, markdown_text) # links 是一个元组列表 [(text, url), ...]我们只关心url return [url for _, url in links]4.2 集成清洗流程到爬虫类修改MarkdownCrawler类加入内容清洗功能。class MarkdownCrawler: def __init__(self, user_agent: str “ClaudeBot”): self.user_agent user_agent self.session requests.Session() self.cleaner ContentCleaner() # 实例化清洗器 # ... 其余初始化代码 ... def fetch_and_clean(self, url: str) - Optional[Dict]: 获取并清洗内容返回结构化信息 :param url: 目标URL :return: 包含原始内容、清洗后内容、链接等信息的字典失败返回None raw_content self.fetch_content(url) if not raw_content: return None cleaned_content self.cleaner.clean_markdown(raw_content) extracted_links self.cleaner.extract_links(cleaned_content) result { ‘url’: url, ‘raw_content’: raw_content, ‘cleaned_content’: cleaned_content, ‘extracted_links’: extracted_links, ‘ad_removed’: len(raw_content) - len(cleaned_content) 100 # 简单判断是否移除了大量内容 } return result4.3 将 Markdown 转换为其他格式可选有时我们需要将 Markdown 转换为 HTML 或纯文本以便进一步分析。import markdown from bs4 import BeautifulSoup class ContentConverter: staticmethod def markdown_to_html(md_text: str) - str: 将 Markdown 转换为 HTML return markdown.markdown(md_text, extensions[‘extra’, ‘codehilite’]) staticmethod def markdown_to_plaintext(md_text: str) - str: 将 Markdown 转换为纯文本去除所有标记 # 先转成HTML html markdown.markdown(md_text) # 再用BeautifulSoup提取文本 soup BeautifulSoup(html, ‘html.parser’) return soup.get_text(separator‘\\n’, stripTrue) # 在爬虫结果处理中使用 if __name__ “__main__”: crawler MarkdownCrawler() result crawler.fetch_and_clean(“https://example.com/article/123”) if result: print(“清洗后的内容长度”, len(result[‘cleaned_content’])) print(“提取到的链接”, result[‘extracted_links’]) converter ContentConverter() plain_text converter.markdown_to_plaintext(result[‘cleaned_content’]) print(“\\n纯文本预览”) print(plain_text[:300])5. 处理动态或 JavaScript 渲染的内容有些网站即使对特定User-Agent返回内容其核心数据仍可能由前端 JavaScript 动态加载。我们的简单requests请求只能获取初始 HTML可能不包含目标 Markdown 内容。这时需要用到无头浏览器。5.1 使用 Selenium 模拟浏览器安装 Selenium 和对应的 WebDriver以 Chrome 为例pip install selenium # 还需下载与本地Chrome版本匹配的 chromedriver并放入 PATH示例代码from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time class DynamicMarkdownCrawler: def __init__(self, user_agent: str “ClaudeBot”): chrome_options Options() chrome_options.add_argument(‘--headless’) # 无头模式不显示GUI chrome_options.add_argument(‘--disable-gpu’) chrome_options.add_argument(f‘user-agent{user_agent}’) # 可选禁用图片加载以加速 prefs {“profile.managed_default_content_settings.images”: 2} chrome_options.add_experimental_option(“prefs”, prefs) self.driver webdriver.Chrome(optionschrome_options) self.wait WebDriverWait(self.driver, 10) def fetch_dynamic_content(self, url: str, content_selector: str “body”) - Optional[str]: 获取动态渲染后的页面内容 :param url: 目标URL :param content_selector: 用于定位核心内容的CSS选择器默认是整个body :return: 页面文本内容 try: self.driver.get(url) # 等待特定元素加载根据实际情况调整 # self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, content_selector))) time.sleep(3) # 简单等待确保JS执行完毕 element self.driver.find_element(By.CSS_SELECTOR, content_selector) return element.text # 或者 element.get_attribute(‘innerHTML’) except Exception as e: print(f“动态获取 {url} 内容失败: {e}”) return None finally: self.driver.quit() # 注意Selenium 更重适合复杂场景。优先尝试 requests无效时才考虑此方案。6. 完整工作流示例与数据存储现在我们将所有模块组合起来实现一个从请求、清洗、转换到存储的完整流程。import json import os from datetime import datetime class MarkdownCrawlerApp: def __init__(self, user_agent“ClaudeBot”, output_dir“./output”): self.crawler MarkdownCrawler(user_agent) self.converter ContentConverter() self.output_dir output_dir os.makedirs(output_dir, exist_okTrue) def process_url(self, url: str, save_raw: bool False): 处理单个URL print(f“正在处理: {url}”) result self.crawler.fetch_and_clean(url) if not result: print(“处理失败。”) return # 转换为其他格式 result[‘html_content’] self.converter.markdown_to_html(result[‘cleaned_content’]) result[‘plain_text’] self.converter.markdown_to_plaintext(result[‘cleaned_content’]) # 生成文件名 timestamp datetime.now().strftime(“%Y%m%d_%H%M%S”) # 简易地从URL提取标识实际项目可能需要更健壮的方法 url_slug re.sub(r‘[^a-zA-Z0-9]’, ‘_’, url)[-30:] base_filename f“{timestamp}_{url_slug}” # 保存清洗后的Markdown md_path os.path.join(self.output_dir, f“{base_filename}.md”) with open(md_path, ‘w’, encoding‘utf-8’) as f: f.write(result[‘cleaned_content’]) print(f“Markdown 已保存至: {md_path}”) # 保存元数据和纯文本 meta_path os.path.join(self.output_dir, f“{base_filename}_meta.json”) # 只保存必要的元数据避免过大 meta_data { ‘url’: result[‘url’], ‘fetch_time’: timestamp, ‘extracted_links’: result[‘extracted_links’], ‘ad_removed’: result[‘ad_removed’], ‘plain_text_length’: len(result[‘plain_text’]) } with open(meta_path, ‘w’, encoding‘utf-8’) as f: json.dump(meta_data, f, ensure_asciiFalse, indent2) # 可选保存原始内容 if save_raw: raw_path os.path.join(self.output_dir, f“{base_filename}_raw.txt”) with open(raw_path, ‘w’, encoding‘utf-8’) as f: f.write(result[‘raw_content’]) print(f“处理完成。清洗后字符数: {len(result[‘cleaned_content’])}”) return result if __name__ “__main__”: app MarkdownCrawlerApp(output_dir“./crawled_data”) # 示例URL列表实际使用时替换 url_list [ “https://example.com/article/1”, “https://example.com/article/2”, ] for url in url_list: app.process_url(url, save_rawTrue) print(“-” * 50)7. 常见问题排查与优化策略在实际运行中你可能会遇到各种问题。下表列出了一些典型问题及其排查思路。问题现象可能原因检查与解决思路返回403 Forbidden或429 Too Many Requests1.User-Agent不被接受。2. 请求频率过高。3. IP 被暂时封禁。1. 尝试更换为更常见的浏览器User-Agent或网站明确允许的爬虫标识。2. 大幅增加request_delay如 5-10 秒。3. 使用代理 IP 池需谨慎评估合规性。4. 检查请求头是否完整添加Referer。返回内容仍是 HTML非 Markdown1. 目标页面不提供 Markdown 版本。2.User-Agent识别逻辑不符。3. 需要特定参数或 Cookie。1. 用浏览器开发者工具检查网络请求看是否有单独的 Markdown API 接口。2. 精确复制浏览器访问时的User-Agent和所有请求头。3. 检查页面源代码看是否有link标签指向text/markdown版本。获取的内容为空或乱码1. 页面是动态渲染的。2. 编码问题。1. 使用Selenium等无头浏览器方案。2. 检查response.encoding或使用response.content.decode(‘utf-8’)手动指定编码。广告过滤不准确误删正文1. 广告模式正则表达式过于宽泛。2. 广告模式与正文有重叠。1. 先保存原始内容用小样本测试和调整正则表达式。2. 采用更精确的定位方式如基于 HTML 结构如果先转成了HTML使用BeautifulSoup删除特定class或id的元素。爬取速度慢1. 单线程请求。2. 动态页面渲染耗时。1. 对于大量 URL考虑使用concurrent.futures或asyncioaiohttp进行并发请求注意控制并发数。2. 动态渲染方案无解可尝试寻找隐藏的静态数据接口。8. 最佳实践与合规性探讨编写此类爬虫时技术实现只是基础遵守规则和伦理同样重要。8.1 技术最佳实践尊重robots.txt在爬取任何网站前先访问https://目标网站/robots.txt检查是否允许你的User-Agent爬取目标路径。设置合理的请求间隔即使网站未明确要求也应避免高频请求通常间隔 2-5 秒是较为礼貌的。识别并处理错误完善的状态码检查、重试机制带退避和日志记录是生产级爬虫的必备功能。缓存已爬取内容对于更新不频繁的内容可以在本地建立缓存避免重复请求。使用连接池requests.Session()可以复用 TCP 连接提升效率。分离配置与代码将User-Agent、请求头、目标 URL 列表、正则表达式模式等写入配置文件如config.yaml便于维护。8.2 法律与合规性边界遵守服务条款仔细阅读目标网站的Terms of Service明确禁止爬取或自动化访问的条款必须遵守。识别“为爬虫提供的数据”的性质网站主动为特定User-Agent提供 Markdown通常意味着允许甚至鼓励自动化获取。但这不意味着可以无限制滥用。应关注其是否有速率限制、数据使用范围等隐含要求。数据用途将爬取的数据用于个人学习、研究或公益项目风险较低。但如果用于商业竞争、训练直接对标的商业模型或对原站造成明显流量和商业损害则法律风险极高。处理广告内容如果网站通过在 Markdown 中嵌入广告来获得收益你在移除广告后大规模使用其内容可能触及版权和不正当竞争问题。一种更合规的做法是保留广告或与原站达成协议。隐私与个人信息绝对不要爬取和存储用户的个人信息如邮箱、电话、地址即使这些信息意外出现在公开页面中。核心建议在启动任何有一定规模的爬虫项目前最稳妥的方式是联系网站管理员询问其是否有公开的 API 或明确的数据使用政策。许多网站乐于为合规的、有价值的应用提供官方数据接口。通过本文的流程你不仅能够构建一个针对特定数据接口的爬虫更能理解从网络请求、内容解析、数据清洗到合规使用的完整链条。在实际应用中请始终将技术能力与法律合规、商业伦理相结合。