"""
将单个网页 HTML 正文转为 Markdown,并把正文中的图片下载到输出目录下的 images/。
用法示例:
pip install requests beautifulsoup4 html2text
python fetch_url_to_md.py --url "https://example.com/page"
python fetch_url_to_md.py -u "https://example.com" -o doc/out --name my-page
不传 --url 时,仍使用脚本内默认示例 URL(乐享 wiki)。
"""
import argparse
import requests
from bs4 import BeautifulSoup
import html2text
import re
import os
from urllib.parse import urljoin, unquote
def fetch_page_content(url):
"""获取页面内容"""
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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
}
response = requests.get(url, headers=headers, timeout=30)
response.encoding = 'utf-8'
return response.text
def download_image(img_url, base_url, download_folder="images"):
"""下载图片并返回相对路径"""
if not img_url or img_url.startswith('data:'):
return None
try:
img_url = urljoin(base_url, img_url)
if not os.path.exists(download_folder):
os.makedirs(download_folder)
img_name = re.sub(r'[^\w\-_.]', '_', unquote(os.path.basename(img_url.split('?')[0])))
if not img_name or len(img_name) > 100:
img_name = str(hash(img_url)) + '.png'
img_path = os.path.join(download_folder, img_name)
if os.path.exists(img_path):
return img_path
img_response = requests.get(img_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
if img_response.status_code == 200:
with open(img_path, 'wb') as f:
f.write(img_response.content)
return img_path
except Exception as e:
print(f"下载图片失败 {img_url}: {e}")
return None
def extract_content_with_images(html_content, base_url, download_folder="images"):
"""提取页面内容,下载所有图片并替换路径"""
soup = BeautifulSoup(html_content, 'html.parser')
content_area = soup.find('div', class_='content')
if not content_area:
content_area = soup.find('div', class_='main-content') or soup.find('article') or soup.find('main') or soup.body
folder_name = os.path.basename(download_folder)
for img in content_area.find_all('img'):
img_url = img.get('src') or img.get('data-src')
if img_url:
local_path = download_image(img_url, base_url, download_folder)
if local_path:
img['src'] = f"{folder_name}/{os.path.basename(local_path)}"
print(f" 下载图片: {img_url} -> {folder_name}/{os.path.basename(local_path)}")
return content_area if content_area else soup.body
def html_to_markdown(content_element):
"""将HTML内容转换为Markdown"""
h = html2text.HTML2Text()
h.ignore_links = False
h.ignore_images = False
h.ignore_tables = False
h.body_width = 0
h.mark_code = True
h.single_line_break = True
h.unicode_snob = True
h.skip_internal_links = False
h.inline_links = True
h.default_image_alt = "图片"
html_str = str(content_element)
html_str = re.sub(r'\sclass="[^"]*"', '', html_str)
html_str = re.sub(r'\sstyle="[^"]*"', '', html_str)
markdown = h.handle(html_str)
markdown = re.sub(r'\n{3,}', '\n\n', markdown)
markdown = re.sub(r'!\[image\]\([^)]+\)', lambda m: re.sub(r'\\+', '/', m.group(0)), markdown)
markdown = re.sub(r'!\[\]\((?!images/)', '
return markdown
def extract_title(html_content):
"""提取页面标题"""
soup = BeautifulSoup(html_content, 'html.parser')
title_tag = soup.find('title') or soup.find('h1')
if title_tag:
return title_tag.get_text().strip()
return "未命名文档"
def url_to_filename(url):
"""从URL提取标题用于文件名"""
match = re.search(r'#([\w\-%]+)', url)
if match:
return unquote(match.group(1))
return "document"
def save_markdown(content, title, output_path):
"""保存Markdown文件"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"文档已保存: {output_path}")
def _safe_filename(name: str) -> str:
"""Windows 等系统下可用的文件名(不含路径分隔符)。"""
name = name.strip() or "document"
for c in '<>:"/\\|?*':
name = name.replace(c, "_")
return name[:200] if len(name) > 200 else name
def url_to_markdown(url, output_dir="output", filename_base=None):
"""主函数:将 URL 对应页面正文转为 Markdown。"""
print(f"正在获取页面: {url}")
html_content = fetch_page_content(url)
title = extract_title(html_content)
print(f"页面标题: {title}")
print("正在提取内容并下载图片...")
content_element = extract_content_with_images(html_content, url, os.path.join(output_dir, "images"))
print("正在转换为Markdown...")
markdown_content = html_to_markdown(content_element)
title_tag = f"# {title}\n\n" if title else ""
full_content = title_tag + markdown_content
os.makedirs(output_dir, exist_ok=True)
base = filename_base if filename_base else url_to_filename(url)
filename = _safe_filename(base) + ".md"
output_path = os.path.join(output_dir, filename)
save_markdown(full_content, title, output_path)
return output_path
if __name__ == "__main__":
default_url = "https://lexiang.tencent.com/wiki/api/#获取-appkey-和-appsecret"
parser = argparse.ArgumentParser(description="抓取单个网页并保存为 Markdown(含图片到 images/)")
parser.add_argument(
"-u",
"--url",
default=None,
help=f"要抓取的页面 URL(省略则使用内置示例:乐享 wiki)",
)
parser.add_argument(
"-o",
"--output",
default="lexiang_output",
help="输出目录(会创建 images/ 子目录),默认 lexiang_output;若指定了 --url 未指定 -o 则用 output",
)
parser.add_argument(
"-n",
"--name",
default=None,
help="输出 .md 文件名(不含扩展名);默认从 URL 的 # 锚点或 document",
)
args = parser.parse_args()
target_url = args.url or default_url
out_dir = args.output
if args.url and args.output == "lexiang_output":
out_dir = "output"
output_file = url_to_markdown(target_url, out_dir, filename_base=args.name)
print(f"\n转换完成!输出文件: {output_file}")