Skip to content

Commit

Permalink
发布 1.7 版本
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeanAmier committed Dec 23, 2023
1 parent 84a0889 commit e0ba7af
Show file tree
Hide file tree
Showing 15 changed files with 485 additions and 344 deletions.
69 changes: 45 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=fff200">
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=for-the-badge&color=1b9cfc">
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=for-the-badge&color=44bd32">
<hr>
<p>🔥 <b>小红书作品采集工具</b>:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
</div>
<h1>📑 功能清单</h1>
<ul>
<li>✅ 采集小红书图文/视频作品信息</li>
<li>✅ 提取小红书图文/视频作品下载地址</li>
<li>✅ 下载小红书无水印图文/视频作品文件</li>
<li>✅ 采集小红书图文 / 视频作品信息</li>
<li>✅ 提取小红书图文 / 视频作品下载地址</li>
<li>✅ 下载小红书无水印图文 / 视频作品文件</li>
<li>✅ 自动跳过已下载的作品文件</li>
<li>✅ 作品文件完整性处理机制</li>
<li>✅ 持久化储存作品信息至文件</li>
<li>✅ 作品文件储存至单独文件夹</li>
<li>☑️ 后台监听剪贴板下载作品</li>
<li>☑️ 支持 API 调用功能</li>
</ul>
<h1>📸 程序截图</h1>
<br>
<img src="static/程序运行截图1.png" alt="">
<hr>
<img src="static/程序运行截图2.png" alt="">
<p><b>🎥 点击图片观看演示视频</b></p>
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/程序运行截图.png" alt=""></a>
<h1>🔗 支持链接</h1>
<ul>
<li><code>https://www.xiaohongshu.com/explore/作品ID</code></li>
Expand All @@ -35,18 +35,19 @@
<h1>🪟 关于终端</h1>
<p>⭐ 推荐使用 <a href="https://learn.microsoft.com/zh-cn/windows/terminal/install">Windows 终端</a> (Windows 11 自带默认终端)运行程序以便获得最佳显示效果!</p>
<h1>🥣 使用方法</h1>
<p>如果仅需下载作品文件,选择 <b>直接运行</b> 或者 <b>源码运行</b> 均可,如果需要获取作品信息,则需要进行二次开发进行调用。</p>
<h2>🖱 直接运行</h2>
<p>前往 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main.exe</code> 即可使用。</p>
<p>如果仅需下载无水印作品文件,建议选择 <b>程序运行</b>;如果有其他需求,建议选择 <b>源码运行</b>!</p>
<h2>🖱 程序运行</h2>
<p>Windows 10 及以上用户可前往 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main.exe</code> 即可使用。</p>
<p>若通过此方式使用程序,文件默认下载路径:<code>.\_internal\Download</code>;配置文件路径:<code>.\_internal\settings.json</code></p>
<h2>⌨️ 源码运行</h2>
<ol>
<li>安装版本号不低于 <code>3.12</code> 的 Python 解释器</li>
<li>运行 <code>pip install -r requirements.txt</code> 命令安装程序所需模块</li>
<li>运行 <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> 命令安装程序所需模块</li>
<li>下载本项目最新的源码或 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 发布的源码至本地</li>
<li>运行 <code>main.py</code> 即可使用</li>
</ol>
<h2>💻 二次开发</h2>
<p>如果需要获取小红书图文/视频作品信息,可以根据 <code>main.py</code> 的注释提示进行代码调用。</p>
<h1>💻 二次开发</h1>
<p>如果有其他需求,可以根据 <code>main.py</code> 的注释提示进行代码调用或修改!</p>
<pre>
# 测试链接
error_demo = "https://github.com/JoeanAmier/XHS_Downloader"
Expand All @@ -58,20 +59,27 @@ path = "" # 作品数据/文件保存根路径,默认值:项目根路径
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
user_agent = "" # 请求头 User-Agent
cookie = "" # 小红书网页版 Cookie,无需登录
proxy = "" # 网络代理
timeout = 5 # 网络请求超时限制,单位:秒,默认值:10
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
proxy = None # 网络代理
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
# async with XHS() as xhs:
# pass # 使用默认参数
record_data = False # 是否记录作品数据至文件
image_format = "jpg" # 图文作品文件名称后缀
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
async with XHS() as xhs:
pass # 使用默认参数
async with XHS(path=path,
folder_name=folder_name,
user_agent=user_agent,
cookie=cookie,
proxy=proxy,
timeout=timeout,
chunk=chunk,
max_retry=max_retry, ) as xhs: # 使用自定义参数
max_retry=max_retry,
record_data=record_data,
image_format=image_format,
folder_mode=folder_mode,
) as xhs: # 使用自定义参数
download = True # 是否下载作品文件,默认值:False
# 返回作品详细信息,包括下载地址
print(await xhs.extract(error_demo, download)) # 获取数据失败时返回空字典
Expand All @@ -81,6 +89,7 @@ async with XHS(path=path,
</pre>
<h1>⚙️ 配置文件</h1>
<p>项目根目录下的 <code>settings.json</code> 文件,首次运行自动生成,可以自定义部分运行参数。</p>
<p>如果您的计算机没有合适的程序编辑 JSON 文件,建议使用 <a href="https://try8.cn/tool/format/json">JSON 在线工具</a> 编辑配置文件内容</p>
<table>
<thead>
<tr>
Expand Down Expand Up @@ -112,14 +121,14 @@ async with XHS(path=path,
<tr>
<td align="center">cookie</td>
<td align="center">str</td>
<td align="center">小红书网页版 Cookie,无需登录</td>
<td align="center">小红书网页版 Cookie,<b>无需登录</b></td>
<td align="center">默认 Cookie</td>
</tr>
<tr>
<td align="center">proxy</td>
<td align="center">str</td>
<td align="center">设置代理</td>
<td align="center"></td>
<td align="center">设置程序代理</td>
<td align="center">null</td>
</tr>
<tr>
<td align="center">timeout</td>
Expand All @@ -142,15 +151,27 @@ async with XHS(path=path,
<tr>
<td align="center">record_data</td>
<td align="center">bool</td>
<td align="center">是否记录作品数据至文件</td>
<td align="center">是否记录作品数据至 <code>TXT</code> 文件</td>
<td align="center">false</td>
</tr>
<tr>
<td align="center">image_format</td>
<td align="center">str</td>
<td align="center">图文作品文件名称后缀,例如:<code>jpg</code>、<code>png</code></td>
<td align="center">图文作品文件名称后缀,不影响实际文件格式</td>
<td align="center">webp</td>
</tr>
<tr>
<td align="center">video_format</td>
<td align="center">str</td>
<td align="center">视频作品文件名称后缀,不影响实际文件格式</td>
<td align="center">mp4</td>
</tr>
<tr>
<td align="center">folder_mode</td>
<td align="center">bool</td>
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
<td align="center">false</td>
</tr>
</tbody>
</table>
<h1>🌐 Cookie</h1>
Expand Down
19 changes: 13 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,27 @@ async def example():
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
user_agent = "" # 请求头 User-Agent
cookie = "" # 小红书网页版 Cookie,无需登录
proxy = "" # 网络代理
timeout = 5 # 网络请求超时限制,单位:秒,默认值:10
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
proxy = None # 网络代理
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
# async with XHS() as xhs:
# pass # 使用默认参数
record_data = False # 是否记录作品数据至文件
image_format = "jpg" # 图文作品文件名称后缀
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
async with XHS() as xhs:
pass # 使用默认参数
async with XHS(path=path,
folder_name=folder_name,
user_agent=user_agent,
cookie=cookie,
proxy=proxy,
timeout=timeout,
chunk=chunk,
max_retry=max_retry, ) as xhs: # 使用自定义参数
max_retry=max_retry,
record_data=record_data,
image_format=image_format,
folder_mode=folder_mode,
) as xhs: # 使用自定义参数
download = True # 是否下载作品文件,默认值:False
# 返回作品详细信息,包括下载地址
print(await xhs.extract(error_demo, download)) # 获取数据失败时返回空字典
Expand Down
140 changes: 140 additions & 0 deletions source/App.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from re import compile

from .Downloader import Download
from .Explore import Explore
from .Html import Html
from .Image import Image
from .Manager import Manager
from .Static import (
ROOT,
ERROR,
WARNING,
)
from .Video import Video


class XHS:
LINK = compile(r"https?://www\.xiaohongshu\.com/explore/[a-z0-9]+")
SHARE = compile(r"https?://www\.xiaohongshu\.com/discovery/item/[a-z0-9]+")
SHORT = compile(r"https?://xhslink\.com/[A-Za-z0-9]+")
__INSTANCE = None
TYPE = {
"视频": "v",
"图文": "n",
}

def __new__(cls, *args, **kwargs):
if not cls.__INSTANCE:
cls.__INSTANCE = super().__new__(cls)
return cls.__INSTANCE

def __init__(
self,
path="",
folder_name="Download",
user_agent: str = None,
cookie: str = None,
proxy: str = None,
timeout=10,
chunk=1024 * 1024,
max_retry=5,
record_data=False,
image_format="webp",
video_format="mp4",
folder_mode=False,
):
self.manager = Manager(
ROOT,
path,
folder_name,
user_agent,
chunk,
cookie,
proxy,
timeout,
max_retry,
record_data,
image_format,
video_format,
folder_mode,
)
self.html = Html(self.manager)
self.image = Image()
self.video = Video()
self.explore = Explore()
self.download = Download(self.manager, )
self.rich_log = self.download.rich_log

def __extract_image(self, container: dict, html: str):
container["下载地址"] = self.image.get_image_link(html)

def __extract_video(self, container: dict, html: str):
container["下载地址"] = self.video.get_video_link(html)

async def __download_files(self, container: dict, download: bool, log, bar):
name = self.__naming_rules(container)
if (u := container["下载地址"]) and download:
await self.download.run(u, name, self.TYPE[container["作品类型"]], log, bar)
elif not u:
self.rich_log(log, "提取作品文件下载地址失败!", ERROR)
self.manager.save_data(name, container)

async def extract(self, url: str, download=False, log=None, bar=None) -> list[dict]:
# return # 调试代码
urls = await self.__extract_links(url)
if not urls:
self.rich_log(log, "提取小红书作品链接失败!", WARNING)
else:
self.rich_log(log, f"共 {len(urls)} 个小红书作品待处理...")
# return urls # 调试代码
return [await self.__deal_extract(i, download, log, bar) for i in urls]

async def __extract_links(self, url: str) -> list:
urls = []
for i in url.split():
if u := self.SHORT.search(i):
i = await self.html.request_url(
u.group(), False)
if u := self.SHARE.search(i):
urls.append(u.group())
elif u := self.LINK.search(i):
urls.append(u.group())
return urls

async def __deal_extract(self, url: str, download: bool, log, bar):
self.rich_log(log, f"开始处理作品:{url}")
html = await self.html.request_url(url)
# self.rich_log(log, html) # 调试代码
if not html:
self.rich_log(log, f"{url} 获取数据失败!", ERROR)
return {}
data = self.explore.run(html)
# self.rich_log(log, data) # 调试代码
if not data:
self.rich_log(log, f"{url} 提取数据失败!", ERROR)
return {}
match data["作品类型"]:
case "视频":
self.__extract_video(data, html)
case "图文":
self.__extract_image(data, html)
case _:
data["下载地址"] = []
await self.__download_files(data, download, log, bar)
self.rich_log(log, f"作品处理完成:{url}")
return data

def __naming_rules(self, data: dict) -> str:
"""下载文件默认使用 作品标题 或 作品 ID 作为文件名称,可修改此方法自定义文件名称格式"""
return self.manager.filter_name(data["作品标题"]) or data["作品ID"]

async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc_value, traceback):
await self.close()

async def close(self):
self.manager.clean()
await self.html.session.close()
await self.download.session.close()
Loading

0 comments on commit e0ba7af

Please sign in to comment.