Skip to content

Commit e0ba7af

Browse files
committed
发布 1.7 版本
1 parent 84a0889 commit e0ba7af

15 files changed

+485
-344
lines changed

README.md

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=fff200">
77
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=for-the-badge&color=1b9cfc">
88
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=for-the-badge&color=44bd32">
9-
<hr>
9+
<p>🔥 <b>小红书作品采集工具</b>:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
1010
</div>
1111
<h1>📑 功能清单</h1>
1212
<ul>
13-
<li>✅ 采集小红书图文/视频作品信息</li>
14-
<li>✅ 提取小红书图文/视频作品下载地址</li>
15-
<li>✅ 下载小红书无水印图文/视频作品文件</li>
13+
<li>✅ 采集小红书图文 / 视频作品信息</li>
14+
<li>✅ 提取小红书图文 / 视频作品下载地址</li>
15+
<li>✅ 下载小红书无水印图文 / 视频作品文件</li>
1616
<li>✅ 自动跳过已下载的作品文件</li>
1717
<li>✅ 作品文件完整性处理机制</li>
1818
<li>✅ 持久化储存作品信息至文件</li>
19+
<li>✅ 作品文件储存至单独文件夹</li>
1920
<li>☑️ 后台监听剪贴板下载作品</li>
2021
<li>☑️ 支持 API 调用功能</li>
2122
</ul>
2223
<h1>📸 程序截图</h1>
2324
<br>
24-
<img src="static/程序运行截图1.png" alt="">
25-
<hr>
26-
<img src="static/程序运行截图2.png" alt="">
25+
<p><b>🎥 点击图片观看演示视频</b></p>
26+
<a href="https://www.bilibili.com/video/BV1nQ4y137it/"><img src="static/程序运行截图.png" alt=""></a>
2727
<h1>🔗 支持链接</h1>
2828
<ul>
2929
<li><code>https://www.xiaohongshu.com/explore/作品ID</code></li>
@@ -35,18 +35,19 @@
3535
<h1>🪟 关于终端</h1>
3636
<p>⭐ 推荐使用 <a href="https://learn.microsoft.com/zh-cn/windows/terminal/install">Windows 终端</a> (Windows 11 自带默认终端)运行程序以便获得最佳显示效果!</p>
3737
<h1>🥣 使用方法</h1>
38-
<p>如果仅需下载作品文件,选择 <b>直接运行</b> 或者 <b>源码运行</b> 均可,如果需要获取作品信息,则需要进行二次开发进行调用。</p>
39-
<h2>🖱 直接运行</h2>
40-
<p>前往 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main.exe</code> 即可使用。</p>
38+
<p>如果仅需下载无水印作品文件,建议选择 <b>程序运行</b>;如果有其他需求,建议选择 <b>源码运行</b>!</p>
39+
<h2>🖱 程序运行</h2>
40+
<p>Windows 10 及以上用户可前往 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main.exe</code> 即可使用。</p>
41+
<p>若通过此方式使用程序,文件默认下载路径:<code>.\_internal\Download</code>;配置文件路径:<code>.\_internal\settings.json</code></p>
4142
<h2>⌨️ 源码运行</h2>
4243
<ol>
4344
<li>安装版本号不低于 <code>3.12</code> 的 Python 解释器</li>
44-
<li>运行 <code>pip install -r requirements.txt</code> 命令安装程序所需模块</li>
45+
<li>运行 <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> 命令安装程序所需模块</li>
4546
<li>下载本项目最新的源码或 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 发布的源码至本地</li>
4647
<li>运行 <code>main.py</code> 即可使用</li>
4748
</ol>
48-
<h2>💻 二次开发</h2>
49-
<p>如果需要获取小红书图文/视频作品信息,可以根据 <code>main.py</code> 的注释提示进行代码调用。</p>
49+
<h1>💻 二次开发</h1>
50+
<p>如果有其他需求,可以根据 <code>main.py</code> 的注释提示进行代码调用或修改!</p>
5051
<pre>
5152
# 测试链接
5253
error_demo = "https://github.com/JoeanAmier/XHS_Downloader"
@@ -58,20 +59,27 @@ path = "" # 作品数据/文件保存根路径,默认值:项目根路径
5859
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
5960
user_agent = "" # 请求头 User-Agent
6061
cookie = "" # 小红书网页版 Cookie,无需登录
61-
proxy = "" # 网络代理
62-
timeout = 5 # 网络请求超时限制,单位:秒,默认值:10
63-
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
62+
proxy = None # 网络代理
63+
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
64+
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
6465
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
65-
# async with XHS() as xhs:
66-
# pass # 使用默认参数
66+
record_data = False # 是否记录作品数据至文件
67+
image_format = "jpg" # 图文作品文件名称后缀
68+
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
69+
async with XHS() as xhs:
70+
pass # 使用默认参数
6771
async with XHS(path=path,
6872
folder_name=folder_name,
6973
user_agent=user_agent,
7074
cookie=cookie,
7175
proxy=proxy,
7276
timeout=timeout,
7377
chunk=chunk,
74-
max_retry=max_retry, ) as xhs: # 使用自定义参数
78+
max_retry=max_retry,
79+
record_data=record_data,
80+
image_format=image_format,
81+
folder_mode=folder_mode,
82+
) as xhs: # 使用自定义参数
7583
download = True # 是否下载作品文件,默认值:False
7684
# 返回作品详细信息,包括下载地址
7785
print(await xhs.extract(error_demo, download)) # 获取数据失败时返回空字典
@@ -81,6 +89,7 @@ async with XHS(path=path,
8189
</pre>
8290
<h1>⚙️ 配置文件</h1>
8391
<p>项目根目录下的 <code>settings.json</code> 文件,首次运行自动生成,可以自定义部分运行参数。</p>
92+
<p>如果您的计算机没有合适的程序编辑 JSON 文件,建议使用 <a href="https://try8.cn/tool/format/json">JSON 在线工具</a> 编辑配置文件内容</p>
8493
<table>
8594
<thead>
8695
<tr>
@@ -112,14 +121,14 @@ async with XHS(path=path,
112121
<tr>
113122
<td align="center">cookie</td>
114123
<td align="center">str</td>
115-
<td align="center">小红书网页版 Cookie,无需登录</td>
124+
<td align="center">小红书网页版 Cookie,<b>无需登录</b></td>
116125
<td align="center">默认 Cookie</td>
117126
</tr>
118127
<tr>
119128
<td align="center">proxy</td>
120129
<td align="center">str</td>
121-
<td align="center">设置代理</td>
122-
<td align="center"></td>
130+
<td align="center">设置程序代理</td>
131+
<td align="center">null</td>
123132
</tr>
124133
<tr>
125134
<td align="center">timeout</td>
@@ -142,15 +151,27 @@ async with XHS(path=path,
142151
<tr>
143152
<td align="center">record_data</td>
144153
<td align="center">bool</td>
145-
<td align="center">是否记录作品数据至文件</td>
154+
<td align="center">是否记录作品数据至 <code>TXT</code> 文件</td>
146155
<td align="center">false</td>
147156
</tr>
148157
<tr>
149158
<td align="center">image_format</td>
150159
<td align="center">str</td>
151-
<td align="center">图文作品文件名称后缀,例如:<code>jpg</code>、<code>png</code></td>
160+
<td align="center">图文作品文件名称后缀,不影响实际文件格式</td>
152161
<td align="center">webp</td>
153162
</tr>
163+
<tr>
164+
<td align="center">video_format</td>
165+
<td align="center">str</td>
166+
<td align="center">视频作品文件名称后缀,不影响实际文件格式</td>
167+
<td align="center">mp4</td>
168+
</tr>
169+
<tr>
170+
<td align="center">folder_mode</td>
171+
<td align="center">bool</td>
172+
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
173+
<td align="center">false</td>
174+
</tr>
154175
</tbody>
155176
</table>
156177
<h1>🌐 Cookie</h1>

main.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,27 @@ async def example():
1616
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
1717
user_agent = "" # 请求头 User-Agent
1818
cookie = "" # 小红书网页版 Cookie,无需登录
19-
proxy = "" # 网络代理
20-
timeout = 5 # 网络请求超时限制,单位:秒,默认值:10
21-
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
19+
proxy = None # 网络代理
20+
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
21+
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
2222
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
23-
# async with XHS() as xhs:
24-
# pass # 使用默认参数
23+
record_data = False # 是否记录作品数据至文件
24+
image_format = "jpg" # 图文作品文件名称后缀
25+
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
26+
async with XHS() as xhs:
27+
pass # 使用默认参数
2528
async with XHS(path=path,
2629
folder_name=folder_name,
2730
user_agent=user_agent,
2831
cookie=cookie,
2932
proxy=proxy,
3033
timeout=timeout,
3134
chunk=chunk,
32-
max_retry=max_retry, ) as xhs: # 使用自定义参数
35+
max_retry=max_retry,
36+
record_data=record_data,
37+
image_format=image_format,
38+
folder_mode=folder_mode,
39+
) as xhs: # 使用自定义参数
3340
download = True # 是否下载作品文件,默认值:False
3441
# 返回作品详细信息,包括下载地址
3542
print(await xhs.extract(error_demo, download)) # 获取数据失败时返回空字典

source/App.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from re import compile
2+
3+
from .Downloader import Download
4+
from .Explore import Explore
5+
from .Html import Html
6+
from .Image import Image
7+
from .Manager import Manager
8+
from .Static import (
9+
ROOT,
10+
ERROR,
11+
WARNING,
12+
)
13+
from .Video import Video
14+
15+
16+
class XHS:
17+
LINK = compile(r"https?://www\.xiaohongshu\.com/explore/[a-z0-9]+")
18+
SHARE = compile(r"https?://www\.xiaohongshu\.com/discovery/item/[a-z0-9]+")
19+
SHORT = compile(r"https?://xhslink\.com/[A-Za-z0-9]+")
20+
__INSTANCE = None
21+
TYPE = {
22+
"视频": "v",
23+
"图文": "n",
24+
}
25+
26+
def __new__(cls, *args, **kwargs):
27+
if not cls.__INSTANCE:
28+
cls.__INSTANCE = super().__new__(cls)
29+
return cls.__INSTANCE
30+
31+
def __init__(
32+
self,
33+
path="",
34+
folder_name="Download",
35+
user_agent: str = None,
36+
cookie: str = None,
37+
proxy: str = None,
38+
timeout=10,
39+
chunk=1024 * 1024,
40+
max_retry=5,
41+
record_data=False,
42+
image_format="webp",
43+
video_format="mp4",
44+
folder_mode=False,
45+
):
46+
self.manager = Manager(
47+
ROOT,
48+
path,
49+
folder_name,
50+
user_agent,
51+
chunk,
52+
cookie,
53+
proxy,
54+
timeout,
55+
max_retry,
56+
record_data,
57+
image_format,
58+
video_format,
59+
folder_mode,
60+
)
61+
self.html = Html(self.manager)
62+
self.image = Image()
63+
self.video = Video()
64+
self.explore = Explore()
65+
self.download = Download(self.manager, )
66+
self.rich_log = self.download.rich_log
67+
68+
def __extract_image(self, container: dict, html: str):
69+
container["下载地址"] = self.image.get_image_link(html)
70+
71+
def __extract_video(self, container: dict, html: str):
72+
container["下载地址"] = self.video.get_video_link(html)
73+
74+
async def __download_files(self, container: dict, download: bool, log, bar):
75+
name = self.__naming_rules(container)
76+
if (u := container["下载地址"]) and download:
77+
await self.download.run(u, name, self.TYPE[container["作品类型"]], log, bar)
78+
elif not u:
79+
self.rich_log(log, "提取作品文件下载地址失败!", ERROR)
80+
self.manager.save_data(name, container)
81+
82+
async def extract(self, url: str, download=False, log=None, bar=None) -> list[dict]:
83+
# return # 调试代码
84+
urls = await self.__extract_links(url)
85+
if not urls:
86+
self.rich_log(log, "提取小红书作品链接失败!", WARNING)
87+
else:
88+
self.rich_log(log, f"共 {len(urls)} 个小红书作品待处理...")
89+
# return urls # 调试代码
90+
return [await self.__deal_extract(i, download, log, bar) for i in urls]
91+
92+
async def __extract_links(self, url: str) -> list:
93+
urls = []
94+
for i in url.split():
95+
if u := self.SHORT.search(i):
96+
i = await self.html.request_url(
97+
u.group(), False)
98+
if u := self.SHARE.search(i):
99+
urls.append(u.group())
100+
elif u := self.LINK.search(i):
101+
urls.append(u.group())
102+
return urls
103+
104+
async def __deal_extract(self, url: str, download: bool, log, bar):
105+
self.rich_log(log, f"开始处理作品:{url}")
106+
html = await self.html.request_url(url)
107+
# self.rich_log(log, html) # 调试代码
108+
if not html:
109+
self.rich_log(log, f"{url} 获取数据失败!", ERROR)
110+
return {}
111+
data = self.explore.run(html)
112+
# self.rich_log(log, data) # 调试代码
113+
if not data:
114+
self.rich_log(log, f"{url} 提取数据失败!", ERROR)
115+
return {}
116+
match data["作品类型"]:
117+
case "视频":
118+
self.__extract_video(data, html)
119+
case "图文":
120+
self.__extract_image(data, html)
121+
case _:
122+
data["下载地址"] = []
123+
await self.__download_files(data, download, log, bar)
124+
self.rich_log(log, f"作品处理完成:{url}")
125+
return data
126+
127+
def __naming_rules(self, data: dict) -> str:
128+
"""下载文件默认使用 作品标题 或 作品 ID 作为文件名称,可修改此方法自定义文件名称格式"""
129+
return self.manager.filter_name(data["作品标题"]) or data["作品ID"]
130+
131+
async def __aenter__(self):
132+
return self
133+
134+
async def __aexit__(self, exc_type, exc_value, traceback):
135+
await self.close()
136+
137+
async def close(self):
138+
self.manager.clean()
139+
await self.html.session.close()
140+
await self.download.session.close()

0 commit comments

Comments
 (0)