mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
更新项目代码
This commit is contained in:
18
README.md
18
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<img src="static/XHS-Downloader.png" alt="" height="256" width="256"><br>
|
||||
<h1>小红书作品采集工具</h1>
|
||||
<h1>XHS-Downloader</h1>
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=for-the-badge">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=for-the-badge&color=c56cf0">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=fff200">
|
||||
@@ -11,21 +11,21 @@
|
||||
<h1>📑 功能清单</h1>
|
||||
<ul>
|
||||
<li>✅ 采集小红书图文/视频作品信息</li>
|
||||
<li>✅ 获取小红书图文/视频作品下载地址</li>
|
||||
<li>✅ 下载小红书图文/视频作品文件</li>
|
||||
<li>✅ 自动跳过已存在的作品文件</li>
|
||||
<li>✅ 获取小红书图文/视频作品文件下载地址</li>
|
||||
<li>✅ 下载小红书无水印图文/视频作品文件</li>
|
||||
<li>✅ 自动跳过已下载的作品文件</li>
|
||||
<li>✅ 作品文件完整性处理机制</li>
|
||||
<li>✅ 批量下载小红书作品文件</li>
|
||||
</ul>
|
||||
<h1>📸 程序截图</h1>
|
||||
<br>
|
||||
<img src="static/程序运行截图.png" alt="">
|
||||
<h1>🔗 支持链接</h1>
|
||||
<ul>
|
||||
<li>https://www.xiaohongshu.com/explore/作品ID</li>
|
||||
<li>https://www.xiaohongshu.com/discovery/item/作品ID</li>
|
||||
<li>https://xhslink.com/分享码</li>
|
||||
<p>可以单次输入多个作品链接,链接之间使用空格分隔。</p>
|
||||
<li><code>https://www.xiaohongshu.com/explore/作品ID</code></li>
|
||||
<li><code>https://www.xiaohongshu.com/discovery/item/作品ID</code></li>
|
||||
<li><code>https://xhslink.com/分享码</code></li>
|
||||
<br/>
|
||||
<p><b>可以单次输入多个作品链接,链接之间使用空格分隔。</b></p>
|
||||
</ul>
|
||||
<h1>🪟 关于终端</h1>
|
||||
<p>⭐ 推荐使用 <a href="https://learn.microsoft.com/zh-cn/windows/terminal/install">Windows 终端</a> (Windows 11 自带默认终端)运行程序以便获得最佳显示效果!</p>
|
||||
|
||||
6
main.py
6
main.py
@@ -1,4 +1,5 @@
|
||||
from source import XHS
|
||||
from source import XHSDownloader
|
||||
|
||||
|
||||
def example():
|
||||
@@ -30,5 +31,6 @@ def example():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
example()
|
||||
# XHSDownloader().run()
|
||||
# example()
|
||||
with XHSDownloader() as xhs:
|
||||
xhs.run()
|
||||
|
||||
@@ -2,14 +2,13 @@ from pathlib import Path
|
||||
from shutil import move
|
||||
from shutil import rmtree
|
||||
|
||||
__all__ = ['Manager']
|
||||
__all__ = ['Manager', "rich_log"]
|
||||
|
||||
|
||||
class Manager:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/119.0.0.0 Safari/537.36",
|
||||
}
|
||||
"Chrome/119.0.0.0 Safari/537.36", }
|
||||
|
||||
def __init__(self, root: Path):
|
||||
self.temp = root.joinpath("./temp")
|
||||
@@ -28,3 +27,8 @@ class Manager:
|
||||
|
||||
def clean(self):
|
||||
rmtree(self.temp.resolve())
|
||||
|
||||
|
||||
def rich_log(log, text):
|
||||
if log:
|
||||
log.write(text)
|
||||
|
||||
@@ -12,13 +12,14 @@ from textual.widgets import Footer
|
||||
from textual.widgets import Header
|
||||
from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import Log
|
||||
from textual.widgets import RichLog
|
||||
|
||||
from .Downloader import Download
|
||||
from .Explore import Explore
|
||||
from .Html import Html
|
||||
from .Image import Image
|
||||
from .Manager import Manager
|
||||
from .Manager import rich_log
|
||||
from .Settings import Settings
|
||||
from .Video import Video
|
||||
|
||||
@@ -54,45 +55,51 @@ class XHS:
|
||||
chunk,
|
||||
timeout)
|
||||
|
||||
def __get_image(self, container: dict, html: str, download):
|
||||
def __get_image(self, container: dict, html: str, download, log):
|
||||
urls = self.image.get_image_link(html)
|
||||
# rich_log(log, urls) # 调试代码
|
||||
if download:
|
||||
self.download.run(urls, self.__naming_rules(container), 1)
|
||||
container["下载地址"] = urls
|
||||
|
||||
def __get_video(self, container: dict, html: str, download):
|
||||
def __get_video(self, container: dict, html: str, download, log):
|
||||
url = self.video.get_video_link(html)
|
||||
# rich_log(log, url) # 调试代码
|
||||
if download:
|
||||
self.download.run(url, self.__naming_rules(container), 0)
|
||||
container["下载地址"] = url
|
||||
|
||||
def extract(self, url: str, download=False) -> list[dict]:
|
||||
def extract(self, url: str, download=False, log=None) -> list[dict]:
|
||||
urls = self.__deal_links(url)
|
||||
# return urls
|
||||
return [self.__deal_extract(i, download) for i in urls]
|
||||
# rich_log(log, urls) # 调试代码
|
||||
# return urls # 调试代码
|
||||
return [self.__deal_extract(i, download, log) for i in urls]
|
||||
|
||||
def __deal_links(self, url: str) -> list:
|
||||
urls = []
|
||||
for i in url.split():
|
||||
if u := self.short.search(i):
|
||||
i = self.html.request_url(u.group(), headers=self.manager.headers, text=False)
|
||||
i = self.html.request_url(
|
||||
u.group(), headers=self.manager.headers, text=False)
|
||||
if u := self.share.search(i):
|
||||
urls.append(u.group())
|
||||
elif u := self.link.search(i):
|
||||
urls.append(u.group())
|
||||
return urls
|
||||
|
||||
def __deal_extract(self, url: str, download: bool):
|
||||
def __deal_extract(self, url: str, download: bool, log):
|
||||
html = self.html.request_url(url)
|
||||
# rich_log(log, html) # 调试代码
|
||||
if not html:
|
||||
return {}
|
||||
data = self.explore.run(html)
|
||||
# rich_log(log, data) # 调试代码
|
||||
if not data:
|
||||
return {}
|
||||
if data["作品类型"] == "视频":
|
||||
self.__get_video(data, html, download)
|
||||
self.__get_video(data, html, download, log)
|
||||
else:
|
||||
self.__get_image(data, html, download)
|
||||
self.__get_image(data, html, download, log)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
@@ -109,35 +116,36 @@ class XHS:
|
||||
|
||||
class XHSDownloader(App):
|
||||
VERSION = 1.6
|
||||
Beta = True
|
||||
BETA = True
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
APP = XHS(**Settings(ROOT).run())
|
||||
CSS_PATH = ROOT.joinpath(
|
||||
"static/XHS-Downloader.tcss")
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="退出程序"),
|
||||
Binding(key="q", action="quit", description="结束运行"),
|
||||
("d", "toggle_dark", "切换主题"),
|
||||
]
|
||||
|
||||
# APP = XHS(**Settings().run())
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
# def __enter__(self):
|
||||
# return self
|
||||
|
||||
# def __exit__(self, exc_type, exc_value, traceback):
|
||||
# self.manager.clean()
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.APP.manager.clean()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(Label("请输入小红书图文/视频作品链接(多个链接使用空格分隔):"),
|
||||
Input(placeholder="URL"),
|
||||
yield ScrollableContainer(Label("请输入小红书图文/视频作品链接:"),
|
||||
Input(placeholder="多个链接之间使用空格分隔"),
|
||||
HorizontalScroll(Button("下载无水印图片/视频", id="deal"),
|
||||
Button("读取剪贴板", id="paste"),
|
||||
Button("清空输入框", id="reset"), ))
|
||||
yield Log(auto_scroll=True)
|
||||
yield RichLog(markup=True)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = f"小红书作品采集工具 V{self.VERSION}{" Beta" if self.Beta else ""}"
|
||||
self.title = f"XHS-Downloader V{
|
||||
self.VERSION}{
|
||||
" Beta" if self.BETA else ""}"
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "deal":
|
||||
@@ -148,14 +156,8 @@ class XHSDownloader(App):
|
||||
self.query_one(Input).value = paste()
|
||||
|
||||
def deal(self):
|
||||
url = self.query_one(Input).value
|
||||
log = self.query_one(Log)
|
||||
if not url:
|
||||
log.write_line("未输入任何作品链接!")
|
||||
else:
|
||||
self.APP.extract(url, True, log)
|
||||
self.query_one(Input).value = ""
|
||||
|
||||
|
||||
class FakeGUI:
|
||||
pass
|
||||
url = self.query_one(Input)
|
||||
log = self.query_one(RichLog)
|
||||
if self.APP.extract(url.value, True, log=log):
|
||||
pass
|
||||
url.value = ""
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
Screen {
|
||||
layout: grid;
|
||||
grid-size: 1 2;
|
||||
grid-rows: 1fr;
|
||||
grid-columns: 1fr;
|
||||
grid-gutter: 1;
|
||||
}
|
||||
Button {
|
||||
width: 1fr;
|
||||
margin: 1 1;
|
||||
text-style: bold;
|
||||
}
|
||||
Button#deal {
|
||||
tint: green 35%;
|
||||
}
|
||||
Button#paste {
|
||||
tint: green 35%;
|
||||
Button#deal, Button#paste {
|
||||
tint: green 40%;
|
||||
}
|
||||
Button#reset {
|
||||
tint: red 35%;
|
||||
tint: red 40%;
|
||||
}
|
||||
Label {
|
||||
width: 100%;
|
||||
@@ -26,11 +16,3 @@ Label {
|
||||
content-align-vertical: middle;
|
||||
text-style: bold;
|
||||
}
|
||||
ScrollableContainer {
|
||||
row-span: 1;
|
||||
column-span: 1;
|
||||
}
|
||||
Log {
|
||||
row-span: 1;
|
||||
column-span: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user