├── v2ex_scrapy ├── __init__.py ├── spiders │ ├── __init__.py │ ├── V2exSpider.py │ ├── V2exMemberSpider.py │ ├── V2exNodeTopicSpider.py │ └── CommonSpider.py ├── utils.py ├── insert_ignore.py ├── pipelines.py ├── DB.py ├── items.py ├── settings.py ├── v2ex_parser.py └── middlewares.py ├── requirements-analysis.txt ├── analysis ├── v2ex-analysis │ ├── env.d.ts │ ├── src │ │ ├── assets │ │ │ ├── base.css │ │ │ ├── logo.svg │ │ │ └── main.css │ │ ├── main.ts │ │ ├── components │ │ │ ├── base │ │ │ │ ├── Tag.vue │ │ │ │ ├── Node.vue │ │ │ │ ├── Member.vue │ │ │ │ ├── Topic.vue │ │ │ │ └── Comment.vue │ │ │ ├── TopTag.vue │ │ │ ├── TopComment.vue │ │ │ ├── TopMember.vue │ │ │ └── TopTopic.vue │ │ ├── types │ │ │ └── F.ts │ │ └── App.vue │ ├── public │ │ ├── favicon.ico │ │ ├── tag-usage-count.json │ │ ├── top-user-by-topic_count.json │ │ ├── top-user-by-comment_count.json │ │ ├── top-topic-by-clicks.json │ │ ├── top-topic-by-votes.json │ │ ├── top-topic-by-thank_count.json │ │ ├── top-topic-by-favorite_count.json │ │ ├── new-topic-every-month.json │ │ ├── new-comment-every-month.json │ │ ├── new-member-every-month.json │ │ └── top-comment.json │ ├── postcss.config.js │ ├── tsconfig.json │ ├── tailwind.config.js │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.app.json │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── pnpm-lock.yaml └── main.py ├── requirements.txt ├── image └── t │ ├── 水深火热-每月新增帖子.png │ ├── 水深火热-每月新增评论.png │ ├── 1688469358680.png │ ├── 1688470374959.png │ └── 1688470738877.png ├── main.py ├── scrapy.cfg ├── .vscode └── launch.json ├── user-agents.txt ├── LICENSE ├── .github └── workflows │ └── gh-pages.yml ├── query.sql ├── .gitignore ├── README.md └── README-en.md /v2ex_scrapy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-analysis.txt: -------------------------------------------------------------------------------- 1 | pandas==2.0.1 2 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | arrow==1.2.3 2 | httpx==0.24.1 3 | Scrapy==2.9.0 4 | SQLAlchemy==2.0.17 5 | -------------------------------------------------------------------------------- /image/t/水深火热-每月新增帖子.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/image/t/水深火热-每月新增帖子.png -------------------------------------------------------------------------------- /image/t/水深火热-每月新增评论.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/image/t/水深火热-每月新增评论.png -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/assets/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /image/t/1688469358680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/image/t/1688469358680.png -------------------------------------------------------------------------------- /image/t/1688470374959.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/image/t/1688470374959.png -------------------------------------------------------------------------------- /image/t/1688470738877.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/image/t/1688470738877.png -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldshensheep/v2ex_scrapy/HEAD/analysis/v2ex-analysis/public/favicon.ico -------------------------------------------------------------------------------- /analysis/v2ex-analysis/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/main.ts: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import App from './App.vue' 5 | 6 | createApp(App).mount('#app') 7 | -------------------------------------------------------------------------------- /v2ex_scrapy/spiders/__init__.py: -------------------------------------------------------------------------------- 1 | # This package will contain the spiders of your Scrapy project 2 | # 3 | # Please refer to the documentation for information on how to create and manage 4 | # your spiders. 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from scrapy.crawler import CrawlerProcess 2 | from scrapy.utils.project import get_project_settings 3 | 4 | process = CrawlerProcess(get_project_settings()) 5 | 6 | process.crawl("v2ex") 7 | process.start() 8 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/base/Tag.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/base/Node.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/base/Member.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [require("daisyui")], 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /scrapy.cfg: -------------------------------------------------------------------------------- 1 | # Automatically created by: scrapy startproject 2 | # 3 | # For more information about the [deploy] section see: 4 | # https://scrapyd.readthedocs.io/en/latest/deploy.html 5 | 6 | [settings] 7 | default = v2ex_scrapy.settings 8 | 9 | [deploy] 10 | #url = http://localhost:6800/ 11 | project = v2ex_scrapy 12 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/base/Topic.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "types": ["node"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Launch Scrapy Spider", 6 | "type": "python", 7 | "request": "launch", 8 | "module": "scrapy", 9 | "args": [ 10 | "runspider", 11 | "${file}" 12 | ], 13 | "console": "integratedTerminal" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /analysis/v2ex-analysis/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue", 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "src/**/__tests__/*" 11 | ], 12 | "compilerOptions": { 13 | "composite": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": [ 17 | "./src/*" 18 | ] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /analysis/v2ex-analysis/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/tag-usage-count.json: -------------------------------------------------------------------------------- 1 | [{"tag":"程序员","count":225},{"tag":"华为","count":201},{"tag":"疫情","count":189},{"tag":"GitHub","count":172},{"tag":"微信","count":158},{"tag":"知乎","count":154},{"tag":"V2EX","count":154},{"tag":"App","count":146},{"tag":"核酸","count":119},{"tag":"帖子","count":119},{"tag":"Google","count":111},{"tag":"百度","count":110},{"tag":"icu","count":105},{"tag":"疫苗","count":98},{"tag":"开源","count":97},{"tag":"房价","count":95},{"tag":"域名","count":87},{"tag":"网站","count":81},{"tag":"代码","count":74},{"tag":"讨论","count":72}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/base/Comment.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /user-agents.txt: -------------------------------------------------------------------------------- 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246 2 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9 3 | Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 OPR/89.0.4447.51 4 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 5 | Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0 -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | /* 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } */ 36 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-user-by-topic_count.json: -------------------------------------------------------------------------------- 1 | [{"username":"vitovan","topic_count":31},{"username":"est","topic_count":25},{"username":"taobibi","topic_count":20},{"username":"xuanwu","topic_count":15},{"username":"youmee","topic_count":14},{"username":"permaylau","topic_count":14},{"username":"weiruanniubi","topic_count":13},{"username":"ech0x","topic_count":13},{"username":"agagega","topic_count":13},{"username":"li24361","topic_count":12},{"username":"labulaka521","topic_count":12},{"username":"kisshere","topic_count":12},{"username":"whwq2012","topic_count":11},{"username":"statement","topic_count":11},{"username":"razios","topic_count":11},{"username":"flowfire","topic_count":11},{"username":"find456789","topic_count":11},{"username":"bclerdx","topic_count":11},{"username":"also24","topic_count":11},{"username":"CEBBCAT","topic_count":11}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/types/F.ts: -------------------------------------------------------------------------------- 1 | export interface TopComment { 2 | topic_id: number; 3 | id: number; 4 | content: string; 5 | thank_count: number; 6 | no: number; 7 | title: string; 8 | } 9 | 10 | export enum TopicBy { 11 | clicks = "clicks", 12 | favorite_count = "favorite_count", 13 | thank_count = "thank_count", 14 | votes = "votes", 15 | } 16 | export enum UserBy { 17 | comment_count = "comment_count", 18 | topic_count = "topic_count", 19 | } 20 | 21 | export interface TopTopic { 22 | id: number; 23 | title: string; 24 | clicks?: number; 25 | favorite_count?: number; 26 | thank_count?: number; 27 | votes?: number; 28 | } 29 | 30 | export interface TopMember { 31 | username: string; 32 | comment_count?: number; 33 | topic_count?: number; 34 | } 35 | 36 | export interface TopTag { 37 | tag: string; 38 | count?: number; 39 | } 40 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-user-by-comment_count.json: -------------------------------------------------------------------------------- 1 | [{"username":"murmur","comment_count":1276},{"username":"glfpes","comment_count":1093},{"username":"reus","comment_count":1055},{"username":"sagaxu","comment_count":1022},{"username":"cmdOptionKana","comment_count":876},{"username":"hoyixi","comment_count":843},{"username":"learningman","comment_count":804},{"username":"Jooooooooo","comment_count":787},{"username":"juded","comment_count":783},{"username":"imn1","comment_count":737},{"username":"x86","comment_count":734},{"username":"optional","comment_count":734},{"username":"TypeError","comment_count":706},{"username":"Livid","comment_count":703},{"username":"yaphets666","comment_count":670},{"username":"ericwoflskin","comment_count":668},{"username":"Leonard","comment_count":662},{"username":"coderluan","comment_count":653},{"username":"est","comment_count":604},{"username":"eason1874","comment_count":552}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v2ex-analysis", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false" 11 | }, 12 | "dependencies": { 13 | "plotly.js-dist-min": "^2.24.3", 14 | "vue": "^3.3.4" 15 | }, 16 | "devDependencies": { 17 | "@tsconfig/node18": "^2.0.1", 18 | "@types/node": "^18.16.17", 19 | "@vitejs/plugin-vue": "^4.2.3", 20 | "@vue/tsconfig": "^0.4.0", 21 | "autoprefixer": "^10.4.14", 22 | "daisyui": "^3.2.1", 23 | "npm-run-all": "^4.1.5", 24 | "postcss": "^8.4.25", 25 | "tailwindcss": "^3.3.2", 26 | "typescript": "~5.0.4", 27 | "vite": "^4.3.9", 28 | "vue-tsc": "^1.6.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/TopTag.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 40 | -------------------------------------------------------------------------------- /v2ex_scrapy/utils.py: -------------------------------------------------------------------------------- 1 | from http.cookies import SimpleCookie 2 | import json 3 | from typing import Union 4 | 5 | import arrow 6 | 7 | 8 | def time_to_timestamp(t: str) -> int: 9 | t = t.strip() 10 | a = None 11 | try: 12 | if t[-2:] == "00": 13 | a = arrow.get(t, "YYYY-MM-DD HH:mm:ss ZZ") 14 | else: 15 | a = arrow.utcnow().dehumanize(t.replace(" ", ""), "zh") 16 | return int(a.to("+08:00").timestamp()) 17 | except Exception: 18 | return 0 19 | 20 | 21 | def none_or_strip(s: Union[str, None]) -> Union[str, None]: 22 | if s is not None: 23 | return s.strip() 24 | return s 25 | 26 | 27 | def json_to_str(j): 28 | return json.dumps(j, ensure_ascii=False) 29 | 30 | 31 | def cookie_str2cookie_dict(cookie_str: str): 32 | simple_cookie = SimpleCookie() 33 | simple_cookie.load(cookie_str) 34 | return {k: v.value for k, v in simple_cookie.items()} 35 | 36 | 37 | if __name__ == "__main__": 38 | a = ["2022-04-28 13:24:38 +08:00", "287 天前", "1 小时前"] 39 | 40 | for i in a: 41 | print(time_to_timestamp(i)) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 oldshensheep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-topic-by-clicks.json: -------------------------------------------------------------------------------- 1 | [{"id":557538,"title":"Openbilibili? B 站在 Github 上公开了自己的后端源代码。","clicks":81613},{"id":388344,"title":"一兆韦德健身房无耻套路(被坑经历)","clicks":55447},{"id":603138,"title":"Vultr 的 ip 是不是被重点关照了?","clicks":54856},{"id":130791,"title":"有没有办法抓到使用国外服务器办色情站的人员?","clicks":46713},{"id":937283,"title":"上海 7 年前端求捞","clicks":43220},{"id":299142,"title":"我想知道在中国如何邮寄一把菜刀","clicks":42597},{"id":315502,"title":"python 刷 V2EX 活跃度","clicks":40046},{"id":865906,"title":"感觉要出大事情了?","clicks":38956},{"id":329592,"title":"防盗版软件黑科技又一案例","clicks":35476},{"id":887715,"title":"国庆刚办完婚礼,现在感觉已经过不下去了","clicks":35418},{"id":670151,"title":"如何看待一个 MC 启动器作者被逼向他软件的破解者道歉?","clicks":34749},{"id":898448,"title":"疫情管控,层层加码,一刀切","clicks":33741},{"id":728186,"title":"8 亿 QQ 手机绑定数据泄露?请问作为用户发现自己手机号被泄露的情况下应该如何做?","clicks":32254},{"id":731552,"title":"有在工作后移民的吗?","clicks":31484},{"id":400557,"title":"北京电信启用了端口白名单","clicks":31299},{"id":554157,"title":"华为开源了个方舟编译器","clicks":30421},{"id":582042,"title":"我为什么说大疆是国产垃圾品牌中的战斗机","clicks":30074},{"id":416067,"title":"Steam 主动限制了中国地区的社区功能","clicks":30029},{"id":604675,"title":"阿里 IDC 真是牛逼。昨天的交换机事件。","clicks":29323},{"id":784789,"title":"原来 Linux 内核贡献第二是这么来的","clicks":29202}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-topic-by-votes.json: -------------------------------------------------------------------------------- 1 | [{"id":473163,"title":"我花了 14 个小时找了一下长春长生们究竟卖到了哪里去","votes":358},{"id":757489,"title":"猴年马月狗日,王伟疯了","votes":67},{"id":759427,"title":"王妈说:“伟宝,你去哪啊?”","votes":65},{"id":670151,"title":"如何看待一个 MC 启动器作者被逼向他软件的破解者道歉?","votes":58},{"id":482085,"title":"[帮转] 大家千万别租桃园地铁站 C 出口南景苑大厦 7M 的房子,那房子房东专门骗钱的!我跟多位租客已受害。。","votes":56},{"id":758647,"title":"王伟站在栅栏前,对面有条流浪狗","votes":49},{"id":606364,"title":"龙芯 & Golang!","votes":48},{"id":756365,"title":"小王,你这个方案还是不行啊","votes":43},{"id":548359,"title":"为何蔡徐坤每条微博转发量 100 万+?用大数据扒一扒他的真假流量粉","votes":39},{"id":784789,"title":"原来 Linux 内核贡献第二是这么来的","votes":39},{"id":624860,"title":"不再购买或推荐某为的任何消费级产品","votes":38},{"id":185123,"title":"我尊重@Livid 的抉择,但我不想让@Livid 以外的人支配我在 V2 的数据,能不能提供一个删档走人的选项?","votes":37},{"id":758236,"title":"王伟今年已经换了三份工作","votes":37},{"id":604675,"title":"阿里 IDC 真是牛逼。昨天的交换机事件。","votes":34},{"id":757833,"title":"王伟凌晨加班回来,感到一阵便意","votes":34},{"id":474773,"title":"我又花了 28 个小时分析了一下各省二类疫苗采购公示数据","votes":33},{"id":595854,"title":"请给出 V2EX 的封禁政策","votes":31},{"id":849140,"title":"给工信部写信了,要求开放苹果的 facetime audio 和 CallKit 功能。","votes":31},{"id":874223,"title":"中文互联网已死","votes":31},{"id":111204,"title":"声讨@Bob 的倒卖(诈骗)行为","votes":30}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-topic-by-thank_count.json: -------------------------------------------------------------------------------- 1 | [{"id":473163,"title":"我花了 14 个小时找了一下长春长生们究竟卖到了哪里去","thank_count":531},{"id":759427,"title":"王妈说:“伟宝,你去哪啊?”","thank_count":132},{"id":474773,"title":"我又花了 28 个小时分析了一下各省二类疫苗采购公示数据","thank_count":115},{"id":606364,"title":"龙芯 & Golang!","thank_count":71},{"id":757489,"title":"猴年马月狗日,王伟疯了","thank_count":65},{"id":758647,"title":"王伟站在栅栏前,对面有条流浪狗","thank_count":65},{"id":859497,"title":"正经脱单教学帖丨关于唐山、上外事件有感。","thank_count":57},{"id":756365,"title":"小王,你这个方案还是不行啊","thank_count":56},{"id":473916,"title":"疫苗批号反查生产企业","thank_count":45},{"id":755305,"title":"早晨,王伟被老婆的尖叫声惊醒","thank_count":39},{"id":757833,"title":"王伟凌晨加班回来,感到一阵便意","thank_count":38},{"id":640127,"title":"17 例新型冠状病毒感染的肺炎死亡病例病情简单分析","thank_count":33},{"id":905479,"title":"网易 2022 年度盘点:致敬每一个扛住了生活的平凡人","thank_count":33},{"id":935642,"title":"欧洲错过了互联网?","thank_count":33},{"id":849140,"title":"给工信部写信了,要求开放苹果的 facetime audio 和 CallKit 功能。","thank_count":32},{"id":950392,"title":"成都经济的支柱","thank_count":32},{"id":953346,"title":"补充补充一些不存在的记忆(政治不正确版)","thank_count":30},{"id":758236,"title":"王伟今年已经换了三份工作","thank_count":29},{"id":760530,"title":"王伟说:“老婆,我也爱你。”","thank_count":28},{"id":754936,"title":"早上睁开眼,王伟说,不想活了","thank_count":26}] -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/TopComment.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | -------------------------------------------------------------------------------- /v2ex_scrapy/insert_ignore.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.compiler import compiles 2 | from sqlalchemy.sql import Insert 3 | 4 | # copy and modified from https://github.com/sqlalchemy/sqlalchemy/issues/5374#issuecomment-752693165 5 | 6 | """ 7 | When imported, automatically make all insert not fail on duplicate keys 8 | """ 9 | 10 | 11 | # modified 12 | @compiles(Insert, "sqlite") 13 | def sqlite_insert_ignore(insert, compiler, **kw): 14 | statement = compiler.visit_insert(insert, **kw) 15 | # len("INSERT") == 6 16 | return f'{statement[:6]}{ " OR IGNORE "}{statement[6:]}' 17 | 18 | 19 | @compiles(Insert, "mysql") 20 | def mysql_insert_ignore(insert, compiler, **kw): 21 | return compiler.visit_insert(insert.prefix_with("IGNORE"), **kw) 22 | 23 | 24 | @compiles(Insert, "postgresql") 25 | def postgresql_on_conflict_do_nothing(insert, compiler, **kw): 26 | statement = compiler.visit_insert(insert, **kw) 27 | # IF we have a "RETURNING" clause, we must insert before it 28 | returning_position = statement.find("RETURNING") 29 | if returning_position >= 0: 30 | return ( 31 | statement[:returning_position] 32 | + "ON CONFLICT DO NOTHING " 33 | + statement[returning_position:] 34 | ) 35 | else: 36 | return statement + " ON CONFLICT DO NOTHING" 37 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-topic-by-favorite_count.json: -------------------------------------------------------------------------------- 1 | [{"id":473163,"title":"我花了 14 个小时找了一下长春长生们究竟卖到了哪里去","favorite_count":588},{"id":329592,"title":"防盗版软件黑科技又一案例","favorite_count":337},{"id":875975,"title":"ShadowTLS:更好用的 TLS 伪装代理","favorite_count":273},{"id":731552,"title":"有在工作后移民的吗?","favorite_count":255},{"id":728186,"title":"8 亿 QQ 手机绑定数据泄露?请问作为用户发现自己手机号被泄露的情况下应该如何做?","favorite_count":221},{"id":608625,"title":"Everything 使用不当会导致各种重要数据泄露公网,可能就有你的个人信息","favorite_count":220},{"id":588732,"title":"阿里 P6 肉翻到新加坡 Shopee 经验分享(可内推)","favorite_count":206},{"id":892528,"title":"全世界最大的电子图书馆被 ban 了~","favorite_count":205},{"id":557538,"title":"Openbilibili? B 站在 Github 上公开了自己的后端源代码。","favorite_count":194},{"id":865906,"title":"感觉要出大事情了?","favorite_count":170},{"id":887715,"title":"国庆刚办完婚礼,现在感觉已经过不下去了","favorite_count":170},{"id":881086,"title":"如果想对经济、局势有一些自己的认识,应该阅读哪些书籍。","favorite_count":159},{"id":759570,"title":"分享一下近一年的欧盟 WeChat 账号与微信账号的使用与功能区别","favorite_count":146},{"id":596113,"title":"女友被打,派出所被迫和解,已投诉市长热线,求助,还应该做什么","favorite_count":142},{"id":271547,"title":"Vim 神级配置,IDE 弱爆了","favorite_count":134},{"id":518047,"title":"都说『明年的经济形势会更差』,你是如何『见微知著,以小见大』的?","favorite_count":125},{"id":611612,"title":"各位有看到比较好的,阐释西方价值观(或者民主、自由)本质的书籍吗?","favorite_count":120},{"id":859497,"title":"正经脱单教学帖丨关于唐山、上外事件有感。","favorite_count":120},{"id":946530,"title":"B 站 70 万粉丝 up 主秋葉 aaaki,月入大几万,互联网他是真玩明白了","favorite_count":119},{"id":521094,"title":"还是房产相关,多地已经触底","favorite_count":117}] -------------------------------------------------------------------------------- /v2ex_scrapy/spiders/V2exSpider.py: -------------------------------------------------------------------------------- 1 | import scrapy 2 | import scrapy.http.response.html 3 | 4 | from v2ex_scrapy.DB import DB 5 | from v2ex_scrapy.items import TopicItem 6 | from v2ex_scrapy.spiders.CommonSpider import CommonSpider 7 | 8 | 9 | class V2exSpider(scrapy.Spider): 10 | name = "v2ex" 11 | FORCE_UPDATE_TOPIC = False 12 | UPDATE_COMMENT = True 13 | 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | self.db = DB() 17 | self.start_id = 1 18 | self.end_id = 1000000 19 | self.common_spider = CommonSpider( 20 | self.logger, update_comment=self.UPDATE_COMMENT 21 | ) 22 | self.logger.info(f"start from topic id {self.start_id}, end at {self.end_id}") 23 | 24 | def start_requests(self): 25 | for i in range(self.start_id + 1, self.end_id + 1): 26 | if ( 27 | self.FORCE_UPDATE_TOPIC 28 | or (not self.db.exist(TopicItem, i)) 29 | or ( 30 | self.db.get_topic_comment_count(i) 31 | > self.db.get_comment_count_by_topic(i) 32 | ) 33 | ): 34 | yield scrapy.Request( 35 | url=f"https://www.v2ex.com/t/{i}", 36 | callback=self.common_spider.parse_topic, 37 | errback=self.common_spider.parse_topic_err, 38 | cb_kwargs={"topic_id": i}, 39 | ) 40 | else: 41 | self.logger.info(f"skip topic {i}") 42 | -------------------------------------------------------------------------------- /v2ex_scrapy/spiders/V2exMemberSpider.py: -------------------------------------------------------------------------------- 1 | import scrapy 2 | import scrapy.http.response.html 3 | from scrapy.spidermiddlewares.httperror import HttpError 4 | 5 | from v2ex_scrapy import v2ex_parser 6 | from v2ex_scrapy.DB import DB 7 | from v2ex_scrapy.items import MemberItem 8 | 9 | 10 | class V2exTopicSpider(scrapy.Spider): 11 | name = "v2ex-member" 12 | 13 | def __init__(self, start_id=1, end_id=635000, *args, **kwargs): 14 | super().__init__(*args, **kwargs) 15 | self.db = DB() 16 | self.start_id = start_id 17 | self.end_id = end_id 18 | self.logger.info(f"start from topic id {self.start_id}, end at {self.end_id}") 19 | 20 | def start_requests(self): 21 | for i in range(self.start_id, self.end_id + 1): 22 | if not self.db.exist(MemberItem, i): 23 | yield scrapy.Request( 24 | url=f"https://www.v2ex.com/uid/{i}", 25 | callback=self.parse, 26 | errback=self.member_err, 27 | cb_kwargs={"uid": i}, 28 | ) 29 | else: 30 | self.logger.info(f"skip member id:{i}, because it exists") 31 | 32 | def parse(self, response: scrapy.http.response.html.HtmlResponse, uid: int): 33 | for i in v2ex_parser.parse_member(response): 34 | i.uid = uid 35 | yield i 36 | 37 | def member_err(self, failure): 38 | if failure.check(HttpError): 39 | yield MemberItem( 40 | username="", 41 | avatar_url="", 42 | create_at=0, 43 | social_link=[], 44 | uid=failure.request.cb_kwargs["uid"], 45 | ) 46 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy GitHub Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | # push: 7 | # branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v3 33 | - uses: pnpm/action-setup@v2 34 | with: 35 | version: 8 36 | - name: Build 37 | run: | 38 | cd analysis/v2ex-analysis 39 | pnpm install 40 | pnpm vite build --base=/v2ex_scrapy 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v1 43 | with: 44 | path: "analysis/v2ex-analysis/dist" 45 | # Deployment job 46 | deploy: 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | runs-on: ubuntu-latest 51 | needs: build 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v2 56 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/README.md: -------------------------------------------------------------------------------- 1 | # v2ex-analysis 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/TopMember.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 68 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/components/TopTopic.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 76 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/src/App.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /v2ex_scrapy/pipelines.py: -------------------------------------------------------------------------------- 1 | # Define your item pipelines here 2 | # 3 | # Don't forget to add your pipeline to the ITEM_PIPELINES setting 4 | # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html 5 | 6 | 7 | # useful for handling different item types with a single interface 8 | 9 | from typing import Any 10 | 11 | # don't remove 12 | from v2ex_scrapy.DB import DB 13 | from v2ex_scrapy.items import CommentItem, MemberItem, TopicItem, TopicSupplementItem 14 | 15 | ItemsType = TopicItem | CommentItem | MemberItem | TopicSupplementItem 16 | 17 | 18 | class TutorialScrapyPipeline: 19 | BATCH = 10 20 | 21 | def __init__(self): 22 | # Connect to SQLite database 23 | self.db = DB() 24 | self.data: dict[Any, list[ItemsType]] = { 25 | TopicItem: [], 26 | CommentItem: [], 27 | MemberItem: [], 28 | TopicSupplementItem: [], 29 | } 30 | 31 | def process_item( 32 | self, 33 | item: ItemsType | Any, 34 | spider, 35 | ): 36 | if isinstance(item, (TopicItem, CommentItem, MemberItem, TopicSupplementItem)): 37 | item_type = type(item) 38 | self.data[item_type].append(item) 39 | if len(self.data[item_type]) >= self.BATCH: 40 | self.process_it(self.data[item_type]) 41 | self.data[item_type] = [] 42 | return item 43 | 44 | def process_it(self, items: list[ItemsType]): 45 | if len(items) > 0 and isinstance(items[0], MemberItem): 46 | self.process_members(items) # type: ignore 47 | else: 48 | self.db.session.add_all(items) 49 | self.db.session.commit() 50 | 51 | def process_members(self, items: list[MemberItem]): 52 | for item in items: 53 | e = ( 54 | self.db.session.query(MemberItem) 55 | .where(MemberItem.username == item.username) 56 | .first() 57 | ) 58 | if e is None: 59 | self.db.session.add_all([item]) 60 | elif e.uid is None: 61 | e.uid = item.uid 62 | self.db.session.commit() 63 | 64 | def save_all(self): 65 | for _, v in self.data.items(): 66 | self.process_it(v) 67 | 68 | def close_spider(self, spider): 69 | self.save_all() 70 | self.db.close() 71 | -------------------------------------------------------------------------------- /analysis/main.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | import sqlite3 3 | 4 | 5 | conn = sqlite3.connect("v2ex.sqlite") 6 | 7 | e = { 8 | "top-comment": """ 9 | select topic_id, c.id, c.content, c.thank_count, c.no, t.title 10 | from comment c 11 | left join topic t on t.id = c.topic_id 12 | order by c.thank_count desc 13 | """, 14 | "top-topic-by-thank_count": """ 15 | select id, title, thank_count 16 | from topic 17 | order by thank_count desc 18 | """, 19 | "top-topic-by-favorite_count": """ 20 | select id, title, favorite_count 21 | from topic 22 | order by favorite_count desc 23 | """, 24 | "top-topic-by-votes": """ 25 | select id, title, votes 26 | from topic 27 | order by votes desc 28 | """, 29 | "top-topic-by-clicks": """ 30 | select id, title, clicks 31 | from topic 32 | order by clicks desc 33 | """, 34 | "tag-usage-count": """ 35 | select t.value as tag, count(*) as count 36 | from topic, 37 | json_each(tag) as t 38 | group by t.value 39 | order by count desc 40 | """, 41 | "top-user-by-comment_count": """ 42 | select commenter as username, count(commenter) as comment_count 43 | from comment 44 | group by commenter 45 | order by comment_count desc 46 | """, 47 | "top-user-by-topic_count": """ 48 | select author as username, count(author) as topic_count 49 | from topic 50 | group by author 51 | order by topic_count desc 52 | """, 53 | } 54 | e2 = { 55 | "new-topic-every-month": """ 56 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS topic_count 57 | FROM topic 58 | GROUP BY date 59 | """, 60 | "new-comment-every-month": """ 61 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS comment_count 62 | FROM comment 63 | GROUP BY date 64 | """, 65 | "new-member-every-month": """ 66 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS member_count 67 | FROM member 68 | GROUP BY date 69 | """, 70 | } 71 | LIMIT = 20 72 | 73 | 74 | def f( 75 | sql: str, 76 | export_name: str, 77 | conn, 78 | limit: int | None = LIMIT, 79 | orient: str | None = "records", 80 | ): 81 | ae = "" if limit is None else f" limit {limit}" 82 | pandas.read_sql(f"{sql} {ae}", conn).to_json( 83 | f"./analysis/v2ex-analysis/public/{export_name}.json", 84 | force_ascii=False, 85 | orient=orient, 86 | ) 87 | 88 | 89 | for i, sql in e.items(): 90 | f(sql=sql, export_name=i, conn=conn) 91 | for i, sql in e2.items(): 92 | f(sql=sql, export_name=i, conn=conn, limit=None, orient=None) 93 | -------------------------------------------------------------------------------- /v2ex_scrapy/DB.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from typing import Type, Union 4 | 5 | from sqlalchemy import create_engine, text 6 | from sqlalchemy.orm import Mapped, Session, mapped_column 7 | 8 | from v2ex_scrapy.items import Base, CommentItem, MemberItem, TopicItem 9 | 10 | 11 | @dataclass(kw_only=True) 12 | class LogItem(Base): 13 | __tablename__ = "log" 14 | 15 | id_: Mapped[int] = mapped_column(name="id", primary_key=True, autoincrement="auto") 16 | url: Mapped[str] = mapped_column() 17 | status_code: Mapped[int] = mapped_column(nullable=False) 18 | create_at: Mapped[int] = mapped_column(nullable=False) 19 | 20 | 21 | class DB: 22 | _instance = None 23 | 24 | def __new__(cls): 25 | if cls._instance is None: 26 | cls._instance = super().__new__(cls) 27 | return cls._instance 28 | 29 | def __init__(self, database_name="v2ex.sqlite"): 30 | self.engine = create_engine( 31 | f"sqlite:///{database_name}", 32 | echo=False, 33 | json_serializer=lambda x: json.dumps(x, ensure_ascii=False), 34 | ) 35 | Base.metadata.create_all(self.engine) 36 | self.session = Session(self.engine) 37 | 38 | def close(self): 39 | self.session.commit() 40 | self.session.close() 41 | 42 | def exist( 43 | self, 44 | type_: Union[Type[TopicItem], Type[CommentItem], Type[MemberItem]], 45 | q: Union[str, int], 46 | ) -> bool: 47 | if type_ == MemberItem: 48 | query = text( 49 | f"SELECT * FROM {type_.__tablename__} WHERE {'username' if type(q) == str else 'uid'} = :q" 50 | ) 51 | else: 52 | query = text(f"SELECT * FROM {type_.__tablename__} WHERE id = :q") 53 | result = self.session.execute(query, {"q": q}).fetchone() 54 | return result is not None 55 | 56 | def get_max_topic_id(self) -> int: 57 | result = self.session.execute(text("SELECT max(id) FROM topic")).fetchone() 58 | if result is None or result[0] is None: 59 | return 1 60 | return int(result[0]) 61 | 62 | def get_topic_comment_count(self, topic_id) -> int: 63 | result = self.session.execute( 64 | text("select reply_count from topic where id = :q"), {"q": topic_id} 65 | ).fetchone() 66 | if result is None or result[0] is None: 67 | return 0 68 | return int(result[0]) 69 | 70 | def get_comment_count_by_topic(self, topic_id) -> int: 71 | result = self.session.execute( 72 | text("select count(*) from comment where topic_id = :q"), {"q": topic_id} 73 | ).fetchone() 74 | if result is None or result[0] is None: 75 | return 0 76 | return int(result[0]) -------------------------------------------------------------------------------- /v2ex_scrapy/spiders/V2exNodeTopicSpider.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import scrapy 3 | import scrapy.http.response.html 4 | from parsel import Selector 5 | from scrapy.utils.project import get_project_settings 6 | 7 | from v2ex_scrapy.DB import DB 8 | from v2ex_scrapy.items import TopicItem 9 | from v2ex_scrapy.spiders.CommonSpider import CommonSpider 10 | from v2ex_scrapy import utils 11 | 12 | 13 | class V2exNodeTopicSpider(scrapy.Spider): 14 | name = "v2ex-node" 15 | 16 | UPDATE_TOPIC_WHEN_REPLY_CHANGE = True 17 | UPDATE_COMMENT = True # only work when UPDATE_TOPIC_WHEN_REPLY_CHANGE = True 18 | URL = "https://www.v2ex.com/go/" 19 | 20 | """ 21 | 现存在的几个问题,因为节点的排序是动态的,如果爬完一页后未爬的主题跑到爬完的页数里那就爬不到了。 22 | 解决方法1,开始爬取时先获取全部帖子ID再开始爬,获取ID的速度比较快所以排序改变的幅度不会很大。 23 | """ 24 | 25 | def __init__(self, node="flamewar", *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | self.db = DB() 28 | self.node = node 29 | self.common_spider = CommonSpider( 30 | self.logger, update_comment=self.UPDATE_COMMENT 31 | ) 32 | settings = get_project_settings() 33 | resp = httpx.get( 34 | f"{self.URL}{self.node}", 35 | timeout=10, 36 | follow_redirects=True, 37 | cookies=utils.cookie_str2cookie_dict(settings.get("COOKIES", "")), # type: ignore 38 | headers={"User-Agent": settings.get("USER_AGENT", "")}, # type: ignore 39 | ).text 40 | max_page = ( 41 | Selector(text=resp) 42 | .xpath('//tr/td[@align="left" and @width="92%"]/a[last()]/text()') 43 | .get("1") 44 | ) 45 | self.max_page = int(max_page) 46 | 47 | def start_requests(self): 48 | for i in range(self.max_page, 0, -1): 49 | yield scrapy.Request( 50 | url=f"{self.URL}{self.node}?p={i}", 51 | callback=self.parse, 52 | cb_kwargs={"page": i}, 53 | ) 54 | 55 | def parse(self, response: scrapy.http.response.html.HtmlResponse, page: int): 56 | topics = [ 57 | (int(x), int(y)) 58 | for x, y in zip( 59 | response.xpath('//span[@class="item_title"]/a/@id').re(r"\d+"), 60 | # not correct when some comments are deleted, fuck 61 | response.xpath('//span[@class="item_title"]/a/@href').re(r"reply(\d+)"), 62 | ) 63 | ] 64 | for i, reply_count in topics: 65 | if not self.db.exist(TopicItem, i) or ( 66 | self.UPDATE_TOPIC_WHEN_REPLY_CHANGE 67 | and self.db.get_topic_comment_count(i) < reply_count 68 | ): 69 | yield scrapy.Request( 70 | url=f"https://www.v2ex.com/t/{i}", 71 | callback=self.common_spider.parse_topic, 72 | errback=self.common_spider.parse_topic_err, 73 | cb_kwargs={"topic_id": i}, 74 | ) 75 | -------------------------------------------------------------------------------- /query.sql: -------------------------------------------------------------------------------- 1 | SELECT (SELECT COUNT(*) FROM comment) AS 评论数, 2 | (SELECT COUNT(*) FROM member) AS 用户数, 3 | (SELECT COUNT(*) FROM topic) AS 主题数; 4 | 5 | -- where create_at between strftime('%s', '2013-01-01') and strftime('%s', '2014-12-31'); 6 | 7 | -- top comment by thank_count 8 | select topic_id, c.id, c.content, c.thank_count, c.no, t.title 9 | from comment c 10 | left join topic t on t.id = c.topic_id 11 | order by c.thank_count desc; 12 | 13 | -- top topic by thank_count 14 | select id, title, votes 15 | from topic 16 | order by thank_count desc; 17 | 18 | -- top topic by favorite_count 19 | select id, title, favorite_count 20 | from topic 21 | order by favorite_count desc; 22 | 23 | -- top topic by votes 24 | select id, title, votes 25 | from topic 26 | order by votes desc; 27 | 28 | -- top topic by clicks 29 | select id, title, clicks 30 | from topic 31 | order by clicks desc; 32 | 33 | 34 | -- top node 35 | select node, count(node) as count 36 | from topic 37 | group by node 38 | order by count desc; 39 | 40 | -- comment number group by user 41 | select commenter as username, count(commenter) as comment_count 42 | from comment 43 | group by commenter 44 | order by comment_count desc; 45 | 46 | -- topic number group by user 47 | select author as username, count(author) as topic_count 48 | from topic 49 | group by author 50 | order by topic_count desc; 51 | 52 | -- topic number group by year-month 53 | SELECT date, 54 | SUM(topic_count) OVER (ORDER BY date ) AS cumulative_topic_count 55 | FROM (SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS topic_count 56 | FROM topic 57 | GROUP BY date) 58 | ORDER BY date; 59 | 60 | -- user number group by year-month 61 | SELECT date, 62 | SUM(user_count) OVER (ORDER BY date ) AS cumulative_user_count 63 | FROM (SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS user_count 64 | FROM member 65 | GROUP BY date) 66 | ORDER BY date; 67 | 68 | -- comment number group by year-month 69 | SELECT date, 70 | SUM(comment_count) OVER (ORDER BY date ) AS cumulative_comment_count 71 | FROM (SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS comment_count 72 | FROM comment 73 | GROUP BY date) 74 | ORDER BY date; 75 | 76 | -- new topic number group by year-month 77 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS topic_count 78 | FROM topic 79 | GROUP BY date; 80 | 81 | -- new user number group by year-month 82 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS user_count 83 | FROM member 84 | GROUP BY date; 85 | 86 | -- new comment number group by year-month 87 | SELECT strftime('%Y-%m', create_at, 'unixepoch') AS date, COUNT(*) AS comment_count 88 | FROM comment 89 | GROUP BY date; 90 | 91 | -- tag usage count 92 | select t.value as tag, count(*) as count 93 | from topic, 94 | json_each(tag) as t 95 | group by t.value 96 | order by count desc; 97 | 98 | -- node usage count 99 | select node, count(*) as count 100 | from topic 101 | group by node 102 | order by count desc; 103 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/new-topic-every-month.json: -------------------------------------------------------------------------------- 1 | {"date":{"0":"2010-08","1":"2010-11","2":"2010-12","3":"2011-01","4":"2011-02","5":"2011-03","6":"2011-04","7":"2011-05","8":"2011-07","9":"2011-09","10":"2011-11","11":"2011-12","12":"2012-01","13":"2012-04","14":"2012-05","15":"2012-06","16":"2012-07","17":"2012-08","18":"2012-09","19":"2012-10","20":"2012-11","21":"2012-12","22":"2013-01","23":"2013-02","24":"2013-03","25":"2013-04","26":"2013-05","27":"2013-06","28":"2013-07","29":"2013-08","30":"2013-09","31":"2013-11","32":"2013-12","33":"2014-01","34":"2014-02","35":"2014-05","36":"2014-06","37":"2014-07","38":"2014-08","39":"2014-09","40":"2014-10","41":"2014-11","42":"2014-12","43":"2015-01","44":"2015-02","45":"2015-03","46":"2015-04","47":"2015-05","48":"2015-06","49":"2015-07","50":"2015-08","51":"2015-09","52":"2015-10","53":"2015-11","54":"2015-12","55":"2016-01","56":"2016-02","57":"2016-03","58":"2016-04","59":"2016-05","60":"2016-06","61":"2016-07","62":"2016-08","63":"2016-09","64":"2016-10","65":"2016-11","66":"2016-12","67":"2017-01","68":"2017-02","69":"2017-03","70":"2017-04","71":"2017-05","72":"2017-06","73":"2017-07","74":"2017-08","75":"2017-09","76":"2017-10","77":"2017-11","78":"2017-12","79":"2018-01","80":"2018-02","81":"2018-03","82":"2018-04","83":"2018-05","84":"2018-06","85":"2018-07","86":"2018-08","87":"2018-09","88":"2018-10","89":"2018-11","90":"2018-12","91":"2019-01","92":"2019-02","93":"2019-03","94":"2019-04","95":"2019-05","96":"2019-06","97":"2019-07","98":"2019-08","99":"2019-09","100":"2019-10","101":"2019-11","102":"2019-12","103":"2020-01","104":"2020-02","105":"2020-03","106":"2020-04","107":"2020-05","108":"2020-06","109":"2020-07","110":"2020-08","111":"2020-09","112":"2020-10","113":"2020-11","114":"2020-12","115":"2021-01","116":"2021-02","117":"2021-03","118":"2021-04","119":"2021-05","120":"2021-06","121":"2021-07","122":"2021-08","123":"2021-09","124":"2021-10","125":"2021-11","126":"2021-12","127":"2022-01","128":"2022-02","129":"2022-03","130":"2022-04","131":"2022-05","132":"2022-06","133":"2022-07","134":"2022-08","135":"2022-09","136":"2022-10","137":"2022-11","138":"2022-12","139":"2023-01","140":"2023-02","141":"2023-03","142":"2023-04","143":"2023-05","144":"2023-06","145":"2023-07"},"topic_count":{"0":1,"1":2,"2":1,"3":4,"4":2,"5":2,"6":1,"7":1,"8":3,"9":3,"10":2,"11":2,"12":1,"13":6,"14":6,"15":1,"16":4,"17":1,"18":4,"19":3,"20":4,"21":4,"22":2,"23":5,"24":1,"25":4,"26":3,"27":1,"28":3,"29":9,"30":1,"31":2,"32":2,"33":1,"34":1,"35":3,"36":8,"37":6,"38":13,"39":7,"40":9,"41":5,"42":22,"43":34,"44":29,"45":31,"46":32,"47":17,"48":22,"49":21,"50":51,"51":29,"52":18,"53":25,"54":38,"55":39,"56":25,"57":33,"58":40,"59":46,"60":55,"61":50,"62":42,"63":62,"64":72,"65":43,"66":63,"67":47,"68":35,"69":55,"70":74,"71":49,"72":50,"73":88,"74":75,"75":60,"76":81,"77":106,"78":61,"79":84,"80":55,"81":84,"82":70,"83":66,"84":84,"85":80,"86":185,"87":118,"88":85,"89":95,"90":128,"91":136,"92":90,"93":152,"94":376,"95":261,"96":226,"97":83,"98":167,"99":148,"100":157,"101":118,"102":166,"103":211,"104":213,"105":125,"106":209,"107":174,"108":146,"109":149,"110":175,"111":179,"112":131,"113":143,"114":182,"115":241,"116":105,"117":259,"118":169,"119":103,"120":130,"121":103,"122":113,"123":66,"124":67,"125":89,"126":146,"127":112,"128":79,"129":165,"130":160,"131":142,"132":113,"133":136,"134":112,"135":156,"136":153,"137":238,"138":211,"139":37,"140":50,"141":98,"142":81,"143":102,"144":118,"145":23}} -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/new-comment-every-month.json: -------------------------------------------------------------------------------- 1 | {"date":{"0":"2010-08","1":"2010-11","2":"2010-12","3":"2011-01","4":"2011-02","5":"2011-03","6":"2011-04","7":"2011-05","8":"2011-06","9":"2011-07","10":"2011-08","11":"2011-09","12":"2011-10","13":"2011-11","14":"2011-12","15":"2012-01","16":"2012-02","17":"2012-04","18":"2012-05","19":"2012-06","20":"2012-07","21":"2012-08","22":"2012-09","23":"2012-10","24":"2012-11","25":"2012-12","26":"2013-01","27":"2013-02","28":"2013-03","29":"2013-04","30":"2013-05","31":"2013-06","32":"2013-07","33":"2013-08","34":"2013-09","35":"2013-10","36":"2013-11","37":"2013-12","38":"2014-01","39":"2014-02","40":"2014-03","41":"2014-04","42":"2014-05","43":"2014-06","44":"2014-07","45":"2014-08","46":"2014-09","47":"2014-10","48":"2014-11","49":"2014-12","50":"2015-01","51":"2015-02","52":"2015-03","53":"2015-04","54":"2015-05","55":"2015-06","56":"2015-07","57":"2015-08","58":"2015-09","59":"2015-10","60":"2015-11","61":"2015-12","62":"2016-01","63":"2016-02","64":"2016-03","65":"2016-04","66":"2016-05","67":"2016-06","68":"2016-07","69":"2016-08","70":"2016-09","71":"2016-10","72":"2016-11","73":"2016-12","74":"2017-01","75":"2017-02","76":"2017-03","77":"2017-04","78":"2017-05","79":"2017-06","80":"2017-07","81":"2017-08","82":"2017-09","83":"2017-10","84":"2017-11","85":"2017-12","86":"2018-01","87":"2018-02","88":"2018-03","89":"2018-04","90":"2018-05","91":"2018-06","92":"2018-07","93":"2018-08","94":"2018-09","95":"2018-10","96":"2018-11","97":"2018-12","98":"2019-01","99":"2019-02","100":"2019-03","101":"2019-04","102":"2019-05","103":"2019-06","104":"2019-07","105":"2019-08","106":"2019-09","107":"2019-10","108":"2019-11","109":"2019-12","110":"2020-01","111":"2020-02","112":"2020-03","113":"2020-04","114":"2020-05","115":"2020-06","116":"2020-07","117":"2020-08","118":"2020-09","119":"2020-10","120":"2020-11","121":"2020-12","122":"2021-01","123":"2021-02","124":"2021-03","125":"2021-04","126":"2021-05","127":"2021-06","128":"2021-07","129":"2021-08","130":"2021-09","131":"2021-10","132":"2021-11","133":"2021-12","134":"2022-01","135":"2022-02","136":"2022-03","137":"2022-04","138":"2022-05","139":"2022-06","140":"2022-07","141":"2022-08","142":"2022-09","143":"2022-10","144":"2022-11","145":"2022-12","146":"2023-01","147":"2023-02","148":"2023-03","149":"2023-04","150":"2023-05","151":"2023-06","152":"2023-07"},"comment_count":{"0":49,"1":42,"2":52,"3":182,"4":44,"5":34,"6":17,"7":48,"8":1,"9":60,"10":1,"11":65,"12":1,"13":48,"14":73,"15":57,"16":5,"17":180,"18":161,"19":18,"20":128,"21":104,"22":126,"23":180,"24":192,"25":105,"26":123,"27":164,"28":53,"29":99,"30":348,"31":4,"32":147,"33":757,"34":174,"35":4,"36":30,"37":26,"38":144,"39":56,"40":3,"41":3,"42":269,"43":644,"44":366,"45":648,"46":479,"47":449,"48":286,"49":839,"50":1179,"51":1016,"52":1527,"53":1905,"54":597,"55":1039,"56":1043,"57":2393,"58":1301,"59":707,"60":1745,"61":1825,"62":1840,"63":791,"64":1826,"65":2579,"66":2947,"67":2816,"68":2292,"69":2897,"70":3532,"71":3112,"72":2366,"73":2863,"74":3027,"75":1557,"76":2487,"77":3543,"78":1338,"79":2468,"80":4010,"81":3312,"82":2909,"83":2395,"84":3057,"85":2369,"86":2960,"87":2376,"88":2824,"89":2605,"90":2577,"91":3320,"92":3798,"93":8143,"94":5222,"95":2946,"96":4074,"97":4590,"98":6005,"99":3726,"100":6416,"101":10941,"102":8871,"103":6404,"104":4549,"105":8419,"106":6374,"107":7087,"108":6853,"109":6355,"110":6474,"111":6449,"112":6167,"113":6764,"114":7132,"115":5096,"116":6641,"117":7947,"118":8642,"119":5694,"120":6656,"121":8615,"122":8899,"123":4894,"124":8747,"125":4202,"126":3137,"127":6306,"128":3550,"129":4182,"130":2482,"131":2883,"132":3852,"133":5221,"134":4334,"135":2896,"136":5800,"137":6875,"138":7055,"139":6092,"140":6495,"141":6403,"142":7875,"143":7115,"144":13524,"145":11616,"146":2572,"147":3940,"148":6361,"149":5279,"150":7118,"151":8278,"152":1278}} -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/new-member-every-month.json: -------------------------------------------------------------------------------- 1 | {"date":{"0":"1970-01","1":"2010-04","2":"2010-05","3":"2010-06","4":"2010-07","5":"2010-08","6":"2010-09","7":"2010-10","8":"2010-11","9":"2010-12","10":"2011-01","11":"2011-02","12":"2011-03","13":"2011-04","14":"2011-05","15":"2011-06","16":"2011-07","17":"2011-08","18":"2011-09","19":"2011-10","20":"2011-11","21":"2011-12","22":"2012-01","23":"2012-02","24":"2012-03","25":"2012-04","26":"2012-05","27":"2012-06","28":"2012-07","29":"2012-08","30":"2012-09","31":"2012-10","32":"2012-11","33":"2012-12","34":"2013-01","35":"2013-02","36":"2013-03","37":"2013-04","38":"2013-05","39":"2013-06","40":"2013-07","41":"2013-08","42":"2013-09","43":"2013-10","44":"2013-11","45":"2013-12","46":"2014-01","47":"2014-02","48":"2014-03","49":"2014-04","50":"2014-05","51":"2014-06","52":"2014-07","53":"2014-08","54":"2014-09","55":"2014-10","56":"2014-11","57":"2014-12","58":"2015-01","59":"2015-02","60":"2015-03","61":"2015-04","62":"2015-05","63":"2015-06","64":"2015-07","65":"2015-08","66":"2015-09","67":"2015-10","68":"2015-11","69":"2015-12","70":"2016-01","71":"2016-02","72":"2016-03","73":"2016-04","74":"2016-05","75":"2016-06","76":"2016-07","77":"2016-08","78":"2016-09","79":"2016-10","80":"2016-11","81":"2016-12","82":"2017-01","83":"2017-02","84":"2017-03","85":"2017-04","86":"2017-05","87":"2017-06","88":"2017-07","89":"2017-08","90":"2017-09","91":"2017-10","92":"2017-11","93":"2017-12","94":"2018-01","95":"2018-02","96":"2018-03","97":"2018-04","98":"2018-05","99":"2018-06","100":"2018-07","101":"2018-08","102":"2018-09","103":"2018-10","104":"2018-11","105":"2018-12","106":"2019-01","107":"2019-02","108":"2019-03","109":"2019-04","110":"2019-05","111":"2019-06","112":"2019-07","113":"2019-08","114":"2019-09","115":"2019-10","116":"2019-11","117":"2019-12","118":"2020-01","119":"2020-02","120":"2020-03","121":"2020-04","122":"2020-05","123":"2020-06","124":"2020-07","125":"2020-08","126":"2020-09","127":"2020-10","128":"2020-11","129":"2020-12","130":"2021-01","131":"2021-02","132":"2021-03","133":"2021-04","134":"2021-05","135":"2021-06","136":"2021-07","137":"2021-08","138":"2021-09","139":"2021-10","140":"2021-11","141":"2021-12","142":"2022-01","143":"2022-02","144":"2022-03","145":"2022-04","146":"2022-05","147":"2022-06","148":"2022-07","149":"2022-08","150":"2022-09","151":"2022-10","152":"2022-11","153":"2022-12","154":"2023-01","155":"2023-02","156":"2023-03","157":"2023-04","158":"2023-05","159":"2023-06","160":"2023-07"},"member_count":{"0":385,"1":120,"2":57,"3":8,"4":66,"5":63,"6":57,"7":93,"8":80,"9":128,"10":118,"11":56,"12":139,"13":102,"14":79,"15":77,"16":85,"17":72,"18":84,"19":85,"20":116,"21":143,"22":72,"23":139,"24":204,"25":173,"26":157,"27":102,"28":156,"29":156,"30":142,"31":142,"32":127,"33":175,"34":174,"35":148,"36":208,"37":142,"38":177,"39":149,"40":170,"41":217,"42":191,"43":183,"44":234,"45":178,"46":190,"47":160,"48":219,"49":233,"50":254,"51":361,"52":351,"53":394,"54":344,"55":455,"56":511,"57":517,"58":677,"59":560,"60":523,"61":569,"62":423,"63":432,"64":425,"65":493,"66":378,"67":388,"68":344,"69":342,"70":325,"71":301,"72":389,"73":499,"74":402,"75":364,"76":391,"77":395,"78":377,"79":417,"80":342,"81":409,"82":330,"83":430,"84":477,"85":350,"86":412,"87":414,"88":600,"89":498,"90":460,"91":473,"92":597,"93":486,"94":749,"95":453,"96":567,"97":486,"98":463,"99":442,"100":715,"101":737,"102":608,"103":466,"104":521,"105":556,"106":556,"107":431,"108":615,"109":662,"110":562,"111":518,"112":463,"113":395,"114":373,"115":380,"116":387,"117":369,"118":291,"119":262,"120":366,"121":324,"122":277,"123":226,"124":248,"125":225,"126":236,"127":253,"128":254,"129":222,"130":246,"131":157,"132":210,"133":171,"134":126,"135":110,"136":121,"137":106,"138":114,"139":114,"140":149,"141":143,"142":126,"143":100,"144":129,"145":118,"146":138,"147":134,"148":113,"149":137,"150":99,"151":131,"152":122,"153":91,"154":72,"155":101,"156":107,"157":77,"158":83,"159":45,"160":1}} -------------------------------------------------------------------------------- /v2ex_scrapy/items.py: -------------------------------------------------------------------------------- 1 | # Define here the models for your scraped items 2 | # 3 | # See documentation in: 4 | # https://docs.scrapy.org/en/latest/topics/items.html 5 | 6 | import json 7 | from dataclasses import dataclass 8 | 9 | from sqlalchemy import Integer, Text, types 10 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 11 | 12 | 13 | class JSONText(types.TypeDecorator): 14 | impl = types.Text 15 | 16 | def process_bind_param(self, value, dialect): 17 | if value is not None: 18 | value = json.dumps(value, ensure_ascii=False) 19 | return value 20 | 21 | def process_result_value(self, value, dialect): 22 | if value is not None: 23 | value = json.loads(value) 24 | return value 25 | 26 | 27 | @dataclass 28 | class Base(DeclarativeBase): 29 | type_annotation_map = { 30 | list[str]: JSONText, 31 | list[dict[str, str]]: JSONText, 32 | int: Integer, 33 | str: Text, 34 | } 35 | 36 | 37 | @dataclass(kw_only=True) 38 | class TopicItem(Base): 39 | __tablename__ = "topic" 40 | 41 | id_: Mapped[int] = mapped_column(name="id", primary_key=True) 42 | author: Mapped[str] = mapped_column(nullable=False) 43 | title: Mapped[str] = mapped_column(nullable=False) 44 | content: Mapped[str] = mapped_column() 45 | node: Mapped[str] = mapped_column(nullable=False) 46 | tag: Mapped[list[str]] = mapped_column(nullable=False) 47 | clicks: Mapped[int] = mapped_column(nullable=False) 48 | votes: Mapped[int] = mapped_column(nullable=False) 49 | create_at: Mapped[int] = mapped_column(nullable=False) 50 | thank_count: Mapped[int] = mapped_column(nullable=False) 51 | favorite_count: Mapped[int] = mapped_column(nullable=False) 52 | reply_count: Mapped[int] = mapped_column(nullable=False) 53 | 54 | @staticmethod 55 | def err_topic(topic_id: int): 56 | return TopicItem( 57 | id_=topic_id, 58 | author="", 59 | title="", 60 | content="", 61 | create_at=0, 62 | node="", 63 | tag=[], 64 | clicks=-1, 65 | votes=-1, 66 | thank_count=-1, 67 | favorite_count=-1, 68 | reply_count=-1, 69 | ) 70 | 71 | 72 | @dataclass(kw_only=True) 73 | class TopicSupplementItem(Base): 74 | __tablename__ = "topic_supplement" 75 | 76 | topic_id: Mapped[int] = mapped_column(primary_key=True) 77 | content: Mapped[str] = mapped_column(primary_key=True) 78 | create_at: Mapped[int] = mapped_column(primary_key=True) 79 | 80 | 81 | @dataclass(kw_only=True) 82 | class CommentItem(Base): 83 | __tablename__ = "comment" 84 | 85 | id_: Mapped[int] = mapped_column(name="id", primary_key=True) 86 | # index used for select count(*) from comment where topic_id = ?, see DB.get_topic_comment_count 87 | topic_id: Mapped[int] = mapped_column(nullable=False, index=True) 88 | commenter: Mapped[str] = mapped_column(nullable=False) 89 | content: Mapped[str] = mapped_column(nullable=False) 90 | thank_count: Mapped[int] = mapped_column(nullable=False) 91 | create_at: Mapped[int] = mapped_column(nullable=False) 92 | no: Mapped[int] = mapped_column(nullable=False) 93 | 94 | 95 | @dataclass(kw_only=True) 96 | class MemberItem(Base): 97 | __tablename__ = "member" 98 | """ 99 | crawl user from topic and comment, then crawl from uid 1 to 1000000, 100 | if get 404, username/uid will be ''/-1, so primary_key(uid ,username) 101 | """ 102 | uid: Mapped[int] = mapped_column(primary_key=True) 103 | username: Mapped[str] = mapped_column(primary_key=True, index=True) 104 | avatar_url: Mapped[str] 105 | create_at: Mapped[int] = mapped_column(nullable=False) 106 | social_link: Mapped[list[dict[str, str]]] 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .vscode/settings.json 163 | *.7z 164 | *.sqlite 165 | -------------------------------------------------------------------------------- /v2ex_scrapy/settings.py: -------------------------------------------------------------------------------- 1 | # My Config 2 | 3 | PROXIES = [] 4 | 5 | COOKIES = """ 6 | """ 7 | 8 | # Scrapy settings for v2ex_scrapy project 9 | # 10 | # For simplicity, this file contains only settings considered important or 11 | # commonly used. You can find more settings consulting the documentation: 12 | # 13 | # https://docs.scrapy.org/en/latest/topics/settings.html 14 | # https://docs.scrapy.org/en/latest/topics/downloader-middleware.html 15 | # https://docs.scrapy.org/en/latest/topics/spider-middleware.html 16 | 17 | BOT_NAME = "v2ex_scrapy" 18 | 19 | SPIDER_MODULES = ["v2ex_scrapy.spiders"] 20 | NEWSPIDER_MODULE = "v2ex_scrapy.spiders" 21 | 22 | # LOG_FILE = "v2ex_scrapy.log" 23 | LOG_FILE_APPEND = False 24 | 25 | # Crawl responsibly by identifying yourself (and your website) on the user-agent 26 | USER_AGENT = "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" 27 | 28 | # Obey robots.txt rules 29 | ROBOTSTXT_OBEY = False 30 | 31 | # Configure maximum concurrent requests performed by Scrapy (default: 16) 32 | CONCURRENT_REQUESTS = 1 33 | 34 | # Configure a delay for requests for the same website (default: 0) 35 | # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay 36 | # See also autothrottle settings and docs 37 | # DOWNLOAD_DELAY = 3 38 | # The download delay setting will honor only one of: 39 | # CONCURRENT_REQUESTS_PER_DOMAIN = 16 40 | # CONCURRENT_REQUESTS_PER_IP = 16 41 | 42 | # Disable cookies (enabled by default) 43 | # COOKIES_ENABLED = False 44 | 45 | # Disable Telnet Console (enabled by default) 46 | # TELNETCONSOLE_ENABLED = False 47 | 48 | # Override the default request headers: 49 | # DEFAULT_REQUEST_HEADERS = { 50 | # "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 51 | # "Accept-Language": "en", 52 | # } 53 | 54 | # Enable or disable spider middlewares 55 | # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html 56 | # SPIDER_MIDDLEWARES = { 57 | # "tutorial_scrapy.middlewares.TutorialScrapySpiderMiddleware": 543, 58 | # } 59 | 60 | # Enable or disable downloader middlewares 61 | # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html 62 | DOWNLOADER_MIDDLEWARES = { 63 | "v2ex_scrapy.middlewares.ProxyAndCookieDownloaderMiddleware": 543, 64 | # "v2ex_scrapy.middlewares.RandomUserAgentMiddleware": 544, 65 | "v2ex_scrapy.middlewares.SaveHttpStatusToDBMiddleware": 545, 66 | } 67 | 68 | # Enable or disable extensions 69 | # See https://docs.scrapy.org/en/latest/topics/extensions.html 70 | # EXTENSIONS = { 71 | # "scrapy.extensions.telnet.TelnetConsole": None, 72 | # } 73 | 74 | # Configure item pipelines 75 | # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html 76 | ITEM_PIPELINES = { 77 | "v2ex_scrapy.pipelines.TutorialScrapyPipeline": 300, 78 | } 79 | 80 | # Enable and configure the AutoThrottle extension (disabled by default) 81 | # See https://docs.scrapy.org/en/latest/topics/autothrottle.html 82 | # AUTOTHROTTLE_ENABLED = True 83 | # The initial download delay 84 | # AUTOTHROTTLE_START_DELAY = 5 85 | # The maximum download delay to be set in case of high latencies 86 | # AUTOTHROTTLE_MAX_DELAY = 60 87 | # The average number of requests Scrapy should be sending in parallel to 88 | # each remote server 89 | # AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 90 | # Enable showing throttling stats for every response received: 91 | # AUTOTHROTTLE_DEBUG = False 92 | 93 | # Enable and configure HTTP caching (disabled by default) 94 | # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings 95 | # HTTPCACHE_ENABLED = True 96 | # HTTPCACHE_EXPIRATION_SECS = 0 97 | # HTTPCACHE_DIR = "httpcache" 98 | # HTTPCACHE_IGNORE_HTTP_CODES = [] 99 | # HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage" 100 | 101 | # Set settings whose default value is deprecated to a future-proof value 102 | REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7" 103 | TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor" 104 | FEED_EXPORT_ENCODING = "utf-8" 105 | -------------------------------------------------------------------------------- /v2ex_scrapy/spiders/CommonSpider.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import scrapy 4 | import scrapy.http.response.html 5 | from scrapy.spidermiddlewares.httperror import HttpError 6 | 7 | from v2ex_scrapy import v2ex_parser 8 | from v2ex_scrapy.DB import DB 9 | from v2ex_scrapy.items import MemberItem, TopicItem 10 | 11 | 12 | class CommonSpider: 13 | def __init__(self, logger, update_member=False, update_comment=False): 14 | self.db = DB() 15 | self.logger = logger 16 | self.UPDATE_MEMBER = update_member 17 | self.UPDATE_COMMENT = update_comment 18 | 19 | def parse_topic_err(self, failure): 20 | if failure.check(HttpError): 21 | topic_id = failure.request.cb_kwargs["topic_id"] 22 | self.logger.warn(f"Crawl Topic Err {topic_id}") 23 | yield TopicItem.err_topic(topic_id=topic_id) 24 | 25 | def parse_topic( 26 | self, response: scrapy.http.response.html.HtmlResponse, topic_id: int 27 | ): 28 | self.logger.info(f"Crawl Topic {topic_id}") 29 | 30 | if response.status == 302: 31 | # need login or account too young 32 | yield TopicItem.err_topic(topic_id=topic_id) 33 | else: 34 | for i in v2ex_parser.parse_topic_supplement(response, topic_id): 35 | yield i 36 | for topic in v2ex_parser.parse_topic(response, topic_id): 37 | yield topic 38 | for i in self.crawl_member(topic.author, response): 39 | yield i 40 | for i in self.parse_comment(response, topic_id): 41 | yield i 42 | # crawl sub page comment 43 | topic_reply_count = int( 44 | response.css( 45 | "#Main > div:nth-child(4) > div:nth-child(1) > span::text" 46 | ).re_first(r"\d+", "-1") 47 | ) 48 | c = self.db.get_topic_comment_count(topic_id) 49 | if ( 50 | # 爬了一部分 并且设置更新评论 51 | (0 < c < topic_reply_count) 52 | and self.UPDATE_COMMENT 53 | ) or ( 54 | # 没有爬 并且有评论 55 | topic_reply_count > 0 56 | and c == 0 57 | ): 58 | total_page = math.ceil(topic_reply_count / 100) 59 | for i in range(max(2, math.ceil(c / 100)), total_page + 1): 60 | for j in self.crawl_comment(topic_id, i, response): 61 | yield j 62 | 63 | def crawl_comment(self, topic_id, page, response): 64 | yield response.follow( 65 | f"/t/{topic_id}?p={page}", 66 | callback=self.parse_comment, 67 | cb_kwargs={"topic_id": topic_id}, 68 | ) 69 | 70 | def parse_comment(self, response: scrapy.http.response.html.HtmlResponse, topic_id): 71 | for comment_item in v2ex_parser.parse_comment(response, topic_id): 72 | yield comment_item 73 | for i in self.crawl_member(comment_item.commenter, response): 74 | yield i 75 | 76 | def crawl_member(self, username, response: scrapy.http.response.html.HtmlResponse): 77 | if username != "" and ( 78 | self.UPDATE_MEMBER or not self.db.exist(MemberItem, username) 79 | ): 80 | yield response.follow( 81 | f"/member/{username}", 82 | callback=self.parse_member, 83 | errback=self.member_err, 84 | cb_kwargs={"username": username}, 85 | ) 86 | 87 | def member_err(self, failure): 88 | if failure.check(HttpError): 89 | username = failure.request.cb_kwargs["username"] 90 | self.logger.warn(f"Crawl Member Err {username}") 91 | yield MemberItem( 92 | username=username, 93 | avatar_url="", 94 | create_at=0, 95 | social_link=[], 96 | uid=-1, 97 | ) 98 | 99 | def parse_member( 100 | self, response: scrapy.http.response.html.HtmlResponse, username: str 101 | ): 102 | self.logger.info(f"Crawl Member {username}") 103 | for i in v2ex_parser.parse_member(response=response): 104 | yield i 105 | -------------------------------------------------------------------------------- /v2ex_scrapy/v2ex_parser.py: -------------------------------------------------------------------------------- 1 | import scrapy 2 | import scrapy.http.response.html 3 | 4 | import v2ex_scrapy.utils as utils 5 | from v2ex_scrapy.items import ( 6 | CommentItem, 7 | MemberItem, 8 | TopicItem, 9 | TopicSupplementItem, 10 | ) 11 | 12 | 13 | def parse_member(response: scrapy.http.response.html.HtmlResponse): 14 | """parse page like https://www.v2ex.com/member/oldshensheep""" 15 | 16 | username = response.xpath("//h1/text()").get("") 17 | avatar_url = response.css(".avatar::attr(src)").get("-1") 18 | 19 | t = response.xpath('//div[@class="cell"]//tr/td/span[@class="gray"]/text()') 20 | no = t.re_first(r"第 (\d+) 号", "-1") 21 | create_at = t.re_first(r"加入于 (.*)", "") 22 | 23 | social_list = [] 24 | for i in response.xpath('//div[@class="widgets"]//a'): 25 | social_list.append({i.xpath(".//img/@alt").get(): i.xpath("./@href").get()}) 26 | yield MemberItem( 27 | username=username, 28 | avatar_url=avatar_url, 29 | create_at=utils.time_to_timestamp(create_at), 30 | social_link=social_list, 31 | uid=int(no), 32 | ) 33 | 34 | 35 | def parse_comment(response: scrapy.http.response.html.HtmlResponse, topic_id): 36 | reply_box = response.css("#Main > .box > .cell[id] > table") 37 | for reply_row in reply_box: 38 | comment_id = reply_row.xpath("..").css(".cell::attr(id)").re_first(r"\d+", "-1") 39 | # if not self.db.exist(CommentItem, comment_id): 40 | cbox = reply_row.css("tr") 41 | author_name = cbox.css(".dark::text").get("-1") 42 | reply_content = cbox.xpath('.//div[@class="reply_content"]').get("") 43 | reply_time = cbox.css(".ago::attr(title)").get("") 44 | thank_count = cbox.css(".fade::text").get("0").strip() 45 | no = cbox.css(".no::text").get("-1").strip() 46 | yield CommentItem( 47 | id_=int(comment_id), 48 | no=int(no), 49 | commenter=author_name, 50 | topic_id=topic_id, 51 | content=reply_content, 52 | create_at=utils.time_to_timestamp(reply_time), 53 | thank_count=int(thank_count), 54 | ) 55 | 56 | 57 | def parse_topic(response: scrapy.http.response.html.HtmlResponse, topic_id): 58 | topic_title = response.xpath("string(//div[@class='header']/h1)").get("") 59 | topic_time = response.css(".header > small > span::attr(title)").get("0") 60 | topic_author = response.css(".header > small > a::text").get("") 61 | topic_node = response.css(".header > a:nth-child(4)::attr(href)").re_first( 62 | r"\/(\w+)$", "" 63 | ) 64 | topic_click_count = response.css(".header > small::text").re_first(r"\d+", "-1") 65 | topic_tags = response.css(".tag::attr(href)").re(r"/tag/(.*)") 66 | topic_vote = response.xpath('(//a[@class="vote"])[1]/text()').re_first(r"\d+", "0") 67 | # need login, some topics may not have 68 | topic_favorite_count = -1 69 | topic_thank_count = -1 70 | if response.css(".topic_stats::text").get() is not None: 71 | topic_favorite_count = response.css(".topic_stats::text").re_first( 72 | r"(\d+) 人收藏", "0" 73 | ) 74 | topic_thank_count = response.css(".topic_stats::text").re_first( 75 | r"(\d+) 人感谢", "0" 76 | ) 77 | 78 | topic_content = response.css(".cell .topic_content").get("") 79 | topic_reply_count = response.css(".box > .cell > .gray::text").re_first( 80 | r"(\d+) 条回复", "0" 81 | ) 82 | yield TopicItem( 83 | id_=topic_id, 84 | author=topic_author, 85 | title=topic_title, 86 | content=topic_content, 87 | create_at=utils.time_to_timestamp(topic_time), 88 | node=topic_node, 89 | tag=topic_tags, 90 | clicks=int(topic_click_count), 91 | votes=int(topic_vote), 92 | thank_count=int(topic_thank_count), 93 | favorite_count=int(topic_favorite_count), 94 | reply_count=int(topic_reply_count), 95 | ) 96 | 97 | 98 | def parse_topic_supplement(response: scrapy.http.response.html.HtmlResponse, topic_id): 99 | for i in response.css(".subtle"): 100 | subtle_content = i.xpath('string(div[@class="topic_content"])').get("") 101 | subtle_create_at = i.xpath("string(//span[@title])").get("") 102 | yield TopicSupplementItem( 103 | topic_id=topic_id, 104 | content=subtle_content, 105 | create_at=utils.time_to_timestamp(subtle_create_at), 106 | ) 107 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/public/top-comment.json: -------------------------------------------------------------------------------- 1 | [{"topic_id":898195,"id":12390171,"content":"
@FKekule<\/a> 麻木的傻狗<\/div>","thank_count":424,"no":7,"title":"一百年来,学生们从未改变"},{"topic_id":920707,"id":12767615,"content":"
属于狗娘养的不要碧莲的一种了<\/div>","thank_count":298,"no":8,"title":"你最好不给你的邻居连你的 wifi,对吧?"},{"topic_id":506105,"id":6428438,"content":"
一定要去公司闹一闹,万一人家公司一直是 965,岂不是自己朦在鼓里。<\/div>","thank_count":274,"no":5,"title":"我女朋友公司已经 9247 连续两个月了。我想去砸了他们公司。"},{"topic_id":525604,"id":6729688,"content":"
当年张小龙的微信界面长期不变化,是因为他的用户群体几亿,所以保持了对改变和加功能的克制;而今天对微信界面进行了巨大的改动,则是不畏惧巨大用户量勇敢地创新。张小龙不愧是产品经理之神。张小龙距离乔布斯只差去世了。<\/div>","thank_count":237,"no":1,"title":"张小龙已经成神了么, 4 个小时的演讲也看啊。。。"},{"topic_id":949869,"id":13237521,"content":"
“基本朝九晚五一年能到手 90 万左右,我知道论坛上很多人估计是看不上这个收入”,我以为我在看知乎<\/div>","thank_count":217,"no":3,"title":"这种情况下的我该不该润出北京"},{"topic_id":742124,"id":10026058,"content":"
@php8<\/a> 回老家过年是陋习??? 凡是你不喜欢的都是陋习?省省吧您?<\/div>","thank_count":214,"no":8,"title":"大部分地区都不鼓励回家了吧,你们过年还回去么?"},{"topic_id":779479,"id":10560980,"content":"
美好的一天从刷到这个帖结束<\/div>","thank_count":214,"no":2,"title":"35 了,咱们程序员未来的路咋走呀"},{"topic_id":861708,"id":11804119,"content":"
再发一遍我之前发过的内容:

我不关心帖子内容, 就是进来吐槽一下这个标题.
上学的时候老师教 题目要能表达意思, 让读者一眼就能知道大意.
现在互联网上各种歪风邪气, 起题目就是让你看不懂大意, 必须要点进来. 骗点击.

不是针对楼主, 但是真的很想对这种标题风格问候他们全家.<\/div>","thank_count":206,"no":7,"title":"突然发现,我在 windows 下面最刚需的软件,居然是这两个..."},{"topic_id":874454,"id":12015290,"content":"
我支持你同事<\/div>","thank_count":197,"no":2,"title":"公司同事为什么可以做到事情没做完就回家的心态"},{"topic_id":612060,"id":8070587,"content":"
你可是真闲,他有你这样的人做同事,真是到了八辈子大霉<\/div>","thank_count":191,"no":5,"title":"同事在工作时间做私活,我应该和领导说吗"},{"topic_id":893942,"id":12320898,"content":"
@fyw321451<\/a> https:\/\/www.v2ex.com\/help\/assertive<\/a>
好好说话
我们希望能够在 V2EX 建立和倡导一种好好说话的氛围。

请尽量描述事实,而非观点。
如果你要反驳什么,请反驳那个主要的要点,而不是一些旁枝末节。
我们建立这里的主要目的是为了讨论技术细节。不要在 V2EX 讨论任何国家的政治。
如果你要说的话是为了伤害别人,那么请不要说。如果你要说的话,你有预感在将来你会想要删掉它,那你最好现在就不要说。
在一个公共空间的公共讨论中,我们应该关注的,是自己能够在这些讨论中提供什么样的建设性增益,而不是那些纯粹个人的感受。比如当大家在讨论一件你不了解的东西时,你没有必要去回复一条“不明觉厉”。
回忆一下你看过的电影里的那些正面角色的说话方式——把一件事情好好陈述出来,没有冷笑,没有嘲讽,没有反问,就只是好好说话而已。<\/div>","thank_count":185,"no":2,"title":"惊闻 Meta 裁员……有没有海的那边的朋友讲讲,现在美国程序员就业情况怎么样?"},{"topic_id":853391,"id":11668978,"content":"
1. 鹤岗的房子更便宜,你咋不买呢?
2. “买个小两室房子一般人攒个几年加上家里的支援” 不是每个家庭都有余力支援
3. 你现在只是首付够了,后面的还贷呢,能保证贷款年限内薪资不降吗
4. 考虑小孩的话,上学怎么办,远城区上下班都要很久,有人接送和辅导小孩吗?
如果一个人辞职顾家,又回到第二点,另一个人的工资够还贷,买奶粉和生活费吗?
房子本就不该这么贵,生活也不该如此辛苦
以多角度看待事情,会有不一样的结果<\/div>","thank_count":179,"no":1,"title":"感觉房价也没有高的离谱,这么说会不会被喷呢"},{"topic_id":905104,"id":12511213,"content":"
因为“国外都是对疫情躺平了”是一种妖魔化的宣传,发达国家从来没有“无为而治”过。只不过人家没有进行“动态清零”
拿加拿大举个例子吧
疫情初期,居家发补贴,学生上网课,口罩令,关闭国境。
疫情中期,居家发补贴,学生上网课,口罩令取消,国境开放,必须打疫苗才可入境否则隔离 2 周。
疫情后期,正常工作学习生活,鼓励居家办公,鼓励戴口罩不强制,取消入境疫苗要求。

现在对中国人进行核酸检测,也只是应对疫情的一种方法而已,显然,来自中国入境人员是对疫情防控的扰动因素,需要加以控制。

其他方面
发达国家推崇大都市圈,摊大饼,大部分人居住在独立的‘农村自建房’中。这样一来,减少了风道,下水道,电梯等公共传播风险,人与人接触几率大大降低。
在交通上,美国 1000 人拥有超过 800 辆车,中国 1000 人只有 200 辆车,美国依赖自驾,又大大降低了交通过程中造成疫情传播风险。我们重度依赖公交,主要城市全部限牌。

以上两点,搞高层建筑同时必须重度依赖公交,背后的罪魁祸首都是红色集团的土地财政和懒政。无法费心搞活经济,只需要卖地,做大地主就可以轻松的获得大量资金。他们做的恶,后果由老百姓承担了。<\/div>","thank_count":170,"no":17,"title":"当国内疫情放开,为什么国外还要查中国旅客的核酸?"},{"topic_id":869372,"id":11930412,"content":"
富二代线上考编轻松录取,
官二代朝九晚五悠闲炫富,
小白领熬夜加班加到猝死,
农村人身残志坚感动中国。<\/div>","thank_count":168,"no":14,"title":"不知道前两天被二舅感动的,再看江西周公子是什么感觉?"},{"topic_id":802674,"id":10890326,"content":"
华为是长颈鹿吗天天就被卡脖子<\/div>","thank_count":163,"no":3,"title":"国内手机厂商最大的软肋就是 android 系统!"},{"topic_id":892495,"id":12297971,"content":"
我建议取消健康码<\/div>","thank_count":162,"no":1,"title":"健康码添加动图,增加辨识度,避免蒙混过关"},{"topic_id":670151,"id":8945700,"content":"
举报翻墙的绝对是坏种,建议全行业拉黑<\/div>","thank_count":158,"no":6,"title":"如何看待一个 MC 启动器作者被逼向他软件的破解者道歉?"},{"topic_id":709336,"id":9532962,"content":"
清明节为什么不给自己头像 p 上蜡烛纸钱<\/div>","thank_count":156,"no":1,"title":"国庆节快到啦,快给头像加上国旗吧"},{"topic_id":758335,"id":10267209,"content":"
人家延迟的是公务员国企事业单位,你想得美 60 了还写代码,35 就把你开了<\/div>","thank_count":156,"no":2,"title":"延迟退休估计要实行了,大家怎么看"},{"topic_id":903635,"id":12484167,"content":"
会不会出新变种以新华社通稿为准,你说了不算<\/div>","thank_count":153,"no":1,"title":"\"多地宣布无症状或轻症可上班\",在这种全民感染的环境下,以中国人口数量,会不会诞生新的变种。高传播高死亡率那种。"}] -------------------------------------------------------------------------------- /v2ex_scrapy/middlewares.py: -------------------------------------------------------------------------------- 1 | # Define here the models for your spider middleware 2 | # 3 | # See documentation in: 4 | # https://docs.scrapy.org/en/latest/topics/spider-middleware.html 5 | 6 | # useful for handling different item types with a single interface 7 | 8 | import logging 9 | import random 10 | import time 11 | 12 | import scrapy 13 | import scrapy.http.response.html 14 | from scrapy import signals 15 | from scrapy.exceptions import IgnoreRequest 16 | 17 | from v2ex_scrapy import utils 18 | from v2ex_scrapy.DB import DB, LogItem 19 | 20 | 21 | class TutorialScrapySpiderMiddleware: 22 | # Not all methods need to be defined. If a method is not defined, 23 | # scrapy acts as if the spider middleware does not modify the 24 | # passed objects. 25 | 26 | @classmethod 27 | def from_crawler(cls, crawler): 28 | # This method is used by Scrapy to create your spiders. 29 | s = cls() 30 | crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) 31 | return s 32 | 33 | def process_spider_input(self, response, spider): 34 | # Called for each response that goes through the spider 35 | # middleware and into the spider. 36 | 37 | # Should return None or raise an exception. 38 | return None 39 | 40 | def process_spider_output(self, response, result, spider): 41 | # Called with the results returned from the Spider, after 42 | # it has processed the response. 43 | 44 | # Must return an iterable of Request, or item objects. 45 | for i in result: 46 | yield i 47 | 48 | def process_spider_exception(self, response, exception, spider): 49 | # Called when a spider or process_spider_input() method 50 | # (from other spider middleware) raises an exception. 51 | 52 | # Should return either None or an iterable of Request or item objects. 53 | pass 54 | 55 | def process_start_requests(self, start_requests, spider): 56 | # Called with the start requests of the spider, and works 57 | # similarly to the process_spider_output() method, except 58 | # that it doesn’t have a response associated. 59 | 60 | # Must return only requests (not items). 61 | for r in start_requests: 62 | yield r 63 | 64 | def spider_opened(self, spider): 65 | spider.logger.info("Spider opened: %s" % spider.name) 66 | 67 | 68 | class ProxyAndCookieDownloaderMiddleware: 69 | # Not all methods need to be defined. If a method is not defined, 70 | # scrapy acts as if the downloader middleware does not modify the 71 | # passed objects. 72 | def __init__(self): 73 | self.proxies: list[str] = [] 74 | self.cookies: dict[str, str] = {} 75 | self.logger = logging.getLogger(__name__) 76 | 77 | @classmethod 78 | def from_crawler(cls, crawler): 79 | # This method is used by Scrapy to create your spiders. 80 | s = cls() 81 | crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) 82 | return s 83 | 84 | def process_request(self, request: scrapy.Request, spider): 85 | if "proxy" not in request.meta and len(self.proxies) > 0: 86 | request.meta["proxy"] = random.choice(self.proxies) 87 | if self.cookies != {} and request.cookies == {}: 88 | request.cookies = self.cookies 89 | # Called for each request that goes through the downloader 90 | # middleware. 91 | 92 | # Must either: 93 | # - return None: continue processing this request 94 | # - or return a Response object 95 | # - or return a Request object 96 | # - or raise IgnoreRequest: process_exception() methods of 97 | # installed downloader middleware will be called 98 | return None 99 | 100 | def process_response( 101 | self, 102 | request: scrapy.Request, 103 | response: scrapy.http.response.html.HtmlResponse, 104 | spider: scrapy.Spider, 105 | ): 106 | # Called with the response returned from the downloader. 107 | if response.status == 403: 108 | self.logger.info(f"skip url:{response.url}, because 403") 109 | raise IgnoreRequest(f"403 url {response.url}") 110 | # Must either; 111 | # - return a Response object 112 | # - return a Request object 113 | # - or raise IgnoreRequest 114 | return response 115 | 116 | def process_exception(self, request, exception, spider): 117 | # Called when a download handler or a process_request() 118 | # (from other downloader middleware) raises an exception. 119 | 120 | # Must either: 121 | # - return None: continue processing this exception 122 | # - return a Response object: stops process_exception() chain 123 | # - return a Request object: stops process_exception() chain 124 | pass 125 | 126 | def spider_opened(self, spider: scrapy.Spider): 127 | self.proxies = spider.settings.get("PROXIES", []) # type: ignore 128 | 129 | cookie_str = spider.settings.get("COOKIES", "") 130 | self.cookies = utils.cookie_str2cookie_dict(cookie_str) # type: ignore 131 | 132 | spider.logger.info("Spider opened: %s" % spider.name) 133 | 134 | 135 | class RandomUserAgentMiddleware: 136 | def __init__(self): 137 | self.user_agents: list[str] = [] 138 | 139 | @classmethod 140 | def from_crawler(cls, crawler): 141 | s = cls() 142 | crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) 143 | return s 144 | 145 | def process_request(self, request: scrapy.Request, spider): 146 | if len(self.user_agents) > 0: 147 | request.headers[b"User-Agent"] = random.choice(self.user_agents) 148 | return None 149 | 150 | def spider_opened(self, spider: scrapy.Spider): 151 | with open("./user-agents.txt") as f: 152 | self.user_agents = f.read().splitlines() 153 | 154 | 155 | class SaveHttpStatusToDBMiddleware: 156 | def __init__(self): 157 | self.db = DB() 158 | 159 | def process_response( 160 | self, request, response: scrapy.http.response.html.HtmlResponse, spider 161 | ): 162 | url = response.url 163 | status_code = response.status 164 | create_at = int(time.time()) 165 | self.db.session.add( 166 | LogItem(url=url, status_code=status_code, create_at=create_at) 167 | ) 168 | return response 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 中文 | [English](./README-en.md) 2 | 3 | # 一个爬取v2ex.com网站的爬虫 4 | 5 | 学习scrapy写的一个小爬虫 6 | 7 | 数据都放在了sqlite数据库,方便分享,整个数据库大小3.7GB。 8 | 9 | 在GitHub我 release 了完整的sqlite数据库文件 10 | 11 | **数据库更新:2023-07-22-full** 12 | 13 | 包含全部帖子数据(水深火热在内),同时帖子和评论的内容不再只爬取文本内容,而是爬取原始的HTML,同时topic增加了单独的reply_count,comment增加了no 14 | 15 | ## 不建议自行运行爬虫,数据已经有了 16 | 17 | 爬取花了几十小时,因为爬快了会封禁IP,并且我也没使用代理池。并发数设置为3基本上可以一直爬。 18 | 19 | [下载数据库](https://github.com/oldshensheep/v2ex_scrapy/releases) 20 | 21 | ## 爬取相关数据说明 22 | 23 | 爬虫从`topic_id = 1`开始爬,路径为`https://www.v2ex.com/t/{topic_id}`。 服务器可能返回404/403/302/200,如果是404说明帖子被删除了,如果是403说明是爬虫被限制了,302一般是跳转到登陆页面,有的也是跳转到主页,200返回正常页面。 24 | 25 | 爬取过程中会帖子内容,评论,以及评论的用户信息。 26 | 27 | 数据库表结构:[表结构源码](./v2ex_scrapy/items.py) 28 | 29 | ## 运行 30 | 31 | 确保python >=3.10 32 | 33 | ### 安装依赖 34 | 35 | ```bash 36 | pip install -r requirements.txt 37 | ``` 38 | 39 | ### 配置 40 | 41 | 默认的并发数设置成了1,如需更改修改`CONCURRENT_REQUESTS` 42 | 43 | #### Cookie 44 | 45 | 部分帖子和部分帖子信息需要登录才能爬取,可以设置Cookie来登录,修改 `v2ex_scrapy/settings.py`中 `COOKIES`的值 46 | 47 | ```python 48 | COOKIES = """ 49 | a=b;c=d;e=f 50 | """ 51 | ``` 52 | 53 | #### 代理 54 | 55 | 更改 `v2ex_scrapy/settings.py` 中 `PROXIES`的值 如 56 | 57 | ```python 58 | [ 59 | "http://127.0.0.1:7890" 60 | ] 61 | ``` 62 | 63 | 请求会随机选择一个代理,如果需要更高级的代理方式可以使用第三方库,或者自行实现Middleware 64 | 65 | #### LOG 66 | 67 | 默认关闭了写入Log文件的功能,如需开启修改`v2ex_scrapy\settings.py`中的这行`# LOG_FILE = "v2ex_scrapy.log"`配置文件,取消注释 68 | 69 | ### 运行爬虫 70 | 71 | 爬取全站帖子、用户信息和评论 72 | 73 | ```bash 74 | scrapy crawl v2ex 75 | ``` 76 | 77 | 爬取指定节点帖子、用户信息和评论,如果node-name为空则爬flamewar 78 | 79 | ```bash 80 | scrapy crawl v2ex-node node=${node-name} 81 | ``` 82 | 83 | 爬取用户信息,从uid=1开始爬到uid=635000 84 | 85 | ```bash 86 | scrapy crawl v2ex-member start_id=${start_id} end_id=${end_id} 87 | ``` 88 | 89 | > `scrapy: command not found` 说明没有添加python包的安装位置到环境变量 90 | 91 | ### 接着上次爬 92 | 93 | 直接运行爬取的命令即可,会自动继续爬。会自动跳过已经爬过的帖子 94 | 95 | ```bash 96 | scrapy crawl v2ex 97 | ``` 98 | 99 | ### 注意事项 100 | 101 | 爬取过程中出现403基本上是因为IP被限制了,等待一段时间即可 102 | 103 | ## 统计分析 104 | 105 | 统计用的SQL在[query.sql](query.sql)这个文件下,图表的源码在[analysis](analysis)这个子项目下,包含一个分析数据导出到JSON的Python脚本和一个前端展示项目 106 | 107 | 第一次的分析见 108 | 109 | 水深火热见 110 | 111 | ### 帖子、评论和用户数量统计 112 | 113 | 帖子总数:801,038 (80万) 114 | 评论总数:10,899,382 (1000万) 115 | 用户总数:194,534 (20万)异常原因见爬取相关数据说明的注2 116 | 117 | ### 获得感谢最多的评论 118 | 119 | 因为部分评论内容较多不方便展示,要查看内容可以点击链接。或者下载数据库使用SQL查询,SQL查询文件也包含在开源文件中 120 | 121 | | 评论链接 | 感谢数 | 122 | | :--- | :--- | 123 | | [https://www.v2ex.com/t/820687#r_11150263](https://www.v2ex.com/t/820687#r_11150263) | 316 | 124 | | [https://www.v2ex.com/t/437760#r_5432223](https://www.v2ex.com/t/437760#r_5432223) | 297 | 125 | | [https://www.v2ex.com/t/915584#r_12684442](https://www.v2ex.com/t/915584#r_12684442) | 248 | 126 | | [https://www.v2ex.com/t/917858#r_12720322](https://www.v2ex.com/t/917858#r_12720322) | 246 | 127 | | [https://www.v2ex.com/t/949195#r_13227124](https://www.v2ex.com/t/949195#r_13227124) | 246 | 128 | | [https://www.v2ex.com/t/881410#r_12126164](https://www.v2ex.com/t/881410#r_12126164) | 245 | 129 | | [https://www.v2ex.com/t/884719#r_12178891](https://www.v2ex.com/t/884719#r_12178891) | 240 | 130 | | [https://www.v2ex.com/t/901263#r_12442916](https://www.v2ex.com/t/901263#r_12442916) | 240 | 131 | | [https://www.v2ex.com/t/749163#r_10129442](https://www.v2ex.com/t/749163#r_10129442) | 217 | 132 | | [https://www.v2ex.com/t/877829#r_12070911](https://www.v2ex.com/t/877829#r_12070911) | 216 | 133 | 134 | ### 正向投票最多的帖子 135 | 136 | | 帖子链接 | 标题 | 票数 | 137 | | :--- | :--- | :--- | 138 | | [https://www.v2ex.com/t/110327](https://www.v2ex.com/t/110327) | UP n DOWN vote in V2EX | 321 | 139 | | [https://www.v2ex.com/t/295433](https://www.v2ex.com/t/295433) | Snipaste - 开发了三年的截图工具,但不只是截图 | 274 | 140 | | [https://www.v2ex.com/t/462641](https://www.v2ex.com/t/462641) | 在 D 版发过了,不过因为不少朋友看不到 D 版,我就放在这里吧,说说我最近做的这个 Project | 200 | 141 | | [https://www.v2ex.com/t/658387](https://www.v2ex.com/t/658387) | 剽窃别人成果的人一直有,不过今天遇到了格外厉害的 | 179 | 142 | | [https://www.v2ex.com/t/745030](https://www.v2ex.com/t/745030) | QQ 正在尝试读取你的浏览记录 | 177 | 143 | | [https://www.v2ex.com/t/689296](https://www.v2ex.com/t/689296) | 早上还在睡觉,自如管家进了我卧室... | 145 | 144 | | [https://www.v2ex.com/t/814025](https://www.v2ex.com/t/814025) | 分享一张我精心修改调整的 M42 猎户座大星云(Orion Nebula)壁纸。用了非常多年,首次分享出来,能和 MBP 2021 新屏幕和谐相处。 | 136 | 145 | | [https://www.v2ex.com/t/511827](https://www.v2ex.com/t/511827) | 23 岁,得了癌症,人生无望 | 129 | 146 | | [https://www.v2ex.com/t/427796](https://www.v2ex.com/t/427796) | 隔壁组的小兵集体情愿 要炒了 team leader | 123 | 147 | | [https://www.v2ex.com/t/534800](https://www.v2ex.com/t/534800) | 使用 Github 账号登录 黑客派 之后, Github 自动 follow | 112 | 148 | 149 | ### 点击次数最多的帖子 150 | 151 | | 帖子链接 | 标题 | 点击数 | 152 | | :--- | :--- | :--- | 153 | | [https://www.v2ex.com/t/510849](https://www.v2ex.com/t/510849) | chrome 签到插件 \[魂签\] 更新啦 | 39,452,510 | 154 | | [https://www.v2ex.com/t/706595](https://www.v2ex.com/t/706595) | 迫于搬家 ··· 继续出 700 本书\~ 四折 非技术书还剩 270 多本· | 2,406,584 | 155 | | [https://www.v2ex.com/t/718092](https://www.v2ex.com/t/718092) | 使用 GitHub 的流量数据为仓库创建访问数和克隆数的徽章 | 1,928,267 | 156 | | [https://www.v2ex.com/t/861832](https://www.v2ex.com/t/861832) | 帮朋友推销下福建古田水蜜桃,欢迎各位购买啊 | 635,832 | 157 | | [https://www.v2ex.com/t/176916](https://www.v2ex.com/t/176916) | 王垠这是在想不开吗 | 329,617 | 158 | | [https://www.v2ex.com/t/303889](https://www.v2ex.com/t/303889) | 关于 V2EX 提供的 Android Captive Portal Server 地址的更新 | 295,681 | 159 | | [https://www.v2ex.com/t/206766](https://www.v2ex.com/t/206766) | 如何找到一些有趣的 telegram 群组? | 294,553 | 160 | | [https://www.v2ex.com/t/265474](https://www.v2ex.com/t/265474) | ngrok 客户端和服务端如何不验证证书 | 271,244 | 161 | | [https://www.v2ex.com/t/308080](https://www.v2ex.com/t/308080) | Element UI——一套基于 Vue 2.0 的桌面端组件库 | 221,099 | 162 | | [https://www.v2ex.com/t/295433](https://www.v2ex.com/t/295433) | Snipaste - 开发了三年的截图工具,但不只是截图 | 210,675 | 163 | 164 | ### 发送评论最多的用户 165 | 166 | | 用户 | 评论数 | 167 | | :--- | :--- | 168 | | [Livid](https://www.v2ex.com/member/Livid) | 19559 | 169 | | [loading](https://www.v2ex.com/member/loading) | 19190 | 170 | | [murmur](https://www.v2ex.com/member/murmur) | 17189 | 171 | | [msg7086](https://www.v2ex.com/member/msg7086) | 16768 | 172 | | [Tink](https://www.v2ex.com/member/Tink) | 15919 | 173 | | [imn1](https://www.v2ex.com/member/imn1) | 11468 | 174 | | [20015jjw](https://www.v2ex.com/member/20015jjw) | 10293 | 175 | | [x86](https://www.v2ex.com/member/x86) | 9704 | 176 | | [opengps](https://www.v2ex.com/member/opengps) | 9694 | 177 | | [est](https://www.v2ex.com/member/est) | 9532 | 178 | 179 | ### 发送帖子最多的用户 180 | 181 | | 用户 | 主题数 | 182 | | :--- | :--- | 183 | | [Livid](https://www.v2ex.com/member/Livid) | 6974 | 184 | | [icedx](https://www.v2ex.com/member/icedx) | 722 | 185 | | [ccming](https://www.v2ex.com/member/ccming) | 646 | 186 | | [2232588429](https://www.v2ex.com/member/2232588429) | 614 | 187 | | [razios](https://www.v2ex.com/member/razios) | 611 | 188 | | [coolair](https://www.v2ex.com/member/coolair) | 604 | 189 | | [Kai](https://www.v2ex.com/member/Kai) | 599 | 190 | | [est](https://www.v2ex.com/member/est) | 571 | 191 | | [Newyorkcity](https://www.v2ex.com/member/Newyorkcity) | 553 | 192 | | [WildCat](https://www.v2ex.com/member/WildCat) | 544 | 193 | 194 | ## 曲线 195 | 196 | 需要详细的数据,建议下载数据库 197 | 198 | ### 每月新用户数折线图 199 | 200 | ![1688470374959](image/t/1688470374959.png) 201 | 202 | ### 每月新帖子数折线图 203 | 204 | ![1688469358680](image/t/1688469358680.png) 205 | 206 | ### 评论数折线图 207 | 208 | ![1688470738877](image/t/1688470738877.png) 209 | 210 | ### 使用次数最多的节点 211 | 212 | | 节点 | 次数 | 213 | | :--- | :--- | 214 | | [qna](https://www.v2ex.com/go/qna) | 188011 | 215 | | [all4all](https://www.v2ex.com/go/all4all) | 103254 | 216 | | [programmer](https://www.v2ex.com/go/programmer) | 51706 | 217 | | [jobs](https://www.v2ex.com/go/jobs) | 49959 | 218 | | [share](https://www.v2ex.com/go/share) | 35942 | 219 | | [apple](https://www.v2ex.com/go/apple) | 20713 | 220 | | [macos](https://www.v2ex.com/go/macos) | 19040 | 221 | | [create](https://www.v2ex.com/go/create) | 18685 | 222 | | [python](https://www.v2ex.com/go/python) | 14124 | 223 | | [career](https://www.v2ex.com/go/career) | 13170 | 224 | 225 | ### 使用次数最多的tag (tag为v2ex自动生成) 226 | 227 | | tag | 次数 | 228 | | :--- | :--- | 229 | | [开发](https://www.v2ex.com/tag/%E5%BC%80%E5%8F%91) | 16414 | 230 | | [App](https://www.v2ex.com/tag/App) | 13240 | 231 | | [Python](https://www.v2ex.com/tag/Python) | 13016 | 232 | | [Mac](https://www.v2ex.com/tag/Mac) | 12931 | 233 | | [Java](https://www.v2ex.com/tag/Java) | 10984 | 234 | | [Pro](https://www.v2ex.com/tag/Pro) | 9375 | 235 | | [iOS](https://www.v2ex.com/tag/iOS) | 9216 | 236 | | [微信](https://www.v2ex.com/tag/%E5%BE%AE%E4%BF%A1) | 8922 | 237 | | [V2EX](https://www.v2ex.com/tag/V2EX) | 8426 | 238 | | [域名](https://www.v2ex.com/tag/%E5%9F%9F%E5%90%8D) | 8424 | 239 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | [中文](./README.md) | English 2 | Translated by ChatGPT 3 | 4 | # A web crawler for v2ex.com 5 | 6 | A small crawler written to learn scrapy. 7 | 8 | The data is stored in an SQLite database for easy sharing, with a total database size of 3.7GB. 9 | 10 | I have released the complete SQLite database file on GitHub. 11 | 12 | **Database Update: 2023-07-22-full** 13 | 14 | It contains all post data, including hot topics. Both post and comment content are now scraped in their original HTML format. Additionally, the "topic" has a new field "reply_count," and "comment" has a new field "no." 15 | 16 | ## It is not recommended to run the crawler as the data is already available 17 | 18 | The crawling process took several dozen hours because rapid crawling can result in IP banning, and I didn't use a proxy pool. Setting the concurrency to 3 should allow for continuous crawling. 19 | 20 | [Download the database](https://github.com/oldshensheep/v2ex_scrapy/releases) 21 | 22 | ## Explanation of Crawled Data 23 | 24 | The crawler starts crawling from `topic_id = 1`, and the path is `https://www.v2ex.com/t/{topic_id}`. The server might return 404/403/302/200 status codes. A 404 indicates that the post has been deleted, 403 indicates that the crawler has been restricted, 302 is usually a redirection to the login page or homepage, and 200 indicates a normal page. 25 | 26 | The crawler fetches post content, comments, and user information during the crawling process. 27 | 28 | Database table structure: [Table structure source code](./v2ex_scrapy/items.py) 29 | 30 | ## Running 31 | 32 | Ensure Python version is >=3.10 33 | 34 | ### Install Dependencies 35 | 36 | ```bash 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | ### Configuration 41 | 42 | The default concurrency is set to 1. To change it, modify `CONCURRENT_REQUESTS`. 43 | 44 | #### Cookie 45 | 46 | Some posts and post information require login for crawling. You can set a Cookie to log in. Modify the `COOKIES` value in `v2ex_scrapy/settings.py`: 47 | 48 | ```python 49 | COOKIES = """ 50 | a=b;c=d;e=f 51 | """ 52 | ``` 53 | 54 | #### Proxy 55 | 56 | Change the value of `PROXIES` in `v2ex_scrapy/settings.py`, for example: 57 | 58 | ```python 59 | [ 60 | "http://127.0.0.1:7890" 61 | ] 62 | ``` 63 | 64 | Requests will randomly choose one of the proxies. If you need a more advanced proxy method, you can use a third-party library or implement Middleware yourself. 65 | 66 | #### LOG 67 | 68 | The writing of Log files is disabled by default. To enable it, uncomment this line in `v2ex_scrapy\settings.py`: 69 | 70 | ```python 71 | LOG_FILE = "v2ex_scrapy.log" 72 | ``` 73 | 74 | ### Run the Crawler 75 | 76 | Crawl all posts, user information, and comments on the entire site: 77 | 78 | ```bash 79 | scrapy crawl v2ex 80 | ``` 81 | 82 | Crawl posts, user information, and comments for a specific node. If node-name is empty, it crawls "flamewar": 83 | 84 | ```bash 85 | scrapy crawl v2ex-node node=${node-name} 86 | ``` 87 | 88 | Crawl user information, starting from uid=1 and crawling up to uid=635000: 89 | 90 | ```bash 91 | scrapy crawl v2ex-member start_id=${start_id} end_id=${end_id} 92 | ``` 93 | 94 | > If you see `scrapy: command not found`, it means the Python package installation path has not been added to the environment variable. 95 | 96 | ### Resuming the Crawl 97 | 98 | Simply run the crawl command again, and it will automatically continue crawling, skipping the posts that have already been crawled: 99 | 100 | ```bash 101 | scrapy crawl v2ex 102 | ``` 103 | 104 | ### Notes 105 | 106 | If you encounter a 403 error during the crawling process, it is likely due to IP restrictions. Wait for a while before trying again. 107 | 108 | ## Statistical Analysis 109 | 110 | The SQL queries used for statistics can be found in the [query.sql](query.sql) file, and the source code for the charts is in the [analysis](analysis) subproject. It includes a Python script for exporting data to JSON for analysis and a frontend display project. 111 | 112 | The first analysis can be found at 113 | 114 | For more detailed data, I suggest downloading the database. 115 | 116 | ### Statistics of Posts, Comments, and Users 117 | 118 | Total posts: 801,038 (800,000) 119 | Total comments: 10,899,382 (10 million) 120 | Total users: 194,534 (200,000) - See Note 2 in the Explanation of Crawled Data 121 | 122 | ### Top Comments by Gratitude 123 | 124 | Gratitude count is too large to display the full content. You can click on the links or download the database to query using SQL. The SQL queries are also included in the open-source files. 125 | 126 | | Comment Link | Gratitude Count | 127 | | :--- | :--- | 128 | | [https://www.v2ex.com/t/820687#r_11150263](https://www.v2ex.com/t/820687#r_11150263) | 316 | 129 | | [https://www.v2ex.com/t/437760#r_5432223](https://www.v2ex.com/t/437760#r_5432223) | 297 | 130 | | [https://www.v2ex.com/t/915584#r_12684442](https://www.v2ex.com/t/915584#r_12684442) | 248 | 131 | | [https://www.v2ex.com/t/917858#r_12720322](https://www.v2ex.com/t/917858#r_12720322) | 246 | 132 | | [https://www.v2ex.com/t/949195#r_13227124](https://www.v2ex.com/t/949195#r_13227124) | 246 | 133 | | [https://www.v2ex.com/t/881410#r_12126164](https://www.v2ex.com/t/881410#r_12126164) | 245 | 134 | | [https://www.v2ex.com/t/884719#r_12178891](https://www.v2ex.com/t/884719#r_12178891) | 240 | 135 | | [https://www.v2ex.com/t/901263#r_12442916](https://www.v2ex.com/t/901263#r_12442916) | 240 | 136 | | [https://www.v2ex.com/t/749163#r_10129442](https://www.v2ex.com/t/749163#r_10129442) | 217 | 137 | | [https://www.v2ex.com/t/877829#r_12070911](https://www.v2ex.com/t/877829#r_12070911) | 216 | 138 | 139 | ### Most Upvoted Posts 140 | 141 | | Post Link | Title | Votes | 142 | | :--- | :--- | :--- | 143 | | [https://www.v2ex.com/t/110327](https://www.v2ex.com/t/110327) | UP n DOWN vote in V2EX | 321 | 144 | | [https://www.v2ex.com/t/295433](https://www.v2ex.com/t/295433) | Snipaste - 开发了三年的截图工具,但不只是截图 | 274 | 145 | | [https://www.v2ex.com/t/462641](https://www.v2ex.com/t/462641) | 在 D 版发过了,不过因为不少朋友看不到 D 版,我就放在这里吧,说说我最近做的这个 Project | 200 | 146 | | [https://www.v2ex.com/t/658387](https://www.v2ex.com/t/658387) | 剽窃别人成果的人一直有,不过今天遇到了格外厉害的 | 179 | 147 | | [https://www.v2ex.com/t/745030](https://www.v2ex.com/t/745030) | QQ 正在尝试读取你的浏览记录 | 177 | 148 | | [https://www.v2ex.com/t/689296](https://www.v2ex.com/t/689296) | 早上还在睡觉,自如管家进了我卧室... | 145 | 149 | | [https://www.v2ex.com/t/814025](https://www.v2ex.com/t/814025) | 分享一张我精心修改调整的 M42 猎户座大星云(Orion Nebula)壁纸。用了非常多年,首次分享出来,能和 MBP 2021 新屏幕和谐相处。 | 136 | 150 | | [https://www.v2ex.com/t/511827](https://www.v2ex.com/t/511827) | 23 岁,得了癌症,人生无望 | 129 | 151 | | [https://www.v2ex.com/t/427796](https://www.v2ex.com/t/427796) | 隔壁组的小兵集体情愿 要炒了 team leader | 123 | 152 | | [https://www.v2ex.com/t/534800](https://www.v2ex.com/t/534800) | 使用 Github 账号登录 黑客派 之后, Github 自动 follow | 112 | 153 | 154 | ### Most Viewed Posts 155 | 156 | | Post Link | Title | Views | 157 | | :--- | :--- | :--- | 158 | | [https://www.v2ex.com/t/510849](https://www.v2ex.com/t/510849) | chrome 签到插件 \[魂签\] 更新啦 | 39,452,510 | 159 | | [https://www.v2ex.com/t/706595](https://www.v2ex.com/t/706595) | 迫于搬家 ··· 继续出 700 本书\~ 四折 非技术书还剩 270 多本· | 2,406,584 | 160 | | [https://www.v2ex.com/t/718092](https://www.v2ex.com/t/718092) | 使用 GitHub 的流量数据为仓库创建访问数和克隆数的徽章 | 1,928,267 | 161 | | [https://www.v2ex.com/t/861832](https://www.v2ex.com/t/861832) | 帮朋友推销下福建古田水蜜桃,欢迎各位购买啊 | 635,832 | 162 | | [https://www.v2ex.com/t/176916](https://www.v2ex.com/t/176916) | 王垠这是在想不开吗 | 329,617 | 163 | | [https://www.v2ex.com/t/303889](https://www.v2ex.com/t/303889) | 关于 V2EX 提供的 Android Captive Portal Server 地址的更新 | 295,681 | 164 | | [https://www.v2ex.com/t/206766](https://www.v2ex.com/t/206766) | 如何找到一些有趣的 telegram 群组? | 294,553 | 165 | | [https://www.v2ex.com/t/265474](https://www.v2ex.com/t/265474) | ngrok 客户端和服务端如何不验证证书 | 271,244 | 166 | | [https://www.v2ex.com/t/308080](https://www.v2ex.com/t/308080) | Element UI——一套基于 Vue 2.0 的桌面端组件库 | 221,099 | 167 | | [https://www.v2ex.com/t/295433](https://www.v2ex.com/t/295433) | Snipaste - 开发了三年的截图工具,但不只是截图 | 210,675 | 168 | 169 | ### Users with Most Comments 170 | 171 | | User | Comment Count | 172 | | :--- | :--- | 173 | | [Livid](https://www.v2ex.com/member/Livid) | 19559 | 174 | | [loading](https://www.v2ex.com/member/loading) | 19190 | 175 | | [murmur](https://www.v2ex.com/member/murmur) | 17189 | 176 | | [msg7086](https://www.v2ex.com/member/msg7086) | 16768 | 177 | | [Tink](https://www.v2ex.com/member/Tink) | 15919 | 178 | | [imn1](https://www.v2ex.com/member/imn1) | 11468 | 179 | | [20015jjw](https://www.v2ex.com/member/20015jjw) | 10293 | 180 | | [x86](https://www.v2ex.com/member/x86) | 9704 | 181 | | [opengps](https://www.v2ex.com/member/opengps) | 9694 | 182 | | [est](https://www.v2ex.com/member/est) | 9532 | 183 | 184 | ### Users with Most Posts 185 | 186 | | User | Topic Count | 187 | | :--- | :--- | 188 | | [Livid](https://www.v2ex.com/member/Livid) | 6974 | 189 | | [icedx](https://www.v2ex.com/member/icedx) | 722 | 190 | | [ccming](https://www.v2ex.com/member/ccming) | 646 | 191 | | [2232588429](https://www.v2ex.com/member/2232588429) | 614 | 192 | | [razios](https://www.v2ex.com/member/razios) | 611 | 193 | | [coolair](https://www.v2ex.com/member/coolair) | 604 | 194 | | [Kai](https://www.v2ex.com/member/Kai) | 599 | 195 | | [est](https://www.v2ex.com/member/est) | 571 | 196 | | [Newyorkcity](https://www.v2ex.com/member/Newyorkcity) | 553 | 197 | | [WildCat](https://www.v2ex.com/member/WildCat) | 544 | 198 | 199 | ## Curves 200 | 201 | For detailed data, I recommend downloading the database. 202 | 203 | ### Line Chart for New Users per Month 204 | 205 | ![1688470374959](image/t/1688470374959.png) 206 | 207 | ### Line Chart for New Posts per Month 208 | 209 | ![1688469358680](image/t/1688469358680.png) 210 | 211 | ### Line Chart for Comment Count 212 | 213 | ![1688470738877](image/t/1688470738877.png) 214 | 215 | ### Most Frequently Used Nodes 216 | 217 | | Node | Count | 218 | | :--- | :--- | 219 | | [qna](https://www.v2ex.com/go/qna) | 188011 | 220 | | [all4all](https://www.v2ex.com/go/all4all) | 103254 | 221 | | [programmer](https://www.v2ex.com/go/programmer) | 51706 | 222 | | [jobs](https://www.v2ex.com/go/jobs) | 49959 | 223 | | [share](https://www.v2ex.com/go/share) | 35942 | 224 | | [apple](https://www.v2ex.com/go/apple) | 20713 | 225 | | [macos](https://www.v2ex.com/go/macos) | 19040 | 226 | | [create](https://www.v2ex.com/go/create) | 18685 | 227 | | [python](https://www.v2ex.com/go/python) | 14124 | 228 | | [career](https://www.v2ex.com/go/career) | 13170 | 229 | 230 | ### Most Frequently Used Tags (auto-generated by V2EX) 231 | 232 | | Tag | Count | 233 | | :--- | :--- | 234 | | [开发](https://www.v2ex.com/tag/%E5%BC%80%E5%8F%91) | 16414 | 235 | | [App](https://www.v2ex.com/tag/App) | 13240 | 236 | | [Python](https://www.v2ex.com/tag/Python) | 13016 | 237 | | [Mac](https://www.v2ex.com/tag/Mac) | 12931 | 238 | | [Java](https://www.v2ex.com/tag/Java) | 10984 | 239 | | [Pro](https://www.v2ex.com/tag/Pro) | 9375 | 240 | | [iOS](https://www.v2ex.com/tag/iOS) | 9216 | 241 | | [微信](https://www.v2ex.com/tag/%E5%BE%AE%E4%BF%A1) | 8922 | 242 | | [V2EX](https://www.v2ex.com/tag/V2EX) | 8426 | 243 | | [域名](https://www.v2ex.com/tag/%E5%9F%9F%E5%90%8D) | 8424 | 244 | -------------------------------------------------------------------------------- /analysis/v2ex-analysis/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | plotly.js-dist-min: 9 | specifier: ^2.24.3 10 | version: 2.24.3 11 | vue: 12 | specifier: ^3.3.4 13 | version: 3.3.4 14 | 15 | devDependencies: 16 | '@tsconfig/node18': 17 | specifier: ^2.0.1 18 | version: 2.0.1 19 | '@types/node': 20 | specifier: ^18.16.17 21 | version: 18.16.17 22 | '@vitejs/plugin-vue': 23 | specifier: ^4.2.3 24 | version: 4.2.3(vite@4.3.9)(vue@3.3.4) 25 | '@vue/tsconfig': 26 | specifier: ^0.4.0 27 | version: 0.4.0 28 | autoprefixer: 29 | specifier: ^10.4.14 30 | version: 10.4.14(postcss@8.4.25) 31 | daisyui: 32 | specifier: ^3.2.1 33 | version: 3.2.1 34 | npm-run-all: 35 | specifier: ^4.1.5 36 | version: 4.1.5 37 | postcss: 38 | specifier: ^8.4.25 39 | version: 8.4.25 40 | tailwindcss: 41 | specifier: ^3.3.2 42 | version: 3.3.2 43 | typescript: 44 | specifier: ~5.0.4 45 | version: 5.0.4 46 | vite: 47 | specifier: ^4.3.9 48 | version: 4.3.9(@types/node@18.16.17) 49 | vue-tsc: 50 | specifier: ^1.6.5 51 | version: 1.6.5(typescript@5.0.4) 52 | 53 | packages: 54 | 55 | /@alloc/quick-lru@5.2.0: 56 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 57 | engines: {node: '>=10'} 58 | dev: true 59 | 60 | /@babel/helper-string-parser@7.22.5: 61 | resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} 62 | engines: {node: '>=6.9.0'} 63 | 64 | /@babel/helper-validator-identifier@7.22.5: 65 | resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} 66 | engines: {node: '>=6.9.0'} 67 | 68 | /@babel/parser@7.22.7: 69 | resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} 70 | engines: {node: '>=6.0.0'} 71 | hasBin: true 72 | dependencies: 73 | '@babel/types': 7.22.5 74 | 75 | /@babel/types@7.22.5: 76 | resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} 77 | engines: {node: '>=6.9.0'} 78 | dependencies: 79 | '@babel/helper-string-parser': 7.22.5 80 | '@babel/helper-validator-identifier': 7.22.5 81 | to-fast-properties: 2.0.0 82 | 83 | /@esbuild/android-arm64@0.17.19: 84 | resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 85 | engines: {node: '>=12'} 86 | cpu: [arm64] 87 | os: [android] 88 | requiresBuild: true 89 | dev: true 90 | optional: true 91 | 92 | /@esbuild/android-arm@0.17.19: 93 | resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 94 | engines: {node: '>=12'} 95 | cpu: [arm] 96 | os: [android] 97 | requiresBuild: true 98 | dev: true 99 | optional: true 100 | 101 | /@esbuild/android-x64@0.17.19: 102 | resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 103 | engines: {node: '>=12'} 104 | cpu: [x64] 105 | os: [android] 106 | requiresBuild: true 107 | dev: true 108 | optional: true 109 | 110 | /@esbuild/darwin-arm64@0.17.19: 111 | resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 112 | engines: {node: '>=12'} 113 | cpu: [arm64] 114 | os: [darwin] 115 | requiresBuild: true 116 | dev: true 117 | optional: true 118 | 119 | /@esbuild/darwin-x64@0.17.19: 120 | resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 121 | engines: {node: '>=12'} 122 | cpu: [x64] 123 | os: [darwin] 124 | requiresBuild: true 125 | dev: true 126 | optional: true 127 | 128 | /@esbuild/freebsd-arm64@0.17.19: 129 | resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 130 | engines: {node: '>=12'} 131 | cpu: [arm64] 132 | os: [freebsd] 133 | requiresBuild: true 134 | dev: true 135 | optional: true 136 | 137 | /@esbuild/freebsd-x64@0.17.19: 138 | resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 139 | engines: {node: '>=12'} 140 | cpu: [x64] 141 | os: [freebsd] 142 | requiresBuild: true 143 | dev: true 144 | optional: true 145 | 146 | /@esbuild/linux-arm64@0.17.19: 147 | resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 148 | engines: {node: '>=12'} 149 | cpu: [arm64] 150 | os: [linux] 151 | requiresBuild: true 152 | dev: true 153 | optional: true 154 | 155 | /@esbuild/linux-arm@0.17.19: 156 | resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 157 | engines: {node: '>=12'} 158 | cpu: [arm] 159 | os: [linux] 160 | requiresBuild: true 161 | dev: true 162 | optional: true 163 | 164 | /@esbuild/linux-ia32@0.17.19: 165 | resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 166 | engines: {node: '>=12'} 167 | cpu: [ia32] 168 | os: [linux] 169 | requiresBuild: true 170 | dev: true 171 | optional: true 172 | 173 | /@esbuild/linux-loong64@0.17.19: 174 | resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 175 | engines: {node: '>=12'} 176 | cpu: [loong64] 177 | os: [linux] 178 | requiresBuild: true 179 | dev: true 180 | optional: true 181 | 182 | /@esbuild/linux-mips64el@0.17.19: 183 | resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 184 | engines: {node: '>=12'} 185 | cpu: [mips64el] 186 | os: [linux] 187 | requiresBuild: true 188 | dev: true 189 | optional: true 190 | 191 | /@esbuild/linux-ppc64@0.17.19: 192 | resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 193 | engines: {node: '>=12'} 194 | cpu: [ppc64] 195 | os: [linux] 196 | requiresBuild: true 197 | dev: true 198 | optional: true 199 | 200 | /@esbuild/linux-riscv64@0.17.19: 201 | resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 202 | engines: {node: '>=12'} 203 | cpu: [riscv64] 204 | os: [linux] 205 | requiresBuild: true 206 | dev: true 207 | optional: true 208 | 209 | /@esbuild/linux-s390x@0.17.19: 210 | resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 211 | engines: {node: '>=12'} 212 | cpu: [s390x] 213 | os: [linux] 214 | requiresBuild: true 215 | dev: true 216 | optional: true 217 | 218 | /@esbuild/linux-x64@0.17.19: 219 | resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 220 | engines: {node: '>=12'} 221 | cpu: [x64] 222 | os: [linux] 223 | requiresBuild: true 224 | dev: true 225 | optional: true 226 | 227 | /@esbuild/netbsd-x64@0.17.19: 228 | resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 229 | engines: {node: '>=12'} 230 | cpu: [x64] 231 | os: [netbsd] 232 | requiresBuild: true 233 | dev: true 234 | optional: true 235 | 236 | /@esbuild/openbsd-x64@0.17.19: 237 | resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 238 | engines: {node: '>=12'} 239 | cpu: [x64] 240 | os: [openbsd] 241 | requiresBuild: true 242 | dev: true 243 | optional: true 244 | 245 | /@esbuild/sunos-x64@0.17.19: 246 | resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 247 | engines: {node: '>=12'} 248 | cpu: [x64] 249 | os: [sunos] 250 | requiresBuild: true 251 | dev: true 252 | optional: true 253 | 254 | /@esbuild/win32-arm64@0.17.19: 255 | resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 256 | engines: {node: '>=12'} 257 | cpu: [arm64] 258 | os: [win32] 259 | requiresBuild: true 260 | dev: true 261 | optional: true 262 | 263 | /@esbuild/win32-ia32@0.17.19: 264 | resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 265 | engines: {node: '>=12'} 266 | cpu: [ia32] 267 | os: [win32] 268 | requiresBuild: true 269 | dev: true 270 | optional: true 271 | 272 | /@esbuild/win32-x64@0.17.19: 273 | resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 274 | engines: {node: '>=12'} 275 | cpu: [x64] 276 | os: [win32] 277 | requiresBuild: true 278 | dev: true 279 | optional: true 280 | 281 | /@jridgewell/gen-mapping@0.3.3: 282 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 283 | engines: {node: '>=6.0.0'} 284 | dependencies: 285 | '@jridgewell/set-array': 1.1.2 286 | '@jridgewell/sourcemap-codec': 1.4.15 287 | '@jridgewell/trace-mapping': 0.3.18 288 | dev: true 289 | 290 | /@jridgewell/resolve-uri@3.1.0: 291 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 292 | engines: {node: '>=6.0.0'} 293 | dev: true 294 | 295 | /@jridgewell/set-array@1.1.2: 296 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 297 | engines: {node: '>=6.0.0'} 298 | dev: true 299 | 300 | /@jridgewell/sourcemap-codec@1.4.14: 301 | resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} 302 | dev: true 303 | 304 | /@jridgewell/sourcemap-codec@1.4.15: 305 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 306 | 307 | /@jridgewell/trace-mapping@0.3.18: 308 | resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} 309 | dependencies: 310 | '@jridgewell/resolve-uri': 3.1.0 311 | '@jridgewell/sourcemap-codec': 1.4.14 312 | dev: true 313 | 314 | /@nodelib/fs.scandir@2.1.5: 315 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 316 | engines: {node: '>= 8'} 317 | dependencies: 318 | '@nodelib/fs.stat': 2.0.5 319 | run-parallel: 1.2.0 320 | dev: true 321 | 322 | /@nodelib/fs.stat@2.0.5: 323 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 324 | engines: {node: '>= 8'} 325 | dev: true 326 | 327 | /@nodelib/fs.walk@1.2.8: 328 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 329 | engines: {node: '>= 8'} 330 | dependencies: 331 | '@nodelib/fs.scandir': 2.1.5 332 | fastq: 1.15.0 333 | dev: true 334 | 335 | /@tsconfig/node18@2.0.1: 336 | resolution: {integrity: sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==} 337 | dev: true 338 | 339 | /@types/node@18.16.17: 340 | resolution: {integrity: sha512-QAkjjRA1N7gPJeAP4WLXZtYv6+eMXFNviqktCDt4GLcmCugMr5BcRHfkOjCQzvCsnMp+L79a54zBkbw356xv9Q==} 341 | dev: true 342 | 343 | /@vitejs/plugin-vue@4.2.3(vite@4.3.9)(vue@3.3.4): 344 | resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==} 345 | engines: {node: ^14.18.0 || >=16.0.0} 346 | peerDependencies: 347 | vite: ^4.0.0 348 | vue: ^3.2.25 349 | dependencies: 350 | vite: 4.3.9(@types/node@18.16.17) 351 | vue: 3.3.4 352 | dev: true 353 | 354 | /@volar/language-core@1.4.1: 355 | resolution: {integrity: sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==} 356 | dependencies: 357 | '@volar/source-map': 1.4.1 358 | dev: true 359 | 360 | /@volar/source-map@1.4.1: 361 | resolution: {integrity: sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==} 362 | dependencies: 363 | muggle-string: 0.2.2 364 | dev: true 365 | 366 | /@volar/typescript@1.4.1-patch.2(typescript@5.0.4): 367 | resolution: {integrity: sha512-lPFYaGt8OdMEzNGJJChF40uYqMO4Z/7Q9fHPQC/NRVtht43KotSXLrkPandVVMf9aPbiJ059eAT+fwHGX16k4w==} 368 | peerDependencies: 369 | typescript: '*' 370 | dependencies: 371 | '@volar/language-core': 1.4.1 372 | typescript: 5.0.4 373 | dev: true 374 | 375 | /@volar/vue-language-core@1.6.5: 376 | resolution: {integrity: sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==} 377 | dependencies: 378 | '@volar/language-core': 1.4.1 379 | '@volar/source-map': 1.4.1 380 | '@vue/compiler-dom': 3.3.4 381 | '@vue/compiler-sfc': 3.3.4 382 | '@vue/reactivity': 3.3.4 383 | '@vue/shared': 3.3.4 384 | minimatch: 9.0.3 385 | muggle-string: 0.2.2 386 | vue-template-compiler: 2.7.14 387 | dev: true 388 | 389 | /@volar/vue-typescript@1.6.5(typescript@5.0.4): 390 | resolution: {integrity: sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==} 391 | peerDependencies: 392 | typescript: '*' 393 | dependencies: 394 | '@volar/typescript': 1.4.1-patch.2(typescript@5.0.4) 395 | '@volar/vue-language-core': 1.6.5 396 | typescript: 5.0.4 397 | dev: true 398 | 399 | /@vue/compiler-core@3.3.4: 400 | resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} 401 | dependencies: 402 | '@babel/parser': 7.22.7 403 | '@vue/shared': 3.3.4 404 | estree-walker: 2.0.2 405 | source-map-js: 1.0.2 406 | 407 | /@vue/compiler-dom@3.3.4: 408 | resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} 409 | dependencies: 410 | '@vue/compiler-core': 3.3.4 411 | '@vue/shared': 3.3.4 412 | 413 | /@vue/compiler-sfc@3.3.4: 414 | resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} 415 | dependencies: 416 | '@babel/parser': 7.22.7 417 | '@vue/compiler-core': 3.3.4 418 | '@vue/compiler-dom': 3.3.4 419 | '@vue/compiler-ssr': 3.3.4 420 | '@vue/reactivity-transform': 3.3.4 421 | '@vue/shared': 3.3.4 422 | estree-walker: 2.0.2 423 | magic-string: 0.30.1 424 | postcss: 8.4.25 425 | source-map-js: 1.0.2 426 | 427 | /@vue/compiler-ssr@3.3.4: 428 | resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} 429 | dependencies: 430 | '@vue/compiler-dom': 3.3.4 431 | '@vue/shared': 3.3.4 432 | 433 | /@vue/reactivity-transform@3.3.4: 434 | resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} 435 | dependencies: 436 | '@babel/parser': 7.22.7 437 | '@vue/compiler-core': 3.3.4 438 | '@vue/shared': 3.3.4 439 | estree-walker: 2.0.2 440 | magic-string: 0.30.1 441 | 442 | /@vue/reactivity@3.3.4: 443 | resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} 444 | dependencies: 445 | '@vue/shared': 3.3.4 446 | 447 | /@vue/runtime-core@3.3.4: 448 | resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} 449 | dependencies: 450 | '@vue/reactivity': 3.3.4 451 | '@vue/shared': 3.3.4 452 | 453 | /@vue/runtime-dom@3.3.4: 454 | resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} 455 | dependencies: 456 | '@vue/runtime-core': 3.3.4 457 | '@vue/shared': 3.3.4 458 | csstype: 3.1.2 459 | 460 | /@vue/server-renderer@3.3.4(vue@3.3.4): 461 | resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} 462 | peerDependencies: 463 | vue: 3.3.4 464 | dependencies: 465 | '@vue/compiler-ssr': 3.3.4 466 | '@vue/shared': 3.3.4 467 | vue: 3.3.4 468 | 469 | /@vue/shared@3.3.4: 470 | resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} 471 | 472 | /@vue/tsconfig@0.4.0: 473 | resolution: {integrity: sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==} 474 | dev: true 475 | 476 | /ansi-styles@3.2.1: 477 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 478 | engines: {node: '>=4'} 479 | dependencies: 480 | color-convert: 1.9.3 481 | dev: true 482 | 483 | /any-promise@1.3.0: 484 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 485 | dev: true 486 | 487 | /anymatch@3.1.3: 488 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 489 | engines: {node: '>= 8'} 490 | dependencies: 491 | normalize-path: 3.0.0 492 | picomatch: 2.3.1 493 | dev: true 494 | 495 | /arg@5.0.2: 496 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 497 | dev: true 498 | 499 | /array-buffer-byte-length@1.0.0: 500 | resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} 501 | dependencies: 502 | call-bind: 1.0.2 503 | is-array-buffer: 3.0.2 504 | dev: true 505 | 506 | /autoprefixer@10.4.14(postcss@8.4.25): 507 | resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} 508 | engines: {node: ^10 || ^12 || >=14} 509 | hasBin: true 510 | peerDependencies: 511 | postcss: ^8.1.0 512 | dependencies: 513 | browserslist: 4.21.9 514 | caniuse-lite: 1.0.30001514 515 | fraction.js: 4.2.0 516 | normalize-range: 0.1.2 517 | picocolors: 1.0.0 518 | postcss: 8.4.25 519 | postcss-value-parser: 4.2.0 520 | dev: true 521 | 522 | /available-typed-arrays@1.0.5: 523 | resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} 524 | engines: {node: '>= 0.4'} 525 | dev: true 526 | 527 | /balanced-match@1.0.2: 528 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 529 | dev: true 530 | 531 | /binary-extensions@2.2.0: 532 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 533 | engines: {node: '>=8'} 534 | dev: true 535 | 536 | /brace-expansion@1.1.11: 537 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 538 | dependencies: 539 | balanced-match: 1.0.2 540 | concat-map: 0.0.1 541 | dev: true 542 | 543 | /brace-expansion@2.0.1: 544 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 545 | dependencies: 546 | balanced-match: 1.0.2 547 | dev: true 548 | 549 | /braces@3.0.2: 550 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 551 | engines: {node: '>=8'} 552 | dependencies: 553 | fill-range: 7.0.1 554 | dev: true 555 | 556 | /browserslist@4.21.9: 557 | resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} 558 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 559 | hasBin: true 560 | dependencies: 561 | caniuse-lite: 1.0.30001514 562 | electron-to-chromium: 1.4.454 563 | node-releases: 2.0.13 564 | update-browserslist-db: 1.0.11(browserslist@4.21.9) 565 | dev: true 566 | 567 | /call-bind@1.0.2: 568 | resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} 569 | dependencies: 570 | function-bind: 1.1.1 571 | get-intrinsic: 1.2.1 572 | dev: true 573 | 574 | /camelcase-css@2.0.1: 575 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 576 | engines: {node: '>= 6'} 577 | dev: true 578 | 579 | /caniuse-lite@1.0.30001514: 580 | resolution: {integrity: sha512-ENcIpYBmwAAOm/V2cXgM7rZUrKKaqisZl4ZAI520FIkqGXUxJjmaIssbRW5HVVR5tyV6ygTLIm15aU8LUmQSaQ==} 581 | dev: true 582 | 583 | /chalk@2.4.2: 584 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 585 | engines: {node: '>=4'} 586 | dependencies: 587 | ansi-styles: 3.2.1 588 | escape-string-regexp: 1.0.5 589 | supports-color: 5.5.0 590 | dev: true 591 | 592 | /chokidar@3.5.3: 593 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 594 | engines: {node: '>= 8.10.0'} 595 | dependencies: 596 | anymatch: 3.1.3 597 | braces: 3.0.2 598 | glob-parent: 5.1.2 599 | is-binary-path: 2.1.0 600 | is-glob: 4.0.3 601 | normalize-path: 3.0.0 602 | readdirp: 3.6.0 603 | optionalDependencies: 604 | fsevents: 2.3.2 605 | dev: true 606 | 607 | /color-convert@1.9.3: 608 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 609 | dependencies: 610 | color-name: 1.1.3 611 | dev: true 612 | 613 | /color-name@1.1.3: 614 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 615 | dev: true 616 | 617 | /colord@2.9.3: 618 | resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} 619 | dev: true 620 | 621 | /commander@4.1.1: 622 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 623 | engines: {node: '>= 6'} 624 | dev: true 625 | 626 | /concat-map@0.0.1: 627 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 628 | dev: true 629 | 630 | /cross-spawn@6.0.5: 631 | resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} 632 | engines: {node: '>=4.8'} 633 | dependencies: 634 | nice-try: 1.0.5 635 | path-key: 2.0.1 636 | semver: 5.7.1 637 | shebang-command: 1.2.0 638 | which: 1.3.1 639 | dev: true 640 | 641 | /css-selector-tokenizer@0.8.0: 642 | resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} 643 | dependencies: 644 | cssesc: 3.0.0 645 | fastparse: 1.1.2 646 | dev: true 647 | 648 | /cssesc@3.0.0: 649 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 650 | engines: {node: '>=4'} 651 | hasBin: true 652 | dev: true 653 | 654 | /csstype@3.1.2: 655 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 656 | 657 | /daisyui@3.2.1: 658 | resolution: {integrity: sha512-gIqE6wiqoJt9G8+n3R/SwLeUnpNCE2eDhT73rP0yZYVaM7o6zVcakBH3aEW5RGpx3UkonPiLuvcgxRcb2lE8TA==} 659 | engines: {node: '>=16.9.0'} 660 | dependencies: 661 | colord: 2.9.3 662 | css-selector-tokenizer: 0.8.0 663 | postcss: 8.4.25 664 | postcss-js: 4.0.1(postcss@8.4.25) 665 | tailwindcss: 3.3.2 666 | transitivePeerDependencies: 667 | - ts-node 668 | dev: true 669 | 670 | /de-indent@1.0.2: 671 | resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} 672 | dev: true 673 | 674 | /define-properties@1.2.0: 675 | resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} 676 | engines: {node: '>= 0.4'} 677 | dependencies: 678 | has-property-descriptors: 1.0.0 679 | object-keys: 1.1.1 680 | dev: true 681 | 682 | /didyoumean@1.2.2: 683 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 684 | dev: true 685 | 686 | /dlv@1.1.3: 687 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 688 | dev: true 689 | 690 | /electron-to-chromium@1.4.454: 691 | resolution: {integrity: sha512-pmf1rbAStw8UEQ0sr2cdJtWl48ZMuPD9Sto8HVQOq9vx9j2WgDEN6lYoaqFvqEHYOmGA9oRGn7LqWI9ta0YugQ==} 692 | dev: true 693 | 694 | /error-ex@1.3.2: 695 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 696 | dependencies: 697 | is-arrayish: 0.2.1 698 | dev: true 699 | 700 | /es-abstract@1.21.2: 701 | resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} 702 | engines: {node: '>= 0.4'} 703 | dependencies: 704 | array-buffer-byte-length: 1.0.0 705 | available-typed-arrays: 1.0.5 706 | call-bind: 1.0.2 707 | es-set-tostringtag: 2.0.1 708 | es-to-primitive: 1.2.1 709 | function.prototype.name: 1.1.5 710 | get-intrinsic: 1.2.1 711 | get-symbol-description: 1.0.0 712 | globalthis: 1.0.3 713 | gopd: 1.0.1 714 | has: 1.0.3 715 | has-property-descriptors: 1.0.0 716 | has-proto: 1.0.1 717 | has-symbols: 1.0.3 718 | internal-slot: 1.0.5 719 | is-array-buffer: 3.0.2 720 | is-callable: 1.2.7 721 | is-negative-zero: 2.0.2 722 | is-regex: 1.1.4 723 | is-shared-array-buffer: 1.0.2 724 | is-string: 1.0.7 725 | is-typed-array: 1.1.10 726 | is-weakref: 1.0.2 727 | object-inspect: 1.12.3 728 | object-keys: 1.1.1 729 | object.assign: 4.1.4 730 | regexp.prototype.flags: 1.5.0 731 | safe-regex-test: 1.0.0 732 | string.prototype.trim: 1.2.7 733 | string.prototype.trimend: 1.0.6 734 | string.prototype.trimstart: 1.0.6 735 | typed-array-length: 1.0.4 736 | unbox-primitive: 1.0.2 737 | which-typed-array: 1.1.9 738 | dev: true 739 | 740 | /es-set-tostringtag@2.0.1: 741 | resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} 742 | engines: {node: '>= 0.4'} 743 | dependencies: 744 | get-intrinsic: 1.2.1 745 | has: 1.0.3 746 | has-tostringtag: 1.0.0 747 | dev: true 748 | 749 | /es-to-primitive@1.2.1: 750 | resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} 751 | engines: {node: '>= 0.4'} 752 | dependencies: 753 | is-callable: 1.2.7 754 | is-date-object: 1.0.5 755 | is-symbol: 1.0.4 756 | dev: true 757 | 758 | /esbuild@0.17.19: 759 | resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 760 | engines: {node: '>=12'} 761 | hasBin: true 762 | requiresBuild: true 763 | optionalDependencies: 764 | '@esbuild/android-arm': 0.17.19 765 | '@esbuild/android-arm64': 0.17.19 766 | '@esbuild/android-x64': 0.17.19 767 | '@esbuild/darwin-arm64': 0.17.19 768 | '@esbuild/darwin-x64': 0.17.19 769 | '@esbuild/freebsd-arm64': 0.17.19 770 | '@esbuild/freebsd-x64': 0.17.19 771 | '@esbuild/linux-arm': 0.17.19 772 | '@esbuild/linux-arm64': 0.17.19 773 | '@esbuild/linux-ia32': 0.17.19 774 | '@esbuild/linux-loong64': 0.17.19 775 | '@esbuild/linux-mips64el': 0.17.19 776 | '@esbuild/linux-ppc64': 0.17.19 777 | '@esbuild/linux-riscv64': 0.17.19 778 | '@esbuild/linux-s390x': 0.17.19 779 | '@esbuild/linux-x64': 0.17.19 780 | '@esbuild/netbsd-x64': 0.17.19 781 | '@esbuild/openbsd-x64': 0.17.19 782 | '@esbuild/sunos-x64': 0.17.19 783 | '@esbuild/win32-arm64': 0.17.19 784 | '@esbuild/win32-ia32': 0.17.19 785 | '@esbuild/win32-x64': 0.17.19 786 | dev: true 787 | 788 | /escalade@3.1.1: 789 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 790 | engines: {node: '>=6'} 791 | dev: true 792 | 793 | /escape-string-regexp@1.0.5: 794 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 795 | engines: {node: '>=0.8.0'} 796 | dev: true 797 | 798 | /estree-walker@2.0.2: 799 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 800 | 801 | /fast-glob@3.3.0: 802 | resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} 803 | engines: {node: '>=8.6.0'} 804 | dependencies: 805 | '@nodelib/fs.stat': 2.0.5 806 | '@nodelib/fs.walk': 1.2.8 807 | glob-parent: 5.1.2 808 | merge2: 1.4.1 809 | micromatch: 4.0.5 810 | dev: true 811 | 812 | /fastparse@1.1.2: 813 | resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==} 814 | dev: true 815 | 816 | /fastq@1.15.0: 817 | resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} 818 | dependencies: 819 | reusify: 1.0.4 820 | dev: true 821 | 822 | /fill-range@7.0.1: 823 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 824 | engines: {node: '>=8'} 825 | dependencies: 826 | to-regex-range: 5.0.1 827 | dev: true 828 | 829 | /for-each@0.3.3: 830 | resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} 831 | dependencies: 832 | is-callable: 1.2.7 833 | dev: true 834 | 835 | /fraction.js@4.2.0: 836 | resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} 837 | dev: true 838 | 839 | /fs.realpath@1.0.0: 840 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 841 | dev: true 842 | 843 | /fsevents@2.3.2: 844 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 845 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 846 | os: [darwin] 847 | requiresBuild: true 848 | dev: true 849 | optional: true 850 | 851 | /function-bind@1.1.1: 852 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 853 | dev: true 854 | 855 | /function.prototype.name@1.1.5: 856 | resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} 857 | engines: {node: '>= 0.4'} 858 | dependencies: 859 | call-bind: 1.0.2 860 | define-properties: 1.2.0 861 | es-abstract: 1.21.2 862 | functions-have-names: 1.2.3 863 | dev: true 864 | 865 | /functions-have-names@1.2.3: 866 | resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} 867 | dev: true 868 | 869 | /get-intrinsic@1.2.1: 870 | resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} 871 | dependencies: 872 | function-bind: 1.1.1 873 | has: 1.0.3 874 | has-proto: 1.0.1 875 | has-symbols: 1.0.3 876 | dev: true 877 | 878 | /get-symbol-description@1.0.0: 879 | resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} 880 | engines: {node: '>= 0.4'} 881 | dependencies: 882 | call-bind: 1.0.2 883 | get-intrinsic: 1.2.1 884 | dev: true 885 | 886 | /glob-parent@5.1.2: 887 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 888 | engines: {node: '>= 6'} 889 | dependencies: 890 | is-glob: 4.0.3 891 | dev: true 892 | 893 | /glob-parent@6.0.2: 894 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 895 | engines: {node: '>=10.13.0'} 896 | dependencies: 897 | is-glob: 4.0.3 898 | dev: true 899 | 900 | /glob@7.1.6: 901 | resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} 902 | dependencies: 903 | fs.realpath: 1.0.0 904 | inflight: 1.0.6 905 | inherits: 2.0.4 906 | minimatch: 3.1.2 907 | once: 1.4.0 908 | path-is-absolute: 1.0.1 909 | dev: true 910 | 911 | /globalthis@1.0.3: 912 | resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} 913 | engines: {node: '>= 0.4'} 914 | dependencies: 915 | define-properties: 1.2.0 916 | dev: true 917 | 918 | /gopd@1.0.1: 919 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 920 | dependencies: 921 | get-intrinsic: 1.2.1 922 | dev: true 923 | 924 | /graceful-fs@4.2.11: 925 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 926 | dev: true 927 | 928 | /has-bigints@1.0.2: 929 | resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} 930 | dev: true 931 | 932 | /has-flag@3.0.0: 933 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 934 | engines: {node: '>=4'} 935 | dev: true 936 | 937 | /has-property-descriptors@1.0.0: 938 | resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} 939 | dependencies: 940 | get-intrinsic: 1.2.1 941 | dev: true 942 | 943 | /has-proto@1.0.1: 944 | resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} 945 | engines: {node: '>= 0.4'} 946 | dev: true 947 | 948 | /has-symbols@1.0.3: 949 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 950 | engines: {node: '>= 0.4'} 951 | dev: true 952 | 953 | /has-tostringtag@1.0.0: 954 | resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} 955 | engines: {node: '>= 0.4'} 956 | dependencies: 957 | has-symbols: 1.0.3 958 | dev: true 959 | 960 | /has@1.0.3: 961 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 962 | engines: {node: '>= 0.4.0'} 963 | dependencies: 964 | function-bind: 1.1.1 965 | dev: true 966 | 967 | /he@1.2.0: 968 | resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 969 | hasBin: true 970 | dev: true 971 | 972 | /hosted-git-info@2.8.9: 973 | resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} 974 | dev: true 975 | 976 | /inflight@1.0.6: 977 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 978 | dependencies: 979 | once: 1.4.0 980 | wrappy: 1.0.2 981 | dev: true 982 | 983 | /inherits@2.0.4: 984 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 985 | dev: true 986 | 987 | /internal-slot@1.0.5: 988 | resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} 989 | engines: {node: '>= 0.4'} 990 | dependencies: 991 | get-intrinsic: 1.2.1 992 | has: 1.0.3 993 | side-channel: 1.0.4 994 | dev: true 995 | 996 | /is-array-buffer@3.0.2: 997 | resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} 998 | dependencies: 999 | call-bind: 1.0.2 1000 | get-intrinsic: 1.2.1 1001 | is-typed-array: 1.1.10 1002 | dev: true 1003 | 1004 | /is-arrayish@0.2.1: 1005 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 1006 | dev: true 1007 | 1008 | /is-bigint@1.0.4: 1009 | resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} 1010 | dependencies: 1011 | has-bigints: 1.0.2 1012 | dev: true 1013 | 1014 | /is-binary-path@2.1.0: 1015 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 1016 | engines: {node: '>=8'} 1017 | dependencies: 1018 | binary-extensions: 2.2.0 1019 | dev: true 1020 | 1021 | /is-boolean-object@1.1.2: 1022 | resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} 1023 | engines: {node: '>= 0.4'} 1024 | dependencies: 1025 | call-bind: 1.0.2 1026 | has-tostringtag: 1.0.0 1027 | dev: true 1028 | 1029 | /is-callable@1.2.7: 1030 | resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} 1031 | engines: {node: '>= 0.4'} 1032 | dev: true 1033 | 1034 | /is-core-module@2.12.1: 1035 | resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} 1036 | dependencies: 1037 | has: 1.0.3 1038 | dev: true 1039 | 1040 | /is-date-object@1.0.5: 1041 | resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} 1042 | engines: {node: '>= 0.4'} 1043 | dependencies: 1044 | has-tostringtag: 1.0.0 1045 | dev: true 1046 | 1047 | /is-extglob@2.1.1: 1048 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 1049 | engines: {node: '>=0.10.0'} 1050 | dev: true 1051 | 1052 | /is-glob@4.0.3: 1053 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 1054 | engines: {node: '>=0.10.0'} 1055 | dependencies: 1056 | is-extglob: 2.1.1 1057 | dev: true 1058 | 1059 | /is-negative-zero@2.0.2: 1060 | resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} 1061 | engines: {node: '>= 0.4'} 1062 | dev: true 1063 | 1064 | /is-number-object@1.0.7: 1065 | resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} 1066 | engines: {node: '>= 0.4'} 1067 | dependencies: 1068 | has-tostringtag: 1.0.0 1069 | dev: true 1070 | 1071 | /is-number@7.0.0: 1072 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 1073 | engines: {node: '>=0.12.0'} 1074 | dev: true 1075 | 1076 | /is-regex@1.1.4: 1077 | resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} 1078 | engines: {node: '>= 0.4'} 1079 | dependencies: 1080 | call-bind: 1.0.2 1081 | has-tostringtag: 1.0.0 1082 | dev: true 1083 | 1084 | /is-shared-array-buffer@1.0.2: 1085 | resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} 1086 | dependencies: 1087 | call-bind: 1.0.2 1088 | dev: true 1089 | 1090 | /is-string@1.0.7: 1091 | resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} 1092 | engines: {node: '>= 0.4'} 1093 | dependencies: 1094 | has-tostringtag: 1.0.0 1095 | dev: true 1096 | 1097 | /is-symbol@1.0.4: 1098 | resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} 1099 | engines: {node: '>= 0.4'} 1100 | dependencies: 1101 | has-symbols: 1.0.3 1102 | dev: true 1103 | 1104 | /is-typed-array@1.1.10: 1105 | resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} 1106 | engines: {node: '>= 0.4'} 1107 | dependencies: 1108 | available-typed-arrays: 1.0.5 1109 | call-bind: 1.0.2 1110 | for-each: 0.3.3 1111 | gopd: 1.0.1 1112 | has-tostringtag: 1.0.0 1113 | dev: true 1114 | 1115 | /is-weakref@1.0.2: 1116 | resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} 1117 | dependencies: 1118 | call-bind: 1.0.2 1119 | dev: true 1120 | 1121 | /isexe@2.0.0: 1122 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1123 | dev: true 1124 | 1125 | /jiti@1.19.1: 1126 | resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==} 1127 | hasBin: true 1128 | dev: true 1129 | 1130 | /json-parse-better-errors@1.0.2: 1131 | resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} 1132 | dev: true 1133 | 1134 | /lilconfig@2.1.0: 1135 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 1136 | engines: {node: '>=10'} 1137 | dev: true 1138 | 1139 | /lines-and-columns@1.2.4: 1140 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 1141 | dev: true 1142 | 1143 | /load-json-file@4.0.0: 1144 | resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} 1145 | engines: {node: '>=4'} 1146 | dependencies: 1147 | graceful-fs: 4.2.11 1148 | parse-json: 4.0.0 1149 | pify: 3.0.0 1150 | strip-bom: 3.0.0 1151 | dev: true 1152 | 1153 | /lru-cache@6.0.0: 1154 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 1155 | engines: {node: '>=10'} 1156 | dependencies: 1157 | yallist: 4.0.0 1158 | dev: true 1159 | 1160 | /magic-string@0.30.1: 1161 | resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} 1162 | engines: {node: '>=12'} 1163 | dependencies: 1164 | '@jridgewell/sourcemap-codec': 1.4.15 1165 | 1166 | /memorystream@0.3.1: 1167 | resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} 1168 | engines: {node: '>= 0.10.0'} 1169 | dev: true 1170 | 1171 | /merge2@1.4.1: 1172 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 1173 | engines: {node: '>= 8'} 1174 | dev: true 1175 | 1176 | /micromatch@4.0.5: 1177 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 1178 | engines: {node: '>=8.6'} 1179 | dependencies: 1180 | braces: 3.0.2 1181 | picomatch: 2.3.1 1182 | dev: true 1183 | 1184 | /minimatch@3.1.2: 1185 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1186 | dependencies: 1187 | brace-expansion: 1.1.11 1188 | dev: true 1189 | 1190 | /minimatch@9.0.3: 1191 | resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 1192 | engines: {node: '>=16 || 14 >=14.17'} 1193 | dependencies: 1194 | brace-expansion: 2.0.1 1195 | dev: true 1196 | 1197 | /muggle-string@0.2.2: 1198 | resolution: {integrity: sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==} 1199 | dev: true 1200 | 1201 | /mz@2.7.0: 1202 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 1203 | dependencies: 1204 | any-promise: 1.3.0 1205 | object-assign: 4.1.1 1206 | thenify-all: 1.6.0 1207 | dev: true 1208 | 1209 | /nanoid@3.3.6: 1210 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 1211 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1212 | hasBin: true 1213 | 1214 | /nice-try@1.0.5: 1215 | resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} 1216 | dev: true 1217 | 1218 | /node-releases@2.0.13: 1219 | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} 1220 | dev: true 1221 | 1222 | /normalize-package-data@2.5.0: 1223 | resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} 1224 | dependencies: 1225 | hosted-git-info: 2.8.9 1226 | resolve: 1.22.2 1227 | semver: 5.7.1 1228 | validate-npm-package-license: 3.0.4 1229 | dev: true 1230 | 1231 | /normalize-path@3.0.0: 1232 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1233 | engines: {node: '>=0.10.0'} 1234 | dev: true 1235 | 1236 | /normalize-range@0.1.2: 1237 | resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 1238 | engines: {node: '>=0.10.0'} 1239 | dev: true 1240 | 1241 | /npm-run-all@4.1.5: 1242 | resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} 1243 | engines: {node: '>= 4'} 1244 | hasBin: true 1245 | dependencies: 1246 | ansi-styles: 3.2.1 1247 | chalk: 2.4.2 1248 | cross-spawn: 6.0.5 1249 | memorystream: 0.3.1 1250 | minimatch: 3.1.2 1251 | pidtree: 0.3.1 1252 | read-pkg: 3.0.0 1253 | shell-quote: 1.8.1 1254 | string.prototype.padend: 3.1.4 1255 | dev: true 1256 | 1257 | /object-assign@4.1.1: 1258 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1259 | engines: {node: '>=0.10.0'} 1260 | dev: true 1261 | 1262 | /object-hash@3.0.0: 1263 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 1264 | engines: {node: '>= 6'} 1265 | dev: true 1266 | 1267 | /object-inspect@1.12.3: 1268 | resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} 1269 | dev: true 1270 | 1271 | /object-keys@1.1.1: 1272 | resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} 1273 | engines: {node: '>= 0.4'} 1274 | dev: true 1275 | 1276 | /object.assign@4.1.4: 1277 | resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} 1278 | engines: {node: '>= 0.4'} 1279 | dependencies: 1280 | call-bind: 1.0.2 1281 | define-properties: 1.2.0 1282 | has-symbols: 1.0.3 1283 | object-keys: 1.1.1 1284 | dev: true 1285 | 1286 | /once@1.4.0: 1287 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1288 | dependencies: 1289 | wrappy: 1.0.2 1290 | dev: true 1291 | 1292 | /parse-json@4.0.0: 1293 | resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} 1294 | engines: {node: '>=4'} 1295 | dependencies: 1296 | error-ex: 1.3.2 1297 | json-parse-better-errors: 1.0.2 1298 | dev: true 1299 | 1300 | /path-is-absolute@1.0.1: 1301 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1302 | engines: {node: '>=0.10.0'} 1303 | dev: true 1304 | 1305 | /path-key@2.0.1: 1306 | resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} 1307 | engines: {node: '>=4'} 1308 | dev: true 1309 | 1310 | /path-parse@1.0.7: 1311 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1312 | dev: true 1313 | 1314 | /path-type@3.0.0: 1315 | resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} 1316 | engines: {node: '>=4'} 1317 | dependencies: 1318 | pify: 3.0.0 1319 | dev: true 1320 | 1321 | /picocolors@1.0.0: 1322 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 1323 | 1324 | /picomatch@2.3.1: 1325 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1326 | engines: {node: '>=8.6'} 1327 | dev: true 1328 | 1329 | /pidtree@0.3.1: 1330 | resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} 1331 | engines: {node: '>=0.10'} 1332 | hasBin: true 1333 | dev: true 1334 | 1335 | /pify@2.3.0: 1336 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 1337 | engines: {node: '>=0.10.0'} 1338 | dev: true 1339 | 1340 | /pify@3.0.0: 1341 | resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} 1342 | engines: {node: '>=4'} 1343 | dev: true 1344 | 1345 | /pirates@4.0.6: 1346 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 1347 | engines: {node: '>= 6'} 1348 | dev: true 1349 | 1350 | /plotly.js-dist-min@2.24.3: 1351 | resolution: {integrity: sha512-NuX+/VaimP++uPlS4YdPOX/fg0ffOOvoaNmJenNcEu9fFWckCWxKkrknA5Jk7ZeweTEpzYTz6K1VGOstf8/PCw==} 1352 | dev: false 1353 | 1354 | /postcss-import@15.1.0(postcss@8.4.25): 1355 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 1356 | engines: {node: '>=14.0.0'} 1357 | peerDependencies: 1358 | postcss: ^8.0.0 1359 | dependencies: 1360 | postcss: 8.4.25 1361 | postcss-value-parser: 4.2.0 1362 | read-cache: 1.0.0 1363 | resolve: 1.22.2 1364 | dev: true 1365 | 1366 | /postcss-js@4.0.1(postcss@8.4.25): 1367 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 1368 | engines: {node: ^12 || ^14 || >= 16} 1369 | peerDependencies: 1370 | postcss: ^8.4.21 1371 | dependencies: 1372 | camelcase-css: 2.0.1 1373 | postcss: 8.4.25 1374 | dev: true 1375 | 1376 | /postcss-load-config@4.0.1(postcss@8.4.25): 1377 | resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} 1378 | engines: {node: '>= 14'} 1379 | peerDependencies: 1380 | postcss: '>=8.0.9' 1381 | ts-node: '>=9.0.0' 1382 | peerDependenciesMeta: 1383 | postcss: 1384 | optional: true 1385 | ts-node: 1386 | optional: true 1387 | dependencies: 1388 | lilconfig: 2.1.0 1389 | postcss: 8.4.25 1390 | yaml: 2.3.1 1391 | dev: true 1392 | 1393 | /postcss-nested@6.0.1(postcss@8.4.25): 1394 | resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} 1395 | engines: {node: '>=12.0'} 1396 | peerDependencies: 1397 | postcss: ^8.2.14 1398 | dependencies: 1399 | postcss: 8.4.25 1400 | postcss-selector-parser: 6.0.13 1401 | dev: true 1402 | 1403 | /postcss-selector-parser@6.0.13: 1404 | resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} 1405 | engines: {node: '>=4'} 1406 | dependencies: 1407 | cssesc: 3.0.0 1408 | util-deprecate: 1.0.2 1409 | dev: true 1410 | 1411 | /postcss-value-parser@4.2.0: 1412 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 1413 | dev: true 1414 | 1415 | /postcss@8.4.25: 1416 | resolution: {integrity: sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==} 1417 | engines: {node: ^10 || ^12 || >=14} 1418 | dependencies: 1419 | nanoid: 3.3.6 1420 | picocolors: 1.0.0 1421 | source-map-js: 1.0.2 1422 | 1423 | /queue-microtask@1.2.3: 1424 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1425 | dev: true 1426 | 1427 | /read-cache@1.0.0: 1428 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 1429 | dependencies: 1430 | pify: 2.3.0 1431 | dev: true 1432 | 1433 | /read-pkg@3.0.0: 1434 | resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} 1435 | engines: {node: '>=4'} 1436 | dependencies: 1437 | load-json-file: 4.0.0 1438 | normalize-package-data: 2.5.0 1439 | path-type: 3.0.0 1440 | dev: true 1441 | 1442 | /readdirp@3.6.0: 1443 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1444 | engines: {node: '>=8.10.0'} 1445 | dependencies: 1446 | picomatch: 2.3.1 1447 | dev: true 1448 | 1449 | /regexp.prototype.flags@1.5.0: 1450 | resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} 1451 | engines: {node: '>= 0.4'} 1452 | dependencies: 1453 | call-bind: 1.0.2 1454 | define-properties: 1.2.0 1455 | functions-have-names: 1.2.3 1456 | dev: true 1457 | 1458 | /resolve@1.22.2: 1459 | resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} 1460 | hasBin: true 1461 | dependencies: 1462 | is-core-module: 2.12.1 1463 | path-parse: 1.0.7 1464 | supports-preserve-symlinks-flag: 1.0.0 1465 | dev: true 1466 | 1467 | /reusify@1.0.4: 1468 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 1469 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1470 | dev: true 1471 | 1472 | /rollup@3.26.2: 1473 | resolution: {integrity: sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==} 1474 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 1475 | hasBin: true 1476 | optionalDependencies: 1477 | fsevents: 2.3.2 1478 | dev: true 1479 | 1480 | /run-parallel@1.2.0: 1481 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1482 | dependencies: 1483 | queue-microtask: 1.2.3 1484 | dev: true 1485 | 1486 | /safe-regex-test@1.0.0: 1487 | resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} 1488 | dependencies: 1489 | call-bind: 1.0.2 1490 | get-intrinsic: 1.2.1 1491 | is-regex: 1.1.4 1492 | dev: true 1493 | 1494 | /semver@5.7.1: 1495 | resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} 1496 | hasBin: true 1497 | dev: true 1498 | 1499 | /semver@7.5.4: 1500 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} 1501 | engines: {node: '>=10'} 1502 | hasBin: true 1503 | dependencies: 1504 | lru-cache: 6.0.0 1505 | dev: true 1506 | 1507 | /shebang-command@1.2.0: 1508 | resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} 1509 | engines: {node: '>=0.10.0'} 1510 | dependencies: 1511 | shebang-regex: 1.0.0 1512 | dev: true 1513 | 1514 | /shebang-regex@1.0.0: 1515 | resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} 1516 | engines: {node: '>=0.10.0'} 1517 | dev: true 1518 | 1519 | /shell-quote@1.8.1: 1520 | resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} 1521 | dev: true 1522 | 1523 | /side-channel@1.0.4: 1524 | resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} 1525 | dependencies: 1526 | call-bind: 1.0.2 1527 | get-intrinsic: 1.2.1 1528 | object-inspect: 1.12.3 1529 | dev: true 1530 | 1531 | /source-map-js@1.0.2: 1532 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 1533 | engines: {node: '>=0.10.0'} 1534 | 1535 | /spdx-correct@3.2.0: 1536 | resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} 1537 | dependencies: 1538 | spdx-expression-parse: 3.0.1 1539 | spdx-license-ids: 3.0.13 1540 | dev: true 1541 | 1542 | /spdx-exceptions@2.3.0: 1543 | resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} 1544 | dev: true 1545 | 1546 | /spdx-expression-parse@3.0.1: 1547 | resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} 1548 | dependencies: 1549 | spdx-exceptions: 2.3.0 1550 | spdx-license-ids: 3.0.13 1551 | dev: true 1552 | 1553 | /spdx-license-ids@3.0.13: 1554 | resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} 1555 | dev: true 1556 | 1557 | /string.prototype.padend@3.1.4: 1558 | resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} 1559 | engines: {node: '>= 0.4'} 1560 | dependencies: 1561 | call-bind: 1.0.2 1562 | define-properties: 1.2.0 1563 | es-abstract: 1.21.2 1564 | dev: true 1565 | 1566 | /string.prototype.trim@1.2.7: 1567 | resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} 1568 | engines: {node: '>= 0.4'} 1569 | dependencies: 1570 | call-bind: 1.0.2 1571 | define-properties: 1.2.0 1572 | es-abstract: 1.21.2 1573 | dev: true 1574 | 1575 | /string.prototype.trimend@1.0.6: 1576 | resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} 1577 | dependencies: 1578 | call-bind: 1.0.2 1579 | define-properties: 1.2.0 1580 | es-abstract: 1.21.2 1581 | dev: true 1582 | 1583 | /string.prototype.trimstart@1.0.6: 1584 | resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} 1585 | dependencies: 1586 | call-bind: 1.0.2 1587 | define-properties: 1.2.0 1588 | es-abstract: 1.21.2 1589 | dev: true 1590 | 1591 | /strip-bom@3.0.0: 1592 | resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 1593 | engines: {node: '>=4'} 1594 | dev: true 1595 | 1596 | /sucrase@3.32.0: 1597 | resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} 1598 | engines: {node: '>=8'} 1599 | hasBin: true 1600 | dependencies: 1601 | '@jridgewell/gen-mapping': 0.3.3 1602 | commander: 4.1.1 1603 | glob: 7.1.6 1604 | lines-and-columns: 1.2.4 1605 | mz: 2.7.0 1606 | pirates: 4.0.6 1607 | ts-interface-checker: 0.1.13 1608 | dev: true 1609 | 1610 | /supports-color@5.5.0: 1611 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 1612 | engines: {node: '>=4'} 1613 | dependencies: 1614 | has-flag: 3.0.0 1615 | dev: true 1616 | 1617 | /supports-preserve-symlinks-flag@1.0.0: 1618 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1619 | engines: {node: '>= 0.4'} 1620 | dev: true 1621 | 1622 | /tailwindcss@3.3.2: 1623 | resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==} 1624 | engines: {node: '>=14.0.0'} 1625 | hasBin: true 1626 | dependencies: 1627 | '@alloc/quick-lru': 5.2.0 1628 | arg: 5.0.2 1629 | chokidar: 3.5.3 1630 | didyoumean: 1.2.2 1631 | dlv: 1.1.3 1632 | fast-glob: 3.3.0 1633 | glob-parent: 6.0.2 1634 | is-glob: 4.0.3 1635 | jiti: 1.19.1 1636 | lilconfig: 2.1.0 1637 | micromatch: 4.0.5 1638 | normalize-path: 3.0.0 1639 | object-hash: 3.0.0 1640 | picocolors: 1.0.0 1641 | postcss: 8.4.25 1642 | postcss-import: 15.1.0(postcss@8.4.25) 1643 | postcss-js: 4.0.1(postcss@8.4.25) 1644 | postcss-load-config: 4.0.1(postcss@8.4.25) 1645 | postcss-nested: 6.0.1(postcss@8.4.25) 1646 | postcss-selector-parser: 6.0.13 1647 | postcss-value-parser: 4.2.0 1648 | resolve: 1.22.2 1649 | sucrase: 3.32.0 1650 | transitivePeerDependencies: 1651 | - ts-node 1652 | dev: true 1653 | 1654 | /thenify-all@1.6.0: 1655 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 1656 | engines: {node: '>=0.8'} 1657 | dependencies: 1658 | thenify: 3.3.1 1659 | dev: true 1660 | 1661 | /thenify@3.3.1: 1662 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1663 | dependencies: 1664 | any-promise: 1.3.0 1665 | dev: true 1666 | 1667 | /to-fast-properties@2.0.0: 1668 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 1669 | engines: {node: '>=4'} 1670 | 1671 | /to-regex-range@5.0.1: 1672 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1673 | engines: {node: '>=8.0'} 1674 | dependencies: 1675 | is-number: 7.0.0 1676 | dev: true 1677 | 1678 | /ts-interface-checker@0.1.13: 1679 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1680 | dev: true 1681 | 1682 | /typed-array-length@1.0.4: 1683 | resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} 1684 | dependencies: 1685 | call-bind: 1.0.2 1686 | for-each: 0.3.3 1687 | is-typed-array: 1.1.10 1688 | dev: true 1689 | 1690 | /typescript@5.0.4: 1691 | resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} 1692 | engines: {node: '>=12.20'} 1693 | hasBin: true 1694 | dev: true 1695 | 1696 | /unbox-primitive@1.0.2: 1697 | resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} 1698 | dependencies: 1699 | call-bind: 1.0.2 1700 | has-bigints: 1.0.2 1701 | has-symbols: 1.0.3 1702 | which-boxed-primitive: 1.0.2 1703 | dev: true 1704 | 1705 | /update-browserslist-db@1.0.11(browserslist@4.21.9): 1706 | resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} 1707 | hasBin: true 1708 | peerDependencies: 1709 | browserslist: '>= 4.21.0' 1710 | dependencies: 1711 | browserslist: 4.21.9 1712 | escalade: 3.1.1 1713 | picocolors: 1.0.0 1714 | dev: true 1715 | 1716 | /util-deprecate@1.0.2: 1717 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1718 | dev: true 1719 | 1720 | /validate-npm-package-license@3.0.4: 1721 | resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} 1722 | dependencies: 1723 | spdx-correct: 3.2.0 1724 | spdx-expression-parse: 3.0.1 1725 | dev: true 1726 | 1727 | /vite@4.3.9(@types/node@18.16.17): 1728 | resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} 1729 | engines: {node: ^14.18.0 || >=16.0.0} 1730 | hasBin: true 1731 | peerDependencies: 1732 | '@types/node': '>= 14' 1733 | less: '*' 1734 | sass: '*' 1735 | stylus: '*' 1736 | sugarss: '*' 1737 | terser: ^5.4.0 1738 | peerDependenciesMeta: 1739 | '@types/node': 1740 | optional: true 1741 | less: 1742 | optional: true 1743 | sass: 1744 | optional: true 1745 | stylus: 1746 | optional: true 1747 | sugarss: 1748 | optional: true 1749 | terser: 1750 | optional: true 1751 | dependencies: 1752 | '@types/node': 18.16.17 1753 | esbuild: 0.17.19 1754 | postcss: 8.4.25 1755 | rollup: 3.26.2 1756 | optionalDependencies: 1757 | fsevents: 2.3.2 1758 | dev: true 1759 | 1760 | /vue-template-compiler@2.7.14: 1761 | resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} 1762 | dependencies: 1763 | de-indent: 1.0.2 1764 | he: 1.2.0 1765 | dev: true 1766 | 1767 | /vue-tsc@1.6.5(typescript@5.0.4): 1768 | resolution: {integrity: sha512-Wtw3J7CC+JM2OR56huRd5iKlvFWpvDiU+fO1+rqyu4V2nMTotShz4zbOZpW5g9fUOcjnyZYfBo5q5q+D/q27JA==} 1769 | hasBin: true 1770 | peerDependencies: 1771 | typescript: '*' 1772 | dependencies: 1773 | '@volar/vue-language-core': 1.6.5 1774 | '@volar/vue-typescript': 1.6.5(typescript@5.0.4) 1775 | semver: 7.5.4 1776 | typescript: 5.0.4 1777 | dev: true 1778 | 1779 | /vue@3.3.4: 1780 | resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} 1781 | dependencies: 1782 | '@vue/compiler-dom': 3.3.4 1783 | '@vue/compiler-sfc': 3.3.4 1784 | '@vue/runtime-dom': 3.3.4 1785 | '@vue/server-renderer': 3.3.4(vue@3.3.4) 1786 | '@vue/shared': 3.3.4 1787 | 1788 | /which-boxed-primitive@1.0.2: 1789 | resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} 1790 | dependencies: 1791 | is-bigint: 1.0.4 1792 | is-boolean-object: 1.1.2 1793 | is-number-object: 1.0.7 1794 | is-string: 1.0.7 1795 | is-symbol: 1.0.4 1796 | dev: true 1797 | 1798 | /which-typed-array@1.1.9: 1799 | resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} 1800 | engines: {node: '>= 0.4'} 1801 | dependencies: 1802 | available-typed-arrays: 1.0.5 1803 | call-bind: 1.0.2 1804 | for-each: 0.3.3 1805 | gopd: 1.0.1 1806 | has-tostringtag: 1.0.0 1807 | is-typed-array: 1.1.10 1808 | dev: true 1809 | 1810 | /which@1.3.1: 1811 | resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} 1812 | hasBin: true 1813 | dependencies: 1814 | isexe: 2.0.0 1815 | dev: true 1816 | 1817 | /wrappy@1.0.2: 1818 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1819 | dev: true 1820 | 1821 | /yallist@4.0.0: 1822 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 1823 | dev: true 1824 | 1825 | /yaml@2.3.1: 1826 | resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} 1827 | engines: {node: '>= 14'} 1828 | dev: true 1829 | --------------------------------------------------------------------------------