首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >问答首页 >命令行工具,列出本地驱动器上的视频文件

命令行工具,列出本地驱动器上的视频文件
EN

Code Review用户
提问于 2018-08-05 12:20:12
回答 2查看 120关注 0票数 6

解释

大约一个月前我开始学习Haskell。作为练习,我重新创建了一个小的命令行工具,这是我以前用PowerShell编写的。在当前状态下,它在特定目录及其子目录中显示视频列表。稍后,我将添加播放或删除这些视频的功能(这应该很容易)。但最重要的是显示列表。目前,这些代码就是这么做的。

到目前为止,我非常喜欢Haskell,但我对这段代码的冗长和复杂感到不快。它是100行,似乎很难读。PowerShell脚本只有70行(完整的回放和删除视频),文字较少,很容易阅读。

这主要是因为我缺乏知识,如何编写好的Haskell代码?还是Haskell不是完成这类任务的好工具?

特别困扰我的是:

  • 我有所有这些小功能,它们执行一项特定的任务,并相互调用以实现最终目标。如果一个人对代码不熟悉,就很难阅读它。因为函数没有在一个明显的控制结构中连接,所以我们必须分析每个函数,看看它调用了什么其他函数,等等,直到图片完成为止。将其与命令式语言进行比较:有一些易于阅读的if语句和一些大型代码块。一目了然,人们就可以看到在哪里做了什么,如果感兴趣的话,就可以更多地了解实现的细节。很容易对程序的总体结构有一种感觉。
  • getRecursiveContents (我从互联网上复制了这个)。它又大又复杂。递归获取文件是一项日常任务--真的没有这样的库函数吗?
  • 很好,我可以描述一个定制类型在打印时应该如何Show本身。但是因为我处理的是列表,所以我不得不unlines $ map show它,这不是很漂亮。

目录结构

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
└───Videos
    │   Heat.1995.1080p.BRrip.x264.YIFY.mp4
    │   heat.png
    │   leon.png
    │   Leon.the.Professional.Extended.1994.BrRip.x264.YIFY.mp4
    │   mononoke hime.png
    │   Mononoke.hime.[Princess.Mononoke].[DUAL.AUDIO]1997.HDTVRip.x264.YIFY.mkv
    │   Oblivion.2013.1080p.BluRay.x264.YIFY.mp4
    │   oblivion.png
    │   terminator 2.png
    │   Terminator.2.Judgment.Day.1991.DC.1080p.BRrip.x264.GAZ.YIFY.mp4
    │   traffic.png
    │
    └───Series
            S01E01.Some.Show.mp4
            S01E02.Some.Show.mp4
            S01E03.Some.Show.mp4

输出

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
     Videos
  1  Heat.1995.1080p.BRrip.x264.YIFY
  2  Leon.the.Professional.Extended.1994.BrRip.x264.YIFY
  3  Mononoke.hime.[Princess.Mononoke].[DUAL.AUDIO]1997.HDTVRip.x264.YIFY
  4  Oblivion.2013.1080p.BluRay.x264.YIFY
  5  Terminator.2.Judgment.Day.1991.DC.1080p.BRrip.x264.GAZ.YIFY

     Series
  6  S01E01.Some.Show
  7  S01E02.Some.Show
  8  S01E03.Some.Show

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
module Main where

import Control.Monad (forM)
import Data.Char (toLower)
import Data.List (isInfixOf, nub, sort, sortBy)
import Data.List.Split (splitOn)
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath (takeBaseName, takeDirectory, takeExtension, ())
import Text.Printf (printf)


videoDirectory = "C:\\Users\\Swonkie\\Downloads\\Videos"
videoExtensions = [".mp4", ".mkv", ".avi", ".m4v"]

-- ANSI / VT color codes
color = "\ESC[1;31m"
reset = "\ESC[m"

type Library = [Directory]

data Directory = Directory { name  :: String
                           , files :: [Video]
                           }
instance Show Directory where
    show (Directory name files) = "     " ++ color ++ name ++ reset ++ "\n" ++ (unlines $ map show files)

data Video = Video { index  :: Integer
                   , path   :: FilePath
                   }
instance Show Video where
    show (Video i path) = printf "%3d  %s" i (takeBaseName path)


isVideoFile :: FilePath -> Bool
isVideoFile path = takeExtension path `elem` videoExtensions

-- | not used yet
getVideoByIndex :: [Video] -> Integer -> Maybe Video
getVideoByIndex files i =
    if length v > 0
        then Just (head v)
        else Nothing
    where v = filter (\ v -> index v == i) files

-- | not used yet
getVideoByName :: [Video] -> String -> Maybe Video
getVideoByName files s =
    if length v > 0
        then Just (head v)
        else Nothing
    where v = filter (\ v -> isInfixOf (map toLower s) (map toLower $ takeBaseName $ path v)) files


-- | The name of the folder containing the file, without its parent folders.
bottomFolder :: FilePath -> String
bottomFolder path = last $ splitOn "\\" $ takeDirectory path

-- | A list of all unique directory names which appear in the list of videos.
getDirectories :: [Video] -> [String]
getDirectories videos = nub $ map (bottomFolder . path) videos

-- | Filters the list of videos down to only those which are in a specific directory.
getVideosInDirectory :: [Video] -> String -> [Video]
getVideosInDirectory videos name = filter (\ v -> (bottomFolder $ path v) == name) videos

-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: [Video] -> String -> Directory
getDirectory videos name = Directory name (getVideosInDirectory videos name)

-- | Creates Video objects with indexes
getVideos :: [FilePath] -> [Video]
getVideos list = [Video (fst tp) (snd tp) | tp <- zip [1..] list]

-- | Gets all the directories of the videos and creates a list of Directory types.
getLibrary :: [Video] -> Library
getLibrary videos = map (getDirectory videos) $ getDirectories videos

getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topdir = do
    names <- listDirectory topdir
    paths <- forM names $ \ name -> do
        let path = topdir  name
        isDirectory <- doesDirectoryExist path
        if isDirectory
            then getRecursiveContents path
            else return [path]
    return (concat paths)

main :: IO ()
main = do
    -- get all video files recursively
    files <- getRecursiveContents videoDirectory    
    let videoFiles = sort $ filter isVideoFile files

    -- adding a character to the end of the path is a hack, to have subdirs sorted below parent dirs
    -- apparently "end of string" is last in the sort order, not first (weird)
    let sortedByDirectory = sortBy (\ a b -> compare (takeDirectory a ++ "$") (takeDirectory b ++ "$")) videoFiles
    let lib = getLibrary $ getVideos sortedByDirectory

    -- show the list of videos
    putStrLn ""
    putStr $ unlines $ map show lib
EN

回答 2

Code Review用户

回答已采纳

发布于 2018-08-06 00:37:20

欢迎来到Haskell编程世界。系好安全带,这将是一段旅程。

案例研究:getVideoBy*

您的函数getVideoByIndexgetVideoByName为改进提供了一个很好的案例研究。目前,这两个功能看起来非常相似。这就引出了我们的第一个原则。

,不要重复,

这两个函数的工作原理相同,我们在结果列表中应用了filter,然后应用了head。我们可以将该函数提取为一个单独的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
find :: (a -> Bool) -> [a] -> Maybe a
find p xs = 
    if length v > 0 
        then Just (head v)
        else Nothing
    where v = filter p xs

-- | not used yet
getVideoByIndex :: [Video] -> Integer -> Maybe Video
getVideoByIndex files i = find (\v -> index v == i) files

-- | not used yet
getVideoByName :: [Video] -> String -> Maybe Video
getVideoByName files s = find (\ v -> isInfixOf (map toLower s) (map toLower $ takeBaseName $ path v)) files

现在,对find的任何改进都将同时改进这两个功能。

使用null而不是length来检查列表是否为空

接下来,我们将检查length v。这是次优的,因为length是\\mathcal O(n)\$。此外,它将在无限列表上失败,例如length [1..] > 0永远不会退出。

相反,我们使用null,它是\\mathcal O(1)\$:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
find :: (a -> Bool) -> [a] -> Maybe a
find p xs = 
    if null v
        then Nothing
        else Just (head v)
    where v = filter p xs

如果要使用

,则使用模式匹配

但是,如果我们意外地写了下面的内容,会发生什么呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
find :: (a -> Bool) -> [a] -> Maybe a
find p xs = 
    if null v
        then Just (head v)
        else Nothing
    where v = filter p xs

那是个窃听器。我们在空列表上使用head。唉哟。如果我们使用模式匹配,我们可以完全消除这种错误:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
find :: (a -> Bool) -> [a] -> Maybe a
find p xs = 
    case filter p xs of
        (x:_) -> Just x
        _     -> Nothing

知道你的标准库

函数find实际上存在。它是由Data.List出口的。我们很容易用Hoogle找到

使用集合作为最后一个参数

下面是一些使用集合作为参数的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
foldl  :: (a -> b -> a) -> a -> [b] -> a
foldr  :: (a -> b -> b) -> b -> [a] -> b
filter :: (a -> Bool)   ->      [a] -> Maybe a
map    :: (a -> b)      ->      [a] -> [b]
delete :: Eq a => a     ->      [a] -> [a]
lookup :: Eq a => a     ->  [(a,b)] -> Maybe b

所有这些函数都使用list作为最后一个参数,因为它允许运行。对于getVideosBy*,我们也应该这样做:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import Data.List (find, isInfixOf)

-- | not used yet
getVideoByIndex :: Integer -> [Video] -> Maybe Video
getVideoByIndex i = find (\v -> index v == i)

-- | not used yet
getVideoByName :: String -> [Video] -> Maybe Video
getVideoByName s = find (\v -> (map toLower s) `isInfixOf` (map toLower $ takeBaseName $ path v))

进一步评论

我不打算将上面的注释应用到代码的其余部分,这只是一个练习。确保检查Prelude中的函数,例如,可以将getVideos编写为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- | Creates Video objects with indexes
getVideos :: [FilePath] -> [Video]
getVideos list = zipWith Video [1..] list
-- or
getVideos = zipWith Video [1..]

你的问题

小函数

我有所有这些小功能,它们执行一项特定的任务,并相互调用以实现最终目标。如果一个人对代码不熟悉,就很难阅读它。

那是Haskell的一部分。但是,如您所见,getVideoBy已经在标准库中了。如果您只使用一个函数一次,有时最好是内联它们。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: [Video] -> String -> Directory
getDirectory videos name = Directory name $ filter (\ v -> (bottomFolder $ path v) == name) videos

或者,您可以使用本地绑定来保留名称:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: [Video] -> String -> Directory
getDirectory videos name = Directory name (getVideosInDirectory videos name)
  where
    getVideosInDirectory videos name = filter (\ v -> (bottomFolder $ path v) == name) videos

既然我们有了一个本地绑定,我们甚至不需要提供这些参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: [Video] -> String -> Directory
getDirectory videos name = Directory name videosInDirectory
  where
    videosInDirectory = filter (\ v -> (bottomFolder $ path v) == name) videos

如果我们希望保留这两个函数,那么如果我们更改参数顺序(请参阅上面的“使用集合作为最后的参数”),它们将变得更容易应用和读取:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: String -> [Video] -> Directory
getDirectory name videos  = Directory name (getVideosInDirectory name videos)

getVideosInDirectory :: String -> [Video] -> [Video] 
getVideosInDirectory name videos = filter (\ v -> (bottomFolder $ path v) == name) videos

,正如我们在“使用集合作为最后的参数”中所看到的,可以将其简化为

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
getDirectory :: String -> [Video] -> Directory
getDirectory name = Directory name . getVideosInDirectory name

getVideosInDirectory :: String -> [Video] -> [Video] 
getVideosInDirectory name = filter (\ v -> (bottomFolder $ path v) == name)

unlines . map show

很好,我可以描述一个定制类型在打印时应该如何显示自己。但是因为我处理的是列表,所以我不得不unlines $ map show它,这不是很漂亮。

你可以用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mapM_ print lib

默认的Show类是非常基本的。实际上,ShowRead只有一个要求:任何自动派生两者的类型都有read (show x) == x。如果您想提供漂亮的格式设置,请使用自定义类型。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Pretty a where
    pretty :: a -> String

能给你更多的控制权。或者,加一个newtype在列表的周围,例如。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
newtype WithNewlines t a = WithNewlines { unWithNewLines :: t a }

instance (Show a, Foldable t) => Show (WithNewlines t a) where
  show = unlines . concatMap (pure . show) . unWithNewLines

然后使用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
putStrLn $ WithNewLines lib

但这也或多或少是神秘的。

票数 2
EN

Code Review用户

发布于 2018-08-05 15:16:34

我认为有些尴尬是因为在构建文件列表时,getRecursiveContents丢弃了目录结构,然后代码试图部分地重构它,以便打印列表。

另一种方法是使用来自Data.Tree容器,并尝试在整个程序的大部分过程中保持树结构,直到生成最终的清单。

例如,下面是getRecursiveContents的一个变体,它保留了目录结构(partitionM额外的可以简化这段代码):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-- (directory name, files in directory)
getRecursiveContents' :: FilePath -> IO (Tree (FilePath,[FilePath]))
getRecursiveContents' = Data.Tree.unfoldTreeM $ \folder -> do
    names <- listDirectory folder
    -- using partitionM would simplify this a lot, but the function is not in base
    let separate name next =
            do (fs,ds) <- next
               exists <- doesDirectoryExist (folder  name)
               pure $ if exists then (fs, name:ds)
                                else (name:fs, ds)
    (fs,ds) <- foldr separate (pure ([],[])) names
    --
    pure ((folder, fs), map (folder ) ds)

我们只能使用以下方式保存视频:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 keepVideos :: Tree (a,[FilePath]) -> Tree (a,[FilePath])
 keepVideos = fmap (fmap (filter isVideoFile)) -- tree, tuple

嵌套fmap在树和元组中操作,它们都是函子。元组的fmap工作在第二个元素之上。

我们还可以在树上对文件编号,例如使用State在所有列表中线程一个计数器:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
numberFiles :: Tree (a,[b]) -> Tree (a,[(Int,b)])
numberFiles tree =
    let tag b = do n <- get
                   modify succ
                   pure (n,b)
     in evalState (traverse (traverse (traverse tag)) tree) 0 -- tree, tuple, list

嵌套的traverses就像嵌套的fmaps,现在只有三个,因为我们也在遍历列表。元组的traverse工作在第二个元素之上。

为了生成最终的列表,我们可以使用来自Data.Foldable的函数来处理节点列表(Trees是Foldable)。这应该很容易,因为我们有每个节点的文件夹名和文件列表。树也可以用更“结构化”的方式使用foldTree,但这里可能不需要这样做。

票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/201029

复制
相关文章
使用 Linux 命令行工具来了解你的 NVMe 驱动器
NVMe 是指非易失性内存规范Non-Volatile Memory Express,它规范了软件和存储通过 PCIe 和其他协议(包括 TCP)进行通信的方式。它是由非营利组织领导的 开放规范,并定义了几种形式的固态存储。
用户9042463
2021/09/26
1.7K0
使用 Linux 命令行工具来了解你的 NVMe 驱动器
NVMe 是指非易失性内存规范,它规范了软件和存储通过 PCIe 和其他协议(包括 TCP)进行通信的方式。它是由非营利组织领导的 开放规范,并定义了几种形式的固态存储。
用户9239730
2021/11/30
2.1K0
Rust 开发命令行工具(上)
在上一篇致所有渴望学习Rust的人的信中我们介绍了Rust可以在命令行工具上也大有建树。
前端柒八九
2023/09/20
8390
Rust 开发命令行工具(上)
列出linux系统的本地锁使用lslk命令
锁可以属于本地系统上的进程,也可以属于本地系统是NFS服务器的NFS客户端系统上的进程。
用户5005176
2021/09/08
1.2K0
使用lslk命令列出linux系统的本地锁
锁可以属于本地系统上的进程,也可以属于本地系统是NFS服务器的NFS客户端系统上的进程。
用户1685462
2021/09/08
1.1K0
小教程:​列出Ubuntu上的磁盘
在本文中,我将向您展示如何从Ubuntu中列出连接到您的计算机上的磁盘(即ssd、HDDs、u盘)。
用户6543014
2020/02/14
5.6K0
小教程:​列出Ubuntu上的磁盘
flv.js 实现播放本地视频文件的技巧
有时候某些播放器无法直接播放本地视频文件,因此需要在本地启一个 HTTP 静态服务,通过 URL 的形式实现播放目的。比如,自己在使用 flv.js 播放本地视频文件时就遇到了这个问题。
liuzhen007
2022/11/08
8.4K0
flv.js 实现播放本地视频文件的技巧
linux/unix 上那些炫酷的命令行工具(一)
工欲善其事,必先利其器,作为一名手艺人, 我们应该利用好工具,以便将我们的时间用在更有效率的事情上,类 Linux 上很多原生的如 cat,grep 等命令给我们带来了很多便利,但其实都或多或少有可以改进的地方,市面上其实出现了大量开发者开发的对这些原生做了很大改进的命令,比原生的命令更好用,大部分可以说是秒杀原生命令了,能让你事半功倍,强烈推荐
kunge
2019/12/22
1.4K0
多媒体 -获取本地图片和视频文件
很多自定义播放器,和自定义多选相册的图片都是需要先获取系统图片库中的所有图片资源或者视屏资源 ,所使用的核心方法就是AssetsLibrary框架里的ALAssetsLibrary。
進无尽
2018/09/12
8750
多媒体 -获取本地图片和视频文件
如何在mac电脑上配置命令行工具
Hi,欢迎大家在有空的时候做客【江涛学编程】,这里是2023年的第7篇原创文章,今天我们来聊一聊如何在mac电脑上配置命令行工具
江涛学编程
2023/05/27
3950
如何在mac电脑上配置命令行工具
命令行上的数据科学第二版 四、创建命令行工具
在整本书中,我将向您介绍许多基本上适合一行的命令和管道。这些被称为一行程序或管道。能够只用一行程序执行复杂的任务是命令行的强大之处。这是一种与编写和使用传统程序截然不同的体验。
ApacheCN_飞龙
2023/03/31
2.3K0
如何在mac电脑上配置命令行工具
Hi,欢迎大家在有空的时候做客【江涛学编程】,这里是2023年的第7篇原创文章,今天我们来聊一聊如何在mac电脑上配置命令行工具
江涛学编程
2023/03/05
7630
支持函数本地部署调试 SCF命令行工具开源上线!
SCF CLI 是腾讯云无服务器云函数 SCF(Serverless Cloud Function)产品的命令行工具。通过SCF命令行工具,用户可以方便的实现函数打包、部署以及本地调试,并在本地生成云函数的项目并基于 demo 项目进一步的开发。
腾讯云serverless团队
2019/06/21
1.7K0
利用Git工具将本地创建的项目上传到Github上
作为一个对前沿技术很看好的小青年,怎么能不会用Github呢?一年前我创建了Github,也知道git,但是尝试过用,但是就没弄明白,很多粉丝都问我Github的账号,想关注一波,无奈里面啥都没有,因此必须学习一下并且写点东西进去,Google了很多东西,尝试了很多次,最后还是成了,以下将分享下我的经验随笔~~~
Angel_Kitty
2018/08/01
2.5K0
利用Git工具将本地创建的项目上传到Github上
微软修复Bitlocker驱动器加密工具的绕过漏洞
微软最近修复了Windows Bitlocker驱动器加密中的一个漏洞,这个漏洞可以被用来快速绕过加密功能获取到受害者加密的重要信息。 加密软件中的漏洞 磁盘加密工具是一个重要的保护软件,所有的电子设备都需要依靠它们来保护用户的数据。在它们出现之前,攻击者只需要启动Linux,然后安装存有用户数据的磁盘就可以访问用户的加密文件了。 但是,这些软件也存在着漏洞给黑客们机会。 九月,谷歌Project Zero团队的安全专家James Forshaw在Windows系统上安装的加密软件TrueCrypt发现
FB客服
2018/02/07
1.5K0
微软修复Bitlocker驱动器加密工具的绕过漏洞
EasyDSS分布式文件系统(CFS)的搭建过程分享
我们接触到的部分EasyDSS项目中需要频繁的对视频做合成处理,但是使用单一服务器会导致CPU占用率一直处于高负载的状态,因此需要采用分布式系统来减小web服务器的CPU负载,需要快速的同步录像视频文件。
TSINGSEE青犀视频
2021/07/29
6810
无法在驱动器0的分区1上安装windows
一、原因分析 win8/win10系统均添加快速启动功能,预装的win8/win10电脑默认都是UEFI引导和GPT硬盘,传统的引导方式为Legacy引导和MBR硬盘,UEFI必须跟GPT对应,同理Legacy必须跟MBR对应。如果BIOS开启UEFI,而硬盘分区表格式为MBR则无法安装;BIOS关闭UEFI而硬盘分区表格式为GPT也是无法安装Windows。
Dabenshi
2023/05/25
3K0
Malwaresearch:在Openmalware.org上查找恶意软件的命令行工具
Malwaresearch是一个在Openmalware.org上查找恶意软件的命令行工具,旨在加快查找及下载恶意软件样本的过程。 该工具旨在通过命令行界面加快查找和下载恶意软件样本的过程。我们已经在脚本中使用了两个主要的恶意软件转储站点(openmalware.org和malwr.com)提供的API,它非常的简单易用,可以允许用户查询有关恶意软件的信息(包括姓名、MD5、SHA-1、SHA-256等),下载所需的恶意软件样本文件,甚至将其数字签名(哈希)与可疑转储中的数字签名进行比较。 我们下一步的目标
FB客服
2018/02/26
1.1K0
Malwaresearch:在Openmalware.org上查找恶意软件的命令行工具
macOS 上如何写自定义命令行工具?
苹果的 masOS 系统的底层核心是 Darwin 系统。Darwin 是类 Unix 系统,所以我们可以在 masOS 上,像 Linux 一样,执行命令行工具。
前端西瓜哥
2022/12/21
6830
macOS 上如何写自定义命令行工具?
IPFS 本地节点搭建(命令行)
上一篇《IPFS 分布式文件存储原理》对于 IPFS 系统的设计理念、功能、工作原理及 IPNS 做了详细的介绍,那么,如何在本地搭建一个 IPFS 节点呢?
pseudoyu
2023/04/11
8060
IPFS 本地节点搭建(命令行)

相似问题

连接视频文件的工具

20

在Linux系统上查找大型文件的命令行工具

10

命令行工具的筛选文件

10

命令行字典工具,使用webscraping

10

Rdoc -启动文档的命令行工具(改进)

10
添加站长 进交流群

领取专属 10元无门槛券

AI混元助手 在线答疑

扫码加入开发者社群
关注 腾讯云开发者公众号

洞察 腾讯核心技术

剖析业界实践案例

扫码关注腾讯云开发者公众号
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文