fetch_url_to_md.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. """
  2. 将单个网页 HTML 正文转为 Markdown,并把正文中的图片下载到输出目录下的 images/。
  3. 用法示例:
  4. pip install requests beautifulsoup4 html2text
  5. python fetch_url_to_md.py --url "https://example.com/page"
  6. python fetch_url_to_md.py -u "https://example.com" -o doc/out --name my-page
  7. 不传 --url 时,仍使用脚本内默认示例 URL(乐享 wiki)。
  8. """
  9. import argparse
  10. import requests
  11. from bs4 import BeautifulSoup
  12. import html2text
  13. import re
  14. import os
  15. from urllib.parse import urljoin, unquote
  16. def fetch_page_content(url):
  17. """获取页面内容"""
  18. headers = {
  19. "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",
  20. "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
  21. "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
  22. }
  23. response = requests.get(url, headers=headers, timeout=30)
  24. response.encoding = 'utf-8'
  25. return response.text
  26. def download_image(img_url, base_url, download_folder="images"):
  27. """下载图片并返回相对路径"""
  28. if not img_url or img_url.startswith('data:'):
  29. return None
  30. try:
  31. img_url = urljoin(base_url, img_url)
  32. if not os.path.exists(download_folder):
  33. os.makedirs(download_folder)
  34. img_name = re.sub(r'[^\w\-_.]', '_', unquote(os.path.basename(img_url.split('?')[0])))
  35. if not img_name or len(img_name) > 100:
  36. img_name = str(hash(img_url)) + '.png'
  37. img_path = os.path.join(download_folder, img_name)
  38. if os.path.exists(img_path):
  39. return img_path
  40. img_response = requests.get(img_url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
  41. if img_response.status_code == 200:
  42. with open(img_path, 'wb') as f:
  43. f.write(img_response.content)
  44. return img_path
  45. except Exception as e:
  46. print(f"下载图片失败 {img_url}: {e}")
  47. return None
  48. def extract_content_with_images(html_content, base_url, download_folder="images"):
  49. """提取页面内容,下载所有图片并替换路径"""
  50. soup = BeautifulSoup(html_content, 'html.parser')
  51. content_area = soup.find('div', class_='content')
  52. if not content_area:
  53. content_area = soup.find('div', class_='main-content') or soup.find('article') or soup.find('main') or soup.body
  54. folder_name = os.path.basename(download_folder)
  55. for img in content_area.find_all('img'):
  56. img_url = img.get('src') or img.get('data-src')
  57. if img_url:
  58. local_path = download_image(img_url, base_url, download_folder)
  59. if local_path:
  60. img['src'] = f"{folder_name}/{os.path.basename(local_path)}"
  61. print(f" 下载图片: {img_url} -> {folder_name}/{os.path.basename(local_path)}")
  62. return content_area if content_area else soup.body
  63. def html_to_markdown(content_element):
  64. """将HTML内容转换为Markdown"""
  65. h = html2text.HTML2Text()
  66. h.ignore_links = False
  67. h.ignore_images = False
  68. h.ignore_tables = False
  69. h.body_width = 0
  70. h.mark_code = True
  71. h.single_line_break = True
  72. h.unicode_snob = True
  73. h.skip_internal_links = False
  74. h.inline_links = True
  75. h.default_image_alt = "图片"
  76. html_str = str(content_element)
  77. html_str = re.sub(r'\sclass="[^"]*"', '', html_str)
  78. html_str = re.sub(r'\sstyle="[^"]*"', '', html_str)
  79. markdown = h.handle(html_str)
  80. markdown = re.sub(r'\n{3,}', '\n\n', markdown)
  81. markdown = re.sub(r'!\[image\]\([^)]+\)', lambda m: re.sub(r'\\+', '/', m.group(0)), markdown)
  82. markdown = re.sub(r'!\[\]\((?!images/)', '![图片](images/', markdown)
  83. return markdown
  84. def extract_title(html_content):
  85. """提取页面标题"""
  86. soup = BeautifulSoup(html_content, 'html.parser')
  87. title_tag = soup.find('title') or soup.find('h1')
  88. if title_tag:
  89. return title_tag.get_text().strip()
  90. return "未命名文档"
  91. def url_to_filename(url):
  92. """从URL提取标题用于文件名"""
  93. match = re.search(r'#([\w\-%]+)', url)
  94. if match:
  95. return unquote(match.group(1))
  96. return "document"
  97. def save_markdown(content, title, output_path):
  98. """保存Markdown文件"""
  99. with open(output_path, 'w', encoding='utf-8') as f:
  100. f.write(content)
  101. print(f"文档已保存: {output_path}")
  102. def _safe_filename(name: str) -> str:
  103. """Windows 等系统下可用的文件名(不含路径分隔符)。"""
  104. name = name.strip() or "document"
  105. for c in '<>:"/\\|?*':
  106. name = name.replace(c, "_")
  107. return name[:200] if len(name) > 200 else name
  108. def url_to_markdown(url, output_dir="output", filename_base=None):
  109. """主函数:将 URL 对应页面正文转为 Markdown。"""
  110. print(f"正在获取页面: {url}")
  111. html_content = fetch_page_content(url)
  112. title = extract_title(html_content)
  113. print(f"页面标题: {title}")
  114. print("正在提取内容并下载图片...")
  115. content_element = extract_content_with_images(html_content, url, os.path.join(output_dir, "images"))
  116. print("正在转换为Markdown...")
  117. markdown_content = html_to_markdown(content_element)
  118. title_tag = f"# {title}\n\n" if title else ""
  119. full_content = title_tag + markdown_content
  120. os.makedirs(output_dir, exist_ok=True)
  121. base = filename_base if filename_base else url_to_filename(url)
  122. filename = _safe_filename(base) + ".md"
  123. output_path = os.path.join(output_dir, filename)
  124. save_markdown(full_content, title, output_path)
  125. return output_path
  126. if __name__ == "__main__":
  127. default_url = "https://lexiang.tencent.com/wiki/api/#获取-appkey-和-appsecret"
  128. parser = argparse.ArgumentParser(description="抓取单个网页并保存为 Markdown(含图片到 images/)")
  129. parser.add_argument(
  130. "-u",
  131. "--url",
  132. default=None,
  133. help=f"要抓取的页面 URL(省略则使用内置示例:乐享 wiki)",
  134. )
  135. parser.add_argument(
  136. "-o",
  137. "--output",
  138. default="lexiang_output",
  139. help="输出目录(会创建 images/ 子目录),默认 lexiang_output;若指定了 --url 未指定 -o 则用 output",
  140. )
  141. parser.add_argument(
  142. "-n",
  143. "--name",
  144. default=None,
  145. help="输出 .md 文件名(不含扩展名);默认从 URL 的 # 锚点或 document",
  146. )
  147. args = parser.parse_args()
  148. target_url = args.url or default_url
  149. out_dir = args.output
  150. if args.url and args.output == "lexiang_output":
  151. out_dir = "output"
  152. output_file = url_to_markdown(target_url, out_dir, filename_base=args.name)
  153. print(f"\n转换完成!输出文件: {output_file}")