├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENCE ├── README.md ├── craco.config.js ├── envs ├── bangdream.sh ├── btr.sh ├── deremas.sh ├── hetalia.sh ├── imas.sh ├── kancolle.sh ├── lovelive.sh ├── revengers.sh ├── touhou.sh ├── umamusume.sh └── vocalo.sh ├── package-lock.json ├── package.json ├── public ├── index.html ├── logo.ico ├── logo.png ├── logo.svg └── robots.txt ├── scraping ├── .prettierrc ├── index.ts ├── package.json ├── target_couplings │ ├── bangdream │ │ ├── getCouplings.js │ │ └── index.json │ ├── btr │ │ ├── getCouplings.js │ │ └── index.json │ ├── deremas │ │ ├── getCouplings.js │ │ └── index.json │ ├── hetalia │ │ ├── getCountriesAndCharacters.js │ │ ├── getCouplings.js │ │ └── index.json │ ├── imas │ │ ├── getCouplings.js │ │ └── index.json │ ├── kancolle │ │ ├── getCouplings.js │ │ └── index.json │ ├── lovelive │ │ ├── getCouplings.js │ │ └── index.json │ ├── revengers │ │ ├── getCouplings.js │ │ └── index.json │ ├── touhou │ │ ├── getCouplings.js │ │ └── index.json │ ├── umamusume │ │ ├── getCouplings.js │ │ └── index.json │ └── vocalo │ │ ├── getCouplings.js │ │ └── index.json └── tsconfig.json ├── scripts ├── deployall.sh ├── scrapingall.sh ├── setenv.sh └── watch-all-scraping-eta.sh ├── src ├── App.tsx ├── CharacterDialog │ ├── CharacterDialog.tsx │ └── recoil.ts ├── FilterSlider │ ├── FilterSlider.tsx │ └── recoil.ts ├── Graph │ ├── Graph.tsx │ ├── Link.tsx │ ├── Node.tsx │ ├── forceSimuration.ts │ ├── recoil.ts │ ├── types.ts │ └── utils.ts ├── Nav │ ├── Nav.tsx │ └── recoil.ts ├── PixivUtils │ └── PixivUtils.tsx ├── Ranking │ └── CouplingRanking.tsx ├── Resolver │ ├── Prioritizer.tsx │ ├── Resolver.tsx │ └── recoil.tsx ├── couplings.ts ├── index.js └── theme.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #### node #### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | TEMP_* 113 | src/couplings.json 114 | build/ 115 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "foxundermoon.shell-format", 5 | "styled-components.vscode-styled-components" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Takuya Shizukuishi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yurigraph 2 | 3 | いろいろな作品のカップリングを可視化します 4 | 5 | ![グラフのデモ画像。アイドルマスターシンデレラガールズのもの](https://user-images.githubusercontent.com/18525488/80941278-0e19f780-8e1d-11ea-915b-838ed7db5ff9.png) 6 | 7 | ## 対応作品 8 | 9 | - [アイドルマスター](https://sititou70.github.io/imasgraph/) 10 | - [アイドルマスターシンデレラガールズ](https://sititou70.github.io/deregraph/) 11 | - [東方 Project](https://sititou70.github.io/touhoumap/) 12 | - [艦隊これくしょん](https://sititou70.github.io/kancollegraph/) 13 | - [ラブライブ!シリーズ](https://sititou70.github.io/lovelivemap/) 14 | - [VOCALOID・VOICEROID](https://sititou70.github.io/vocalomap/) 15 | - [Axis Powers ヘタリア](https://sititou70.github.io/hetagraph/) 16 | - [BanG Dream!](https://sititou70.github.io/bangdreamgraph/) 17 | - [東京卍リベンジャーズ](https://sititou70.github.io/revengersmgraph/) 18 | - [ぼっち・ざ・ろっく!](https://sititou70.github.io/btrmap/) 19 | - [ウマ娘 プリティーダービー](https://sititou70.github.io/umamusumegraph/) 20 | 21 | ## 開発 22 | 23 | ### 依存 24 | 25 | - Node.js v18.12.1 26 | - npm v8.19.2 27 | 28 | ### セットアップ 29 | 30 | ```javascript 31 | cd yurigraph 32 | npm i 33 | ``` 34 | 35 | ### 開発用サーバーを起動 36 | 37 | ```javascript 38 | cd yurigraph 39 | npm start [revengers | touhou | bangdream | lovelive | vocalo | kancolle | deremas | imas | hetalia | btr | umamusume] 40 | ``` 41 | 42 | ### 対応作品を追加する 43 | 44 | 1. 作品のカップリングのリストを追加する必要があります.`yurigraph/scraping/target_couplings/[作品名]/index.json`にカップリングのリストを配置します.json の型やリストの作成方法に関しては,既存の作品のディレクトリを参照してください. 45 | 1. アプリの環境変数を設定する必要があります.`yurigraph/envs/`内に新しい作品の変数を設定するスクリプトを追加してください.詳しくは,`yurigraph/envs/`内にある他の作品のスクリプトを参照してください. 46 | 47 | ## 情報元 48 | 49 | 本アプリのカップリング情報は,[pixiv](https://www.pixiv.net/)のタグをもとに生成されています. 50 | 51 | ## ポリシー 52 | 53 | 本アプリに,他作品のコンテンツイメージを損なわせるような意図はありません. 54 | 55 | 本アプリは,[pixiv ガイドライン](https://www.pixiv.net/terms/?page=guideline)に準拠し,作品の収集等を行っていません. 56 | 57 | pixiv からタグ情報を取得する際には,リクエストごとに15秒以上のインターバルを設定しています.これにより,pixiv のサーバーへの極端な負荷を防止しています. 58 | 59 | 本アプリに問題がある場合は,本リポジトリの[issue](https://github.com/sititou70/yurigraph/issues)へご一報ください. 60 | 61 | ## Contributors ✨ 62 | 63 | 64 | 65 | 68 |

defaultcf 66 |

KobayashiTakaki 67 |
69 | 70 | ## License 71 | 72 | MIT 73 | 74 | ## さらに読む 75 | 76 | 本アプリを開発した経緯等に関しては以下の記事もご覧ください 77 | 78 | [百合オタクの脳内を可視化する,その名も yurigraph](https://sititou70.github.io/%E7%99%BE%E5%90%88%E3%82%AA%E3%82%BF%E3%82%AF%E3%81%AE%E8%84%B3%E5%86%85%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%99%E3%82%8B%EF%BC%8C%E3%81%9D%E3%81%AE%E5%90%8D%E3%82%82yurigraph/) 79 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babel: { 3 | presets: [ 4 | [ 5 | '@babel/preset-react', 6 | { runtime: 'automatic', importSource: '@emotion/react' }, 7 | ], 8 | ], 9 | plugins: ['@emotion/babel-plugin'], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /envs/bangdream.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/bangdream/index.json" 5 | export REACT_APP_APP_NAME="BangDreamGraph" 6 | export REACT_APP_CONTENT_NAME="BanG Dream!" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=100 8 | export REACT_APP_MAIN_COLOR="#5fc7d4" 9 | export REACT_APP_ACCENT_COLOR="#e50150" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/bangdreamgraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/btr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/btr/index.json" 5 | export REACT_APP_APP_NAME="BTRMap" 6 | export REACT_APP_CONTENT_NAME="ぼっち・ざ・ろっく!" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=150 8 | export REACT_APP_MAIN_COLOR="#fff000" 9 | export REACT_APP_ACCENT_COLOR="#ea608e" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/btrmap.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/deremas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/deremas/index.json" 5 | export REACT_APP_APP_NAME="DereGraph" 6 | export REACT_APP_CONTENT_NAME="アイドルマスターシンデレラガールズ" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=100 8 | export REACT_APP_MAIN_COLOR="#01baef" 9 | export REACT_APP_ACCENT_COLOR="#e32079" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/deregraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/hetalia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/hetalia/index.json" 5 | export REACT_APP_APP_NAME="HetaGraph" 6 | export REACT_APP_CONTENT_NAME="Axis Powers ヘタリア" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=900 8 | export REACT_APP_MAIN_COLOR="#22add6" 9 | export REACT_APP_ACCENT_COLOR="#fee502" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/hetagraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/imas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/imas/index.json" 5 | export REACT_APP_APP_NAME="ImasGraph" 6 | export REACT_APP_CONTENT_NAME="アイドルマスター" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=200 8 | export REACT_APP_MAIN_COLOR="#005693" 9 | export REACT_APP_ACCENT_COLOR="#ed246e" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/imasgraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/kancolle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/kancolle/index.json" 5 | export REACT_APP_APP_NAME="KancolleGraph" 6 | export REACT_APP_CONTENT_NAME="艦隊これくしょん" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=100 8 | export REACT_APP_MAIN_COLOR="#0779e4" 9 | export REACT_APP_ACCENT_COLOR="#f0c645" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/kancollegraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/lovelive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/lovelive/index.json" 5 | export REACT_APP_APP_NAME="LoveliveMap" 6 | export REACT_APP_CONTENT_NAME="ラブライブ!シリーズ" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=876 8 | export REACT_APP_MAIN_COLOR="#384685" 9 | export REACT_APP_ACCENT_COLOR="#e4007f" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/lovelivemap.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/revengers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/revengers/index.json" 5 | export REACT_APP_APP_NAME="RevengersGraph" 6 | export REACT_APP_CONTENT_NAME="東京卍リベンジャーズ" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=300 8 | export REACT_APP_MAIN_COLOR="#5fc7d4" 9 | export REACT_APP_ACCENT_COLOR="#e50150" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/revengersmgraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/touhou.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/touhou/index.json" 5 | export REACT_APP_APP_NAME="TouhouMap" 6 | export REACT_APP_CONTENT_NAME="東方Project" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=350 8 | # 天色 https://www.colordic.org/colorsample/2312 9 | export REACT_APP_MAIN_COLOR="#2ca9e1" 10 | # 紅 https://www.colordic.org/colorsample/2014 11 | export REACT_APP_ACCENT_COLOR="#d7003a" 12 | export DEPLOY_REPOSITORY="git@github.com:sititou70/touhoumap.git" 13 | export DEPLOY_BRANCH="gh-pages" 14 | -------------------------------------------------------------------------------- /envs/umamusume.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/umamusume/index.json" 5 | export REACT_APP_APP_NAME="UmaMusumeGraph" 6 | export REACT_APP_CONTENT_NAME="ウマ娘 プリティーダービー" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=100 8 | export REACT_APP_MAIN_COLOR="#309cfc" 9 | export REACT_APP_ACCENT_COLOR="#43a938" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/umamusumegraph.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /envs/vocalo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | export TARGET_COUPLINGS_JSON="scraping/target_couplings/vocalo/index.json" 5 | export REACT_APP_APP_NAME="VocaloMap" 6 | export REACT_APP_CONTENT_NAME="VOCALOID・VOICEROID" 7 | export REACT_APP_DEFAULT_FILTER_VALUE=291 8 | export REACT_APP_MAIN_COLOR="#3d9bab" 9 | export REACT_APP_ACCENT_COLOR="#b8396a" 10 | export DEPLOY_REPOSITORY="git@github.com:sititou70/vocalomap.git" 11 | export DEPLOY_BRANCH="gh-pages" 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yurigraph", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": ".", 6 | "dependencies": { 7 | "@craco/craco": "7.0.0-alpha.9", 8 | "@emotion/babel-plugin": "11.10.5", 9 | "@emotion/react": "11.10.5", 10 | "@emotion/styled": "11.10.5", 11 | "@mui/icons-material": "5.10.9", 12 | "@mui/material": "5.10.12", 13 | "@react-hook/window-size": "3.1.1", 14 | "@types/d3": "7.4.0", 15 | "@types/progress": "2.0.7", 16 | "@types/react": "18.0.25", 17 | "@types/stats-lite": "2.2.0", 18 | "awesome-sigmoid": "1.0.2", 19 | "d3": "7.6.1", 20 | "deep-copy": "1.4.2", 21 | "gh-pages": "4.0.0", 22 | "mix-color": "1.1.2", 23 | "node-fetch": "3.2.10", 24 | "normalize.css": "8.0.1", 25 | "npm-run-all": "4.1.5", 26 | "prettier-plugin-organize-imports": "3.1.1", 27 | "progress": "2.0.3", 28 | "react": "18.2.0", 29 | "react-dom": "18.2.0", 30 | "react-ga": "3.3.1", 31 | "react-scripts": "5.0.1", 32 | "recoil": "0.7.6", 33 | "sanitize-filename": "1.6.3", 34 | "stats-lite": "2.2.0", 35 | "ts-node": "10.9.1", 36 | "typescript": "4.8.4", 37 | "yurigraph-scraping": "file:scraping" 38 | }, 39 | "scripts": { 40 | "_scraping": "cd scraping && NODE_OPTIONS='--loader ts-node/esm' ts-node index.ts", 41 | "_start": "GENERATE_SOURCEMAP=false craco start", 42 | "_build": "GENERATE_SOURCEMAP=false craco build", 43 | "_deploy": "npm run _build && gh-pages -d build -r $DEPLOY_REPOSITORY -b $DEPLOY_BRANCH", 44 | "scraping": "EXEC='npm run _scraping' scripts/setenv.sh", 45 | "scrapingall": "cd scripts && ./scrapingall.sh", 46 | "start": "EXEC='run-p {_scraping,_start}' scripts/setenv.sh", 47 | "deploy": "EXEC='run-s {_scraping,_deploy}' scripts/setenv.sh", 48 | "deployall": "cd scripts && ./deployall.sh" 49 | }, 50 | "eslintConfig": { 51 | "extends": "react-app" 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all" 58 | ], 59 | "development": [ 60 | "last 1 chrome version", 61 | "last 1 firefox version", 62 | "last 1 safari version" 63 | ] 64 | }, 65 | "engines": { 66 | "node": "18.12.1", 67 | "npm": "8.19.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | 23 | カップリング可視化・ランキング - %REACT_APP_CONTENT_NAME% | 24 | %REACT_APP_APP_NAME% 25 | 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sititou70/yurigraph/b1dc5b6e81959e04c4f20cb74a9efe39391b742a/public/logo.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sititou70/yurigraph/b1dc5b6e81959e04c4f20cb74a9efe39391b742a/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 57 | 64 | 71 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /scraping/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /scraping/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import fetch from 'node-fetch'; 3 | import path from 'path'; 4 | import Progress from 'progress'; 5 | import sanitize from 'sanitize-filename'; 6 | 7 | // settings 8 | const FETCH_INTERVAL = 15000; 9 | const CACHE_DIR = 'TEMP_CACHE'; 10 | const INPUT_COUPLINGS_JSON = path.join( 11 | '..', 12 | process.env.TARGET_COUPLINGS_JSON as string 13 | ); 14 | const DEST_COUPLINGS_JSON = '../src/couplings.json'; 15 | 16 | // type 17 | export type Character = { 18 | name: string; // キャラクター名(例:博麗霊夢) 19 | dict_entry?: string; // Pixiv百科事典のエントリー名.キャラクター名とエントリー名が異なる場合に指定する.指定されない場合はキャラクター名がエントリー名とみなされる 20 | }; 21 | 22 | //// スクレイピング対象のカップリング 23 | export type TargetCoupling = { 24 | characters: [Character, Character]; 25 | tags: { 26 | name: string; // タグ名(例:ゆかれいむ) 27 | }[]; 28 | }; 29 | //// スクレイピング結果 30 | export type Coupling = { 31 | characters: [Character, Character]; 32 | tags: { 33 | name: string; 34 | num: number; // 作品数 35 | }[]; 36 | }; 37 | export type Couplings = Coupling[]; 38 | 39 | // utils 40 | const compTargetCouplings = (x: TargetCoupling, y: TargetCoupling): boolean => { 41 | if (x.characters.length !== 2) return false; 42 | if (y.characters.length !== 2) return false; 43 | 44 | if ( 45 | x.characters[0].name === y.characters[0].name && 46 | x.characters[1].name === y.characters[1].name 47 | ) 48 | return true; 49 | if ( 50 | x.characters[0].name === y.characters[1].name && 51 | x.characters[1].name === y.characters[0].name 52 | ) 53 | return true; 54 | 55 | return false; 56 | }; 57 | 58 | // main 59 | const main = async () => { 60 | // ensure cache directory 61 | try { 62 | fs.mkdirSync(CACHE_DIR); 63 | } catch (e) {} 64 | 65 | const couplings: TargetCoupling[] = groupingTargetCouplings( 66 | JSON.parse(fs.readFileSync(INPUT_COUPLINGS_JSON).toString()) 67 | ).filter((x) => !isSelfCoupling(x)); 68 | 69 | const tags_len = couplings.flatMap((coupling) => coupling.tags).length; 70 | const bar = new Progress('[:bar]\t:percent\t:rest_min min\t:tag_name\n', { 71 | complete: '=', 72 | incomplete: ' ', 73 | width: 50, 74 | total: tags_len, 75 | }); 76 | 77 | let dest_couplings: Couplings = []; 78 | for (const coupling of couplings) { 79 | let tags: Coupling['tags'] = []; 80 | for (const tag of coupling.tags) { 81 | tags.push({ name: tag.name, num: await getNumsFromTag(tag.name) }); 82 | 83 | bar.tick(1, { 84 | rest_min: ( 85 | Math.round( 86 | (((tags_len - bar.curr) * FETCH_INTERVAL) / 1000 / 60) * 100 87 | ) / 100 88 | ) 89 | .toString() 90 | .padEnd(5, ' '), 91 | tag_name: tag.name, 92 | }); 93 | } 94 | 95 | dest_couplings = [ 96 | ...dest_couplings, 97 | { 98 | ...coupling, 99 | tags, 100 | }, 101 | ]; 102 | } 103 | 104 | fs.writeFileSync(DEST_COUPLINGS_JSON, JSON.stringify(dest_couplings)); 105 | }; 106 | 107 | const groupingTargetCouplings = ( 108 | couplings: TargetCoupling[] 109 | ): TargetCoupling[] => { 110 | let lest_couplings = couplings; 111 | let grouped_couplings: TargetCoupling[] = []; 112 | 113 | do { 114 | let group: TargetCoupling[] = []; 115 | let pivot = lest_couplings[0]; 116 | 117 | let prev_lest_couplings = lest_couplings; 118 | lest_couplings = []; 119 | prev_lest_couplings.forEach((x) => { 120 | if (compTargetCouplings(pivot, x)) { 121 | group.push(x); 122 | } else { 123 | lest_couplings.push(x); 124 | } 125 | }); 126 | 127 | let grouped_coupling = group.reduce((s, x) => ({ 128 | ...s, 129 | tags: [...s.tags, ...x.tags], 130 | })); 131 | grouped_coupling = { 132 | ...grouped_coupling, 133 | tags: grouped_coupling.tags.filter( 134 | (x, i, self) => self.findIndex((y) => x.name === y.name) === i 135 | ), 136 | }; 137 | 138 | grouped_couplings.push(grouped_coupling); 139 | } while (lest_couplings.length !== 0); 140 | 141 | return grouped_couplings; 142 | }; 143 | 144 | const isSelfCoupling = (coupling: TargetCoupling) => 145 | coupling.characters[0].name === coupling.characters[1].name; 146 | 147 | const sleep = async (millis: number): Promise => 148 | new Promise((resolve) => setTimeout(() => resolve(), millis)); 149 | 150 | const getNumsFromTag = async (tag: string): Promise => { 151 | const cache_file_path = path.join(CACHE_DIR, `/nums_${sanitize(tag)}`); 152 | try { 153 | // return num if cache is exist 154 | return parseInt(fs.readFileSync(cache_file_path).toString()); 155 | } catch (e) {} 156 | 157 | await sleep(FETCH_INTERVAL); 158 | 159 | const url: string = `https://www.pixiv.net/tags/${encodeURIComponent(tag)}/`; 160 | const row = await fetch(url); 161 | const html = await row.text(); 162 | const num = getNum(html, 'イラスト') + getNum(html, '小説'); 163 | 164 | // caching num 165 | fs.writeFileSync(cache_file_path, num.toString()); 166 | return num; 167 | }; 168 | 169 | const getNum = (html: string, type: string): number => { 170 | const regex = new RegExp(`(\\d+)件の${type}`); 171 | const match = html.match(regex); 172 | if (match === null) return 0; 173 | return parseInt(match[1]); 174 | }; 175 | 176 | main(); 177 | -------------------------------------------------------------------------------- /scraping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yurigraph-scraping", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /scraping/target_couplings/bangdream/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/BanG_Dream%21%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | const getDictTitleFromURL = (url) => 6 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 7 | 8 | let characterDictTitles = {}; 9 | let result = []; 10 | 11 | const tables = document.querySelectorAll('article table'); 12 | [ 13 | tables[0], 14 | tables[1], 15 | tables[2], 16 | tables[3], 17 | tables[4], 18 | tables[5], 19 | tables[6], 20 | tables[7], 21 | tables[8], 22 | tables[9], 23 | ].forEach((table) => { 24 | Array.from(table.querySelectorAll('tr')) 25 | .map((tr) => Array.from(tr.querySelectorAll('td'))) 26 | .filter((tds) => tds.length !== 0) 27 | .forEach((tds) => { 28 | tds[1].querySelectorAll('a').forEach((a) => { 29 | if (!characterDictTitles[a.text]) { 30 | characterDictTitles[a.text] = getDictTitleFromURL(a.href); 31 | } 32 | }); 33 | result.push({ 34 | characters: tds[1].innerText.split('、').map((name) => ({ 35 | name: characterDictTitles[name] ? characterDictTitles[name] : name, 36 | })), 37 | tags: [tds[0].querySelector('a'), tds[2].querySelector('a')] 38 | .filter((a) => a !== null) 39 | .map((a) => ({ name: getDictTitleFromURL(a.href) })), 40 | }); 41 | }); 42 | }); 43 | 44 | copy(JSON.stringify(result)); 45 | })(); 46 | -------------------------------------------------------------------------------- /scraping/target_couplings/bangdream/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "characters": [{ "name": "戸山香澄" }, { "name": "市ヶ谷有咲" }], 4 | "tags": [{ "name": "かすあり" }, { "name": "ありかす" }] 5 | }, 6 | { 7 | "characters": [{ "name": "山吹沙綾" }, { "name": "市ヶ谷有咲" }], 8 | "tags": [{ "name": "ありさあや" }] 9 | }, 10 | { 11 | "characters": [{ "name": "戸山香澄" }, { "name": "山吹沙綾" }], 12 | "tags": [{ "name": "さーかす" }, { "name": "さあかす" }] 13 | }, 14 | { 15 | "characters": [{ "name": "花園たえ" }, { "name": "山吹沙綾" }], 16 | "tags": [{ "name": "たえさあや" }] 17 | }, 18 | { 19 | "characters": [{ "name": "戸山香澄" }, { "name": "花園たえ" }], 20 | "tags": [{ "name": "かすたえ" }] 21 | }, 22 | { 23 | "characters": [{ "name": "戸山香澄" }, { "name": "牛込りみ" }], 24 | "tags": [{ "name": "かすりみ" }] 25 | }, 26 | { 27 | "characters": [{ "name": "花園たえ" }, { "name": "牛込りみ" }], 28 | "tags": [{ "name": "たえりみ" }] 29 | }, 30 | { 31 | "characters": [{ "name": "花園たえ" }, { "name": "市ヶ谷有咲" }], 32 | "tags": [{ "name": "たえあり" }, { "name": "ありたえ" }] 33 | }, 34 | { 35 | "characters": [{ "name": "牛込りみ" }, { "name": "山吹沙綾" }], 36 | "tags": [{ "name": "りみさあや" }] 37 | }, 38 | { 39 | "characters": [{ "name": "牛込りみ" }, { "name": "市ヶ谷有咲" }], 40 | "tags": [{ "name": "りみあり" }, { "name": "ありりみ" }] 41 | }, 42 | { 43 | "characters": [{ "name": "美竹蘭" }, { "name": "青葉モカ" }], 44 | "tags": [{ "name": "モカ蘭" }, { "name": "蘭モカ" }] 45 | }, 46 | { 47 | "characters": [{ "name": "上原ひまり" }, { "name": "宇田川巴" }], 48 | "tags": [{ "name": "ともひま" }] 49 | }, 50 | { 51 | "characters": [{ "name": "美竹蘭" }, { "name": "羽沢つぐみ" }], 52 | "tags": [{ "name": "蘭つぐ" }] 53 | }, 54 | { 55 | "characters": [{ "name": "美竹蘭" }, { "name": "上原ひまり" }], 56 | "tags": [{ "name": "蘭ひま" }, { "name": "ひま蘭" }] 57 | }, 58 | { 59 | "characters": [{ "name": "美竹蘭" }, { "name": "宇田川巴" }], 60 | "tags": [{ "name": "巴蘭" }, { "name": "とも蘭" }] 61 | }, 62 | { 63 | "characters": [{ "name": "青葉モカ" }, { "name": "上原ひまり" }], 64 | "tags": [{ "name": "モカひま" }] 65 | }, 66 | { 67 | "characters": [{ "name": "青葉モカ" }, { "name": "宇田川巴" }], 68 | "tags": [{ "name": "モカとも" }] 69 | }, 70 | { 71 | "characters": [{ "name": "青葉モカ" }, { "name": "羽沢つぐみ" }], 72 | "tags": [{ "name": "モカつぐ" }] 73 | }, 74 | { 75 | "characters": [{ "name": "上原ひまり" }, { "name": "羽沢つぐみ" }], 76 | "tags": [{ "name": "つぐひま" }, { "name": "ひまつぐ" }] 77 | }, 78 | { 79 | "characters": [{ "name": "宇田川巴" }, { "name": "羽沢つぐみ" }], 80 | "tags": [{ "name": "巴つぐ" }] 81 | }, 82 | { 83 | "characters": [{ "name": "丸山彩" }, { "name": "白鷺千聖" }], 84 | "tags": [{ "name": "あやちさ" }, { "name": "ちさあや" }] 85 | }, 86 | { 87 | "characters": [{ "name": "丸山彩" }, { "name": "氷川日菜" }], 88 | "tags": [{ "name": "ひなあや" }, { "name": "あやひな" }] 89 | }, 90 | { 91 | "characters": [{ "name": "大和麻弥" }, { "name": "若宮イヴ" }], 92 | "tags": [{ "name": "麻弥イヴ" }, { "name": "イヴ麻弥" }] 93 | }, 94 | { 95 | "characters": [{ "name": "丸山彩" }, { "name": "大和麻弥" }], 96 | "tags": [{ "name": "まやあや" }] 97 | }, 98 | { 99 | "characters": [{ "name": "丸山彩" }, { "name": "若宮イヴ" }], 100 | "tags": [{ "name": "あやイヴ" }, { "name": "イヴあや" }] 101 | }, 102 | { 103 | "characters": [{ "name": "氷川日菜" }, { "name": "白鷺千聖" }], 104 | "tags": [{ "name": "ひなちさ" }] 105 | }, 106 | { 107 | "characters": [{ "name": "氷川日菜" }, { "name": "大和麻弥" }], 108 | "tags": [{ "name": "ひなまや" }] 109 | }, 110 | { 111 | "characters": [{ "name": "氷川日菜" }, { "name": "若宮イヴ" }], 112 | "tags": [{ "name": "ひなイヴ" }] 113 | }, 114 | { 115 | "characters": [{ "name": "白鷺千聖" }, { "name": "大和麻弥" }], 116 | "tags": [{ "name": "麻弥ちさ" }] 117 | }, 118 | { 119 | "characters": [{ "name": "白鷺千聖" }, { "name": "若宮イヴ" }], 120 | "tags": [{ "name": "イヴちさ" }] 121 | }, 122 | { 123 | "characters": [{ "name": "湊友希那" }, { "name": "今井リサ" }], 124 | "tags": [{ "name": "リサゆき" }, { "name": "ゆきリサ" }] 125 | }, 126 | { 127 | "characters": [{ "name": "氷川紗夜" }, { "name": "今井リサ" }], 128 | "tags": [{ "name": "リサさよ" }, { "name": "さよリサ" }] 129 | }, 130 | { 131 | "characters": [{ "name": "湊友希那" }, { "name": "氷川紗夜" }], 132 | "tags": [{ "name": "ゆきさよ" }, { "name": "さよゆき" }] 133 | }, 134 | { 135 | "characters": [{ "name": "氷川紗夜" }, { "name": "白金燐子" }], 136 | "tags": [{ "name": "りんさよ" }] 137 | }, 138 | { 139 | "characters": [{ "name": "宇田川あこ" }, { "name": "白金燐子" }], 140 | "tags": [{ "name": "りんあこ" }, { "name": "あこりん" }] 141 | }, 142 | { 143 | "characters": [{ "name": "湊友希那" }, { "name": "宇田川あこ" }], 144 | "tags": [{ "name": "あこゆき" }] 145 | }, 146 | { 147 | "characters": [{ "name": "湊友希那" }, { "name": "白金燐子" }], 148 | "tags": [{ "name": "ゆきりん(バンドリ)" }, { "name": "りんゆき" }] 149 | }, 150 | { 151 | "characters": [{ "name": "氷川紗夜" }, { "name": "宇田川あこ" }], 152 | "tags": [{ "name": "さよあこ" }] 153 | }, 154 | { 155 | "characters": [{ "name": "今井リサ" }, { "name": "宇田川あこ" }], 156 | "tags": [{ "name": "リサあこ" }] 157 | }, 158 | { 159 | "characters": [{ "name": "今井リサ" }, { "name": "白金燐子" }], 160 | "tags": [{ "name": "りんリサ" }] 161 | }, 162 | { 163 | "characters": [{ "name": "弦巻こころ" }, { "name": "奥沢美咲" }], 164 | "tags": [{ "name": "みさここ" }, { "name": "ミシェここ" }] 165 | }, 166 | { 167 | "characters": [{ "name": "松原花音" }, { "name": "奥沢美咲" }], 168 | "tags": [{ "name": "みさかのん" }] 169 | }, 170 | { 171 | "characters": [{ "name": "弦巻こころ" }, { "name": "瀬田薫" }], 172 | "tags": [{ "name": "かおここ" }] 173 | }, 174 | { 175 | "characters": [{ "name": "弦巻こころ" }, { "name": "北沢はぐみ" }], 176 | "tags": [{ "name": "ここはぐ" }] 177 | }, 178 | { 179 | "characters": [{ "name": "弦巻こころ" }, { "name": "松原花音" }], 180 | "tags": [{ "name": "ここかの" }] 181 | }, 182 | { 183 | "characters": [{ "name": "瀬田薫" }, { "name": "北沢はぐみ" }], 184 | "tags": [{ "name": "かおはぐ" }] 185 | }, 186 | { 187 | "characters": [{ "name": "瀬田薫" }, { "name": "松原花音" }], 188 | "tags": [{ "name": "かおかの" }] 189 | }, 190 | { 191 | "characters": [{ "name": "瀬田薫" }, { "name": "奥沢美咲" }], 192 | "tags": [{ "name": "かおみさ" }] 193 | }, 194 | { 195 | "characters": [{ "name": "北沢はぐみ" }, { "name": "松原花音" }], 196 | "tags": [{ "name": "かのはぐ" }, { "name": "はぐかのん" }] 197 | }, 198 | { 199 | "characters": [{ "name": "北沢はぐみ" }, { "name": "奥沢美咲" }], 200 | "tags": [{ "name": "はぐみさ" }] 201 | }, 202 | { 203 | "characters": [{ "name": "和奏レイ" }, { "name": "佐藤ますき" }], 204 | "tags": [{ "name": "レイマス" }, { "name": "マスレイ" }] 205 | }, 206 | { 207 | "characters": [{ "name": "鳰原令王那" }, { "name": "珠手ちゆ" }], 208 | "tags": [{ "name": "チュチュパレ" }, { "name": "ちゆれお" }] 209 | }, 210 | { 211 | "characters": [{ "name": "朝日六花" }, { "name": "佐藤ますき" }], 212 | "tags": [{ "name": "マス六" }] 213 | }, 214 | { 215 | "characters": [{ "name": "朝日六花" }, { "name": "珠手ちゆ" }], 216 | "tags": [{ "name": "ろかちゆ" }] 217 | }, 218 | { 219 | "characters": [{ "name": "倉田ましろ" }, { "name": "二葉つくし" }], 220 | "tags": [{ "name": "ましつく" }, { "name": "つくまし" }] 221 | }, 222 | { 223 | "characters": [{ "name": "倉田ましろ" }, { "name": "広町七深" }], 224 | "tags": [{ "name": "ななまし" }, { "name": "ましなな" }] 225 | }, 226 | { 227 | "characters": [{ "name": "倉田ましろ" }, { "name": "八潮瑠唯" }], 228 | "tags": [{ "name": "るいまし" }] 229 | }, 230 | { 231 | "characters": [{ "name": "倉田ましろ" }, { "name": "桐ヶ谷透子" }], 232 | "tags": [{ "name": "透まし" }] 233 | }, 234 | { 235 | "characters": [{ "name": "桐ヶ谷透子" }, { "name": "八潮瑠唯" }], 236 | "tags": [{ "name": "とうるい" }] 237 | }, 238 | { 239 | "characters": [{ "name": "広町七深" }, { "name": "二葉つくし" }], 240 | "tags": [{ "name": "ななつく" }] 241 | }, 242 | { 243 | "characters": [{ "name": "高松燈" }, { "name": "千早愛音" }], 244 | "tags": [{ "name": "ともあの" }, { "name": "あのとも" }] 245 | }, 246 | { 247 | "characters": [{ "name": "椎名立希" }, { "name": "千早愛音" }], 248 | "tags": [{ "name": "たきあの" }] 249 | }, 250 | { 251 | "characters": [{ "name": "千早愛音" }, { "name": "長崎そよ" }], 252 | "tags": [{ "name": "あのそよ" }, { "name": "ansy" }] 253 | }, 254 | { 255 | "characters": [{ "name": "高松燈" }, { "name": "椎名立希" }], 256 | "tags": [{ "name": "たきとも" }] 257 | }, 258 | { 259 | "characters": [{ "name": "椎名立希" }, { "name": "要楽奈" }], 260 | "tags": [{ "name": "たきらな" }] 261 | }, 262 | { 263 | "characters": [{ "name": "椎名立希" }, { "name": "長崎そよ" }], 264 | "tags": [{ "name": "たきそよ" }] 265 | }, 266 | { 267 | "characters": [{ "name": "高松燈" }, { "name": "長崎そよ" }], 268 | "tags": [{ "name": "ともそよ" }] 269 | }, 270 | { 271 | "characters": [{ "name": "高松燈" }, { "name": "要楽奈" }], 272 | "tags": [{ "name": "ともらな" }] 273 | }, 274 | { 275 | "characters": [{ "name": "長崎そよ" }, { "name": "要楽奈" }], 276 | "tags": [{ "name": "そよらな" }] 277 | }, 278 | { 279 | "characters": [{ "name": "若葉睦" }, { "name": "豊川祥子" }], 280 | "tags": [{ "name": "むつさき" }, { "name": "さきむつ" }] 281 | }, 282 | { 283 | "characters": [{ "name": "三角初華" }, { "name": "豊川祥子" }], 284 | "tags": [{ "name": "ういさき" }, { "name": "さきうい" }] 285 | }, 286 | { 287 | "characters": [{ "name": "八幡海鈴" }, { "name": "豊川祥子" }], 288 | "tags": [{ "name": "うみさき" }] 289 | }, 290 | { 291 | "characters": [{ "name": "祐天寺にゃむ" }, { "name": "豊川祥子" }], 292 | "tags": [{ "name": "にゃむさき" }] 293 | }, 294 | { 295 | "characters": [{ "name": "祐天寺にゃむ" }, { "name": "若葉睦" }], 296 | "tags": [{ "name": "にゃむむつ" }, { "name": "むつにゃむ" }] 297 | }, 298 | { 299 | "characters": [{ "name": "三角初華" }, { "name": "若葉睦" }], 300 | "tags": [{ "name": "ういむつ" }] 301 | }, 302 | { 303 | "characters": [{ "name": "八幡海鈴" }, { "name": "三角初華" }], 304 | "tags": [{ "name": "うみうい" }] 305 | }, 306 | { 307 | "characters": [{ "name": "八幡海鈴" }, { "name": "若葉睦" }], 308 | "tags": [{ "name": "うみむつ" }] 309 | }, 310 | { 311 | "characters": [{ "name": "八幡海鈴" }, { "name": "祐天寺にゃむ" }], 312 | "tags": [{ "name": "うみにゃむ" }] 313 | }, 314 | { 315 | "characters": [{ "name": "氷川日菜" }, { "name": "氷川紗夜" }], 316 | "tags": [{ "name": "さよひな" }, { "name": "氷川姉妹" }] 317 | }, 318 | { 319 | "characters": [{ "name": "羽沢つぐみ" }, { "name": "氷川紗夜" }], 320 | "tags": [{ "name": "さよつぐ" }] 321 | }, 322 | { 323 | "characters": [{ "name": "白鷺千聖" }, { "name": "瀬田薫" }], 324 | "tags": [{ "name": "かおちさ" }, { "name": "ちさかお" }] 325 | }, 326 | { 327 | "characters": [{ "name": "美竹蘭" }, { "name": "湊友希那" }], 328 | "tags": [{ "name": "ゆき蘭" }, { "name": "蘭ゆき" }] 329 | }, 330 | { 331 | "characters": [{ "name": "青葉モカ" }, { "name": "今井リサ" }], 332 | "tags": [{ "name": "モカリサ" }, { "name": "リサモカ" }] 333 | }, 334 | { 335 | "characters": [{ "name": "羽沢つぐみ" }, { "name": "氷川日菜" }], 336 | "tags": [{ "name": "ひなつぐ" }] 337 | }, 338 | { 339 | "characters": [{ "name": "羽沢つぐみ" }, { "name": "大和麻弥" }], 340 | "tags": [{ "name": "つぐまや" }, { "name": "まやつぐ" }] 341 | }, 342 | { 343 | "characters": [{ "name": "白鷺千聖" }, { "name": "松原花音" }], 344 | "tags": [{ "name": "ちさかの" }, { "name": "かのちさ" }] 345 | }, 346 | { 347 | "characters": [{ "name": "戸山香澄" }, { "name": "湊友希那" }], 348 | "tags": [{ "name": "かすゆき" }, { "name": "ゆきかす" }] 349 | }, 350 | { 351 | "characters": [{ "name": "戸山香澄" }, { "name": "北沢はぐみ" }], 352 | "tags": [{ "name": "かすはぐ" }] 353 | }, 354 | { 355 | "characters": [{ "name": "戸山香澄" }, { "name": "倉田ましろ" }], 356 | "tags": [{ "name": "かすまし" }] 357 | }, 358 | { 359 | "characters": [{ "name": "戸山香澄" }, { "name": "牛込ゆり" }], 360 | "tags": [{ "name": "かすゆり" }] 361 | }, 362 | { 363 | "characters": [{ "name": "牛込ゆり" }, { "name": "牛込りみ" }], 364 | "tags": [{ "name": "牛込姉妹" }] 365 | }, 366 | { 367 | "characters": [{ "name": "戸山香澄" }, { "name": "戸山明日香" }], 368 | "tags": [{ "name": "戸山姉妹" }, { "name": "かすあす" }] 369 | }, 370 | { 371 | "characters": [{ "name": "花園たえ" }, { "name": "白鷺千聖" }], 372 | "tags": [{ "name": "たえちさ" }] 373 | }, 374 | { 375 | "characters": [{ "name": "花園たえ" }, { "name": "氷川紗夜" }], 376 | "tags": [{ "name": "たえさよ" }] 377 | }, 378 | { 379 | "characters": [{ "name": "牛込りみ" }, { "name": "奥沢美咲" }], 380 | "tags": [{ "name": "りみさき" }, { "name": "みさりみ" }] 381 | }, 382 | { 383 | "characters": [{ "name": "山吹沙綾" }, { "name": "宇田川巴" }], 384 | "tags": [{ "name": "ともさあや" }] 385 | }, 386 | { 387 | "characters": [{ "name": "市ヶ谷有咲" }, { "name": "氷川紗夜" }], 388 | "tags": [{ "name": "さよあり" }] 389 | }, 390 | { 391 | "characters": [{ "name": "市ヶ谷有咲" }, { "name": "今井リサ" }], 392 | "tags": [{ "name": "リサありさ" }] 393 | }, 394 | { 395 | "characters": [{ "name": "市ヶ谷有咲" }, { "name": "白金燐子" }], 396 | "tags": [{ "name": "りんあり" }] 397 | }, 398 | { 399 | "characters": [{ "name": "市ヶ谷有咲" }, { "name": "奥沢美咲" }], 400 | "tags": [{ "name": "みさあり" }] 401 | }, 402 | { 403 | "characters": [{ "name": "美竹蘭" }, { "name": "丸山彩" }], 404 | "tags": [{ "name": "蘭彩" }] 405 | }, 406 | { 407 | "characters": [{ "name": "美竹蘭" }, { "name": "今井リサ" }], 408 | "tags": [{ "name": "リサ蘭" }] 409 | }, 410 | { 411 | "characters": [{ "name": "青葉モカ" }, { "name": "奥沢美咲" }], 412 | "tags": [{ "name": "みさモカ" }] 413 | }, 414 | { 415 | "characters": [{ "name": "宇田川巴" }, { "name": "宇田川あこ" }], 416 | "tags": [{ "name": "宇田川姉妹" }] 417 | }, 418 | { 419 | "characters": [{ "name": "宇田川巴" }, { "name": "瀬田薫" }], 420 | "tags": [{ "name": "かおとも" }] 421 | }, 422 | { 423 | "characters": [{ "name": "丸山彩" }, { "name": "氷川紗夜" }], 424 | "tags": [{ "name": "あやさよ" }] 425 | }, 426 | { 427 | "characters": [{ "name": "丸山彩" }, { "name": "白金燐子" }], 428 | "tags": [{ "name": "りんあや" }] 429 | }, 430 | { 431 | "characters": [{ "name": "丸山彩" }, { "name": "松原花音" }], 432 | "tags": [{ "name": "あやかのん" }, { "name": "あやかの" }] 433 | }, 434 | { 435 | "characters": [{ "name": "氷川日菜" }, { "name": "今井リサ" }], 436 | "tags": [{ "name": "ひなリサ" }] 437 | }, 438 | { 439 | "characters": [{ "name": "白鷺千聖" }, { "name": "氷川紗夜" }], 440 | "tags": [{ "name": "さよちさ" }] 441 | }, 442 | { 443 | "characters": [{ "name": "大和麻弥" }, { "name": "瀬田薫" }], 444 | "tags": [{ "name": "まやかお" }, { "name": "かおまや" }] 445 | }, 446 | { 447 | "characters": [{ "name": "湊友希那" }, { "name": "珠手ちゆ" }], 448 | "tags": [{ "name": "ゆきチュチュ" }] 449 | }, 450 | { 451 | "characters": [{ "name": "氷川紗夜" }, { "name": "奥沢美咲" }], 452 | "tags": [{ "name": "みささよ" }] 453 | }, 454 | { 455 | "characters": [{ "name": "白金燐子" }, { "name": "八潮瑠唯" }], 456 | "tags": [{ "name": "りんるい" }, { "name": "るいりん" }] 457 | }, 458 | { 459 | "characters": [{ "name": "和奏レイ" }, { "name": "花園たえ" }], 460 | "tags": [{ "name": "レイたえ" }, { "name": "たえレイ" }] 461 | }, 462 | { 463 | "characters": [{ "name": "朝日六花" }, { "name": "倉田ましろ" }], 464 | "tags": [{ "name": "六まし" }, { "name": "ましロック" }] 465 | }, 466 | { 467 | "characters": [{ "name": "朝日六花" }, { "name": "戸山明日香" }], 468 | "tags": [{ "name": "あすろっか" }, { "name": "六明日" }] 469 | }, 470 | { 471 | "characters": [{ "name": "高松燈" }, { "name": "豊川祥子" }], 472 | "tags": [{ "name": "ともさき" }] 473 | }, 474 | { 475 | "characters": [{ "name": "長崎そよ" }, { "name": "豊川祥子" }], 476 | "tags": [{ "name": "そよさき" }] 477 | }, 478 | { 479 | "characters": [{ "name": "長崎そよ" }, { "name": "若葉睦" }], 480 | "tags": [{ "name": "むつそよ" }, { "name": "そよ睦" }] 481 | }, 482 | { 483 | "characters": [{ "name": "椎名立希" }, { "name": "八幡海鈴" }], 484 | "tags": [{ "name": "うみたき" }] 485 | }, 486 | { 487 | "characters": [{ "name": "千早愛音" }, { "name": "豊川祥子" }], 488 | "tags": [{ "name": "あのさき" }, { "name": "爱祥" }] 489 | } 490 | ] 491 | -------------------------------------------------------------------------------- /scraping/target_couplings/btr/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%81%BC%E3%81%A3%E3%81%A1%E3%83%BB%E3%81%96%E3%83%BB%E3%82%8D%E3%81%A3%E3%81%8F%21%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | // utils 6 | const getDictTitleFromURL = (url) => 7 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 8 | 9 | // get couplings from table that like following format 10 | // |ぼ虹/虹ぼ|後藤ひとり/伊地知虹夏| 11 | // ... 12 | const getCouplingsFromTable = (table) => { 13 | return Array.from(table.querySelectorAll('tr')) 14 | .filter((tr) => tr.querySelector('a') !== null) 15 | .map((tr) => tr.querySelectorAll('td')) 16 | .filter( 17 | ([_, characters]) => characters.querySelectorAll('a').length === 2 18 | ) 19 | .map(([tag, characters]) => ({ 20 | tags: Array.from(tag.querySelectorAll('a')).map((a) => ({ 21 | name: getDictTitleFromURL(a.href), 22 | })), 23 | characters: [ 24 | { 25 | name: getDictTitleFromURL(characters.querySelectorAll('a')[0].href), 26 | }, 27 | { 28 | name: getDictTitleFromURL(characters.querySelectorAll('a')[1].href), 29 | }, 30 | ], 31 | })); 32 | }; 33 | 34 | // get target couplings 35 | const target_couplings = getCouplingsFromTable( 36 | document.querySelector('article table') 37 | ); 38 | copy(JSON.stringify(target_couplings)); 39 | })(); 40 | -------------------------------------------------------------------------------- /scraping/target_couplings/btr/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "tags": [{ "name": "ぼ喜多" }, { "name": "喜多ぼ" }], 4 | "characters": [{ "name": "後藤ひとり" }, { "name": "喜多郁代" }] 5 | }, 6 | { 7 | "tags": [{ "name": "ぼ虹" }, { "name": "虹ぼ" }], 8 | "characters": [{ "name": "後藤ひとり" }, { "name": "伊地知虹夏" }] 9 | }, 10 | { 11 | "tags": [{ "name": "星ぼ" }], 12 | "characters": [{ "name": "伊地知星歌" }, { "name": "後藤ひとり" }] 13 | }, 14 | { 15 | "tags": [{ "name": "ぼリョウ" }, { "name": "リョウぼ" }], 16 | "characters": [ 17 | { "name": "後藤ひとり" }, 18 | { "name": "山田リョウ(ぼっち・ざ・ろっく!)" } 19 | ] 20 | }, 21 | { 22 | "tags": [{ "name": "リョウ虹" }, { "name": "虹リョウ" }], 23 | "characters": [ 24 | { "name": "山田リョウ(ぼっち・ざ・ろっく!)" }, 25 | { "name": "伊地知虹夏" } 26 | ] 27 | }, 28 | { 29 | "tags": [{ "name": "虹喜多" }], 30 | "characters": [{ "name": "伊地知虹夏" }, { "name": "喜多郁代" }] 31 | }, 32 | { 33 | "tags": [{ "name": "リョウ喜多" }], 34 | "characters": [ 35 | { "name": "山田リョウ(ぼっち・ざ・ろっく!)" }, 36 | { "name": "喜多郁代" } 37 | ] 38 | }, 39 | { 40 | "tags": [{ "name": "伊地知姉妹" }], 41 | "characters": [{ "name": "伊地知星歌" }, { "name": "伊地知虹夏" }] 42 | }, 43 | { 44 | "tags": [{ "name": "PA星" }], 45 | "characters": [{ "name": "PAさん" }, { "name": "伊地知星歌" }] 46 | }, 47 | { 48 | "tags": [{ "name": "きく星" }], 49 | "characters": [{ "name": "廣井きくり" }, { "name": "伊地知星歌" }] 50 | }, 51 | { 52 | "tags": [{ "name": "廣ぼ" }], 53 | "characters": [{ "name": "廣井きくり" }, { "name": "後藤ひとり" }] 54 | }, 55 | { 56 | "tags": [{ "name": "廣志麻" }], 57 | "characters": [{ "name": "廣井きくり" }, { "name": "岩下志麻" }] 58 | }, 59 | { 60 | "tags": [{ "name": "ひとPA" }], 61 | "characters": [{ "name": "後藤ひとり" }, { "name": "PAさん" }] 62 | }, 63 | { 64 | "tags": [{ "name": "ぼヨヨ" }], 65 | "characters": [{ "name": "後藤ひとり" }, { "name": "大槻ヨヨコ" }] 66 | }, 67 | { 68 | "tags": [{ "name": "後藤姉妹" }], 69 | "characters": [{ "name": "後藤ひとり" }, { "name": "後藤ふたり" }] 70 | }, 71 | { 72 | "tags": [{ "name": "ふた喜多" }], 73 | "characters": [{ "name": "後藤ふたり" }, { "name": "喜多郁代" }] 74 | }, 75 | { 76 | "tags": [{ "name": "佐々ぼ" }], 77 | "characters": [{ "name": "佐々木次子" }, { "name": "後藤ひとり" }] 78 | }, 79 | { 80 | "tags": [{ "name": "しばやみ" }], 81 | "characters": [{ "name": "司馬都" }, { "name": "ぽいずん♡やみ" }] 82 | }, 83 | { 84 | "tags": [{ "name": "ふーはー" }], 85 | "characters": [{ "name": "本城楓子" }, { "name": "長谷川あくび" }] 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /scraping/target_couplings/deremas/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%82%A2%E3%82%A4%E3%83%89%E3%83%AB%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%B7%E3%83%B3%E3%83%87%E3%83%AC%E3%83%A9%E3%82%AC%E3%83%BC%E3%83%AB%E3%82%BA%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | // get coupling from like following table format 6 | // 「あい真奈」【ハンサムレディース】 7 | // 東郷あい × 木場真奈美 8 | const getCouplingsMultiRow = (table) => { 9 | const tags = Array.from(table.querySelectorAll('tbody > tr')) 10 | .filter((_, i) => i % 3 === 0) 11 | .map((x) => 12 | Array.from(x.querySelectorAll('th > a')).map((x) => x.innerText) 13 | ); 14 | 15 | const characters = Array.from(table.querySelectorAll('tbody > tr')) 16 | .filter((_, i) => i % 3 === 1) 17 | .map((x) => 18 | Array.from(x.querySelectorAll('a')).map((x) => ({ 19 | name: x.innerText, 20 | })) 21 | ); 22 | 23 | return tags.map((_, i) => ({ 24 | characters: characters[i], 25 | tags: tags[i].map((x) => ({ name: x })), 26 | })); 27 | }; 28 | 29 | // get coupling from like following table format 30 | // アキレイナ / 池袋晶葉 × 小関麗奈 31 | const getCouplingsSingleRow = (table) => 32 | Array.from(table.querySelectorAll('tbody > tr')) 33 | .filter((_, i) => i % 2 === 0) 34 | .map((x) => 35 | Array.from(x.querySelectorAll('th > a')).map((x) => x.innerText) 36 | ) 37 | .map((x) => ({ 38 | tags: x.slice(0, -2).map((x) => ({ name: x })), 39 | characters: x.slice(-2).map((x) => ({ name: x })), 40 | })) 41 | .filter((x) => x.tags.length >= 1 && x.characters.length === 2); 42 | 43 | const tables = document.querySelectorAll('article table'); 44 | const couplings = [ 45 | //公式ユニットとして登場したもの 46 | ...getCouplingsMultiRow(tables[0]), 47 | //アニメからユニット化したもの 48 | ...getCouplingsMultiRow(tables[1]), 49 | //スターライトステージで初登場のユニット 50 | ...getCouplingsMultiRow(tables[2]), 51 | //固有のユニット名が無いもの 52 | ...getCouplingsSingleRow(tables[3]), 53 | //公式媒体に登場するもの 54 | ...getCouplingsSingleRow(tables[4]), 55 | //非公式なもの 56 | ...getCouplingsSingleRow(tables[5]), 57 | //非アイドルを含むもの 58 | ...getCouplingsSingleRow(tables[6]), 59 | ]; 60 | 61 | copy(JSON.stringify(couplings)); 62 | })(); 63 | -------------------------------------------------------------------------------- /scraping/target_couplings/hetalia/getCountriesAndCharacters.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%83%98%E3%82%BF%E3%83%AA%E3%82%A2%E3%81%AE%E4%BA%BA%E5%90%8D%E3%83%BB%E6%84%9B%E7%A7%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | const getDictTitleFromURL = (url) => 6 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 7 | 8 | // get countories and characters mapping from following table 9 | // |日|日本|本田菊| 10 | // ... 11 | const getCharactersFromRow = (row) => { 12 | const cells = Array.from(row.querySelectorAll(':scope > *')); 13 | return { 14 | short_country_name: cells[0].innerText, 15 | country_name: cells[1].innerText, 16 | name: getDictTitleFromURL(cells[2].querySelector('a').href), 17 | }; 18 | }; 19 | const getCharactersFromTable = (table) => { 20 | const rows = Array.from(table.querySelectorAll('tr')); 21 | return rows.map(getCharactersFromRow); 22 | }; 23 | 24 | const tables = document.querySelectorAll('article table'); 25 | const characters = [ 26 | // 人名あり 27 | tables[2], 28 | // 人名なし 29 | tables[3], 30 | ] 31 | .map((x) => getCharactersFromTable(x)) 32 | .reduce((s, x) => [...s, ...x]); 33 | 34 | copy(JSON.stringify(characters)); 35 | })(); 36 | -------------------------------------------------------------------------------- /scraping/target_couplings/hetalia/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%83%98%E3%82%BF%E3%83%AA%E3%82%A2%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%93%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // https://dic.pixiv.net/a/%E8%85%90%E5%90%91%E3%81%91%E3%83%98%E3%82%BF%E3%83%AA%E3%82%A2 3 | // usage: paste this script to javascript console 4 | 5 | (() => { 6 | // prettier-ignore 7 | const characters = [{"short_country_name":"北伊","country_name":"北イタリア","name":"フェリシアーノ・ヴァルガス"},{"short_country_name":"南伊","country_name":"南イタリア","name":"ロヴィーノ・ヴァルガス"},{"short_country_name":"独","country_name":"ドイツ","name":"ルートヴィッヒ"},{"short_country_name":"普","country_name":"プロイセン","name":"ギルベルト・バイルシュミット"},{"short_country_name":"日","country_name":"日本","name":"本田菊"},{"short_country_name":"米","country_name":"アメリカ","name":"アルフレッド・F・ジョーンズ"},{"short_country_name":"英","country_name":"イギリス","name":"アーサー・カークランド"},{"short_country_name":"仏","country_name":"フランス","name":"フランシス・ボヌフォワ"},{"short_country_name":"中","country_name":"中国","name":"王耀"},{"short_country_name":"露","country_name":"ロシア","name":"イヴァン・ブラギンスキ"},{"short_country_name":"加","country_name":"カナダ","name":"マシュー・ウィリアムズ"},{"short_country_name":"西","country_name":"スペイン","name":"アントーニョ・ヘルナンデス・カリエド"},{"short_country_name":"瑞","country_name":"スイス","name":"バッシュ・ツヴィンクリ"},{"short_country_name":"墺","country_name":"オーストリア","name":"ローデリヒ・エーデルシュタイン"},{"short_country_name":"洪","country_name":"ハンガリー","name":"エリザベータ・ヘーデルヴァーリ"},{"short_country_name":"辺","country_name":"ベラルーシ","name":"ナターリヤ・アルロフスカヤ"},{"short_country_name":"波","country_name":"ポーランド","name":"フェリクス・ウカシェヴィチ"},{"short_country_name":"立","country_name":"リトアニア","name":"トーリス・ロリナイティス"},{"short_country_name":"拉","country_name":"ラトビア","name":"ライヴィス・ガランテ"},{"short_country_name":"愛","country_name":"エストニア","name":"エドァルド・フォンヴォック"},{"short_country_name":"芬","country_name":"フィンランド","name":"ティノ・ヴァイナマイネン"},{"short_country_name":"典","country_name":"スウェーデン","name":"ベールヴァルド・オキセンスシェルナ"},{"short_country_name":"海","country_name":"シーランド","name":"ピーター・カークランド"},{"short_country_name":"希","country_name":"ギリシャ","name":"ヘラクレス・カルプシ"},{"short_country_name":"土","country_name":"トルコ","name":"サディク・アドナン"},{"short_country_name":"埃","country_name":"エジプト","name":"グプタ・ハッサン"},{"short_country_name":"韓","country_name":"韓国","name":"任勇洙"},{"short_country_name":"丁","country_name":"デンマーク","name":"マー君"},{"short_country_name":"諾","country_name":"ノルウェー","name":"ノル君"},{"short_country_name":"氷","country_name":"アイスランド","name":"17億"},{"short_country_name":"蘭","country_name":"オランダ","name":"蘭にいさん"},{"short_country_name":"白","country_name":"ベルギー","name":"ベルベル"},{"short_country_name":"葡","country_name":"ポルトガル","name":"ポルトさん"},{"short_country_name":"盧","country_name":"ルクセンブルク","name":"ルクセン"},{"short_country_name":"摩","country_name":"モナコ","name":"モナ子さん"},{"short_country_name":"列","country_name":"リヒテンシュタイン","name":"リヒテン"},{"short_country_name":"機","country_name":"キプロス","name":"キプくん"},{"short_country_name":"","country_name":"北キプロス","name":"北キプ"},{"short_country_name":"宇","country_name":"ウクライナ","name":"とばっちり子"},{"short_country_name":"尼","country_name":"ルーマニア","name":"ルーさん"},{"short_country_name":"勃","country_name":"ブルガリア","name":"勃くん"},{"short_country_name":"喪","country_name":"モルドバ","name":"モル君"},{"short_country_name":"捷","country_name":"チェコ","name":"チェ子さん"},{"short_country_name":"斯","country_name":"スロバキア","name":"バキアくん"},{"short_country_name":"濠","country_name":"オーストラリア","name":"濠くん"},{"short_country_name":"新","country_name":"ニュージーランド","name":"新さん"},{"short_country_name":"玖","country_name":"キューバ","name":"玖さん"},{"short_country_name":"泰","country_name":"タイ","name":"泰さん"},{"short_country_name":"越","country_name":"ベトナム","name":"越っちゃん"},{"short_country_name":"印","country_name":"インド","name":"印さん"},{"short_country_name":"蒙","country_name":"モンゴル","name":"蒙さん"},{"short_country_name":"台","country_name":"台湾","name":"気苦労娘"},{"short_country_name":"香","country_name":"香港","name":"大英帝国の呪い"},{"short_country_name":"澳","country_name":"マカオ","name":"澳門さん"},{"short_country_name":"蔵","country_name":"チベット","name":"チベさん"},{"short_country_name":"比","country_name":"フィリピン","name":"フィリーさん"},{"short_country_name":"尼","country_name":"インドネシア","name":"ネシアさん"},{"short_country_name":"星","country_name":"シンガポール","name":"星さん"},{"short_country_name":"馬","country_name":"マレーシア","name":"マレーさん"},{"short_country_name":"塞","country_name":"セーシェル","name":"セーちゃん"},{"short_country_name":"夏","country_name":"カメルーン","name":"カメちゃん"},{"short_country_name":"蘇","country_name":"スコットランド","name":"スコ君"},{"short_country_name":"威","country_name":"ウェールズ","name":"ウェーくん"},{"short_country_name":"北愛","country_name":"北アイルランド","name":"のんくん"},{"short_country_name":"愛","country_name":"アイルランド","name":"アイルさん"},{"short_country_name":"","country_name":"エクアドル","name":"エクさん"},{"short_country_name":"","country_name":"セボルガ公国","name":"セボくん"},{"short_country_name":"","country_name":"ワイ公国","name":"ワイちゃん"},{"short_country_name":"","country_name":"クーゲルムーゲル","name":"クーゲルちゃん"},{"short_country_name":"","country_name":"モロッシア","name":"モロさん"},{"short_country_name":"","country_name":"ハット・リバー王国","name":"トリバさん"},{"short_country_name":"","country_name":"ニコニコ共和国","name":"ニコニコさん"},{"short_country_name":"","country_name":"ラドニア","name":"ラド君"},{"short_country_name":"","country_name":"ピカルディ","name":"ピカルディ君"},{"short_country_name":"","country_name":"セルビア","name":"セルくん"},{"short_country_name":"","country_name":"アエリカ帝国","name":"アエリカさん"},{"short_country_name":"","country_name":"スロージャマスタン共和国","name":"スロタン"},{"short_country_name":"古埃","country_name":"古代エジプト","name":"埃ママ"},{"short_country_name":"古希","country_name":"古代ギリシア","name":"ヘラママ"},{"short_country_name":"羅","country_name":"ローマ帝国","name":"ローマ爺ちゃん"},{"short_country_name":"","country_name":"ゲルマン","name":"ゲルマンさん"},{"short_country_name":"波","country_name":"ペルシア","name":"ペルシアさん"},{"short_country_name":"神羅","country_name":"神聖ローマ帝国","name":"神聖ローマ"},{"short_country_name":"","country_name":"ジョチ・ウルス","name":"ジョチさん"},{"short_country_name":"","country_name":"ヘッセン","name":"ヘッシャン"},{"short_country_name":"","country_name":"テンプル騎士団","name":"テンプルさん"},{"short_country_name":"","country_name":"聖ヨハネ騎士団","name":"ヨハネさん"},{"short_country_name":"","country_name":"ジェノヴァ","name":"ジェノさん"},{"short_country_name":"","country_name":"日本各地","name":"尾張さん"}]; 8 | 9 | const getDictTitleFromURL = (url) => 10 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 11 | 12 | const getNextMatchElement = (start_element, selector_string) => { 13 | let current_element = start_element; 14 | do { 15 | current_element = current_element.nextSibling; 16 | } while ( 17 | current_element !== null && 18 | (!current_element.matches || !current_element.matches(selector_string)) 19 | ); 20 | 21 | return current_element; 22 | }; 23 | 24 | // get target coupling from charactor's table like following format 25 | // <a>フェリシアーノ・ヴァルガス</a> 26 | // ... 27 | // |南伊|ロヴィフェリ|西|トニョフェリ| 28 | // ... 29 | const getCouplingFromCharactersTable = (title_tag) => { 30 | const current_character_link = title_tag.querySelector('a'); 31 | if (current_character_link === null) return []; 32 | 33 | const current_character_name = getDictTitleFromURL( 34 | current_character_link.href 35 | ); 36 | const current_character = characters.find( 37 | (x) => x.name === current_character_name 38 | ); 39 | if (current_character === undefined) return []; 40 | 41 | const target_table = getNextMatchElement( 42 | title_tag, 43 | 'div#article-table' 44 | ).querySelector('table'); 45 | const couplings = Array.from(target_table.querySelectorAll('tr')) 46 | .map((x) => Array.from(x.querySelectorAll('th,td'))) 47 | .map((x) => [x.slice(0, 2), x.slice(2, 4)]) 48 | .reduce((s, x) => [...s, ...x]) 49 | .filter((x) => x.length === 2) 50 | .map((x) => { 51 | const short_country_name = x[0].innerText; 52 | const character = characters.find( 53 | (x) => x.short_country_name === short_country_name 54 | ); 55 | if (character === undefined) return undefined; 56 | 57 | const tag_name = getDictTitleFromURL(x[1].querySelector('a').href); 58 | 59 | const getShortName = (name) => name.split('・').shift(); 60 | return { 61 | characters: [ 62 | { 63 | name: `${current_character.country_name}(${getShortName( 64 | current_character.name 65 | )})`, 66 | dict_entry: current_character.name, 67 | }, 68 | { 69 | name: `${character.country_name}(${getShortName( 70 | character.name 71 | )})`, 72 | dict_entry: character.name, 73 | }, 74 | ], 75 | tags: [{ name: tag_name }], 76 | }; 77 | }) 78 | .filter((x) => x); 79 | 80 | return couplings; 81 | }; 82 | 83 | const title_tags = [ 84 | ...(location.href === 85 | 'https://dic.pixiv.net/a/%E3%83%98%E3%82%BF%E3%83%AA%E3%82%A2%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%93%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7' 86 | ? Array.from(document.querySelectorAll('article h3')) 87 | : []), 88 | ...(location.href === 89 | 'https://dic.pixiv.net/a/%E8%85%90%E5%90%91%E3%81%91%E3%83%98%E3%82%BF%E3%83%AA%E3%82%A2' 90 | ? Array.from(document.querySelectorAll('article p')).filter((x) => 91 | x.querySelector('b > a') 92 | ) 93 | : []), 94 | ]; 95 | const target_couplings = title_tags 96 | .map((x) => getCouplingFromCharactersTable(x)) 97 | .reduce((s, x) => [...s, ...x]); 98 | 99 | copy(JSON.stringify(target_couplings)); 100 | })(); 101 | -------------------------------------------------------------------------------- /scraping/target_couplings/imas/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%82%A2%E3%82%A4%E3%83%89%E3%83%AB%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | const getDictTitleFromURL = (url) => 6 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 7 | 8 | const getNextMatchElement = (start_element, selector_string) => { 9 | let current_element = start_element; 10 | do { 11 | current_element = current_element.nextSibling; 12 | } while ( 13 | current_element !== null && 14 | !current_element.matches(selector_string) 15 | ); 16 | 17 | return current_element; 18 | }; 19 | 20 | // get target coupling from charactor's table like following format 21 | // <a>春香</a> 22 | // ... 23 | // 24 | // 25 | // 26 | // 27 | // 33 | // 39 | // 47 | // 48 | // ... 49 | const getCouplingFromCharactorsTable = (title_tag) => { 50 | const current_charactor = getDictTitleFromURL( 51 | title_tag.querySelector('a').href 52 | ); 53 | 54 | const target_table = getNextMatchElement( 55 | title_tag, 56 | 'div#article-table' 57 | ).querySelector('table'); 58 | const couplings = Array.from(target_table.querySelectorAll('tr')) 59 | .map((x) => Array.from(x.querySelectorAll('th,td'))) 60 | .map((x) => [x.slice(0, 2), x.slice(2, 4)]) 61 | .reduce((s, x) => [...s, ...x]) 62 | .filter((x) => x.length === 2) 63 | .map((x) => ({ 64 | friend_link: x[0].querySelector('a'), 65 | tags_link: Array.from(x[1].querySelectorAll('a')), 66 | })) 67 | .filter((x) => x.friend_link !== null && x.tags_link.length !== 0) 68 | .map((x) => ({ 69 | characters: [ 70 | { name: current_charactor }, 71 | { name: getDictTitleFromURL(x.friend_link.href) }, 72 | ], 73 | tags: x.tags_link.map((x) => ({ name: getDictTitleFromURL(x) })), 74 | })); 75 | 76 | return couplings; 77 | }; 78 | 79 | const target_couplings = Array.from(document.querySelectorAll('h4')) 80 | .filter((x) => x.innerText.startsWith('【')) 81 | .map((x) => getCouplingFromCharactorsTable(x)) 82 | .reduce((s, x) => [...s, ...x]); 83 | 84 | copy(JSON.stringify(target_couplings)); 85 | })(); 86 | -------------------------------------------------------------------------------- /scraping/target_couplings/imas/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "characters": [{ "name": "天海春香" }, { "name": "如月千早" }], 4 | "tags": [{ "name": "はるちは" }] 5 | }, 6 | { 7 | "characters": [{ "name": "天海春香" }, { "name": "萩原雪歩" }], 8 | "tags": [{ "name": "はるゆき" }] 9 | }, 10 | { 11 | "characters": [{ "name": "天海春香" }, { "name": "高槻やよい" }], 12 | "tags": [{ "name": "はるやよ" }] 13 | }, 14 | { 15 | "characters": [{ "name": "天海春香" }, { "name": "水瀬伊織" }], 16 | "tags": [{ "name": "はるいお" }] 17 | }, 18 | { 19 | "characters": [{ "name": "天海春香" }, { "name": "菊地真" }], 20 | "tags": [{ "name": "はるまこ" }] 21 | }, 22 | { 23 | "characters": [{ "name": "天海春香" }, { "name": "双海亜美" }], 24 | "tags": [{ "name": "はるあみ" }] 25 | }, 26 | { 27 | "characters": [{ "name": "天海春香" }, { "name": "星井美希" }], 28 | "tags": [{ "name": "はるみき" }] 29 | }, 30 | { 31 | "characters": [{ "name": "天海春香" }, { "name": "我那覇響" }], 32 | "tags": [{ "name": "がなはる" }] 33 | }, 34 | { 35 | "characters": [{ "name": "天海春香" }, { "name": "四条貴音" }], 36 | "tags": [{ "name": "たかはる" }] 37 | }, 38 | { 39 | "characters": [{ "name": "天海春香" }, { "name": "日高愛" }], 40 | "tags": [{ "name": "あいはる" }] 41 | }, 42 | { 43 | "characters": [{ "name": "天海春香" }, { "name": "天ヶ瀬冬馬" }], 44 | "tags": [{ "name": "あまあまコンビ" }] 45 | }, 46 | { 47 | "characters": [{ "name": "如月千早" }, { "name": "天海春香" }], 48 | "tags": [{ "name": "はるちは" }] 49 | }, 50 | { 51 | "characters": [{ "name": "如月千早" }, { "name": "萩原雪歩" }], 52 | "tags": [{ "name": "ちはゆき" }] 53 | }, 54 | { 55 | "characters": [{ "name": "如月千早" }, { "name": "高槻やよい" }], 56 | "tags": [{ "name": "ちはやよい" }] 57 | }, 58 | { 59 | "characters": [{ "name": "如月千早" }, { "name": "三浦あずさ" }], 60 | "tags": [{ "name": "あずちは" }] 61 | }, 62 | { 63 | "characters": [{ "name": "如月千早" }, { "name": "菊地真" }], 64 | "tags": [{ "name": "ちはまこ" }] 65 | }, 66 | { 67 | "characters": [{ "name": "如月千早" }, { "name": "双海真美" }], 68 | "tags": [{ "name": "まみちは" }] 69 | }, 70 | { 71 | "characters": [{ "name": "如月千早" }, { "name": "星井美希" }], 72 | "tags": [{ "name": "ちはみき" }] 73 | }, 74 | { 75 | "characters": [{ "name": "如月千早" }, { "name": "我那覇響" }], 76 | "tags": [{ "name": "ちはひび" }] 77 | }, 78 | { 79 | "characters": [{ "name": "如月千早" }, { "name": "四条貴音" }], 80 | "tags": [{ "name": "たかちは" }] 81 | }, 82 | { 83 | "characters": [{ "name": "如月千早" }, { "name": "秋月涼" }], 84 | "tags": [{ "name": "ちはりょう" }] 85 | }, 86 | { 87 | "characters": [{ "name": "如月千早" }, { "name": "伊集院北斗" }], 88 | "tags": [{ "name": "ほくちは" }] 89 | }, 90 | { 91 | "characters": [{ "name": "萩原雪歩" }, { "name": "天海春香" }], 92 | "tags": [{ "name": "はるゆき" }] 93 | }, 94 | { 95 | "characters": [{ "name": "萩原雪歩" }, { "name": "如月千早" }], 96 | "tags": [{ "name": "ちはゆき" }] 97 | }, 98 | { 99 | "characters": [{ "name": "萩原雪歩" }, { "name": "高槻やよい" }], 100 | "tags": [{ "name": "やよゆき" }] 101 | }, 102 | { 103 | "characters": [{ "name": "萩原雪歩" }, { "name": "秋月律子" }], 104 | "tags": [{ "name": "ゆきりつ" }] 105 | }, 106 | { 107 | "characters": [{ "name": "萩原雪歩" }, { "name": "三浦あずさ" }], 108 | "tags": [{ "name": "あずゆき" }] 109 | }, 110 | { 111 | "characters": [{ "name": "萩原雪歩" }, { "name": "水瀬伊織" }], 112 | "tags": [{ "name": "いおゆき" }] 113 | }, 114 | { 115 | "characters": [{ "name": "萩原雪歩" }, { "name": "菊地真" }], 116 | "tags": [{ "name": "ゆきまこ" }] 117 | }, 118 | { 119 | "characters": [{ "name": "萩原雪歩" }, { "name": "双海亜美" }], 120 | "tags": [{ "name": "ゆきとかち" }] 121 | }, 122 | { 123 | "characters": [{ "name": "萩原雪歩" }, { "name": "双海真美" }], 124 | "tags": [{ "name": "ゆきまみ" }, { "name": "ゆきとかち" }] 125 | }, 126 | { 127 | "characters": [{ "name": "萩原雪歩" }, { "name": "星井美希" }], 128 | "tags": [{ "name": "ゆきみき" }] 129 | }, 130 | { 131 | "characters": [{ "name": "萩原雪歩" }, { "name": "我那覇響" }], 132 | "tags": [{ "name": "ひびゆき" }] 133 | }, 134 | { 135 | "characters": [{ "name": "萩原雪歩" }, { "name": "四条貴音" }], 136 | "tags": [{ "name": "たかゆき" }] 137 | }, 138 | { 139 | "characters": [{ "name": "萩原雪歩" }, { "name": "日高愛" }], 140 | "tags": [{ "name": "あいゆき" }] 141 | }, 142 | { 143 | "characters": [{ "name": "萩原雪歩" }, { "name": "水谷絵理" }], 144 | "tags": [{ "name": "ゆきえり" }] 145 | }, 146 | { 147 | "characters": [{ "name": "萩原雪歩" }, { "name": "秋月涼" }], 148 | "tags": [{ "name": "りょうゆき" }] 149 | }, 150 | { 151 | "characters": [{ "name": "萩原雪歩" }, { "name": "天ヶ瀬冬馬" }], 152 | "tags": [{ "name": "とうゆき" }] 153 | }, 154 | { 155 | "characters": [{ "name": "高槻やよい" }, { "name": "天海春香" }], 156 | "tags": [{ "name": "はるやよ" }] 157 | }, 158 | { 159 | "characters": [{ "name": "高槻やよい" }, { "name": "如月千早" }], 160 | "tags": [{ "name": "ちはやよい" }] 161 | }, 162 | { 163 | "characters": [{ "name": "高槻やよい" }, { "name": "萩原雪歩" }], 164 | "tags": [{ "name": "やよゆき" }] 165 | }, 166 | { 167 | "characters": [{ "name": "高槻やよい" }, { "name": "秋月律子" }], 168 | "tags": [{ "name": "やよりつ" }] 169 | }, 170 | { 171 | "characters": [{ "name": "高槻やよい" }, { "name": "水瀬伊織" }], 172 | "tags": [{ "name": "やよいおり" }] 173 | }, 174 | { 175 | "characters": [{ "name": "高槻やよい" }, { "name": "菊地真" }], 176 | "tags": [{ "name": "やよまこ" }] 177 | }, 178 | { 179 | "characters": [{ "name": "高槻やよい" }, { "name": "双海亜美" }], 180 | "tags": [{ "name": "ロリチーム(im%40s)" }] 181 | }, 182 | { 183 | "characters": [{ "name": "高槻やよい" }, { "name": "双海真美" }], 184 | "tags": [{ "name": "ロリチーム(im%40s)" }] 185 | }, 186 | { 187 | "characters": [{ "name": "高槻やよい" }, { "name": "星井美希" }], 188 | "tags": [{ "name": "やよみき" }] 189 | }, 190 | { 191 | "characters": [{ "name": "高槻やよい" }, { "name": "我那覇響" }], 192 | "tags": [{ "name": "ひびやよ" }] 193 | }, 194 | { 195 | "characters": [{ "name": "高槻やよい" }, { "name": "御手洗翔太" }], 196 | "tags": [{ "name": "やよしょう" }] 197 | }, 198 | { 199 | "characters": [{ "name": "秋月律子" }, { "name": "萩原雪歩" }], 200 | "tags": [{ "name": "ゆきりつ" }] 201 | }, 202 | { 203 | "characters": [{ "name": "秋月律子" }, { "name": "高槻やよい" }], 204 | "tags": [{ "name": "やよりつ" }] 205 | }, 206 | { 207 | "characters": [{ "name": "秋月律子" }, { "name": "三浦あずさ" }], 208 | "tags": [{ "name": "あずりつ" }] 209 | }, 210 | { 211 | "characters": [{ "name": "秋月律子" }, { "name": "水瀬伊織" }], 212 | "tags": [{ "name": "いおりつこ" }] 213 | }, 214 | { 215 | "characters": [{ "name": "秋月律子" }, { "name": "菊地真" }], 216 | "tags": [{ "name": "まこりつ(アイマス)" }] 217 | }, 218 | { 219 | "characters": [{ "name": "秋月律子" }, { "name": "星井美希" }], 220 | "tags": [{ "name": "みきりつ" }] 221 | }, 222 | { 223 | "characters": [{ "name": "秋月律子" }, { "name": "秋月涼" }], 224 | "tags": [{ "name": "りょうりつ" }] 225 | }, 226 | { 227 | "characters": [{ "name": "秋月律子" }, { "name": "音無小鳥" }], 228 | "tags": [{ "name": "ことりつこ" }] 229 | }, 230 | { 231 | "characters": [{ "name": "三浦あずさ" }, { "name": "如月千早" }], 232 | "tags": [{ "name": "あずちは" }] 233 | }, 234 | { 235 | "characters": [{ "name": "三浦あずさ" }, { "name": "萩原雪歩" }], 236 | "tags": [{ "name": "あずゆき" }] 237 | }, 238 | { 239 | "characters": [{ "name": "三浦あずさ" }, { "name": "秋月律子" }], 240 | "tags": [{ "name": "あずりつ" }] 241 | }, 242 | { 243 | "characters": [{ "name": "三浦あずさ" }, { "name": "水瀬伊織" }], 244 | "tags": [{ "name": "あずいお" }] 245 | }, 246 | { 247 | "characters": [{ "name": "三浦あずさ" }, { "name": "菊地真" }], 248 | "tags": [{ "name": "あずまこ" }] 249 | }, 250 | { 251 | "characters": [{ "name": "三浦あずさ" }, { "name": "四条貴音" }], 252 | "tags": [{ "name": "あずたか" }] 253 | }, 254 | { 255 | "characters": [{ "name": "水瀬伊織" }, { "name": "天海春香" }], 256 | "tags": [{ "name": "はるいお" }] 257 | }, 258 | { 259 | "characters": [{ "name": "水瀬伊織" }, { "name": "萩原雪歩" }], 260 | "tags": [{ "name": "いおゆき" }] 261 | }, 262 | { 263 | "characters": [{ "name": "水瀬伊織" }, { "name": "高槻やよい" }], 264 | "tags": [{ "name": "やよいおり" }] 265 | }, 266 | { 267 | "characters": [{ "name": "水瀬伊織" }, { "name": "秋月律子" }], 268 | "tags": [{ "name": "いおりつこ" }] 269 | }, 270 | { 271 | "characters": [{ "name": "水瀬伊織" }, { "name": "三浦あずさ" }], 272 | "tags": [{ "name": "あずいお" }] 273 | }, 274 | { 275 | "characters": [{ "name": "水瀬伊織" }, { "name": "菊地真" }], 276 | "tags": [{ "name": "いおまこ" }] 277 | }, 278 | { 279 | "characters": [{ "name": "水瀬伊織" }, { "name": "星井美希" }], 280 | "tags": [{ "name": "みきいお" }] 281 | }, 282 | { 283 | "characters": [{ "name": "水瀬伊織" }, { "name": "四条貴音" }], 284 | "tags": [{ "name": "たかいお" }] 285 | }, 286 | { 287 | "characters": [{ "name": "菊地真" }, { "name": "天海春香" }], 288 | "tags": [{ "name": "はるまこ" }] 289 | }, 290 | { 291 | "characters": [{ "name": "菊地真" }, { "name": "如月千早" }], 292 | "tags": [{ "name": "ちはまこ" }] 293 | }, 294 | { 295 | "characters": [{ "name": "菊地真" }, { "name": "萩原雪歩" }], 296 | "tags": [{ "name": "ゆきまこ" }] 297 | }, 298 | { 299 | "characters": [{ "name": "菊地真" }, { "name": "高槻やよい" }], 300 | "tags": [{ "name": "やよまこ" }] 301 | }, 302 | { 303 | "characters": [{ "name": "菊地真" }, { "name": "秋月律子" }], 304 | "tags": [{ "name": "りつまこ" }] 305 | }, 306 | { 307 | "characters": [{ "name": "菊地真" }, { "name": "三浦あずさ" }], 308 | "tags": [{ "name": "あずまこ" }] 309 | }, 310 | { 311 | "characters": [{ "name": "菊地真" }, { "name": "水瀬伊織" }], 312 | "tags": [{ "name": "いおまこ" }] 313 | }, 314 | { 315 | "characters": [{ "name": "菊地真" }, { "name": "星井美希" }], 316 | "tags": [{ "name": "みきまこ" }] 317 | }, 318 | { 319 | "characters": [{ "name": "菊地真" }, { "name": "我那覇響" }], 320 | "tags": [{ "name": "ひびまこ" }] 321 | }, 322 | { 323 | "characters": [{ "name": "菊地真" }, { "name": "四条貴音" }], 324 | "tags": [{ "name": "たかまこ" }] 325 | }, 326 | { 327 | "characters": [{ "name": "菊地真" }, { "name": "日高愛" }], 328 | "tags": [{ "name": "あいまこ" }] 329 | }, 330 | { 331 | "characters": [{ "name": "菊地真" }, { "name": "秋月涼" }], 332 | "tags": [{ "name": "りょうまこ" }] 333 | }, 334 | { 335 | "characters": [{ "name": "菊地真" }, { "name": "伊集院北斗" }], 336 | "tags": [{ "name": "ほくまこ" }] 337 | }, 338 | { 339 | "characters": [{ "name": "菊地真" }, { "name": "音無小鳥" }], 340 | "tags": [{ "name": "まことり" }, { "name": "ぴよまこ" }] 341 | }, 342 | { 343 | "characters": [{ "name": "双海亜美" }, { "name": "天海春香" }], 344 | "tags": [{ "name": "はるあみ" }] 345 | }, 346 | { 347 | "characters": [{ "name": "双海亜美" }, { "name": "高槻やよい" }], 348 | "tags": [{ "name": "ロリチーム(im%40s)" }] 349 | }, 350 | { 351 | "characters": [{ "name": "双海亜美" }, { "name": "双海真美" }], 352 | "tags": [{ "name": "亜美真美" }, { "name": "あみまみ" }] 353 | }, 354 | { 355 | "characters": [{ "name": "双海真美" }, { "name": "如月千早" }], 356 | "tags": [{ "name": "まみちは" }] 357 | }, 358 | { 359 | "characters": [{ "name": "双海真美" }, { "name": "萩原雪歩" }], 360 | "tags": [{ "name": "まみゆき" }, { "name": "ゆきまみ" }] 361 | }, 362 | { 363 | "characters": [{ "name": "双海真美" }, { "name": "高槻やよい" }], 364 | "tags": [{ "name": "ロリチーム(im%40s)" }] 365 | }, 366 | { 367 | "characters": [{ "name": "双海真美" }, { "name": "双海亜美" }], 368 | "tags": [{ "name": "亜美真美" }, { "name": "あみまみ" }] 369 | }, 370 | { 371 | "characters": [{ "name": "星井美希" }, { "name": "天海春香" }], 372 | "tags": [{ "name": "はるみき" }] 373 | }, 374 | { 375 | "characters": [{ "name": "星井美希" }, { "name": "如月千早" }], 376 | "tags": [{ "name": "ちはみき" }] 377 | }, 378 | { 379 | "characters": [{ "name": "星井美希" }, { "name": "萩原雪歩" }], 380 | "tags": [{ "name": "みきゆき" }] 381 | }, 382 | { 383 | "characters": [{ "name": "星井美希" }, { "name": "高槻やよい" }], 384 | "tags": [{ "name": "やよみき" }] 385 | }, 386 | { 387 | "characters": [{ "name": "星井美希" }, { "name": "秋月律子" }], 388 | "tags": [{ "name": "みきりつ" }] 389 | }, 390 | { 391 | "characters": [{ "name": "星井美希" }, { "name": "水瀬伊織" }], 392 | "tags": [{ "name": "みきいお" }] 393 | }, 394 | { 395 | "characters": [{ "name": "星井美希" }, { "name": "菊地真" }], 396 | "tags": [{ "name": "みきまこ" }] 397 | }, 398 | { 399 | "characters": [{ "name": "星井美希" }, { "name": "我那覇響" }], 400 | "tags": [{ "name": "ひびみき" }] 401 | }, 402 | { 403 | "characters": [{ "name": "星井美希" }, { "name": "天ヶ瀬冬馬" }], 404 | "tags": [{ "name": "Wアホ毛" }] 405 | }, 406 | { 407 | "characters": [{ "name": "我那覇響" }, { "name": "天海春香" }], 408 | "tags": [{ "name": "がなはる" }] 409 | }, 410 | { 411 | "characters": [{ "name": "我那覇響" }, { "name": "如月千早" }], 412 | "tags": [{ "name": "ちはひび" }] 413 | }, 414 | { 415 | "characters": [{ "name": "我那覇響" }, { "name": "萩原雪歩" }], 416 | "tags": [{ "name": "ひびゆき" }] 417 | }, 418 | { 419 | "characters": [{ "name": "我那覇響" }, { "name": "高槻やよい" }], 420 | "tags": [{ "name": "ひびやよ" }] 421 | }, 422 | { 423 | "characters": [{ "name": "我那覇響" }, { "name": "菊地真" }], 424 | "tags": [{ "name": "ひびまこ" }] 425 | }, 426 | { 427 | "characters": [{ "name": "我那覇響" }, { "name": "星井美希" }], 428 | "tags": [{ "name": "ひびみき" }] 429 | }, 430 | { 431 | "characters": [{ "name": "我那覇響" }, { "name": "四条貴音" }], 432 | "tags": [{ "name": "ひびたか" }] 433 | }, 434 | { 435 | "characters": [{ "name": "我那覇響" }, { "name": "天ヶ瀬冬馬" }], 436 | "tags": [{ "name": "とうひび" }] 437 | }, 438 | { 439 | "characters": [{ "name": "四条貴音" }, { "name": "天海春香" }], 440 | "tags": [{ "name": "たかはる" }] 441 | }, 442 | { 443 | "characters": [{ "name": "四条貴音" }, { "name": "如月千早" }], 444 | "tags": [{ "name": "たかちは" }] 445 | }, 446 | { 447 | "characters": [{ "name": "四条貴音" }, { "name": "萩原雪歩" }], 448 | "tags": [{ "name": "たかゆき" }] 449 | }, 450 | { 451 | "characters": [{ "name": "四条貴音" }, { "name": "三浦あずさ" }], 452 | "tags": [{ "name": "あずたか" }] 453 | }, 454 | { 455 | "characters": [{ "name": "四条貴音" }, { "name": "水瀬伊織" }], 456 | "tags": [{ "name": "たかいお" }] 457 | }, 458 | { 459 | "characters": [{ "name": "四条貴音" }, { "name": "菊地真" }], 460 | "tags": [{ "name": "たかまこ" }] 461 | }, 462 | { 463 | "characters": [{ "name": "四条貴音" }, { "name": "我那覇響" }], 464 | "tags": [{ "name": "ひびたか" }] 465 | }, 466 | { 467 | "characters": [{ "name": "日高愛" }, { "name": "天海春香" }], 468 | "tags": [{ "name": "あいはる" }] 469 | }, 470 | { 471 | "characters": [{ "name": "日高愛" }, { "name": "萩原雪歩" }], 472 | "tags": [{ "name": "あいゆき" }] 473 | }, 474 | { 475 | "characters": [{ "name": "日高愛" }, { "name": "菊地真" }], 476 | "tags": [{ "name": "あいまこ" }] 477 | }, 478 | { 479 | "characters": [{ "name": "日高愛" }, { "name": "水谷絵理" }], 480 | "tags": [{ "name": "あいえり" }] 481 | }, 482 | { 483 | "characters": [{ "name": "日高愛" }, { "name": "秋月涼" }], 484 | "tags": [{ "name": "りょうあい" }] 485 | }, 486 | { 487 | "characters": [{ "name": "日高愛" }, { "name": "天ヶ瀬冬馬" }], 488 | "tags": [{ "name": "あいとう" }] 489 | }, 490 | { 491 | "characters": [{ "name": "水谷絵理" }, { "name": "萩原雪歩" }], 492 | "tags": [{ "name": "ゆきえり" }] 493 | }, 494 | { 495 | "characters": [{ "name": "水谷絵理" }, { "name": "日高愛" }], 496 | "tags": [{ "name": "あいえり" }] 497 | }, 498 | { 499 | "characters": [{ "name": "水谷絵理" }, { "name": "秋月涼" }], 500 | "tags": [{ "name": "りょうえり" }] 501 | }, 502 | { 503 | "characters": [{ "name": "秋月涼" }, { "name": "如月千早" }], 504 | "tags": [{ "name": "ちはりょう" }] 505 | }, 506 | { 507 | "characters": [{ "name": "秋月涼" }, { "name": "萩原雪歩" }], 508 | "tags": [{ "name": "りょうゆき" }] 509 | }, 510 | { 511 | "characters": [{ "name": "秋月涼" }, { "name": "秋月律子" }], 512 | "tags": [{ "name": "りょうりつ" }] 513 | }, 514 | { 515 | "characters": [{ "name": "秋月涼" }, { "name": "菊地真" }], 516 | "tags": [{ "name": "りょうまこ" }] 517 | }, 518 | { 519 | "characters": [{ "name": "秋月涼" }, { "name": "日高愛" }], 520 | "tags": [{ "name": "りょうあい" }] 521 | }, 522 | { 523 | "characters": [{ "name": "秋月涼" }, { "name": "水谷絵理" }], 524 | "tags": [{ "name": "りょうえり" }] 525 | }, 526 | { 527 | "characters": [{ "name": "秋月涼" }, { "name": "天ヶ瀬冬馬" }], 528 | "tags": [{ "name": "りょうとう" }] 529 | }, 530 | { 531 | "characters": [{ "name": "秋月涼" }, { "name": "音無小鳥" }], 532 | "tags": [{ "name": "りょうぴよ" }] 533 | }, 534 | { 535 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "天海春香" }], 536 | "tags": [{ "name": "あまあまコンビ" }] 537 | }, 538 | { 539 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "萩原雪歩" }], 540 | "tags": [{ "name": "とうゆき" }] 541 | }, 542 | { 543 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "菊地真" }], 544 | "tags": [{ "name": "とうまこ" }] 545 | }, 546 | { 547 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "星井美希" }], 548 | "tags": [{ "name": "Wアホ毛" }] 549 | }, 550 | { 551 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "我那覇響" }], 552 | "tags": [{ "name": "とうひび" }] 553 | }, 554 | { 555 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "日高愛" }], 556 | "tags": [{ "name": "あいとう" }] 557 | }, 558 | { 559 | "characters": [{ "name": "天ヶ瀬冬馬" }, { "name": "秋月涼" }], 560 | "tags": [{ "name": "りょうとう" }] 561 | }, 562 | { 563 | "characters": [{ "name": "御手洗翔太" }, { "name": "高槻やよい" }], 564 | "tags": [{ "name": "やよしょう" }] 565 | }, 566 | { 567 | "characters": [{ "name": "御手洗翔太" }, { "name": "菊地真" }], 568 | "tags": [{ "name": "しょうまこ" }] 569 | }, 570 | { 571 | "characters": [{ "name": "伊集院北斗" }, { "name": "如月千早" }], 572 | "tags": [{ "name": "ほくちは" }] 573 | }, 574 | { 575 | "characters": [{ "name": "伊集院北斗" }, { "name": "菊地真" }], 576 | "tags": [{ "name": "ほくまこ" }] 577 | }, 578 | { 579 | "characters": [{ "name": "音無小鳥" }, { "name": "秋月律子" }], 580 | "tags": [{ "name": "ことりつこ" }] 581 | }, 582 | { 583 | "characters": [{ "name": "音無小鳥" }, { "name": "菊地真" }], 584 | "tags": [{ "name": "ぴよまこ" }, { "name": "まことり" }] 585 | }, 586 | { 587 | "characters": [{ "name": "音無小鳥" }, { "name": "秋月涼" }], 588 | "tags": [{ "name": "りょうぴよ" }] 589 | } 590 | ] 591 | -------------------------------------------------------------------------------- /scraping/target_couplings/kancolle/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E8%89%A6%E9%9A%8A%E3%81%93%E3%82%8C%E3%81%8F%E3%81%97%E3%82%87%E3%82%93%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | // utils 6 | const getDictTitleFromURL = (url) => 7 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 8 | 9 | // get target couplings from following table format 10 | //|カップリング名|前者の名前|後者の名前|...| 11 | //|金榛|金剛|榛名|...| 12 | const getTargetCouplings = (table) => 13 | Array.from(table.querySelectorAll('tr')) 14 | .map((x) => Array.from(x.querySelectorAll('td'))) 15 | .filter((x) => x.length !== 0) 16 | .map((x) => ({ 17 | characters: [x[1].querySelector('a'), x[2].querySelector('a')].map( 18 | (x) => ({ name: getDictTitleFromURL(x.href) }) 19 | ), 20 | tags: Array.from(x[0].querySelectorAll('a')) 21 | .map((x) => getDictTitleFromURL(x.href)) 22 | .map((x) => ({ name: x })), 23 | })); 24 | 25 | const target_couplings = [ 26 | ...Array.from(document.querySelectorAll('div#article-table > table')) 27 | .filter( 28 | (table) => table.querySelectorAll('tr:first-of-type > th').length === 4 29 | ) 30 | .map((x) => getTargetCouplings(x)) 31 | .reduce((s, x) => [...s, ...x]), 32 | ]; 33 | 34 | copy(JSON.stringify(target_couplings)); 35 | })(); 36 | -------------------------------------------------------------------------------- /scraping/target_couplings/lovelive/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%83%A9%E3%83%96%E3%83%A9%E3%82%A4%E3%83%96%21%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // https://dic.pixiv.net/a/%E3%83%A9%E3%83%96%E3%83%A9%E3%82%A4%E3%83%96%21%E3%82%B5%E3%83%B3%E3%82%B7%E3%83%A3%E3%82%A4%E3%83%B3%21%21%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 3 | // https://dic.pixiv.net/a/Aqours%C3%97%CE%BC%27s%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 4 | // usage: paste this script to javascript console 5 | 6 | (() => { 7 | // utils 8 | const getDictTitleFromURL = (url) => 9 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 10 | 11 | const getFullname = (name) => 12 | ({ 13 | 穂乃果: '高坂穂乃果', 14 | 絵里: '絢瀬絵里', 15 | ことり: '南ことり', 16 | 海未: '園田海', 17 | 凛: '星空凛', 18 | 真姫: '西木野真姫', 19 | 希: '東條希', 20 | 花陽: '小泉花陽', 21 | にこ: '矢澤にこ', 22 | 千歌: '高海千歌', 23 | 梨子: '桜内梨子', 24 | 果南: '松浦果南', 25 | ダイヤ: '黒澤ダイヤ', 26 | 曜: '渡辺曜', 27 | 善子: '津島善子', 28 | 花丸: '国木田花丸', 29 | 鞠莉: '小原鞠莉', 30 | ルビィ: '黒澤ルビィ', 31 | })[name]; 32 | 33 | // get couplings from table that like following format 34 | // | |穂乃果 |絵里 |... 35 | // |穂乃果| -- |ほのこと|... 36 | // |絵里 |えりほの| -- |... 37 | // ... 38 | const getTargetCouplings = (table) => { 39 | const header_characters = Array.from( 40 | table.querySelectorAll('tr:first-child > th') 41 | ) 42 | .map((x) => getFullname(x.innerText)) 43 | .filter((x) => x !== undefined); 44 | 45 | const data_table_rows = Array.from( 46 | table.querySelectorAll('tr:not(first-child)') 47 | ); 48 | return data_table_rows 49 | .map((row) => { 50 | const character = getFullname(row.querySelector('th').innerText); 51 | const coupling_cells = Array.from( 52 | row.querySelectorAll(':scope > *:not(:first-child)') 53 | ); 54 | 55 | return coupling_cells 56 | .map((x, i) => ({ 57 | characters: [{ name: character }, { name: header_characters[i] }], 58 | tags: Array.from(x.querySelectorAll('a')) 59 | .map((x) => ({ name: getDictTitleFromURL(x.href) })) 60 | .filter((x) => x.name !== ''), 61 | })) 62 | .filter((x) => x.tags.length !== 0); 63 | }) 64 | .reduce((s, x) => [...s, ...x]); 65 | }; 66 | 67 | // get target couplings 68 | const target_table = document.querySelector('article table'); 69 | const target_couplings = getTargetCouplings(target_table); 70 | copy(JSON.stringify(target_couplings)); 71 | })(); 72 | -------------------------------------------------------------------------------- /scraping/target_couplings/lovelive/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "characters": [{ "name": "高坂穂乃果" }, { "name": "絢瀬絵里" }], 4 | "tags": [{ "name": "ほのえり" }] 5 | }, 6 | { 7 | "characters": [{ "name": "高坂穂乃果" }, { "name": "南ことり" }], 8 | "tags": [{ "name": "ほのこと" }] 9 | }, 10 | { 11 | "characters": [{ "name": "高坂穂乃果" }, { "name": "園田海" }], 12 | "tags": [{ "name": "ほのうみ" }] 13 | }, 14 | { 15 | "characters": [{ "name": "高坂穂乃果" }, { "name": "星空凛" }], 16 | "tags": [{ "name": "ほのりん" }] 17 | }, 18 | { 19 | "characters": [{ "name": "高坂穂乃果" }, { "name": "西木野真姫" }], 20 | "tags": [{ "name": "ほのまき" }] 21 | }, 22 | { 23 | "characters": [{ "name": "高坂穂乃果" }, { "name": "東條希" }], 24 | "tags": [{ "name": "ほののぞ" }] 25 | }, 26 | { 27 | "characters": [{ "name": "高坂穂乃果" }, { "name": "小泉花陽" }], 28 | "tags": [{ "name": "ほのぱな" }] 29 | }, 30 | { 31 | "characters": [{ "name": "高坂穂乃果" }, { "name": "矢澤にこ" }], 32 | "tags": [{ "name": "ほのにこ" }] 33 | }, 34 | { 35 | "characters": [{ "name": "絢瀬絵里" }, { "name": "園田海" }], 36 | "tags": [{ "name": "えりうみ" }] 37 | }, 38 | { 39 | "characters": [{ "name": "絢瀬絵里" }, { "name": "星空凛" }], 40 | "tags": [{ "name": "えりりん(ラブライブ!)" }] 41 | }, 42 | { 43 | "characters": [{ "name": "絢瀬絵里" }, { "name": "西木野真姫" }], 44 | "tags": [{ "name": "えりまき" }] 45 | }, 46 | { 47 | "characters": [{ "name": "絢瀬絵里" }, { "name": "東條希" }], 48 | "tags": [{ "name": "えりのぞ" }] 49 | }, 50 | { 51 | "characters": [{ "name": "絢瀬絵里" }, { "name": "小泉花陽" }], 52 | "tags": [{ "name": "えりぱな" }] 53 | }, 54 | { 55 | "characters": [{ "name": "南ことり" }, { "name": "高坂穂乃果" }], 56 | "tags": [{ "name": "ことほの" }] 57 | }, 58 | { 59 | "characters": [{ "name": "南ことり" }, { "name": "絢瀬絵里" }], 60 | "tags": [{ "name": "ことえり(ラブライブ!)" }] 61 | }, 62 | { 63 | "characters": [{ "name": "南ことり" }, { "name": "園田海" }], 64 | "tags": [{ "name": "ことうみ" }] 65 | }, 66 | { 67 | "characters": [{ "name": "南ことり" }, { "name": "星空凛" }], 68 | "tags": [{ "name": "ことりん" }] 69 | }, 70 | { 71 | "characters": [{ "name": "南ことり" }, { "name": "西木野真姫" }], 72 | "tags": [{ "name": "ことまき" }] 73 | }, 74 | { 75 | "characters": [{ "name": "南ことり" }, { "name": "東條希" }], 76 | "tags": [{ "name": "ことのぞ" }] 77 | }, 78 | { 79 | "characters": [{ "name": "南ことり" }, { "name": "小泉花陽" }], 80 | "tags": [{ "name": "ことぱな" }] 81 | }, 82 | { 83 | "characters": [{ "name": "南ことり" }, { "name": "矢澤にこ" }], 84 | "tags": [{ "name": "ことにこ" }] 85 | }, 86 | { 87 | "characters": [{ "name": "園田海" }, { "name": "絢瀬絵里" }], 88 | "tags": [{ "name": "うみえり" }] 89 | }, 90 | { 91 | "characters": [{ "name": "園田海" }, { "name": "南ことり" }], 92 | "tags": [{ "name": "うみこと" }] 93 | }, 94 | { 95 | "characters": [{ "name": "園田海" }, { "name": "星空凛" }], 96 | "tags": [{ "name": "うみりん" }] 97 | }, 98 | { 99 | "characters": [{ "name": "園田海" }, { "name": "西木野真姫" }], 100 | "tags": [{ "name": "うみまき" }] 101 | }, 102 | { 103 | "characters": [{ "name": "園田海" }, { "name": "小泉花陽" }], 104 | "tags": [{ "name": "うみぱな" }] 105 | }, 106 | { 107 | "characters": [{ "name": "園田海" }, { "name": "矢澤にこ" }], 108 | "tags": [{ "name": "うみにこ" }] 109 | }, 110 | { 111 | "characters": [{ "name": "星空凛" }, { "name": "西木野真姫" }], 112 | "tags": [{ "name": "りんまき" }] 113 | }, 114 | { 115 | "characters": [{ "name": "星空凛" }, { "name": "小泉花陽" }], 116 | "tags": [{ "name": "りんぱな" }] 117 | }, 118 | { 119 | "characters": [{ "name": "西木野真姫" }, { "name": "園田海" }], 120 | "tags": [{ "name": "まきうみ" }] 121 | }, 122 | { 123 | "characters": [{ "name": "西木野真姫" }, { "name": "星空凛" }], 124 | "tags": [{ "name": "まきりん" }] 125 | }, 126 | { 127 | "characters": [{ "name": "西木野真姫" }, { "name": "小泉花陽" }], 128 | "tags": [{ "name": "まきぱな" }] 129 | }, 130 | { 131 | "characters": [{ "name": "西木野真姫" }, { "name": "矢澤にこ" }], 132 | "tags": [{ "name": "まきにこ" }] 133 | }, 134 | { 135 | "characters": [{ "name": "東條希" }, { "name": "絢瀬絵里" }], 136 | "tags": [{ "name": "のぞえり" }] 137 | }, 138 | { 139 | "characters": [{ "name": "東條希" }, { "name": "園田海" }], 140 | "tags": [{ "name": "のぞうみ" }] 141 | }, 142 | { 143 | "characters": [{ "name": "東條希" }, { "name": "星空凛" }], 144 | "tags": [{ "name": "のぞりん(ラブライブ)" }] 145 | }, 146 | { 147 | "characters": [{ "name": "東條希" }, { "name": "西木野真姫" }], 148 | "tags": [{ "name": "のぞまき" }] 149 | }, 150 | { 151 | "characters": [{ "name": "東條希" }, { "name": "小泉花陽" }], 152 | "tags": [{ "name": "のぞぱな" }] 153 | }, 154 | { 155 | "characters": [{ "name": "東條希" }, { "name": "矢澤にこ" }], 156 | "tags": [{ "name": "のぞにこ" }] 157 | }, 158 | { 159 | "characters": [{ "name": "小泉花陽" }, { "name": "園田海" }], 160 | "tags": [{ "name": "ぱなうみ" }] 161 | }, 162 | { 163 | "characters": [{ "name": "小泉花陽" }, { "name": "星空凛" }], 164 | "tags": [{ "name": "はなりん" }] 165 | }, 166 | { 167 | "characters": [{ "name": "小泉花陽" }, { "name": "矢澤にこ" }], 168 | "tags": [{ "name": "ぱなにこ" }] 169 | }, 170 | { 171 | "characters": [{ "name": "矢澤にこ" }, { "name": "絢瀬絵里" }], 172 | "tags": [{ "name": "にこえり" }] 173 | }, 174 | { 175 | "characters": [{ "name": "矢澤にこ" }, { "name": "南ことり" }], 176 | "tags": [{ "name": "にことり" }] 177 | }, 178 | { 179 | "characters": [{ "name": "矢澤にこ" }, { "name": "星空凛" }], 180 | "tags": [{ "name": "にこりん" }] 181 | }, 182 | { 183 | "characters": [{ "name": "矢澤にこ" }, { "name": "西木野真姫" }], 184 | "tags": [{ "name": "にこまき" }] 185 | }, 186 | { 187 | "characters": [{ "name": "矢澤にこ" }, { "name": "東條希" }], 188 | "tags": [{ "name": "にこのぞ" }] 189 | }, 190 | { 191 | "characters": [{ "name": "矢澤にこ" }, { "name": "小泉花陽" }], 192 | "tags": [{ "name": "にこぱな" }] 193 | }, 194 | { 195 | "characters": [{ "name": "高海千歌" }, { "name": "桜内梨子" }], 196 | "tags": [{ "name": "ちかりこ" }] 197 | }, 198 | { 199 | "characters": [{ "name": "高海千歌" }, { "name": "松浦果南" }], 200 | "tags": [{ "name": "ちかなん" }] 201 | }, 202 | { 203 | "characters": [{ "name": "高海千歌" }, { "name": "黒澤ダイヤ" }], 204 | "tags": [{ "name": "ちかダイ" }, { "name": "ちかだい" }] 205 | }, 206 | { 207 | "characters": [{ "name": "高海千歌" }, { "name": "渡辺曜" }], 208 | "tags": [{ "name": "ちかよう" }] 209 | }, 210 | { 211 | "characters": [{ "name": "高海千歌" }, { "name": "津島善子" }], 212 | "tags": [{ "name": "ちかよし" }] 213 | }, 214 | { 215 | "characters": [{ "name": "高海千歌" }, { "name": "国木田花丸" }], 216 | "tags": [{ "name": "ちかまる" }] 217 | }, 218 | { 219 | "characters": [{ "name": "高海千歌" }, { "name": "小原鞠莉" }], 220 | "tags": [{ "name": "ちかまり" }] 221 | }, 222 | { 223 | "characters": [{ "name": "高海千歌" }, { "name": "黒澤ルビィ" }], 224 | "tags": [{ "name": "ちかるび" }, { "name": "ちかルビ" }] 225 | }, 226 | { 227 | "characters": [{ "name": "桜内梨子" }, { "name": "津島善子" }], 228 | "tags": [{ "name": "りこよし" }] 229 | }, 230 | { 231 | "characters": [{ "name": "桜内梨子" }, { "name": "国木田花丸" }], 232 | "tags": [{ "name": "りこまる" }] 233 | }, 234 | { 235 | "characters": [{ "name": "桜内梨子" }, { "name": "小原鞠莉" }], 236 | "tags": [{ "name": "りこまり" }] 237 | }, 238 | { 239 | "characters": [{ "name": "桜内梨子" }, { "name": "黒澤ルビィ" }], 240 | "tags": [{ "name": "りこるび" }] 241 | }, 242 | { 243 | "characters": [{ "name": "松浦果南" }, { "name": "桜内梨子" }], 244 | "tags": [{ "name": "かなりこ" }] 245 | }, 246 | { 247 | "characters": [{ "name": "松浦果南" }, { "name": "黒澤ダイヤ" }], 248 | "tags": [{ "name": "かなダイ" }] 249 | }, 250 | { 251 | "characters": [{ "name": "松浦果南" }, { "name": "渡辺曜" }], 252 | "tags": [{ "name": "ようかな" }] 253 | }, 254 | { 255 | "characters": [{ "name": "松浦果南" }, { "name": "津島善子" }], 256 | "tags": [{ "name": "かなよし" }] 257 | }, 258 | { 259 | "characters": [{ "name": "松浦果南" }, { "name": "国木田花丸" }], 260 | "tags": [{ "name": "かなまる" }, { "name": "なんまる" }] 261 | }, 262 | { 263 | "characters": [{ "name": "松浦果南" }, { "name": "小原鞠莉" }], 264 | "tags": [{ "name": "かなまり" }] 265 | }, 266 | { 267 | "characters": [{ "name": "松浦果南" }, { "name": "黒澤ルビィ" }], 268 | "tags": [{ "name": "かなルビ" }] 269 | }, 270 | { 271 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "高海千歌" }], 272 | "tags": [{ "name": "だいちか" }] 273 | }, 274 | { 275 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "桜内梨子" }], 276 | "tags": [{ "name": "ダイりこ" }, { "name": "だいりこ" }] 277 | }, 278 | { 279 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "松浦果南" }], 280 | "tags": [{ "name": "だいなん" }] 281 | }, 282 | { 283 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "渡辺曜" }], 284 | "tags": [{ "name": "ダイよう" }] 285 | }, 286 | { 287 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "津島善子" }], 288 | "tags": [{ "name": "だいよし" }] 289 | }, 290 | { 291 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "国木田花丸" }], 292 | "tags": [{ "name": "ダイまる" }] 293 | }, 294 | { 295 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "小原鞠莉" }], 296 | "tags": [{ "name": "ダイマリ" }, { "name": "だいまり" }] 297 | }, 298 | { 299 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "黒澤ルビィ" }], 300 | "tags": [ 301 | { "name": "黒澤姉妹" }, 302 | { "name": "ダイルビ(ラブライブ!サンシャイン!!)" } 303 | ] 304 | }, 305 | { 306 | "characters": [{ "name": "渡辺曜" }, { "name": "高海千歌" }], 307 | "tags": [{ "name": "ようちか" }] 308 | }, 309 | { 310 | "characters": [{ "name": "渡辺曜" }, { "name": "桜内梨子" }], 311 | "tags": [{ "name": "ようりこ" }] 312 | }, 313 | { 314 | "characters": [{ "name": "渡辺曜" }, { "name": "松浦果南" }], 315 | "tags": [{ "name": "ようかな" }] 316 | }, 317 | { 318 | "characters": [{ "name": "渡辺曜" }, { "name": "津島善子" }], 319 | "tags": [{ "name": "ようよし" }] 320 | }, 321 | { 322 | "characters": [{ "name": "渡辺曜" }, { "name": "国木田花丸" }], 323 | "tags": [{ "name": "ようまる" }] 324 | }, 325 | { 326 | "characters": [{ "name": "渡辺曜" }, { "name": "小原鞠莉" }], 327 | "tags": [{ "name": "ようまり" }] 328 | }, 329 | { 330 | "characters": [{ "name": "渡辺曜" }, { "name": "黒澤ルビィ" }], 331 | "tags": [{ "name": "ようルビ" }] 332 | }, 333 | { 334 | "characters": [{ "name": "津島善子" }, { "name": "桜内梨子" }], 335 | "tags": [ 336 | { "name": "よしりこ" }, 337 | { "name": "ヨハリリ" }, 338 | { "name": "ヨハりこ" } 339 | ] 340 | }, 341 | { 342 | "characters": [{ "name": "津島善子" }, { "name": "国木田花丸" }], 343 | "tags": [{ "name": "よしまる" }, { "name": "ヨハマル" }] 344 | }, 345 | { 346 | "characters": [{ "name": "津島善子" }, { "name": "小原鞠莉" }], 347 | "tags": [{ "name": "よしまり" }, { "name": "ヨハマリ" }] 348 | }, 349 | { 350 | "characters": [{ "name": "津島善子" }, { "name": "黒澤ルビィ" }], 351 | "tags": [{ "name": "よしるび" }, { "name": "よしルビ" }] 352 | }, 353 | { 354 | "characters": [{ "name": "国木田花丸" }, { "name": "黒澤ダイヤ" }], 355 | "tags": [{ "name": "まるだい" }] 356 | }, 357 | { 358 | "characters": [{ "name": "国木田花丸" }, { "name": "黒澤ルビィ" }], 359 | "tags": [{ "name": "はなまるびぃ" }] 360 | }, 361 | { 362 | "characters": [{ "name": "小原鞠莉" }, { "name": "桜内梨子" }], 363 | "tags": [{ "name": "まりこ(ラブライブ!サンシャイン!!)" }] 364 | }, 365 | { 366 | "characters": [{ "name": "小原鞠莉" }, { "name": "黒澤ダイヤ" }], 367 | "tags": [{ "name": "まりだい" }] 368 | }, 369 | { 370 | "characters": [{ "name": "小原鞠莉" }, { "name": "国木田花丸" }], 371 | "tags": [{ "name": "まりまる" }] 372 | }, 373 | { 374 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "桜内梨子" }], 375 | "tags": [{ "name": "るびりこ" }] 376 | }, 377 | { 378 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "国木田花丸" }], 379 | "tags": [{ "name": "ルビまる" }] 380 | }, 381 | { 382 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "小原鞠莉" }], 383 | "tags": [{ "name": "ルビまり" }] 384 | }, 385 | { 386 | "characters": [{ "name": "高海千歌" }, { "name": "高坂穂乃果" }], 387 | "tags": [{ "name": "ほのちか" }] 388 | }, 389 | { 390 | "characters": [{ "name": "高海千歌" }, { "name": "絢瀬絵里" }], 391 | "tags": [{ "name": "えりちか" }] 392 | }, 393 | { 394 | "characters": [{ "name": "高海千歌" }, { "name": "園田海" }], 395 | "tags": [{ "name": "ちかうみ" }] 396 | }, 397 | { 398 | "characters": [{ "name": "高海千歌" }, { "name": "星空凛" }], 399 | "tags": [{ "name": "りんちか" }] 400 | }, 401 | { 402 | "characters": [{ "name": "高海千歌" }, { "name": "小泉花陽" }], 403 | "tags": [{ "name": "ちかぱな" }] 404 | }, 405 | { 406 | "characters": [{ "name": "桜内梨子" }, { "name": "高坂穂乃果" }], 407 | "tags": [{ "name": "ほのりこ" }] 408 | }, 409 | { 410 | "characters": [{ "name": "桜内梨子" }, { "name": "絢瀬絵里" }], 411 | "tags": [{ "name": "えりりこ" }] 412 | }, 413 | { 414 | "characters": [{ "name": "桜内梨子" }, { "name": "南ことり" }], 415 | "tags": [{ "name": "ことりこ" }] 416 | }, 417 | { 418 | "characters": [{ "name": "桜内梨子" }, { "name": "園田海" }], 419 | "tags": [{ "name": "うみりこ" }, { "name": "りこうみ" }] 420 | }, 421 | { 422 | "characters": [{ "name": "桜内梨子" }, { "name": "西木野真姫" }], 423 | "tags": [{ "name": "りこまき" }, { "name": "まきりこ" }] 424 | }, 425 | { 426 | "characters": [{ "name": "桜内梨子" }, { "name": "小泉花陽" }], 427 | "tags": [{ "name": "ぱなりこ" }] 428 | }, 429 | { 430 | "characters": [{ "name": "桜内梨子" }, { "name": "矢澤にこ" }], 431 | "tags": [{ "name": "にこりこ" }] 432 | }, 433 | { 434 | "characters": [{ "name": "松浦果南" }, { "name": "高坂穂乃果" }], 435 | "tags": [{ "name": "ほのなん" }] 436 | }, 437 | { 438 | "characters": [{ "name": "松浦果南" }, { "name": "絢瀬絵里" }], 439 | "tags": [{ "name": "えりなん" }] 440 | }, 441 | { 442 | "characters": [{ "name": "松浦果南" }, { "name": "南ことり" }], 443 | "tags": [{ "name": "ことなん" }] 444 | }, 445 | { 446 | "characters": [{ "name": "松浦果南" }, { "name": "園田海" }], 447 | "tags": [{ "name": "うみなん" }, { "name": "かなうみ" }] 448 | }, 449 | { 450 | "characters": [{ "name": "松浦果南" }, { "name": "星空凛" }], 451 | "tags": [{ "name": "りんかな" }] 452 | }, 453 | { 454 | "characters": [{ "name": "松浦果南" }, { "name": "西木野真姫" }], 455 | "tags": [{ "name": "かなまき" }] 456 | }, 457 | { 458 | "characters": [{ "name": "松浦果南" }, { "name": "東條希" }], 459 | "tags": [{ "name": "のぞなん" }] 460 | }, 461 | { 462 | "characters": [{ "name": "松浦果南" }, { "name": "小泉花陽" }], 463 | "tags": [{ "name": "はななん" }] 464 | }, 465 | { 466 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "絢瀬絵里" }], 467 | "tags": [{ "name": "えりだい" }, { "name": "だいえり" }] 468 | }, 469 | { 470 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "園田海" }], 471 | "tags": [{ "name": "うみだい" }] 472 | }, 473 | { 474 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "西木野真姫" }], 475 | "tags": [{ "name": "まきだい" }] 476 | }, 477 | { 478 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "小泉花陽" }], 479 | "tags": [{ "name": "ぱなだい" }, { "name": "かよだい" }] 480 | }, 481 | { 482 | "characters": [{ "name": "黒澤ダイヤ" }, { "name": "矢澤にこ" }], 483 | "tags": [{ "name": "にこだい" }] 484 | }, 485 | { 486 | "characters": [{ "name": "渡辺曜" }, { "name": "高坂穂乃果" }], 487 | "tags": [{ "name": "ほのよう" }] 488 | }, 489 | { 490 | "characters": [{ "name": "渡辺曜" }, { "name": "絢瀬絵里" }], 491 | "tags": [{ "name": "えりよう" }] 492 | }, 493 | { 494 | "characters": [{ "name": "渡辺曜" }, { "name": "南ことり" }], 495 | "tags": [{ "name": "ようこと" }, { "name": "ことよう" }] 496 | }, 497 | { 498 | "characters": [{ "name": "渡辺曜" }, { "name": "園田海" }], 499 | "tags": [{ "name": "うみよう" }] 500 | }, 501 | { 502 | "characters": [{ "name": "渡辺曜" }, { "name": "星空凛" }], 503 | "tags": [{ "name": "りんよう" }] 504 | }, 505 | { 506 | "characters": [{ "name": "渡辺曜" }, { "name": "西木野真姫" }], 507 | "tags": [{ "name": "ようまき" }] 508 | }, 509 | { 510 | "characters": [{ "name": "渡辺曜" }, { "name": "東條希" }], 511 | "tags": [{ "name": "のぞよう" }] 512 | }, 513 | { 514 | "characters": [{ "name": "渡辺曜" }, { "name": "小泉花陽" }], 515 | "tags": [{ "name": "はなよう" }] 516 | }, 517 | { 518 | "characters": [{ "name": "渡辺曜" }, { "name": "矢澤にこ" }], 519 | "tags": [{ "name": "にこよう" }, { "name": "ようにこ" }] 520 | }, 521 | { 522 | "characters": [{ "name": "津島善子" }, { "name": "高坂穂乃果" }], 523 | "tags": [{ "name": "ほのよし" }] 524 | }, 525 | { 526 | "characters": [{ "name": "津島善子" }, { "name": "絢瀬絵里" }], 527 | "tags": [{ "name": "えりよし" }] 528 | }, 529 | { 530 | "characters": [{ "name": "津島善子" }, { "name": "南ことり" }], 531 | "tags": [{ "name": "ことよし" }] 532 | }, 533 | { 534 | "characters": [{ "name": "津島善子" }, { "name": "園田海" }], 535 | "tags": [{ "name": "うみよし" }] 536 | }, 537 | { 538 | "characters": [{ "name": "津島善子" }, { "name": "星空凛" }], 539 | "tags": [{ "name": "りんよし" }] 540 | }, 541 | { 542 | "characters": [{ "name": "津島善子" }, { "name": "西木野真姫" }], 543 | "tags": [{ "name": "よしまき" }, { "name": "まきよし" }] 544 | }, 545 | { 546 | "characters": [{ "name": "津島善子" }, { "name": "東條希" }], 547 | "tags": [{ "name": "のぞよし" }] 548 | }, 549 | { 550 | "characters": [{ "name": "津島善子" }, { "name": "小泉花陽" }], 551 | "tags": [{ "name": "よしぱな" }] 552 | }, 553 | { 554 | "characters": [{ "name": "津島善子" }, { "name": "矢澤にこ" }], 555 | "tags": [{ "name": "にこよし" }, { "name": "よしにこ" }] 556 | }, 557 | { 558 | "characters": [{ "name": "国木田花丸" }, { "name": "高坂穂乃果" }], 559 | "tags": [{ "name": "ほのまる" }] 560 | }, 561 | { 562 | "characters": [{ "name": "国木田花丸" }, { "name": "絢瀬絵里" }], 563 | "tags": [{ "name": "えりまる" }] 564 | }, 565 | { 566 | "characters": [{ "name": "国木田花丸" }, { "name": "南ことり" }], 567 | "tags": [{ "name": "ことまる" }] 568 | }, 569 | { 570 | "characters": [{ "name": "国木田花丸" }, { "name": "園田海" }], 571 | "tags": [{ "name": "うみまる" }] 572 | }, 573 | { 574 | "characters": [{ "name": "国木田花丸" }, { "name": "星空凛" }], 575 | "tags": [{ "name": "りんまる" }] 576 | }, 577 | { 578 | "characters": [{ "name": "国木田花丸" }, { "name": "東條希" }], 579 | "tags": [{ "name": "のぞまる" }] 580 | }, 581 | { 582 | "characters": [{ "name": "国木田花丸" }, { "name": "小泉花陽" }], 583 | "tags": [{ "name": "ぱなまる" }, { "name": "まるぱな" }] 584 | }, 585 | { 586 | "characters": [{ "name": "国木田花丸" }, { "name": "矢澤にこ" }], 587 | "tags": [{ "name": "にこまる" }] 588 | }, 589 | { 590 | "characters": [{ "name": "小原鞠莉" }, { "name": "高坂穂乃果" }], 591 | "tags": [{ "name": "ほのまり" }] 592 | }, 593 | { 594 | "characters": [{ "name": "小原鞠莉" }, { "name": "絢瀬絵里" }], 595 | "tags": [{ "name": "えりまり" }, { "name": "まりえり" }] 596 | }, 597 | { 598 | "characters": [{ "name": "小原鞠莉" }, { "name": "南ことり" }], 599 | "tags": [{ "name": "ことまり" }] 600 | }, 601 | { 602 | "characters": [{ "name": "小原鞠莉" }, { "name": "園田海" }], 603 | "tags": [{ "name": "うみまり" }] 604 | }, 605 | { 606 | "characters": [{ "name": "小原鞠莉" }, { "name": "星空凛" }], 607 | "tags": [{ "name": "りんまり" }] 608 | }, 609 | { 610 | "characters": [{ "name": "小原鞠莉" }, { "name": "西木野真姫" }], 611 | "tags": [{ "name": "まきまり" }] 612 | }, 613 | { 614 | "characters": [{ "name": "小原鞠莉" }, { "name": "東條希" }], 615 | "tags": [{ "name": "のぞまり" }] 616 | }, 617 | { 618 | "characters": [{ "name": "小原鞠莉" }, { "name": "小泉花陽" }], 619 | "tags": [{ "name": "まりぱな" }] 620 | }, 621 | { 622 | "characters": [{ "name": "小原鞠莉" }, { "name": "矢澤にこ" }], 623 | "tags": [{ "name": "にこまり" }] 624 | }, 625 | { 626 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "高坂穂乃果" }], 627 | "tags": [{ "name": "ほのルビ" }] 628 | }, 629 | { 630 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "南ことり" }], 631 | "tags": [{ "name": "ことルビ" }] 632 | }, 633 | { 634 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "園田海" }], 635 | "tags": [{ "name": "うみるび" }] 636 | }, 637 | { 638 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "星空凛" }], 639 | "tags": [{ "name": "りんルビ" }] 640 | }, 641 | { 642 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "西木野真姫" }], 643 | "tags": [{ "name": "ルビまき" }] 644 | }, 645 | { 646 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "東條希" }], 647 | "tags": [{ "name": "のぞるび" }] 648 | }, 649 | { 650 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "小泉花陽" }], 651 | "tags": [{ "name": "るびぱな" }, { "name": "ルビぱな" }] 652 | }, 653 | { 654 | "characters": [{ "name": "黒澤ルビィ" }, { "name": "矢澤にこ" }], 655 | "tags": [{ "name": "にこルビ" }] 656 | } 657 | ] 658 | -------------------------------------------------------------------------------- /scraping/target_couplings/revengers/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E6%9D%B1%E4%BA%AC%E3%80%90%E8%85%90%E3%80%91%E3%83%AA%E3%83%99%E3%83%B3%E3%82%B8%E3%83%A3%E3%83%BC%E3%82%BA 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | const shortNameMap = { 6 | タケ: '花垣武道', 7 | 武: '花垣武道', 8 | ナオ: '橘直人', 9 | マイ: '佐野万次郎', 10 | ドラ: '龍宮寺堅', 11 | バジ: '場地圭介', 12 | ばじ: '場地圭介', 13 | ふゆ: '松野千冬', 14 | みつ: '三ツ谷隆', 15 | はち: '柴八戒', 16 | パー: '林田春樹', 17 | ペー: '林良平', 18 | ナホ: '河田ナホヤ', 19 | ソヤ: '河田ソウヤ', 20 | ム: '武藤泰宏', 21 | 春: '三途春千夜', 22 | はる: '三途春千夜', 23 | 三: '三途春千夜', 24 | キサ: '稀咲鉄太', 25 | 稀: '稀咲鉄太', 26 | 半: '半間修二', 27 | とら: '羽宮一虎', 28 | ココ: '九井一', 29 | イヌ: '乾青宗', 30 | たい: '柴大寿', 31 | イザ: '黒川イザナ', 32 | 蘭: '灰谷蘭', 33 | 竜: '灰谷竜胆', 34 | カク: '鶴蝶', 35 | おみ: '明司武臣', 36 | 臣: '明司武臣', 37 | ワカ: '今牛若狭', 38 | あつ: '千堂敦', 39 | 真: '佐野真一郎', 40 | }; 41 | 42 | const getDictTitleFromURL = (url) => 43 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 44 | 45 | const charactersFromCoupling = (text) => { 46 | const keyChar1 = Object.keys(shortNameMap).find((e) => text.startsWith(e)); 47 | if (!keyChar1) { 48 | return []; 49 | } 50 | const keyChar2 = Object.keys(shortNameMap).find((e) => 51 | text.replace(keyChar1, '').endsWith(e) 52 | ); 53 | if (!keyChar2) { 54 | return []; 55 | } 56 | return [shortNameMap[keyChar1], shortNameMap[keyChar2]].sort(); 57 | }; 58 | 59 | let couplingTags = {}; 60 | let targetTables = Array.from(document.querySelectorAll('table')); 61 | targetTables 62 | .map((table) => Array.from(table.querySelectorAll('td'))) 63 | .forEach((tds) => { 64 | tds.forEach((td) => { 65 | td.querySelectorAll('a').forEach((a) => { 66 | const characters = charactersFromCoupling(a.text); 67 | if (characters.length < 2) { 68 | return true; 69 | } 70 | if (!couplingTags[characters.join('_')]) { 71 | couplingTags[characters.join('_')] = [getDictTitleFromURL(a.href)]; 72 | } else { 73 | couplingTags[characters.join('_')].push( 74 | getDictTitleFromURL(a.href) 75 | ); 76 | } 77 | }); 78 | }); 79 | }); 80 | 81 | const result = Object.keys(couplingTags).map((k) => { 82 | return { 83 | characters: k.split('_').map((e) => { 84 | return { name: e }; 85 | }), 86 | tags: couplingTags[k].map((e) => { 87 | return { name: e }; 88 | }), 89 | }; 90 | }); 91 | 92 | copy(JSON.stringify(result)); 93 | })(); 94 | -------------------------------------------------------------------------------- /scraping/target_couplings/revengers/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "characters": [{ "name": "佐野万次郎" }, { "name": "花垣武道" }], 4 | "tags": [{ "name": "マイ武" }, { "name": "タケマイ" }] 5 | }, 6 | { 7 | "characters": [{ "name": "松野千冬" }, { "name": "花垣武道" }], 8 | "tags": [{ "name": "ふゆタケ" }, { "name": "タケふゆ" }] 9 | }, 10 | { 11 | "characters": [{ "name": "花垣武道" }, { "name": "龍宮寺堅" }], 12 | "tags": [{ "name": "ドラ武" }, { "name": "武ドラ" }] 13 | }, 14 | { 15 | "characters": [{ "name": "三ツ谷隆" }, { "name": "花垣武道" }], 16 | "tags": [{ "name": "みつ武" }] 17 | }, 18 | { 19 | "characters": [{ "name": "場地圭介" }, { "name": "花垣武道" }], 20 | "tags": [{ "name": "バジ武" }] 21 | }, 22 | { 23 | "characters": [{ "name": "稀咲鉄太" }, { "name": "花垣武道" }], 24 | "tags": [{ "name": "キサタケ" }, { "name": "タケキサ" }] 25 | }, 26 | { 27 | "characters": [{ "name": "柴八戒" }, { "name": "花垣武道" }], 28 | "tags": [{ "name": "はち武" }] 29 | }, 30 | { 31 | "characters": [{ "name": "林良平" }, { "name": "花垣武道" }], 32 | "tags": [{ "name": "ペー武" }] 33 | }, 34 | { 35 | "characters": [{ "name": "武藤泰宏" }, { "name": "花垣武道" }], 36 | "tags": [{ "name": "ムー武" }] 37 | }, 38 | { 39 | "characters": [{ "name": "林田春樹" }, { "name": "花垣武道" }], 40 | "tags": [{ "name": "パー武" }] 41 | }, 42 | { 43 | "characters": [{ "name": "羽宮一虎" }, { "name": "花垣武道" }], 44 | "tags": [{ "name": "とら武" }] 45 | }, 46 | { 47 | "characters": [{ "name": "半間修二" }, { "name": "花垣武道" }], 48 | "tags": [{ "name": "半武" }] 49 | }, 50 | { 51 | "characters": [{ "name": "九井一" }, { "name": "花垣武道" }], 52 | "tags": [{ "name": "ココ武" }] 53 | }, 54 | { 55 | "characters": [{ "name": "乾青宗" }, { "name": "花垣武道" }], 56 | "tags": [{ "name": "イヌ武" }] 57 | }, 58 | { 59 | "characters": [{ "name": "花垣武道" }, { "name": "黒川イザナ" }], 60 | "tags": [{ "name": "イザ武" }] 61 | }, 62 | { 63 | "characters": [{ "name": "灰谷蘭" }, { "name": "花垣武道" }], 64 | "tags": [{ "name": "蘭武" }] 65 | }, 66 | { 67 | "characters": [{ "name": "灰谷竜胆" }, { "name": "花垣武道" }], 68 | "tags": [{ "name": "竜武" }] 69 | }, 70 | { 71 | "characters": [{ "name": "花垣武道" }, { "name": "鶴蝶" }], 72 | "tags": [{ "name": "カク武" }] 73 | }, 74 | { 75 | "characters": [{ "name": "今牛若狭" }, { "name": "花垣武道" }], 76 | "tags": [{ "name": "ワカ武" }] 77 | }, 78 | { 79 | "characters": [{ "name": "明司武臣" }, { "name": "花垣武道" }], 80 | "tags": [{ "name": "臣武" }] 81 | }, 82 | { 83 | "characters": [{ "name": "佐野真一郎" }, { "name": "花垣武道" }], 84 | "tags": [{ "name": "真武" }] 85 | }, 86 | { 87 | "characters": [{ "name": "橘直人" }, { "name": "花垣武道" }], 88 | "tags": [{ "name": "ナオ武" }, { "name": "タケナオ" }] 89 | }, 90 | { 91 | "characters": [{ "name": "佐野万次郎" }, { "name": "龍宮寺堅" }], 92 | "tags": [{ "name": "ドラマイ" }, { "name": "マイドラ" }] 93 | }, 94 | { 95 | "characters": [{ "name": "三途春千夜" }, { "name": "佐野万次郎" }], 96 | "tags": [{ "name": "春マイ" }, { "name": "マイ春" }] 97 | }, 98 | { 99 | "characters": [{ "name": "佐野万次郎" }, { "name": "黒川イザナ" }], 100 | "tags": [{ "name": "イザマイ" }, { "name": "マイイザ" }] 101 | }, 102 | { 103 | "characters": [{ "name": "佐野万次郎" }, { "name": "場地圭介" }], 104 | "tags": [{ "name": "バジマイ" }, { "name": "マイバジ" }] 105 | }, 106 | { 107 | "characters": [{ "name": "三ツ谷隆" }, { "name": "佐野万次郎" }], 108 | "tags": [{ "name": "みつマイ" }, { "name": "マイみつ" }] 109 | }, 110 | { 111 | "characters": [{ "name": "佐野万次郎" }, { "name": "鶴蝶" }], 112 | "tags": [{ "name": "カクマイ" }] 113 | }, 114 | { 115 | "characters": [{ "name": "佐野万次郎" }, { "name": "灰谷蘭" }], 116 | "tags": [{ "name": "蘭マイ" }] 117 | }, 118 | { 119 | "characters": [{ "name": "佐野万次郎" }, { "name": "灰谷竜胆" }], 120 | "tags": [{ "name": "竜マイ" }] 121 | }, 122 | { 123 | "characters": [{ "name": "九井一" }, { "name": "佐野万次郎" }], 124 | "tags": [{ "name": "ココマイ" }] 125 | }, 126 | { 127 | "characters": [{ "name": "佐野万次郎" }, { "name": "松野千冬" }], 128 | "tags": [{ "name": "ふゆマイ" }, { "name": "マイふゆ" }] 129 | }, 130 | { 131 | "characters": [{ "name": "三ツ谷隆" }, { "name": "龍宮寺堅" }], 132 | "tags": [{ "name": "みつドラ" }, { "name": "ドラみつ" }] 133 | }, 134 | { 135 | "characters": [{ "name": "乾青宗" }, { "name": "龍宮寺堅" }], 136 | "tags": [{ "name": "イヌドラ" }, { "name": "ドライヌ" }] 137 | }, 138 | { 139 | "characters": [{ "name": "半間修二" }, { "name": "龍宮寺堅" }], 140 | "tags": [{ "name": "半ドラ" }, { "name": "ドラ半" }] 141 | }, 142 | { 143 | "characters": [{ "name": "場地圭介" }, { "name": "松野千冬" }], 144 | "tags": [{ "name": "ふゆばじ" }, { "name": "ばじふゆ" }] 145 | }, 146 | { 147 | "characters": [{ "name": "場地圭介" }, { "name": "羽宮一虎" }], 148 | "tags": [{ "name": "とらばじ" }, { "name": "ばじとら" }] 149 | }, 150 | { 151 | "characters": [{ "name": "佐野真一郎" }, { "name": "場地圭介" }], 152 | "tags": [{ "name": "真ばじ" }, { "name": "真バジ" }] 153 | }, 154 | { 155 | "characters": [{ "name": "松野千冬" }, { "name": "羽宮一虎" }], 156 | "tags": [{ "name": "とらふゆ" }, { "name": "ふゆとら" }] 157 | }, 158 | { 159 | "characters": [{ "name": "三ツ谷隆" }, { "name": "松野千冬" }], 160 | "tags": [{ "name": "みつふゆ" }] 161 | }, 162 | { 163 | "characters": [{ "name": "松野千冬" }, { "name": "龍宮寺堅" }], 164 | "tags": [{ "name": "ドラふゆ" }] 165 | }, 166 | { 167 | "characters": [{ "name": "三ツ谷隆" }, { "name": "柴大寿" }], 168 | "tags": [{ "name": "たいみつ" }, { "name": "みつたい" }] 169 | }, 170 | { 171 | "characters": [{ "name": "三ツ谷隆" }, { "name": "灰谷蘭" }], 172 | "tags": [{ "name": "蘭みつ" }, { "name": "みつ蘭" }] 173 | }, 174 | { 175 | "characters": [{ "name": "三ツ谷隆" }, { "name": "乾青宗" }], 176 | "tags": [{ "name": "イヌみつ" }] 177 | }, 178 | { 179 | "characters": [{ "name": "三ツ谷隆" }, { "name": "場地圭介" }], 180 | "tags": [{ "name": "ばじみつ" }] 181 | }, 182 | { 183 | "characters": [{ "name": "三ツ谷隆" }, { "name": "灰谷竜胆" }], 184 | "tags": [{ "name": "竜みつ" }] 185 | }, 186 | { 187 | "characters": [{ "name": "三ツ谷隆" }, { "name": "柴八戒" }], 188 | "tags": [{ "name": "みつはち" }] 189 | }, 190 | { 191 | "characters": [{ "name": "柴八戒" }, { "name": "柴大寿" }], 192 | "tags": [{ "name": "たいはち" }, { "name": "はちたい" }] 193 | }, 194 | { 195 | "characters": [{ "name": "林田春樹" }, { "name": "林良平" }], 196 | "tags": [{ "name": "ペーパー" }, { "name": "パーペー" }] 197 | }, 198 | { 199 | "characters": [{ "name": "武藤泰宏" }, { "name": "河田ナホヤ" }], 200 | "tags": [{ "name": "ムーナホ" }] 201 | }, 202 | { 203 | "characters": [{ "name": "河田ソウヤ" }, { "name": "河田ナホヤ" }], 204 | "tags": [{ "name": "ソヤナホ" }, { "name": "ナホソヤ" }] 205 | }, 206 | { 207 | "characters": [{ "name": "河田ソウヤ" }, { "name": "灰谷竜胆" }], 208 | "tags": [{ "name": "竜ソヤ" }] 209 | }, 210 | { 211 | "characters": [{ "name": "三途春千夜" }, { "name": "武藤泰宏" }], 212 | "tags": [{ "name": "三ム" }, { "name": "ム三" }] 213 | }, 214 | { 215 | "characters": [{ "name": "三途春千夜" }, { "name": "灰谷蘭" }], 216 | "tags": [{ "name": "蘭はる" }, { "name": "春蘭" }] 217 | }, 218 | { 219 | "characters": [{ "name": "三途春千夜" }, { "name": "灰谷竜胆" }], 220 | "tags": [{ "name": "竜春" }, { "name": "春竜" }] 221 | }, 222 | { 223 | "characters": [{ "name": "三途春千夜" }, { "name": "九井一" }], 224 | "tags": [{ "name": "ココ春" }, { "name": "春ココ" }] 225 | }, 226 | { 227 | "characters": [{ "name": "半間修二" }, { "name": "稀咲鉄太" }], 228 | "tags": [{ "name": "半稀" }, { "name": "稀半" }] 229 | }, 230 | { 231 | "characters": [{ "name": "佐野万次郎" }, { "name": "羽宮一虎" }], 232 | "tags": [{ "name": "マイとら" }] 233 | }, 234 | { 235 | "characters": [{ "name": "灰谷竜胆" }, { "name": "羽宮一虎" }], 236 | "tags": [{ "name": "竜とら" }] 237 | }, 238 | { 239 | "characters": [{ "name": "灰谷蘭" }, { "name": "羽宮一虎" }], 240 | "tags": [{ "name": "蘭とら" }] 241 | }, 242 | { 243 | "characters": [{ "name": "九井一" }, { "name": "乾青宗" }], 244 | "tags": [{ "name": "イヌココ" }, { "name": "ココイヌ" }] 245 | }, 246 | { 247 | "characters": [{ "name": "九井一" }, { "name": "灰谷蘭" }], 248 | "tags": [{ "name": "蘭ココ" }, { "name": "ココ蘭" }] 249 | }, 250 | { 251 | "characters": [{ "name": "九井一" }, { "name": "灰谷竜胆" }], 252 | "tags": [{ "name": "竜ココ" }, { "name": "ココ竜" }] 253 | }, 254 | { 255 | "characters": [{ "name": "乾青宗" }, { "name": "佐野真一郎" }], 256 | "tags": [{ "name": "真イヌ" }] 257 | }, 258 | { 259 | "characters": [{ "name": "乾青宗" }, { "name": "今牛若狭" }], 260 | "tags": [{ "name": "ワカイヌ" }, { "name": "イヌワカ" }] 261 | }, 262 | { 263 | "characters": [{ "name": "鶴蝶" }, { "name": "黒川イザナ" }], 264 | "tags": [{ "name": "カクイザ" }, { "name": "イザカク" }] 265 | }, 266 | { 267 | "characters": [{ "name": "佐野真一郎" }, { "name": "黒川イザナ" }], 268 | "tags": [{ "name": "真イザ" }, { "name": "イザ真" }] 269 | }, 270 | { 271 | "characters": [{ "name": "灰谷竜胆" }, { "name": "灰谷蘭" }], 272 | "tags": [{ "name": "竜蘭" }, { "name": "蘭竜" }] 273 | }, 274 | { 275 | "characters": [{ "name": "半間修二" }, { "name": "灰谷蘭" }], 276 | "tags": [{ "name": "半蘭" }] 277 | }, 278 | { 279 | "characters": [{ "name": "灰谷蘭" }, { "name": "黒川イザナ" }], 280 | "tags": [{ "name": "イザ蘭" }] 281 | }, 282 | { 283 | "characters": [{ "name": "灰谷蘭" }, { "name": "鶴蝶" }], 284 | "tags": [{ "name": "カク蘭" }, { "name": "蘭カク" }] 285 | }, 286 | { 287 | "characters": [{ "name": "半間修二" }, { "name": "灰谷竜胆" }], 288 | "tags": [{ "name": "半竜" }] 289 | }, 290 | { 291 | "characters": [{ "name": "佐野真一郎" }, { "name": "明司武臣" }], 292 | "tags": [{ "name": "真臣" }] 293 | }, 294 | { 295 | "characters": [{ "name": "今牛若狭" }, { "name": "佐野真一郎" }], 296 | "tags": [{ "name": "真ワカ" }, { "name": "ワカ真" }] 297 | }, 298 | { 299 | "characters": [{ "name": "千堂敦" }, { "name": "花垣武道" }], 300 | "tags": [{ "name": "武あつ" }] 301 | } 302 | ] 303 | -------------------------------------------------------------------------------- /scraping/target_couplings/touhou/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E6%9D%B1%E6%96%B9Project%E3%81%AE%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // usage: paste this script to javascript console 3 | 4 | (() => { 5 | // utils 6 | const getDictTitleFromURL = (url) => 7 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 8 | 9 | // usage: target_couplings.map(addcharactor("username")) 10 | const addcharactor = (charactor) => (x) => ({ 11 | ...x, 12 | characters: [...x.characters, { name: charactor }], 13 | }); 14 | 15 | // usage: target_couplings.filter(excludeSameCharactorsCoupling) 16 | const excludeSameCharactorsCoupling = (x) => 17 | x.characters.length === 2 && x.characters[0] !== x.characters[1]; 18 | 19 | // add coupling synonym to target coupling from synonym table, table format like below example 20 | // 21 | // 22 | // 23 | // 24 | //... 25 | //usage: target_coupling.map(addSynonymCouplingTag(synonym_table)) 26 | const addSynonymCouplingTag = (synonym_table) => { 27 | const synonyms = Array.from(synonym_table.querySelectorAll('tr')) 28 | .map((x) => ({ 29 | tag: x.querySelector('th'), 30 | synonym: x.querySelector('td'), 31 | })) 32 | .filter((x) => x.tag !== null && x.synonym !== null) 33 | .map((x) => ({ 34 | tags: Array.from(x.tag.querySelectorAll('a')).map((x) => 35 | getDictTitleFromURL(x.href) 36 | ), 37 | synonyms: Array.from(x.synonym.querySelectorAll('a')).map((x) => 38 | getDictTitleFromURL(x.href) 39 | ), 40 | })); 41 | 42 | return (x) => { 43 | const current_synonyms = x.tags 44 | .map((x) => synonyms.find((y) => y.tags.indexOf(x.name) !== -1)) 45 | .filter((x) => x !== undefined) 46 | .reduce((s, x) => [...s, ...x.synonyms], []) 47 | .filter((x, i, self) => self.indexOf(x) === i) 48 | .map((x) => ({ name: x })); 49 | 50 | return { ...x, tags: [...x.tags, ...current_synonyms] }; 51 | }; 52 | }; 53 | 54 | // get target coupling from table like following format 55 | // 56 | // 57 | // 58 | // 59 | //... 60 | const getCouplingFromTable = (table) => 61 | Array.from(table.querySelectorAll('tr')) 62 | .map((x) => ({ 63 | header: x.querySelector('th'), 64 | data: x.querySelector('td'), 65 | })) 66 | .filter((x) => x.header !== null && x.data !== null) 67 | .map((x) => ({ 68 | characters: Array.from(x.header.querySelectorAll('a')).map((x) => ({ 69 | name: getDictTitleFromURL(x.href), 70 | })), 71 | tags: Array.from(x.data.querySelectorAll('a')).map((x) => ({ 72 | name: getDictTitleFromURL(x.href), 73 | })), 74 | })); 75 | 76 | //target tables 77 | const tables = Array.from(document.querySelectorAll('article table')); 78 | const target_couplings = [ 79 | //作品別 80 | ...tables 81 | .slice(3, 37) 82 | .map((x) => getCouplingFromTable(x)) 83 | .reduce((s, x) => [...s, ...x]), 84 | //キャラ別 85 | ////博麗霊夢関係 86 | ...getCouplingFromTable(tables[0]).map(addcharactor('博麗霊夢')), 87 | ////霧雨魔理沙関係 88 | ...getCouplingFromTable(tables[1]).map(addcharactor('霧雨魔理沙')), 89 | ////アリス・マーガトロイド関係 90 | ...getCouplingFromTable(tables[2]).map( 91 | addcharactor('アリス・マーガトロイド') 92 | ), 93 | ] 94 | .filter(excludeSameCharactorsCoupling) 95 | .map( 96 | //派生タグ 97 | addSynonymCouplingTag(tables[37]) 98 | ); 99 | 100 | copy(JSON.stringify(target_couplings)); 101 | })(); 102 | -------------------------------------------------------------------------------- /scraping/target_couplings/umamusume/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/%E3%82%A6%E3%83%9E%E5%A8%98%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%93%E3%83%BB%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // https://dic.pixiv.net/a/%E5%8F%B2%E5%AE%9F%E5%A4%AB%E5%A9%A6 3 | // usage: paste this script to javascript console 4 | 5 | (() => { 6 | // utils 7 | const getDictTitleFromURL = (url) => 8 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 9 | 10 | const getDictTitleFromLinkText = (() => { 11 | const text2title = new Map( 12 | Array.from(document.querySelectorAll('a')) 13 | .filter((a) => a.href.startsWith('https://dic.pixiv.net/a/')) 14 | .map((a) => [ 15 | a.innerText, 16 | decodeURI(a.href).replace('https://dic.pixiv.net/a/', ''), 17 | ]) 18 | ); 19 | 20 | return (text) => text2title.get(text); 21 | })(); 22 | 23 | // get couplings from table that like following format 24 | // |☆ウオスカ / ウオダス / スカウオ|ウオッカ|ダイワスカーレット|共にチームスピカのメンバー。史実では同世代(07世代)、5回対戦した好敵手| 25 | // ... 26 | const getDictTitleFromTd = (td) => { 27 | const aArray = td.querySelectorAll('a'); 28 | if (aArray.length === 1) return getDictTitleFromURL(aArray[0].href); 29 | 30 | const title = getDictTitleFromLinkText(td.innerText); 31 | if (td.innerText !== '' && title !== undefined) return title; 32 | 33 | return null; 34 | }; 35 | const getCouplingsFromTable = (table) => { 36 | return Array.from(table.querySelectorAll('tr')) 37 | .map((tr) => tr.querySelectorAll('th, td')) 38 | .filter( 39 | ([th, td1, td2]) => 40 | th.querySelector('a') !== null && 41 | getDictTitleFromTd(td1) !== null && 42 | getDictTitleFromTd(td2) !== null 43 | ) 44 | .map(([th, td1, td2]) => ({ 45 | tags: Array.from(th.querySelectorAll('a')).map((a) => ({ 46 | name: getDictTitleFromURL(a.href), 47 | })), 48 | characters: [ 49 | { 50 | name: getDictTitleFromTd(td1), 51 | }, 52 | { 53 | name: getDictTitleFromTd(td2), 54 | }, 55 | ], 56 | })); 57 | }; 58 | 59 | if ( 60 | location.href === 61 | 'https://dic.pixiv.net/a/%E3%82%A6%E3%83%9E%E5%A8%98%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%93%E3%83%BB%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%BB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7' 62 | ) { 63 | const tables = document.querySelectorAll('article table'); 64 | const target_couplings = [ 65 | ...getCouplingsFromTable(tables[0]), 66 | ...getCouplingsFromTable(tables[1]), 67 | ...getCouplingsFromTable(tables[2]), 68 | ...getCouplingsFromTable(tables[3]), 69 | ...getCouplingsFromTable(tables[4]), 70 | ...getCouplingsFromTable(tables[5]), 71 | ...getCouplingsFromTable(tables[6]), 72 | // 3人以上の組み合わせは取得しない 73 | ...getCouplingsFromTable(tables[14]), 74 | ]; 75 | copy(JSON.stringify(target_couplings)); 76 | return; 77 | } 78 | 79 | if ( 80 | location.href === 81 | 'https://dic.pixiv.net/a/%E5%8F%B2%E5%AE%9F%E5%A4%AB%E5%A9%A6' 82 | ) { 83 | const target_couplings = getCouplingsFromTable( 84 | document.querySelector('table') 85 | ); 86 | copy(JSON.stringify(target_couplings)); 87 | return; 88 | } 89 | })(); 90 | -------------------------------------------------------------------------------- /scraping/target_couplings/vocalo/getCouplings.js: -------------------------------------------------------------------------------- 1 | // https://dic.pixiv.net/a/VOCALOID%E3%82%B3%E3%83%B3%E3%83%93%E3%83%BB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 2 | // https://dic.pixiv.net/a/VOICEROID%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7 3 | // usage: paste this script to javascript console 4 | 5 | (() => { 6 | // utils 7 | const getDictTitleFromURL = (url) => 8 | decodeURI(url).replace(/https?:\/\/dic\.pixiv\.net\/a\//, ''); 9 | 10 | const characters = [ 11 | '徵羽摩柯', 12 | '乐正绫', 13 | '乐正龙牙', 14 | 'AZUKI', 15 | 'Append', 16 | 'Avanna', 17 | 'BIG-AL', 18 | 'Bruno', 19 | 'CUL', 20 | 'CYBER_DIVA', 21 | 'CYBER_SONGMAN', 22 | 'Chika', 23 | 'Chris', 24 | 'Clara', 25 | 'Fukase', 26 | 'GUMI', 27 | 'IA', 28 | 'KAITO', 29 | 'Kaori', 30 | 'Ken', 31 | 'LEON', 32 | 'LOLA', 33 | 'LUMi', 34 | 'Lily', 35 | 'MAIKA', 36 | 'MATCHA', 37 | 'MAYU', 38 | 'MEIKO', 39 | 'MIRIAM', 40 | 'Mac音ナナ', 41 | 'Megpoid', 42 | 'Mew', 43 | 'Oliver', 44 | 'ONE', 45 | 'PRIMA', 46 | 'Rana', 47 | 'Ruby', 48 | 'SF-A2', 49 | 'SONIKA', 50 | 'Sachiko', 51 | 'SeeU', 52 | 'SweetAnn', 53 | 'TONIO', 54 | 'UNI', 55 | 'VY1', 56 | 'VY2', 57 | 'YOHIOloid', 58 | 'ZOLA_PROJECT', 59 | 'kokone(心響)', 60 | 'miki', 61 | 'v_flower', 62 | 'v4_flower', 63 | 'がくっぽいど', 64 | 'すずきつづみ', 65 | 'ついなちゃん', 66 | 'アルスロイド', 67 | 'ガチャッポイド', 68 | 'ギャラ子', 69 | 'タカハシ(CeVIO)', 70 | 'ボカロ小学生', 71 | 'ボカロ先生', 72 | 'マクネナナ', 73 | 'ミライ小町', 74 | 'メグッポイド', 75 | 'メルリ', 76 | 'ユニティちゃん', 77 | 'リュウト', 78 | '杏音鳥音', 79 | '伊織弓鶴', 80 | '音街ウナ', 81 | '歌愛ユキ', 82 | '歌手音ピコ', 83 | '吉田くん', 84 | '鏡音リン', 85 | '鏡音レン', 86 | '琴葉葵', 87 | '琴葉茜', 88 | '結月ゆかり', 89 | '結月ゆかりの双子の弟', 90 | '月読アイ', 91 | '月読ショウタ', 92 | '弦巻マキ', 93 | '言和', 94 | '桜乃そら', 95 | '巡音ルカ', 96 | '初音ミク', 97 | '心華', 98 | '神威がくぽ', 99 | '水奈瀬コウ', 100 | '京町セイカ', 101 | '星尘', 102 | '蒼姫ラピス', 103 | '兎眠りおん', 104 | '東北きりたん', 105 | '東北ずん子', 106 | '東北イタコ', 107 | '猫村いろは', 108 | '氷山キヨテル', 109 | '墨清弦', 110 | '夢眠ネム', 111 | '鳴花ヒメ・ミコト', 112 | '洛天依', 113 | '紲星あかり', 114 | 'ずんだもん', 115 | '春日部つむぎ', 116 | '四国めたん', 117 | 'さとうささら', 118 | ]; 119 | 120 | const getFullname = (name) => characters.find((x) => x.indexOf(name) !== -1); 121 | 122 | // get couplings from table that like following format 123 | // | |KAITO |レン |... 124 | // |MEIKO|カイメイ|レンメイ|... 125 | // |ミク |カイミク|レンミク|... 126 | // ... 127 | const getTableCouplings = (table) => { 128 | const header_characters = Array.from( 129 | table.querySelectorAll('tr:first-child > th') 130 | ) 131 | .map((x) => getFullname(x.innerText)) 132 | .filter((x) => x !== undefined); 133 | 134 | const data_table_rows = Array.from( 135 | table.querySelectorAll('tr:not(:first-child)') 136 | ); 137 | return data_table_rows 138 | .map((row) => { 139 | const character = getFullname(row.querySelector('th').innerText); 140 | const coupling_cells = Array.from( 141 | row.querySelectorAll(':scope > *:not(:first-child)') 142 | ); 143 | 144 | return coupling_cells 145 | .map((x, i) => ({ 146 | characters: [{ name: character }, { name: header_characters[i] }], 147 | tags: Array.from(x.querySelectorAll('a')) 148 | .map((x) => ({ name: getDictTitleFromURL(x.href) })) 149 | .filter((x) => x.name !== ''), 150 | })) 151 | .filter((x) => x.tags.length !== 0); 152 | }) 153 | .reduce((s, x) => [...s, ...x]); 154 | }; 155 | 156 | // get couplings list that like following format 157 | // * ゆづきず、きずゆか(結月ゆかり×紲星あかり) 158 | // ... 159 | const list_tag_splitter = /、/; 160 | const getListCouplings = (list) => 161 | Array.from(list.querySelectorAll('li')) 162 | .map((x) => x.textContent.match(/(.+)\((.+)\)/)) 163 | .filter((x) => x !== null) 164 | .map((x) => ({ 165 | characters: x[2].split('×').map((x) => ({ name: getFullname(x) })), 166 | tags: x[1].split(list_tag_splitter).map((x) => ({ name: x })), 167 | })) 168 | .filter((x) => x.characters.length === 2); 169 | 170 | // get target couplings 171 | let target_couplings; 172 | if ( 173 | location.href === 174 | 'https://dic.pixiv.net/a/VOCALOID%E3%82%B3%E3%83%B3%E3%83%93%E3%83%BB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7' 175 | ) { 176 | const tables = document.querySelectorAll('article table'); 177 | target_couplings = [tables[0], tables[2], tables[4]] 178 | .map((x) => getTableCouplings(x)) 179 | .reduce((s, x) => [...s, ...x]); 180 | } 181 | if ( 182 | location.href === 183 | 'https://dic.pixiv.net/a/VOICEROID%E3%82%AB%E3%83%83%E3%83%97%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%BF%E3%82%B0%E4%B8%80%E8%A6%A7' 184 | ) { 185 | const uls = Array.from(document.querySelectorAll('article ul')); 186 | target_couplings = [ 187 | uls[2], 188 | uls[3], 189 | uls[4], 190 | uls[5], 191 | uls[6], 192 | uls[7], 193 | uls[8], 194 | uls[9], 195 | uls[10], 196 | uls[11], 197 | uls[12], 198 | ] 199 | .map((ul) => getListCouplings(ul)) 200 | .reduce((s, x) => [...s, ...x]); 201 | } 202 | copy(JSON.stringify(target_couplings)); 203 | })(); 204 | -------------------------------------------------------------------------------- /scraping/target_couplings/vocalo/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "characters": [{ "name": "MEIKO" }, { "name": "KAITO" }], 4 | "tags": [ 5 | { "name": "カイメイ" }, 6 | { "name": "メイカイ" }, 7 | { "name": "年長組" } 8 | ] 9 | }, 10 | { 11 | "characters": [{ "name": "MEIKO" }, { "name": "鏡音レン" }], 12 | "tags": [{ "name": "レンメイ" }, { "name": "メイレン" }] 13 | }, 14 | { 15 | "characters": [{ "name": "MEIKO" }, { "name": "神威がくぽ" }], 16 | "tags": [{ "name": "がくメイ" }] 17 | }, 18 | { 19 | "characters": [{ "name": "MEIKO" }, { "name": "氷山キヨテル" }], 20 | "tags": [{ "name": "キヨメイ" }] 21 | }, 22 | { 23 | "characters": [{ "name": "初音ミク" }, { "name": "KAITO" }], 24 | "tags": [ 25 | { "name": "カイミク" }, 26 | { "name": "ミクカイ" }, 27 | { "name": "寒色兄妹" } 28 | ] 29 | }, 30 | { 31 | "characters": [{ "name": "初音ミク" }, { "name": "鏡音レン" }], 32 | "tags": [{ "name": "レンミク" }, { "name": "ミクレン" }] 33 | }, 34 | { 35 | "characters": [{ "name": "初音ミク" }, { "name": "神威がくぽ" }], 36 | "tags": [{ "name": "がくミク" }, { "name": "みくぽ" }] 37 | }, 38 | { 39 | "characters": [{ "name": "初音ミク" }, { "name": "氷山キヨテル" }], 40 | "tags": [{ "name": "キヨミク" }] 41 | }, 42 | { 43 | "characters": [{ "name": "初音ミク" }, { "name": "歌手音ピコ" }], 44 | "tags": [{ "name": "ピコミク" }] 45 | }, 46 | { 47 | "characters": [{ "name": "鏡音リン" }, { "name": "KAITO" }], 48 | "tags": [{ "name": "カイリン" }, { "name": "リンカイ" }] 49 | }, 50 | { 51 | "characters": [{ "name": "鏡音リン" }, { "name": "鏡音レン" }], 52 | "tags": [{ "name": "レンリン" }, { "name": "リンレン" }] 53 | }, 54 | { 55 | "characters": [{ "name": "鏡音リン" }, { "name": "神威がくぽ" }], 56 | "tags": [{ "name": "がくリン" }, { "name": "リンぽ" }] 57 | }, 58 | { 59 | "characters": [{ "name": "鏡音リン" }, { "name": "歌手音ピコ" }], 60 | "tags": [{ "name": "ピコリン" }] 61 | }, 62 | { 63 | "characters": [{ "name": "巡音ルカ" }, { "name": "KAITO" }], 64 | "tags": [{ "name": "カイルカ" }, { "name": "ルカイト" }] 65 | }, 66 | { 67 | "characters": [{ "name": "巡音ルカ" }, { "name": "鏡音レン" }], 68 | "tags": [{ "name": "レンルカ" }, { "name": "ルカレン" }] 69 | }, 70 | { 71 | "characters": [{ "name": "巡音ルカ" }, { "name": "神威がくぽ" }], 72 | "tags": [{ "name": "がくルカ" }, { "name": "ぽルカ" }] 73 | }, 74 | { 75 | "characters": [{ "name": "巡音ルカ" }, { "name": "氷山キヨテル" }], 76 | "tags": [{ "name": "テルカ" }] 77 | }, 78 | { 79 | "characters": [{ "name": "GUMI" }, { "name": "KAITO" }], 80 | "tags": [{ "name": "カイグミ" }] 81 | }, 82 | { 83 | "characters": [{ "name": "GUMI" }, { "name": "鏡音レン" }], 84 | "tags": [{ "name": "レングミ" }, { "name": "グミレン" }] 85 | }, 86 | { 87 | "characters": [{ "name": "GUMI" }, { "name": "神威がくぽ" }], 88 | "tags": [{ "name": "がくグミ" }, { "name": "ぽいど兄妹" }] 89 | }, 90 | { 91 | "characters": [{ "name": "GUMI" }, { "name": "リュウト" }], 92 | "tags": [{ "name": "ガチャグミ" }] 93 | }, 94 | { 95 | "characters": [{ "name": "miki" }, { "name": "KAITO" }], 96 | "tags": [{ "name": "カイミキ" }] 97 | }, 98 | { 99 | "characters": [{ "name": "miki" }, { "name": "鏡音レン" }], 100 | "tags": [{ "name": "レンミキ" }] 101 | }, 102 | { 103 | "characters": [{ "name": "miki" }, { "name": "歌手音ピコ" }], 104 | "tags": [{ "name": "ピコミキ" }, { "name": "ミキピコ" }] 105 | }, 106 | { 107 | "characters": [{ "name": "猫村いろは" }, { "name": "KAITO" }], 108 | "tags": [{ "name": "かいねこ" }] 109 | }, 110 | { 111 | "characters": [{ "name": "猫村いろは" }, { "name": "氷山キヨテル" }], 112 | "tags": [{ "name": "キヨいろ" }] 113 | }, 114 | { 115 | "characters": [{ "name": "歌愛ユキ" }, { "name": "氷山キヨテル" }], 116 | "tags": [{ "name": "キヨユキ" }] 117 | }, 118 | { 119 | "characters": [{ "name": "歌愛ユキ" }, { "name": "リュウト" }], 120 | "tags": [{ "name": "ガチャユキ" }] 121 | }, 122 | { 123 | "characters": [{ "name": "MEIKO" }, { "name": "MEIKO" }], 124 | "tags": [{ "name": "メイメイ" }] 125 | }, 126 | { 127 | "characters": [{ "name": "MEIKO" }, { "name": "初音ミク" }], 128 | "tags": [{ "name": "メイミク" }] 129 | }, 130 | { 131 | "characters": [{ "name": "MEIKO" }, { "name": "巡音ルカ" }], 132 | "tags": [{ "name": "メイルカ" }] 133 | }, 134 | { 135 | "characters": [{ "name": "MEIKO" }, { "name": "GUMI" }], 136 | "tags": [{ "name": "メイグミ" }] 137 | }, 138 | { 139 | "characters": [{ "name": "初音ミク" }, { "name": "MEIKO" }], 140 | "tags": [{ "name": "ミクメイ" }] 141 | }, 142 | { 143 | "characters": [{ "name": "初音ミク" }, { "name": "初音ミク" }], 144 | "tags": [{ "name": "ミクミク" }] 145 | }, 146 | { 147 | "characters": [{ "name": "初音ミク" }, { "name": "鏡音リン" }], 148 | "tags": [{ "name": "ミクリン" }] 149 | }, 150 | { 151 | "characters": [{ "name": "初音ミク" }, { "name": "巡音ルカ" }], 152 | "tags": [{ "name": "ネギトロ" }, { "name": "ミクルカ" }] 153 | }, 154 | { 155 | "characters": [{ "name": "初音ミク" }, { "name": "GUMI" }], 156 | "tags": [{ "name": "ミクグミ" }] 157 | }, 158 | { 159 | "characters": [{ "name": "初音ミク" }, { "name": "IA" }], 160 | "tags": [{ "name": "ミクイア" }] 161 | }, 162 | { 163 | "characters": [{ "name": "鏡音リン" }, { "name": "MEIKO" }], 164 | "tags": [{ "name": "リンメイ" }] 165 | }, 166 | { 167 | "characters": [{ "name": "鏡音リン" }, { "name": "初音ミク" }], 168 | "tags": [{ "name": "リンミク" }] 169 | }, 170 | { 171 | "characters": [{ "name": "鏡音リン" }, { "name": "巡音ルカ" }], 172 | "tags": [{ "name": "マグローラー" }, { "name": "リンルカ" }] 173 | }, 174 | { 175 | "characters": [{ "name": "鏡音リン" }, { "name": "GUMI" }], 176 | "tags": [{ "name": "リングミ" }] 177 | }, 178 | { 179 | "characters": [{ "name": "巡音ルカ" }, { "name": "MEIKO" }], 180 | "tags": [{ "name": "ルカメイ" }] 181 | }, 182 | { 183 | "characters": [{ "name": "巡音ルカ" }, { "name": "初音ミク" }], 184 | "tags": [{ "name": "ネギトロ" }, { "name": "ルカミク" }] 185 | }, 186 | { 187 | "characters": [{ "name": "巡音ルカ" }, { "name": "鏡音リン" }], 188 | "tags": [{ "name": "マグローラー" }, { "name": "ルカリン" }] 189 | }, 190 | { 191 | "characters": [{ "name": "巡音ルカ" }, { "name": "GUMI" }], 192 | "tags": [{ "name": "ルカグミ" }] 193 | }, 194 | { 195 | "characters": [{ "name": "GUMI" }, { "name": "MEIKO" }], 196 | "tags": [{ "name": "グミメイ" }] 197 | }, 198 | { 199 | "characters": [{ "name": "GUMI" }, { "name": "初音ミク" }], 200 | "tags": [{ "name": "ぐみく" }] 201 | }, 202 | { 203 | "characters": [{ "name": "GUMI" }, { "name": "鏡音リン" }], 204 | "tags": [{ "name": "ぐみりん" }] 205 | }, 206 | { 207 | "characters": [{ "name": "GUMI" }, { "name": "巡音ルカ" }], 208 | "tags": [{ "name": "グミルカ" }] 209 | }, 210 | { 211 | "characters": [{ "name": "GUMI" }, { "name": "IA" }], 212 | "tags": [{ "name": "ぐみいあ" }] 213 | }, 214 | { 215 | "characters": [{ "name": "Lily" }, { "name": "初音ミク" }], 216 | "tags": [{ "name": "リリミク" }] 217 | }, 218 | { 219 | "characters": [{ "name": "Lily" }, { "name": "鏡音リン" }], 220 | "tags": [{ "name": "リリリン" }] 221 | }, 222 | { 223 | "characters": [{ "name": "Lily" }, { "name": "巡音ルカ" }], 224 | "tags": [{ "name": "リリルカ" }] 225 | }, 226 | { 227 | "characters": [{ "name": "Lily" }, { "name": "GUMI" }], 228 | "tags": [{ "name": "リリグミ" }] 229 | }, 230 | { 231 | "characters": [{ "name": "Lily" }, { "name": "IA" }], 232 | "tags": [{ "name": "リリイア" }] 233 | }, 234 | { 235 | "characters": [{ "name": "KAITO" }, { "name": "KAITO" }], 236 | "tags": [{ "name": "カイカイ" }] 237 | }, 238 | { 239 | "characters": [{ "name": "KAITO" }, { "name": "鏡音レン" }], 240 | "tags": [{ "name": "バナナアイス" }, { "name": "カイレン" }] 241 | }, 242 | { 243 | "characters": [{ "name": "KAITO" }, { "name": "神威がくぽ" }], 244 | "tags": [{ "name": "ユニット「ナイス」" }, { "name": "カイがく" }] 245 | }, 246 | { 247 | "characters": [{ "name": "KAITO" }, { "name": "氷山キヨテル" }], 248 | "tags": [{ "name": "カイキヨ" }] 249 | }, 250 | { 251 | "characters": [{ "name": "鏡音レン" }, { "name": "KAITO" }], 252 | "tags": [{ "name": "バナナアイス" }, { "name": "レンカイ" }] 253 | }, 254 | { 255 | "characters": [{ "name": "鏡音レン" }, { "name": "鏡音レン" }], 256 | "tags": [{ "name": "レンレン" }] 257 | }, 258 | { 259 | "characters": [{ "name": "鏡音レン" }, { "name": "神威がくぽ" }], 260 | "tags": [{ "name": "バナナス" }, { "name": "レンがく" }] 261 | }, 262 | { 263 | "characters": [{ "name": "鏡音レン" }, { "name": "氷山キヨテル" }], 264 | "tags": [{ "name": "レンキヨ" }] 265 | }, 266 | { 267 | "characters": [{ "name": "鏡音レン" }, { "name": "歌手音ピコ" }], 268 | "tags": [{ "name": "レンピコ" }] 269 | }, 270 | { 271 | "characters": [{ "name": "神威がくぽ" }, { "name": "KAITO" }], 272 | "tags": [{ "name": "ユニット「ナイス」" }, { "name": "がくカイ" }] 273 | }, 274 | { 275 | "characters": [{ "name": "神威がくぽ" }, { "name": "鏡音レン" }], 276 | "tags": [{ "name": "バナナス" }, { "name": "がくレン" }] 277 | }, 278 | { 279 | "characters": [{ "name": "神威がくぽ" }, { "name": "氷山キヨテル" }], 280 | "tags": [{ "name": "ナスマウンテン" }] 281 | }, 282 | { 283 | "characters": [{ "name": "神威がくぽ" }, { "name": "歌手音ピコ" }], 284 | "tags": [{ "name": "がくピコ" }] 285 | }, 286 | { 287 | "characters": [{ "name": "氷山キヨテル" }, { "name": "KAITO" }], 288 | "tags": [{ "name": "キヨカイ" }] 289 | }, 290 | { 291 | "characters": [{ "name": "氷山キヨテル" }, { "name": "鏡音レン" }], 292 | "tags": [{ "name": "キヨレン" }] 293 | }, 294 | { 295 | "characters": [{ "name": "氷山キヨテル" }, { "name": "神威がくぽ" }], 296 | "tags": [{ "name": "キヨがく" }] 297 | }, 298 | { 299 | "characters": [{ "name": "歌手音ピコ" }, { "name": "鏡音レン" }], 300 | "tags": [{ "name": "ピコレン" }] 301 | }, 302 | { 303 | "characters": [{ "name": "リュウト" }, { "name": "神威がくぽ" }], 304 | "tags": [{ "name": "ぽいど兄弟" }] 305 | }, 306 | { 307 | "characters": [{ "name": "VY2" }, { "name": "KAITO" }], 308 | "tags": [{ "name": "勇カイ" }] 309 | }, 310 | { 311 | "characters": [{ "name": "琴葉茜" }, { "name": "伊織弓鶴" }], 312 | "tags": [{ "name": "あかいお" }] 313 | }, 314 | { 315 | "characters": [{ "name": "琴葉葵" }, { "name": "伊織弓鶴" }], 316 | "tags": [{ "name": "あおいお" }] 317 | }, 318 | { 319 | "characters": [{ "name": "ついなちゃん" }, { "name": "伊織弓鶴" }], 320 | "tags": [{ "name": "ついおり" }] 321 | }, 322 | { 323 | "characters": [{ "name": "水奈瀬コウ" }, { "name": "京町セイカ" }], 324 | "tags": [{ "name": "コウセイ" }] 325 | }, 326 | { 327 | "characters": [{ "name": "水奈瀬コウ" }, { "name": "紲星あかり" }], 328 | "tags": [{ "name": "コウきず" }] 329 | }, 330 | { 331 | "characters": [{ "name": "伊織弓鶴" }, { "name": "紲星あかり" }], 332 | "tags": [{ "name": "いおきず" }] 333 | }, 334 | { 335 | "characters": [{ "name": "結月ゆかり" }, { "name": "弦巻マキ" }], 336 | "tags": [{ "name": "ゆかマキ" }] 337 | }, 338 | { 339 | "characters": [{ "name": "結月ゆかり" }, { "name": "琴葉茜" }], 340 | "tags": [{ "name": "ゆかあか" }] 341 | }, 342 | { 343 | "characters": [{ "name": "結月ゆかり" }, { "name": "東北ずん子" }], 344 | "tags": [{ "name": "ゆかずん" }] 345 | }, 346 | { 347 | "characters": [{ "name": "結月ゆかり" }, { "name": "東北きりたん" }], 348 | "tags": [{ "name": "ゆかきり" }] 349 | }, 350 | { 351 | "characters": [{ "name": "結月ゆかり" }, { "name": "紲星あかり" }], 352 | "tags": [{ "name": "ゆづきず" }, { "name": "きずゆか" }] 353 | }, 354 | { 355 | "characters": [{ "name": "琴葉茜" }, { "name": "琴葉葵" }], 356 | "tags": [{ "name": "琴葉姉妹" }] 357 | }, 358 | { 359 | "characters": [{ "name": "琴葉茜" }, { "name": "紲星あかり" }], 360 | "tags": [{ "name": "あかきず" }] 361 | }, 362 | { 363 | "characters": [{ "name": "琴葉葵" }, { "name": "弦巻マキ" }], 364 | "tags": [{ "name": "あおマキ" }] 365 | }, 366 | { 367 | "characters": [{ "name": "琴葉葵" }, { "name": "東北きりたん" }], 368 | "tags": [{ "name": "あおきり" }] 369 | }, 370 | { 371 | "characters": [{ "name": "東北ずん子" }, { "name": "東北きりたん" }], 372 | "tags": [{ "name": "ずんきり" }] 373 | }, 374 | { 375 | "characters": [{ "name": "東北きりたん" }, { "name": "琴葉茜" }], 376 | "tags": [{ "name": "きりあか" }] 377 | }, 378 | { 379 | "characters": [{ "name": "音街ウナ" }, { "name": "東北きりたん" }], 380 | "tags": [{ "name": "ウナきり" }] 381 | }, 382 | { 383 | "characters": [{ "name": "水奈瀬コウ" }, { "name": "すずきつづみ" }], 384 | "tags": [{ "name": "コウつづ" }] 385 | }, 386 | { 387 | "characters": [{ "name": "結月ゆかりの双子の弟" }, { "name": "ONE" }], 388 | "tags": [{ "name": "おとおね" }] 389 | }, 390 | { 391 | "characters": [{ "name": "結月ゆかり" }, { "name": "IA" }], 392 | "tags": [{ "name": "ゆかいあ" }] 393 | }, 394 | { 395 | "characters": [{ "name": "タカハシ(CeVIO)" }, { "name": "水奈瀬コウ" }], 396 | "tags": [{ "name": "タカコウ" }] 397 | }, 398 | { 399 | "characters": [{ "name": "初音ミク" }, { "name": "結月ゆかり" }], 400 | "tags": [{ "name": "みくゆか" }] 401 | }, 402 | { 403 | "characters": [{ "name": "猫村いろは" }, { "name": "結月ゆかり" }], 404 | "tags": [{ "name": "ねこうさ" }] 405 | }, 406 | { 407 | "characters": [{ "name": "結月ゆかり" }, { "name": "CUL" }], 408 | "tags": [{ "name": "ゆかる" }] 409 | }, 410 | { 411 | "characters": [{ "name": "ずんだもん" }, { "name": "春日部つむぎ" }], 412 | "tags": [{ "name": "ずんつむ" }] 413 | }, 414 | { 415 | "characters": [{ "name": "ずんだもん" }, { "name": "四国めたん" }], 416 | "tags": [{ "name": "もんめた" }] 417 | }, 418 | { 419 | "characters": [{ "name": "東北ずん子" }, { "name": "四国めたん" }], 420 | "tags": [{ "name": "ずんめた" }] 421 | }, 422 | { 423 | "characters": [{ "name": "ずんだもん" }, { "name": "東北きりたん" }], 424 | "tags": [{ "name": "もんきり" }] 425 | }, 426 | { 427 | "characters": [{ "name": "さとうささら" }, { "name": "すずきつづみ" }], 428 | "tags": [{ "name": "ささつづ" }] 429 | } 430 | ] 431 | -------------------------------------------------------------------------------- /scraping/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scripts/deployall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | cd $(dirname $0) 4 | 5 | find ../envs -type f | 6 | xargs -n 1 -Ixxx basename xxx .sh | 7 | xargs -n 1 npm run deploy 8 | -------------------------------------------------------------------------------- /scripts/scrapingall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | cd $(dirname $0) 4 | 5 | find ../envs -type f | 6 | xargs -n 1 -Ixxx basename xxx .sh | 7 | xargs -n 1 npm run scraping 8 | -------------------------------------------------------------------------------- /scripts/setenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | cd $(dirname $0) 4 | 5 | cd .. 6 | 7 | # utils 8 | usage_exit() { 9 | set +u 10 | NPM_SCRIPT_NAME="[script name]" 11 | [ "$npm_lifecycle_event" ] && NPM_SCRIPT_NAME=$npm_lifecycle_event 12 | set -u 13 | 14 | AVAILABLE_TARGET_CONTENTS=$( 15 | find envs -type f | 16 | xargs -n 1 -Ixxx basename xxx .sh | 17 | xargs echo | 18 | sed -e "s/ / | /g" 19 | ) 20 | echo usage: npm run $NPM_SCRIPT_NAME [TARGET_CONTENT] 21 | echo available TARGET_CONTENT: $AVAILABLE_TARGET_CONTENTS 22 | exit 0 23 | } 24 | 25 | get_repos_jsonl() { 26 | for env_file in $(find envs -type f); do 27 | ( 28 | . $env_file 29 | echo "{ \ 30 | \"content_name\": \"$REACT_APP_CONTENT_NAME\", \ 31 | \"repo\": \"$DEPLOY_REPOSITORY\" \ 32 | }" | tr -s " " 33 | ) 34 | done 35 | } 36 | 37 | # process args 38 | set +u 39 | export TARGET_CONTENT="$1" 40 | [ "$TARGET_CONTENT" = "" ] && echo TARGET_CONTENT is required! && usage_exit 41 | set -u 42 | 43 | # main 44 | ENV_FILE="envs/${TARGET_CONTENT}.sh" 45 | if [ ! -e $ENV_FILE ]; then 46 | echo unknown TARGET_CONTENT name: $TARGET_CONTENT 47 | usage_exit 48 | fi 49 | 50 | export REACT_APP_BUILD_DATE="$(date -I)" 51 | export REACT_APP_REPOS_JSONL="$(get_repos_jsonl)" 52 | . $ENV_FILE 53 | 54 | $EXEC 55 | -------------------------------------------------------------------------------- /scripts/watch-all-scraping-eta.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | cd $(dirname $0) 4 | 5 | cd .. 6 | 7 | # check 8 | for command in grep cut sed cat jq sort uniq wc find date bc node sleep; do 9 | if ! type $command >/dev/null; then 10 | echo "$command not found !!!" 11 | exit 1 12 | fi 13 | done 14 | 15 | # main 16 | fetch_interval_ms=$( 17 | grep { 41 | useEffect(() => { 42 | ReactGA.initialize('UA-158683797-1'); 43 | ReactGA.pageview(window.location.pathname + window.location.search); 44 | }, []); 45 | 46 | return ( 47 | <> 48 | 49 | 50 | {/* graph */} 51 |
58 |
65 |
67 | 68 | 69 | 70 | 71 |
79 | 80 |
81 | 82 |
92 | 93 |
94 |
95 | 96 | {/* ranking */} 97 |
103 |
110 | 118 | {process.env.REACT_APP_CONTENT_NAME}のカップリングランキング 119 | 120 | 121 | 更新: {process.env.REACT_APP_BUILD_DATE} 122 | 123 | 124 |
125 |
126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/CharacterDialog/CharacterDialog.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import CloseIcon from '@mui/icons-material/Close'; 3 | import { Dialog, DialogTitle, IconButton } from '@mui/material'; 4 | import { FC, useId } from 'react'; 5 | import { useRecoilState, useRecoilValue } from 'recoil'; 6 | import { Character, Coupling } from 'yurigraph-scraping'; 7 | import { couplings } from '../couplings'; 8 | import { PixivDictLink, PixivTagLink } from '../PixivUtils/PixivUtils'; 9 | import theme from '../theme'; 10 | import { character_dialog_name, character_dialog_open } from './recoil'; 11 | 12 | const name2character: Map = new Map( 13 | couplings.flatMap((coupling) => 14 | coupling.characters.map((character) => [character.name, character]) 15 | ) 16 | ); 17 | 18 | const name2couplings = new Map(); 19 | couplings.forEach((coupling) => 20 | coupling.characters.forEach((character) => 21 | name2couplings.set(character.name, []) 22 | ) 23 | ); 24 | couplings.forEach((coupling) => 25 | coupling.characters.forEach((character) => { 26 | const couplings = name2couplings.get(character.name); 27 | if (couplings === undefined) return; 28 | name2couplings.set(character.name, [...couplings, coupling]); 29 | }) 30 | ); 31 | 32 | export const CharacterDialog: FC = () => { 33 | const [open, setOpen] = useRecoilState(character_dialog_open); 34 | const name = useRecoilValue(character_dialog_name); 35 | 36 | const character = name && name2character.get(name); 37 | const couplings = name && name2couplings.get(name); 38 | 39 | const id = useId(); 40 | 41 | return ( 42 | setOpen(false)} 47 | > 48 | 59 |
64 | {character && ( 65 | 71 | )} 72 | のカップリング一覧 73 |
74 | setOpen(false)} 77 | css={css` 78 | flex: 0 0; 79 | margin-left: auto; 80 | `} 81 | > 82 | 83 | 84 |
85 | 86 |
91 |
    92 | {couplings && 93 | couplings 94 | .map((x) => ({ 95 | ...x, 96 | num: x.tags.map((x) => x.num).reduce((s, x) => (s > x ? s : x)), 97 | })) 98 | .sort((x, y) => y.num - x.num) 99 | .map((x) => { 100 | const friend = x.characters.find( 101 | (x) => x.name !== name 102 | ) as Character; 103 | 104 | return ( 105 |
  1. 106 | { 107 | 114 | } 115 |
      116 | {x.tags 117 | .sort((x, y) => y.num - x.num) 118 | .map((x) => ( 119 |
    • 120 | ({x.num}作品) 121 |
    • 122 | ))} 123 |
    124 |
  2. 125 | ); 126 | })} 127 |
128 |
129 |
130 | ); 131 | }; 132 | -------------------------------------------------------------------------------- /src/CharacterDialog/recoil.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const character_dialog_open = atom({ 4 | key: 'character_dialog/open', 5 | default: false, 6 | }); 7 | 8 | export const character_dialog_name = atom({ 9 | key: 'character_dialog/name', 10 | default: '', 11 | }); 12 | -------------------------------------------------------------------------------- /src/FilterSlider/FilterSlider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from '@mui/material'; 2 | import { FC, useCallback } from 'react'; 3 | import { useRecoilState } from 'recoil'; 4 | import stats from 'stats-lite'; 5 | import { couplings } from '../couplings'; 6 | import { filter_slider_value } from './recoil'; 7 | 8 | const nums = couplings 9 | .map((x) => x.tags.map((x) => x.num)) 10 | .reduce((s, x) => [...s, ...x]); 11 | const num_stats = { 12 | max: nums.reduce((s, x) => (s > x ? s : x)), 13 | min: nums.reduce((s, x) => (s < x ? s : x)), 14 | center: parseInt(process.env.REACT_APP_DEFAULT_FILTER_VALUE as string), 15 | stdev: stats.stdev(nums), 16 | }; 17 | 18 | const step = Math.floor(num_stats.stdev / 10); 19 | const min = 0; 20 | const max = Math.floor(num_stats.center + num_stats.stdev); 21 | 22 | export const FilterSlider: FC = () => { 23 | const [filter_num, setFilterNum] = useRecoilState(filter_slider_value); 24 | const getLabelText = useCallback((x: number) => `${x}作品以上`, []); 25 | 26 | return ( 27 | typeof v === 'number' && setFilterNum(v)} 36 | marks 37 | /> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/FilterSlider/recoil.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const filter_slider_value = atom({ 4 | key: 'filter_slider/value', 5 | default: parseInt(process.env.REACT_APP_DEFAULT_FILTER_VALUE as string), 6 | }); 7 | -------------------------------------------------------------------------------- /src/Graph/Graph.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { useWindowSize } from '@react-hook/window-size'; 3 | import * as d3 from 'd3'; 4 | import mixColor from 'mix-color'; 5 | import { FC, useEffect, useMemo, useRef } from 'react'; 6 | import { useRecoilValue } from 'recoil'; 7 | import { resolver_enable } from '../Resolver/recoil'; 8 | import theme from '../theme'; 9 | import { 10 | getSimuration, 11 | getSimurationForResolvedGraph, 12 | } from './forceSimuration'; 13 | import { Link, linkTickHandler } from './Link'; 14 | import { Node, nodeTickHandler } from './Node'; 15 | import { graph_d3graph, graph_sigmoid } from './recoil'; 16 | import { D3Graph } from './types'; 17 | import { getLinkDetail, getNodeDetail } from './utils'; 18 | 19 | const deepCopy = require('deep-copy'); 20 | 21 | export const Graph: FC = () => { 22 | const _d3graph = useRecoilValue(graph_d3graph); 23 | const d3graph = useMemo(() => deepCopy(_d3graph) as D3Graph, [_d3graph]); 24 | 25 | const sigmoid = useRecoilValue(graph_sigmoid); 26 | const [width, height] = useWindowSize(); 27 | const resolve_mode = useRecoilValue(resolver_enable); 28 | const simuration = useMemo( 29 | () => 30 | (!resolve_mode ? getSimuration : getSimurationForResolvedGraph)(d3graph, { 31 | window_size: { width, height }, 32 | sigmoid, 33 | }), 34 | [d3graph, sigmoid, width, height, resolve_mode] 35 | ); 36 | 37 | const svg_ref = useRef(null); 38 | useEffect(() => { 39 | if (svg_ref.current === null) return; 40 | 41 | const root = d3.select(svg_ref.current); 42 | simuration.on('tick', () => { 43 | root.call((selection) => { 44 | nodeTickHandler(selection); 45 | linkTickHandler(selection); 46 | }); 47 | }); 48 | }, [simuration]); 49 | 50 | const graph_position = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); 51 | const root_group = useRef(null); 52 | const setupMoveHandler = (svg: Element | null) => { 53 | if (svg === null) return; 54 | d3.select(svg as Element).call( 55 | d3 56 | .drag() 57 | .subject(() => { 58 | if (root_group.current === null) return; 59 | return { x: graph_position.current.x, y: graph_position.current.y }; 60 | }) 61 | .on('drag', (event: any) => { 62 | if (root_group.current === null) return; 63 | root_group.current.style.transform = `translate(${event.x}px, ${event.y}px)`; 64 | graph_position.current = { x: event.x, y: event.y }; 65 | }) 66 | ); 67 | }; 68 | 69 | // render 70 | const link_components = d3graph.links.map((link) => { 71 | const detail = getLinkDetail(link.link_id); 72 | if (detail === undefined) return null; 73 | 74 | return ; 75 | }); 76 | const node_components = d3graph.nodes.map((node) => { 77 | const detail = getNodeDetail(node.node_id); 78 | if (detail === undefined) return null; 79 | 80 | return ( 81 | 87 | ); 88 | }); 89 | 90 | return ( 91 | { 106 | svg_ref.current = svg; 107 | setupMoveHandler(svg); 108 | }} 109 | > 110 | 111 | {link_components} 112 | {node_components} 113 | 114 | 115 | ); 116 | }; 117 | 118 | export default Graph; 119 | -------------------------------------------------------------------------------- /src/Graph/Link.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import * as d3 from 'd3'; 3 | import mixColor from 'mix-color'; 4 | import { FC, useId } from 'react'; 5 | import { useRecoilValue } from 'recoil'; 6 | import theme from '../theme'; 7 | import { graph_focused_node, graph_sigmoid } from './recoil'; 8 | import { D3Link, D3Node } from './types'; 9 | import { LinkDetail } from './utils'; 10 | 11 | const ROOT_CLASS_NAME = 'link'; 12 | 13 | export const Link: FC<{ 14 | link: D3Link; 15 | detail: LinkDetail; 16 | }> = ({ link, detail }) => { 17 | const sigmoid = useRecoilValue(graph_sigmoid); 18 | const weight = sigmoid(detail.tag.num); 19 | 20 | const focused_node_index = useRecoilValue(graph_focused_node); 21 | const mode: 'normal' | 'activated' | 'inactive' = (() => { 22 | if ( 23 | link.node_ids[0] === focused_node_index || 24 | link.node_ids[1] === focused_node_index 25 | ) 26 | return 'activated'; 27 | if (focused_node_index !== null) return 'inactive'; 28 | return 'normal'; 29 | })(); 30 | 31 | const id = useId(); 32 | 33 | const registerLink = (element: Element | null) => { 34 | if (element === null) return; 35 | const root = d3.select(element); 36 | root.datum(link); 37 | }; 38 | 39 | return ( 40 | <> 41 | 53 | {mode === 'activated' ? ( 54 | 62 | 63 | {detail.tag.name} 64 | 65 | 66 | ) : null} 67 | 68 | ); 69 | }; 70 | 71 | export const linkTickHandler = ( 72 | selection: d3.Selection 73 | ) => { 74 | selection 75 | .selectAll(`.${ROOT_CLASS_NAME}`) 76 | .call((selection) => 77 | ( 78 | selection as unknown as d3.Selection< 79 | SVGPathElement, 80 | D3Link & { source: D3Node; target: D3Node }, 81 | SVGSVGElement, 82 | unknown 83 | > 84 | ).attr( 85 | 'd', 86 | (link) => 87 | `M ${link.source.x},${link.source.y} L ${link.target.x},${link.target.y}` 88 | ) 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/Graph/Node.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import * as d3 from 'd3'; 3 | import { Simulation } from 'd3'; 4 | import { FC } from 'react'; 5 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 6 | import { 7 | character_dialog_name, 8 | character_dialog_open, 9 | } from '../CharacterDialog/recoil'; 10 | import theme from '../theme'; 11 | import { graph_activated_nodes, graph_focused_node } from './recoil'; 12 | import { D3Link, D3Node } from './types'; 13 | import { NodeDetail } from './utils'; 14 | 15 | const ROOT_CLASS_NAME = 'node'; 16 | 17 | export const Node: FC<{ 18 | node: D3Node; 19 | detail: NodeDetail; 20 | simuration: Simulation; 21 | }> = ({ node, detail, simuration }) => { 22 | const setFocusedNode = useSetRecoilState(graph_focused_node); 23 | const activated_nodes = useRecoilValue(graph_activated_nodes); 24 | const active = activated_nodes === null || activated_nodes?.has(node.node_id); 25 | 26 | const setCharacterDialogOpen = useSetRecoilState(character_dialog_open); 27 | const setCharacterDialogName = useSetRecoilState(character_dialog_name); 28 | 29 | const registerNode = (element: Element | null) => { 30 | if (element === null) return; 31 | const root = d3.select(element); 32 | root.datum(node); 33 | }; 34 | 35 | const setupDragHandler = (element: Element | null) => { 36 | if (element === null) return; 37 | 38 | d3.select(element).call( 39 | d3 40 | .drag() 41 | .on('start', (_, d: any) => { 42 | simuration.alphaTarget(0.1).restart(); 43 | d.fx = d.x; 44 | d.fy = d.y; 45 | }) 46 | .on('drag', (event: any, d: any) => { 47 | d.fx = event.x; 48 | d.fy = event.y; 49 | }) 50 | .on('end', (_, d: any) => { 51 | simuration.alphaTarget(0).restart(); 52 | d.fx = null; 53 | d.fy = null; 54 | }) 55 | ); 56 | }; 57 | 58 | return ( 59 | { 73 | setFocusedNode(node.node_id); 74 | }} 75 | onMouseLeave={() => { 76 | setFocusedNode(null); 77 | }} 78 | onClick={() => { 79 | setCharacterDialogOpen(true); 80 | setCharacterDialogName(detail.name); 81 | }} 82 | ref={(g) => { 83 | registerNode(g); 84 | setupDragHandler(g); 85 | }} 86 | > 87 | 97 | 111 | {detail.name.replace(/\(.*\)/, '')} 112 | 113 | 114 | ); 115 | }; 116 | 117 | export const nodeTickHandler = ( 118 | selection: d3.Selection 119 | ) => { 120 | selection 121 | .selectAll(`.${ROOT_CLASS_NAME}`) 122 | .call((selection) => 123 | ( 124 | selection as unknown as d3.Selection< 125 | SVGCircleElement, 126 | D3Node, 127 | SVGSVGElement, 128 | unknown 129 | > 130 | ).attr('transform', (node) => `translate(${node.x}, ${node.y})`) 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /src/Graph/forceSimuration.ts: -------------------------------------------------------------------------------- 1 | import makeSigmoid from 'awesome-sigmoid'; 2 | import * as d3 from 'd3'; 3 | import { Simulation } from 'd3'; 4 | import { D3Graph, D3Link, D3Node } from './types'; 5 | import { getLinkDetail } from './utils'; 6 | 7 | export const LINK_LENGTH = 100; 8 | 9 | export const getSimuration = ( 10 | graph: D3Graph, 11 | options: { 12 | window_size: { width: number; height: number }; 13 | sigmoid: ReturnType; 14 | } 15 | ): Simulation => 16 | d3 17 | .forceSimulation(graph.nodes) 18 | .force( 19 | 'link', 20 | d3 21 | .forceLink(graph.links) 22 | .distance(LINK_LENGTH) 23 | .strength( 24 | (link) => 25 | 0.1 + 26 | options.sigmoid(getLinkDetail(link.link_id)?.tag.num ?? 0) * 0.9 27 | ) 28 | ) 29 | .force('collide', d3.forceCollide(LINK_LENGTH / 2).strength(0.5)) 30 | .force('charge', d3.forceManyBody().strength(-LINK_LENGTH * 0.25)) 31 | .force( 32 | 'center', 33 | d3 34 | .forceCenter() 35 | .x(options.window_size.width / 2) 36 | .y(options.window_size.height / 2) 37 | ); 38 | 39 | export const getSimurationForResolvedGraph = ( 40 | graph: D3Graph, 41 | options: { 42 | window_size: { width: number; height: number }; 43 | sigmoid: ReturnType; 44 | } 45 | ): Simulation => 46 | d3 47 | .forceSimulation(graph.nodes) 48 | .force('link', d3.forceLink(graph.links).distance(LINK_LENGTH)) 49 | .force('collide', d3.forceCollide(10)) 50 | .force('charge', d3.forceManyBody().strength(-200).distanceMax(200)) 51 | .force( 52 | 'center', 53 | d3 54 | .forceCenter() 55 | .x(options.window_size.width / 2) 56 | .y(options.window_size.height / 2) 57 | ); 58 | -------------------------------------------------------------------------------- /src/Graph/recoil.ts: -------------------------------------------------------------------------------- 1 | import { atom, selector } from 'recoil'; 2 | import { filter_slider_value } from '../FilterSlider/recoil'; 3 | import { 4 | resolver_enable, 5 | resolver_prioritizer_links, 6 | } from '../Resolver/recoil'; 7 | import { NodeId } from './types'; 8 | import { 9 | all_graph, 10 | filterGraphByNum, 11 | getD3GraphByGraph, 12 | getRelatedNodesByLinks, 13 | getSigmoidByLinks, 14 | resolveGraphOneOnOne, 15 | } from './utils'; 16 | 17 | export const graph_graph = selector({ 18 | key: 'graph/graph', 19 | get: ({ get }) => { 20 | const filter_value = get(filter_slider_value); 21 | const filtered_graph = filterGraphByNum(all_graph, filter_value); 22 | 23 | const resolve_enable = get(resolver_enable); 24 | const prioritized_links = get(resolver_prioritizer_links); 25 | const resolved_graph = resolve_enable 26 | ? resolveGraphOneOnOne(filtered_graph, prioritized_links) 27 | : filtered_graph; 28 | 29 | return resolved_graph; 30 | }, 31 | }); 32 | export const graph_d3graph = selector({ 33 | key: 'graph/d3graph', 34 | get: ({ get }) => { 35 | const graph = get(graph_graph); 36 | return getD3GraphByGraph(graph); 37 | }, 38 | }); 39 | 40 | export const graph_sigmoid = selector({ 41 | key: 'graph/sigmoid', 42 | get: ({ get }) => { 43 | const graph = get(graph_graph); 44 | return getSigmoidByLinks(graph.links); 45 | }, 46 | }); 47 | 48 | export const graph_focused_node = atom({ 49 | key: 'graph/graph_focused_node', 50 | default: null, 51 | }); 52 | export const graph_activated_nodes = selector | null>({ 53 | key: 'graph/graph_activated_nodes', 54 | get: ({ get }) => { 55 | const index = get(graph_focused_node); 56 | if (index === null) return null; 57 | 58 | const graph = get(graph_graph); 59 | const indexes = getRelatedNodesByLinks(graph.links).get(index); 60 | if (indexes === undefined) return null; 61 | 62 | return new Set([index, ...indexes]); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/Graph/types.ts: -------------------------------------------------------------------------------- 1 | import { SimulationLinkDatum, SimulationNodeDatum } from 'd3'; 2 | 3 | // 汎用のグラフデータ構造.フィルタリングや1対1解決など,抽象的なグラフ操作で使用する 4 | export type NodeId = number; 5 | 6 | export type LinkId = number; 7 | export type Link = { 8 | id: LinkId; 9 | nodes: [NodeId, NodeId]; 10 | num: number; 11 | }; 12 | 13 | export type Graph = { 14 | nodes: NodeId[]; 15 | links: Link[]; 16 | }; 17 | 18 | // D3のグラフデータ構造.配置や描画に使用する 19 | export interface D3Node extends SimulationNodeDatum { 20 | node_id: NodeId; 21 | } 22 | 23 | export interface D3Link extends SimulationLinkDatum { 24 | link_id: LinkId; 25 | node_ids: [NodeId, NodeId]; 26 | } 27 | 28 | export type D3Graph = { 29 | nodes: D3Node[]; 30 | links: D3Link[]; 31 | }; 32 | -------------------------------------------------------------------------------- /src/Graph/utils.ts: -------------------------------------------------------------------------------- 1 | import makeSigmoid from 'awesome-sigmoid'; 2 | import stats from 'stats-lite'; 3 | import { Character, Coupling } from 'yurigraph-scraping'; 4 | import { couplings } from '../couplings'; 5 | import { D3Graph, D3Link, D3Node, Graph, Link, LinkId, NodeId } from './types'; 6 | 7 | // setup fundamental graph 8 | //// node 9 | const characters: Character[] = (() => { 10 | const name2character: Map = new Map(); 11 | couplings.forEach((coupling) => 12 | coupling.characters.forEach((character) => 13 | name2character.set(character.name, character) 14 | ) 15 | ); 16 | return Array.from(name2character.values()); 17 | })(); 18 | 19 | const node_id2character: Map = new Map( 20 | characters.map((character, i) => [i, character]) 21 | ); 22 | const all_nodes: NodeId[] = Array.from(node_id2character.keys()); 23 | 24 | export type NodeDetail = Character; 25 | export const getNodeDetail = (node_id: NodeId): NodeDetail | undefined => 26 | node_id2character.get(node_id); 27 | 28 | //// link 29 | const name2node_id = new Map( 30 | Array.from(node_id2character.entries()).map(([id, character]) => [ 31 | character.name, 32 | id, 33 | ]) 34 | ); 35 | 36 | export type LinkDetail = { 37 | coupling: Coupling; 38 | tag: Coupling['tags'][number]; 39 | }; 40 | const link_id2detail = new Map(); 41 | 42 | const all_links: Link[] = couplings 43 | .map((coupling, id): Omit | undefined => { 44 | if (coupling.tags.length === 0) return undefined; 45 | const tag = coupling.tags.reduce((x, y) => (x.num > y.num ? x : y)); 46 | 47 | const node1 = name2node_id.get(coupling.characters[0].name); 48 | const node2 = name2node_id.get(coupling.characters[1].name); 49 | if (node1 === undefined) return undefined; 50 | if (node2 === undefined) return undefined; 51 | 52 | link_id2detail.set(id, { coupling, tag }); 53 | return { 54 | id, 55 | num: tag.num, 56 | nodes: [node1, node2], 57 | }; 58 | }) 59 | .filter((link): link is Exclude => link !== undefined) 60 | .map((link, index) => ({ ...link, index })); 61 | 62 | export const getLinkDetail = (link_id: LinkId): LinkDetail | undefined => 63 | link_id2detail.get(link_id); 64 | 65 | //// graph 66 | export const all_graph: Graph = { 67 | nodes: all_nodes, 68 | links: all_links, 69 | }; 70 | 71 | // graph utils 72 | export const filterGraphByNum = (graph: Graph, num: number): Graph => { 73 | const links = graph.links.filter((link) => link.num >= num); 74 | const nodes = Array.from(new Set(links.flatMap((link) => link.nodes))); 75 | return { nodes, links }; 76 | }; 77 | 78 | export const resolveGraphOneOnOne = ( 79 | graph: Graph, 80 | prioritized_links: Link[] 81 | ): Graph => { 82 | const resolved_links: Link[] = []; 83 | const resolved_nodes = new Set(); 84 | const resolveLink = (link: Link, force: boolean | undefined = false) => { 85 | const resolvable = 86 | !resolved_nodes.has(link.nodes[0]) && !resolved_nodes.has(link.nodes[1]); 87 | 88 | if (resolvable || force) { 89 | resolved_links.push(link); 90 | resolved_nodes.add(link.nodes[0]); 91 | resolved_nodes.add(link.nodes[1]); 92 | } 93 | }; 94 | 95 | // first, resolve prioritized links 96 | prioritized_links.forEach((link) => resolveLink(link, true)); 97 | 98 | // next, resolve normal links 99 | graph.links 100 | .concat() 101 | .sort((x, y) => y.num - x.num) 102 | .forEach((link) => resolveLink(link)); 103 | 104 | return { nodes: graph.nodes, links: resolved_links }; 105 | }; 106 | 107 | export const getRelatedNodesByLinks = (links: Link[]) => { 108 | const related_nodes: Map> = new Map( 109 | all_nodes.map((node) => [node, new Set()]) 110 | ); 111 | const addNode = (source: NodeId, target: NodeId) => { 112 | const set = related_nodes.get(source); 113 | if (set === undefined) return; 114 | set.add(target); 115 | }; 116 | links.forEach((link) => { 117 | addNode(link.nodes[0], link.nodes[1]); 118 | addNode(link.nodes[1], link.nodes[0]); 119 | }); 120 | 121 | return related_nodes; 122 | }; 123 | 124 | export const getSigmoidByLinks = (links: Link[]) => { 125 | const nums = links.flatMap((link) => link.num); 126 | return makeSigmoid({ 127 | center: stats.mean(nums), 128 | deviation: stats.stdev(nums), 129 | deviation_output: 0.9, 130 | }); 131 | }; 132 | 133 | export const getD3GraphByGraph = (graph: Graph): D3Graph => { 134 | const sorted_links = graph.links.concat().sort((x, y) => y.num - x.num); 135 | const sorted_nodes = [ 136 | ...sorted_links.flatMap((link) => link.nodes), 137 | ...graph.nodes, 138 | ].filter((x, i, self) => self.indexOf(x) === i); 139 | 140 | const d3_nodes: D3Node[] = sorted_nodes.map((node_id, index) => ({ 141 | index, 142 | node_id, 143 | })); 144 | 145 | const old_node2new_node = new Map(sorted_nodes.map((node, i) => [node, i])); 146 | const d3_links: D3Link[] = sorted_links 147 | .map((link, index): D3Link | undefined => { 148 | const source = old_node2new_node.get(link.nodes[0]); 149 | const target = old_node2new_node.get(link.nodes[1]); 150 | if (source === undefined) return undefined; 151 | if (target === undefined) return undefined; 152 | 153 | return { 154 | index, 155 | source, 156 | target, 157 | link_id: link.id, 158 | node_ids: link.nodes, 159 | }; 160 | }) 161 | .filter( 162 | (link): link is Exclude => link !== undefined 163 | ); 164 | 165 | return { nodes: d3_nodes, links: d3_links }; 166 | }; 167 | -------------------------------------------------------------------------------- /src/Nav/Nav.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import CloseIcon from '@mui/icons-material/Close'; 3 | import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; 4 | import GitHubIcon from '@mui/icons-material/GitHub'; 5 | import MenuIcon from '@mui/icons-material/Menu'; 6 | import { Divider, Drawer, IconButton, Typography } from '@mui/material'; 7 | import { useRecoilState } from 'recoil'; 8 | import theme from '../theme'; 9 | import { nav_open } from './recoil'; 10 | 11 | const repos = process.env.REACT_APP_REPOS_JSONL?.split('\n').map((x) => 12 | JSON.parse(x) 13 | ) as { content_name: string; repo: string }[]; 14 | 15 | // components 16 | export const Nav = () => { 17 | const [open, setOpen] = useRecoilState(nav_open); 18 | 19 | return ( 20 | <> 21 | 40 | 41 | setOpen(false)}> 42 |
54 | setOpen(false)}> 55 | 56 | 57 |
58 | 59 |
77 | setOpen(false)}> 78 | 79 | カップリングランキング 80 | 81 | 82 | 87 | 88 | 89 | 他の作品 90 | 91 |
    92 | {repos 93 | .filter( 94 | ({ content_name }) => 95 | content_name !== process.env.REACT_APP_CONTENT_NAME 96 | ) 97 | .map(({ content_name, repo }) => ( 98 |
  • 99 | 104 | {content_name} 105 | 106 |
  • 107 | ))} 108 |
109 | 110 | 115 | 116 | 121 | 122 | ソースコード 123 | 124 |
125 |
126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/Nav/recoil.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | export const nav_open = atom({ key: 'nav/open', default: false }); 4 | -------------------------------------------------------------------------------- /src/PixivUtils/PixivUtils.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | export const PixivDictLink: FC<{ title: string; label?: string }> = ({ 4 | title, 5 | label, 6 | }) => ( 7 | 12 | {(label ?? title).replace(/\(.*\)/, '')} 13 | 14 | ); 15 | 16 | export const PixivTagLink: FC<{ tag: string; label?: string }> = ({ 17 | tag, 18 | label, 19 | }) => ( 20 | 25 | {(label ?? tag).replace(/\(.*\)/, '')} 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/Ranking/CouplingRanking.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { FC } from 'react'; 3 | import { Coupling } from 'yurigraph-scraping'; 4 | import { couplings } from '../couplings'; 5 | import { PixivDictLink, PixivTagLink } from '../PixivUtils/PixivUtils'; 6 | 7 | type CouplingTag = { 8 | characters: Coupling['characters']; 9 | tag: Coupling['tags'][0]; 10 | }; 11 | 12 | export const CouplingRanking: FC<{}> = () => { 13 | const coupling_tags: CouplingTag[] = couplings 14 | .map((x) => x.tags.map((y) => ({ characters: x.characters, tag: y }))) 15 | .reduce((s, x) => [...s, ...x]) 16 | .sort((x, y) => y.tag.num - x.tag.num); 17 | 18 | return ( 19 |
    20 | {coupling_tags.map((coupling_tag, i) => { 21 | const rank = i + 1; 22 | 23 | return ( 24 |
  1. 30 | 31 | 38 | {coupling_tag.characters 39 | .map((character) => ( 40 | 48 | )) 49 | .reduce((s, x) => ( 50 | <> 51 | {s} × {x} 52 | 53 | ))} 54 | 55 | 62 | ({coupling_tag.tag.num}作品) 63 | 64 |
  2. 65 | ); 66 | })} 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/Resolver/Prioritizer.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; 3 | import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; 4 | import { Checkbox, FormControlLabel, IconButton, Tooltip } from '@mui/material'; 5 | import { FC, memo, useCallback, useMemo } from 'react'; 6 | import { useRecoilState, useRecoilValue } from 'recoil'; 7 | import { filter_slider_value } from '../FilterSlider/recoil'; 8 | import { graph_graph } from '../Graph/recoil'; 9 | import { Link } from '../Graph/types'; 10 | import { all_graph, getLinkDetail, LinkDetail } from '../Graph/utils'; 11 | import theme from '../theme'; 12 | import { 13 | resolver_prioritizer_links, 14 | resolver_prioritizer_one_on_one_mode, 15 | } from './recoil'; 16 | 17 | export const Prioritizer: FC = () => { 18 | const [_prioritized_links, _setPrioritizedLinks] = useRecoilState( 19 | resolver_prioritizer_links 20 | ); 21 | const addPrioritizedLinks = useCallback( 22 | (link: Link) => { 23 | _setPrioritizedLinks((prev_links) => [...prev_links, link]); 24 | }, 25 | [_setPrioritizedLinks] 26 | ); 27 | const deletePrioritizedLinks = useCallback( 28 | (link: Link) => { 29 | _setPrioritizedLinks((prev_links) => 30 | prev_links.filter((prioritized_link) => prioritized_link.id !== link.id) 31 | ); 32 | }, 33 | [_setPrioritizedLinks] 34 | ); 35 | const resetPrioritizedLinks = useCallback(() => { 36 | _setPrioritizedLinks([]); 37 | }, [_setPrioritizedLinks]); 38 | const prioritized_link_ids = new Set( 39 | _prioritized_links.map((link) => link.id) 40 | ); 41 | 42 | const prioritized_nodes = new Set( 43 | _prioritized_links.flatMap((link) => link.nodes) 44 | ); 45 | 46 | const filter_value = useRecoilValue(filter_slider_value); 47 | const link_details = useMemo( 48 | () => 49 | all_graph.links 50 | .filter((link) => link.num >= filter_value) 51 | .concat() 52 | .sort((x, y) => y.num - x.num) 53 | .map((link) => { 54 | const detail = getLinkDetail(link.id); 55 | return detail ? { link, detail } : undefined; 56 | }) 57 | .filter( 58 | ( 59 | link_detail 60 | ): link_detail is Exclude => 61 | link_detail !== undefined 62 | ), 63 | [filter_value] 64 | ); 65 | 66 | const graph = useRecoilValue(graph_graph); 67 | const auto_selected_link_ids = new Set(graph.links.map((link) => link.id)); 68 | 69 | const [one_on_one_mode, setOneOnOneMode] = useRecoilState( 70 | resolver_prioritizer_one_on_one_mode 71 | ); 72 | 73 | const onChangeListItem = useCallback( 74 | (e: React.ChangeEvent, link: Link) => { 75 | if (e.target.checked) { 76 | addPrioritizedLinks(link); 77 | } else { 78 | deletePrioritizedLinks(link); 79 | } 80 | }, 81 | [addPrioritizedLinks, deletePrioritizedLinks] 82 | ); 83 | 84 | return ( 85 |
96 |

優先したいカップリングを選択してください

97 |
98 | 使い方 99 |
    109 |
  • 110 | 111 | :優先できます.「1対1に解決」で採用されなかったカップリングです. 112 |
  • 113 |
  • 114 | 115 | :優先しています. 116 |
  • 117 |
  • 118 | 124 | :「1対1に解決」によって自動的に成立しているカップリングです. 125 |
  • 126 |
  • 127 | 128 | :「1対1にこだわる」によって優先できません.同じキャラクターを含む他のカップリングが優先済みです. 129 |
  • 130 |
131 |
132 | 133 | setOneOnOneMode(e.target.checked)} 138 | /> 139 | } 140 | label={ 141 | 142 | 1対1にこだわる 143 | 147 | 148 | 149 | 150 | } 151 | /> 152 | 155 | 156 | 157 | } 158 | label="設定をリセット" 159 | /> 160 |
    167 | {link_details.map(({ link, detail }) => ( 168 | 186 | ))} 187 |
188 |
189 | ); 190 | }; 191 | 192 | const ListItem: FC<{ 193 | link: Link; 194 | detail: LinkDetail; 195 | checked: boolean; 196 | indeterminate: boolean; 197 | disabled: boolean; 198 | onChange: (event: React.ChangeEvent, link: Link) => void; 199 | }> = memo(({ link, detail, onChange, ...checkbox_props }) => { 200 | return ( 201 |
  • 202 | onChange(e, link)} 207 | {...checkbox_props} 208 | /> 209 | } 210 | label={ 211 | 212 | {detail.tag.name} 213 | 217 | 218 | 219 | 220 | } 221 | /> 222 |
  • 223 | ); 224 | }); 225 | -------------------------------------------------------------------------------- /src/Resolver/Resolver.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 3 | import SettingsIcon from '@mui/icons-material/Settings'; 4 | import { Checkbox, Drawer, FormControlLabel, IconButton } from '@mui/material'; 5 | import { FC } from 'react'; 6 | import { useRecoilState } from 'recoil'; 7 | import theme from '../theme'; 8 | import { Prioritizer } from './Prioritizer'; 9 | import { resolver_enable, resolver_prioritizer_open } from './recoil'; 10 | 11 | export const Resolver: FC = () => { 12 | const [enable, setEnable] = useRecoilState(resolver_enable); 13 | const [prioritizer_open, setPrioritizerOpen] = useRecoilState( 14 | resolver_prioritizer_open 15 | ); 16 | 17 | return ( 18 |
    27 | setEnable(e.target.checked)} 32 | /> 33 | } 34 | label="1対1に解決" 35 | /> 36 | 37 | setPrioritizerOpen(true)} 41 | > 42 | 43 | 44 | 56 |
    67 | { 69 | setPrioritizerOpen(false); 70 | }} 71 | > 72 | 73 | 74 |
    75 | {prioritizer_open && } 76 |
    77 |
    78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/Resolver/recoil.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { Link } from '../Graph/types'; 3 | 4 | export const resolver_enable = atom({ key: 'resolver/enable', default: false }); 5 | 6 | export const resolver_prioritizer_open = atom({ 7 | key: 'resolver/prioritizer/open', 8 | default: false, 9 | }); 10 | export const resolver_prioritizer_links = atom({ 11 | key: 'resolver/prioritizer/links', 12 | default: [], 13 | }); 14 | export const resolver_prioritizer_one_on_one_mode = atom({ 15 | key: 'resolver/prioritizer/one_on_one_mode', 16 | default: true, 17 | }); 18 | -------------------------------------------------------------------------------- /src/couplings.ts: -------------------------------------------------------------------------------- 1 | import { Couplings } from 'yurigraph-scraping'; 2 | import couplings_json from './couplings.json'; 3 | 4 | export const couplings = couplings_json as Couplings; 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@mui/material'; 2 | import ReactDOM from 'react-dom'; 3 | import { RecoilRoot } from 'recoil'; 4 | import { App } from './App'; 5 | import { muiTheme } from './theme'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | import { pink } from '@mui/material/colors'; 3 | 4 | export const muiTheme = createTheme({ 5 | palette: { 6 | primary: { 7 | main: pink[600], 8 | }, 9 | }, 10 | }); 11 | 12 | export const makePxScaleUtil = 13 | (base: number) => 14 | (scale: number = 1): string => 15 | `${base * scale}px`; 16 | 17 | const base_color = process.env.REACT_APP_BASE_COLOR; 18 | const main_color = process.env.REACT_APP_MAIN_COLOR; 19 | const accent_color = process.env.REACT_APP_ACCENT_COLOR; 20 | 21 | export const colors = { 22 | base: base_color ? base_color : '#f8f8f8', 23 | main: main_color ? main_color : '#00a8cc', 24 | accent: accent_color ? accent_color : '#fe346e', 25 | border: '#ddd', 26 | text: '#123', 27 | }; 28 | export const px = { 29 | grid: makePxScaleUtil(20), 30 | font_size: makePxScaleUtil(16), 31 | max_width: makePxScaleUtil(900), 32 | border_radius: makePxScaleUtil(3), 33 | }; 34 | 35 | export const theme = { colors, px }; 36 | 37 | export default theme; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "downlevelIteration": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "jsx": "react-jsx", 19 | "jsxImportSource": "@emotion/react" 20 | }, 21 | "include": ["src"] 22 | } 23 | --------------------------------------------------------------------------------
    本人 28 | // 30 | // はるはる 31 | // 32 | // 34 | // 36 | // 千早 37 | // 38 | // 40 | // 41 | // 43 | // はるちは 44 | // 45 | // 46 | //
    レイマリ僕の見つけた真実はレイマリ
    ルーミア×大妖精るーだい