知乎首页信息流过滤

可以按照标签过滤知乎首页信息流,隐藏含不喜欢标签的回答、文章及视频。并且可以在信息流标题下显示发布时间与编辑时间。

  1. // ==UserScript==
  2. // @name 知乎首页信息流过滤
  3. // @namespace https://zhaoji.wang/
  4. // @version 0.2.2
  5. // @description 可以按照标签过滤知乎首页信息流,隐藏含不喜欢标签的回答、文章及视频。并且可以在信息流标题下显示发布时间与编辑时间。
  6. // @author Zhaoji Wang
  7. // @license Apache-2.0
  8. // @match https://www.zhihu.com/
  9. // @match https://www.zhihu.com/follow
  10. // @icon https://www.google.com/s2/favicons?domain=zhihu.com
  11. // @require https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js
  12. // @require https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js
  13. // @require https://cdn.bootcdn.net/ajax/libs/localforage/1.9.0/localforage.min.js
  14. // @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/dayjs.min.js
  15. // @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/locale/zh-cn.min.js
  16. // @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/plugin/duration.min.js
  17. // @require https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/plugin/relativeTime.min.js
  18. // @grant GM_xmlhttpRequest
  19. // @connect www.zhihu.com
  20. // @connect zhuanlan.zhihu.com
  21. // ==/UserScript==
  22. "use strict";
  23.  
  24. $(() => {
  25. // 加载 Day.js
  26. dayjs.locale("zh-cn");
  27. dayjs.extend(dayjs_plugin_duration);
  28. dayjs.extend(dayjs_plugin_relativeTime); // 将 GM_xmlhttpRequest 函数 Promise 化
  29.  
  30. const get = (url) =>
  31. new Promise((resolve, reject) =>
  32. GM_xmlhttpRequest({
  33. method: "GET",
  34. url,
  35.  
  36. onload(response) {
  37. resolve(response.responseText);
  38. },
  39.  
  40. onerror(error) {
  41. reject(error);
  42. }
  43. })
  44. );
  45.  
  46. $(".Topstory-mainColumn").prepend('<div id="filter-rules"></div>');
  47. const app = new Vue({
  48. el: "#filter-rules",
  49. template: `
  50. <div
  51. id="filter-rules"
  52. style="background: #fff; box-shadow: 0 1px 3px rgb(18 18 18 / 10%); padding: 20px; border-bottom: 1px solid #f0f2f7; margin-bottom: 10px; border-radius: 2px;"
  53. v-if="isLoadConfigDone"
  54. >
  55. <div class="header" style="position: relative;">
  56. <div style="font-size: 18px; font-weight: 600; line-height: 1.6;">
  57. 过滤规则
  58. </div>
  59. <div
  60. class="action-bar"
  61. style="position: absolute; right: 0; top: 2px;"
  62. >
  63. <button
  64. @click="addRule()"
  65. style="line-height: 2; padding: 0 12px; color: #06f; text-align: center; border: 1px solid; border-radius: 3px; cursor: pointer; font-size: 12px;"
  66. >
  67. 添加标签
  68. </button>
  69. <button
  70. @click="toggleBarDisplayStatus()"
  71. style="line-height: 2; padding: 0 12px; color: #06f; text-align: center; border: 1px solid; border-radius: 3px; cursor: pointer; font-size: 12px;"
  72. >
  73. {{ barIsShown ? '折叠' : '展开' }}
  74. </button>
  75. </div>
  76. </div>
  77. <div
  78. v-show="barIsShown"
  79. style="line-height: 1.67; margin-top: 9px;"
  80. >
  81. <div
  82. class="rule-tag"
  83. title="点击可删除该标签"
  84. style="position: relative; display: inline-block; height: 30px; padding: 0 12px; font-size: 14px; line-height: 30px; color: #06f; border-radius: 100px; background: rgba(0,102,255,.1); margin: 3px 5px 3px 0; vertical-align: middle; cursor: pointer;"
  85. v-if="rules.length"
  86. v-for="(v, i) in rules"
  87. :key="v"
  88. @click="removeRule(i)"
  89. >
  90. {{ v }}
  91. </div>
  92. <p v-if="!rules.length">
  93. 当前尚未设置规则
  94. </p>
  95. </div>
  96. </div>
  97. `,
  98.  
  99. data() {
  100. return {
  101. isLoadConfigDone: false,
  102. rules: [],
  103. titles: [],
  104. tags: {},
  105. times: {},
  106. barIsShown: true
  107. };
  108. },
  109.  
  110. methods: {
  111. async loadConfig() {
  112. let config = await localforage.getItem("zhihu-filter-config");
  113.  
  114. if (!config) {
  115. config = await localforage.setItem("zhihu-filter-config", {
  116. barIsShown: true,
  117. rules: []
  118. });
  119. }
  120.  
  121. this.barIsShown = config.barIsShown;
  122. this.rules = config.rules;
  123. this.isLoadConfigDone = true;
  124. },
  125.  
  126. async saveConfig() {
  127. await localforage.setItem("zhihu-filter-config", {
  128. barIsShown: this.barIsShown,
  129. rules: this.rules
  130. });
  131. },
  132.  
  133. async toggleBarDisplayStatus() {
  134. this.barIsShown = !this.barIsShown;
  135. await this.saveConfig();
  136. },
  137.  
  138. async addRule() {
  139. const newTag = prompt("请输入需要被过滤的标签");
  140.  
  141. if (newTag) {
  142. this.rules = Array.from(new Set([...this.rules, newTag]));
  143. await this.saveConfig();
  144. }
  145. },
  146.  
  147. async removeRule(index) {
  148. this.rules.splice(index, 1);
  149. await this.saveConfig();
  150. },
  151.  
  152. updateTitles() {
  153. this.titles = Array.from($(".ContentItem-title a")).map((v) => ({
  154. title: $(v).text(),
  155. href: $(v).attr("href")
  156. }));
  157. setTimeout(this.updateTitles, 100);
  158. },
  159.  
  160. updateTagsAndTimes() {
  161. this.titles.forEach(async (v) => {
  162. if (!this.tags[v.title]) {
  163. if (v.href.includes("question") && !v.href.includes("answer")) {
  164. // 知乎问题
  165. const html = await get(v.href);
  166. const tags = Array.from($(".QuestionTopic", html)).map((e) =>
  167. $(e).text()
  168. );
  169. const { created: createdTime, updatedTime } = Object.values(
  170. JSON.parse(
  171. Array.from($(html)).filter(
  172. (v) => v.id === "js-initialData"
  173. )[0].innerHTML
  174. ).initialState.entities.questions
  175. )[0];
  176. this.tags[v.title] = tags;
  177. this.times[v.title] = {
  178. createdTime,
  179. updatedTime
  180. };
  181. } else if (v.href.includes("question") && v.href.includes("answer")) {
  182. // 知乎问题的回答
  183. const html = await get(v.href);
  184. const tags = Array.from($(".QuestionTopic", html)).map((e) =>
  185. $(e).text()
  186. );
  187. const { createdTime, updatedTime } = Object.values(
  188. JSON.parse(
  189. Array.from($(html)).filter(
  190. (v) => v.id === "js-initialData"
  191. )[0].innerHTML
  192. ).initialState.entities.answers
  193. )[0];
  194. this.tags[v.title] = tags;
  195. this.times[v.title] = {
  196. createdTime,
  197. updatedTime
  198. };
  199. } else if (v.href.includes("zhuanlan")) {
  200. // 知乎专栏的文章
  201. const html = await get(v.href);
  202. const tags = Array.from($(".Tag.Topic", html)).map((e) =>
  203. $(e).text()
  204. );
  205. const { created: createdTime, updated: updatedTime } =
  206. Object.values(
  207. JSON.parse(
  208. Array.from($(html)).filter(
  209. (v) => v.id === "js-initialData"
  210. )[0].innerHTML
  211. ).initialState.entities.articles
  212. )[0];
  213. this.tags[v.title] = tags;
  214. this.times[v.title] = {
  215. createdTime,
  216. updatedTime
  217. };
  218. } else if (v.href.includes("zvideo")) {
  219. // 知乎视频
  220. const html = await get(v.href);
  221. const tags = Array.from($(".ZVideoTag", html)).map((e) =>
  222. $(e).text()
  223. );
  224. const { publishedAt: createdTime, updatedAt: updatedTime } =
  225. Object.values(
  226. JSON.parse(
  227. Array.from($(html)).filter(
  228. (v) => v.id === "js-initialData"
  229. )[0].innerHTML
  230. ).initialState.entities.zvideos
  231. )[0];
  232. this.tags[v.title] = tags;
  233. this.times[v.title] = {
  234. createdTime,
  235. updatedTime
  236. };
  237. } else {
  238. this.tags[v.title] = true;
  239. }
  240. }
  241. });
  242. setTimeout(this.updateTagsAndTimes, 1000);
  243. },
  244.  
  245. updateQuestionsDisplayStatus() {
  246. Array.from($(".TopstoryItem")).forEach((v, i) => {
  247. const title = $(v).find(".ContentItem-title a").text();
  248.  
  249. if (
  250. !$(v).is(":hidden") &&
  251. this.tags[title] &&
  252. this.tags[title] !== true &&
  253. this.tags[title].some((tag) => this.rules.includes(tag))
  254. ) {
  255. $(v).hide();
  256. console.log("已过滤问题:", title);
  257. }
  258. });
  259. setTimeout(this.updateQuestionsDisplayStatus, 100);
  260. },
  261.  
  262. updateQuestionsTimeMark() {
  263. Array.from($(".TopstoryItem")).forEach((v, i) => {
  264. const $title = $(v).find(".ContentItem-title a");
  265. const title = $title.text();
  266.  
  267. if (
  268. !$(v).is(":hidden") &&
  269. !$(v).find(".time-mark").length &&
  270. this.times[title] &&
  271. this.times[title] !== true
  272. ) {
  273. const createdTime = this.times[title].createdTime;
  274. const updatedTime = this.times[title].updatedTime;
  275. const createdTimeStr = `${dayjs
  276. .duration(dayjs().diff(this.times[title].createdTime * 1000))
  277. .humanize()}前`;
  278. const updatedTimeStr = `${dayjs
  279. .duration(dayjs().diff(this.times[title].updatedTime * 1000))
  280. .humanize()}前`;
  281.  
  282. if (createdTime === updatedTime) {
  283. $title.parent().after(
  284. `<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">发布于 ${createdTimeStr}</div>`
  285. );
  286. } else {
  287. if (createdTimeStr === updatedTimeStr) {
  288. $title.parent().after(
  289. `<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">编辑于 ${updatedTimeStr}</div>`
  290. );
  291. } else {
  292. $title.parent().after(
  293. `<div class="time-mark" style="font-size: 14px; color: #8590a6; line-height: 1.67; margin-top: 5px; font-weight: 400;">发布于 ${createdTimeStr} 编辑于 ${updatedTimeStr}</div>`
  294. );
  295. }
  296. }
  297. }
  298. });
  299. setTimeout(this.updateQuestionsTimeMark, 100);
  300. }
  301. },
  302.  
  303. async created() {
  304. await this.loadConfig();
  305. this.updateTitles();
  306. this.updateTagsAndTimes();
  307. this.updateQuestionsDisplayStatus();
  308. this.updateQuestionsTimeMark();
  309. }
  310. });
  311. });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址