| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- """
- 将单个网页 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}")
|