はてなキーワード: console.logとは
はてなブックマークの増田一覧の、さらに「すべて」(1 user)をチェックしている希有な人向けのユーザースクリプトを公開します。
https://b.hatena.ne.jp/site/anond.hatelabo.jp/?sort=eid
// ==UserScript== // @name Hatena Bookmark Anond Filter // @namespace https://b.hatena.ne.jp/site/anond.hatelabo.jp/ // @description はてなブックマークの『はてな匿名ダイアリー』の記事のうち、指定したNGワードが含まれる投稿を非表示にします。 // @match https://b.hatena.ne.jp/site/anond.hatelabo.jp/* // @grant none // @version 2.0.2026.01.28.0015 // ==/UserScript== (function(){ const SCRIPTID = 'HatenaBookmarkAnondFilter'; console.time(SCRIPTID); const CLASSNAME = 'filtered';/*フィルタ該当要素*/ const CHECKED = 'checked';/*二重チェック回避フラグ*/ const ONCE = 1, AP = 2, INTERVAL = 3;/*適用タイミング*/ const NGWORDS = {/* 合計100ポイントで非表示判定(ただし1つの記事内で同じワードが複数使われても1度しか加算しない) */ '100': [/*即NG確定ワード*/ 'dorawii', 'あおやまちゃん', 'ボスマン', 'MNK', '電気通信大学たいてい', 'なんぴょん', 'れめくん', 'リュックサック野郎', 'boushi_inst', 'hakaikami', 'Rekyu', 'iloveootaku_2',/*電気通信大学たいてい鉄道研究会れめくん(頻出)*/ /*A-G*/'a9w8ru6fqyxqfv9', 'admirail_togo', 'akibakko6348', 'alf1974al', 'amatukiseiru', 'anapgoeson', 'aoi_mizuho', 'asapgoeson', 'asupgoeson', 'b6jbpsji91ieigt', 'bmi22yo', 'boushi_instrail', 'boushi_ob', 'buscholarx', 'bw0531', 'circlecavok', 'disney1007cla', 'dora22sibuya', 'donkotrain', 'ecotosk', 'electlone', 'factomodachi', 'fft_dareka', 'gmhtcyznf_abc', 'goesonanap', 'gyudon_honmono', /*H-N*/'h13_yokohama', 'h2twi', 'H2TWR', 'hamaishogo1111', 'haru_mofumoffu', 'hermitv8', 'hirabiscus', 'hinolovelove', 'hnmk0127_03', 'inaken17_', 'inte235dy', 'ixtabes', 'jamcombatge', 'kawachiasukanew', 'kaoru_ai1991', 'keio9730F', 'kiha2228', 'koreanlabsfc', 'koyounoyooko', 'kqlex1500', 'kurakamasan', 'kurotamaxxx', 'kt_ruma_1372', 'lightningreen77', 'mamadoll_kun', 'matya_uec', 'minamihinawot', 'minori0151', 'monkichi_22', 'mugen_08i', 'mukoroku651', 'nakano6409', 'nanpyong', 'new_oer', 'nimouec', 'NoName_thUFO', 'norannnde', /*O-U*/'oreizmmiporin', 'orenotanoshimi', 'osaka_sirokichi', 'papepoco', 'pasotokon', 'pm95uq', 'reme_kun', 'ruin_2002', 's03_amurtk2731', 'sacchan03110319', 'seisu_bot', 'senanana_cos', 'shinano_115', 'shineleaf1372', 'shop_bullet', 'shurimpy', 'soroisoroi', 'sui_pm95uq', 'sweidan821858', 'taiyaki_level2', 'takao_straight5', 'taking0000', 'tarotaromusic1', 'tc201_501', 'tocarbarn', 'toshikimiyazaki', 'train_magician', 'tx9y2cpwdz27255', 'u2fap5u4zw57811', 'uec15take', 'uecdaisuki', 'UECert', 'uecrail', /*V-Z_0-9*/'vampire_mio', 'vbdmnwefknmxsdm', 'vp20th', 'wafue', 'wakasato_', 'walkingniwatori', 'wataameexpress', 'ya4975349616894', 'ymbk_arisa', 'yms_uec16', 'yuuya_1104_uec', '__________ob', '_chocorail_', '_doitforthewin_', '_toeshin', '_unigmo', '100mph_no_yuuki', '169_D51_protect', '2969364x', '2rtkvn34il2783', '36kyo', '86lilxw1', /*tadaup.jp*/'1dOaKWk3.jpeg', '1sL2VBZ5.jpeg', '1uNK2iEP.jpeg', 'CBUHadpD.jpeg', 'CgJlF4Wr.jpeg', 'CGTtm0Ev.jpeg', 'CIxj8clS.jpeg', 'CqbERPdQ.jpeg', 'CTZsA2wM.jpeg', 'CWY2m7rS.jpeg', 'CZVCEgd1.jpeg', 'テクウヨ', '自己放尿', ' ーーーーーーーー', 'https://anond.hatelabo.jp/20260107144223',/*AI問答貼り付け増田*/ '†噛み締めて行こうな†', '困難女性(コンジョ)', '困難女性(コンジョ)', 'コンジョ自警団', ], '90': [/*ほぼNG*/ 'megalodon.jp', '鉄道研究会', '鉄研', '通勤特快', '不正乗車', 'こども料金', '性慾', '穢い', 'エッタ', 'キセル', 'uec', ], '10': [/*NG*/ '電気通信大学', '電通大', '駿河台大学', '大学院', '学生課', '教務課', ], }; const sites = { 'prefix': [ ['selector', '(modifier)', '(css)', '(REPEAT)'], ], 'https://b.hatena.ne.jp/site/anond.hatelabo.jp/': [ ['section.entrylist-unit li.js-keyboard-selectable-item', li => {li.querySelector('li.entrylist-contents-category > a').textContent = li.dataset.matches}, `.${CLASSNAME}{display: none;}`, AP], ], }; const rules = sites[Object.keys(sites).find(prefix => location.href.startsWith(prefix))]; if(rules === undefined) return console.log(SCRIPTID, 'Not found any sites.'); const scores = Object.keys(NGWORDS).map(Number).reverse();/*数値インデックス順に取り出されたkeysを逆順にして100から並べ直す*/ const filter = function(selector, modifier){/*各要素に対してNGワード判定して、該当したら追加でmodifierも適用する*/ document.querySelectorAll(selector).forEach(e => { if(e.dataset[CHECKED]) return; e.dataset[CHECKED] = 'true'; const text = e.textContent.toLowerCase(); let total = 0, matches = []; for(const score of scores){ for(const word of NGWORDS[String(score)]){ if(text.includes(word.toLowerCase())){ total += score; matches.push(word); if(total >= 100){ e.classList.add(CLASSNAME); e.dataset.matches = matches.join(', '); if(modifier) modifier(e); return; } } } } }); }; /* ONCE(一括適用) */ rules.forEach(rule => { const [selector, modifier, css] = rule; console.log(SCRIPTID, 'ONCE:', selector); filter(selector, modifier); if(css){ const style = document.createElement('style'); style.dataset.script = SCRIPTID; style.type = 'text/css'; style.textContent = css; document.head.appendChild(style); } }); /* AP(AutoPagerize) */ rules.filter(rule => rule[3] === AP).forEach(rule => { const [selector, modifier] = rule; document.addEventListener('GM_AutoPagerizeNextPageLoaded', e => { console.log(SCRIPTID, 'AP:', selector); filter(selector, modifier); }); }); /* INTERVAL */ rules.filter(rule => rule[3] === INTERVAL).forEach(rule => { const [selector, modifier] = rule; setInterval(function(){ console.log(SCRIPTID, 'INTERVAL:', selector); filter(selector, modifier); }, 1000); }); console.timeEnd(SCRIPTID); })();
/* Hatena Bookmark Anond Filter */ .filtered{ display: block !important;/*上書き*/ opacity: .25 !important; } .filtered:hover{ opacity: .75 !important; } .filtered li.entrylist-contents-category{ background: red !important; font-weight: bold; }
検索用: はてなブックマーク はてブ はてな匿名ダイアリー 増田 スパム キーワード NGワード フィルター ミュート 非表示 削除 隠す ブロック ユーザースクリプト ユーザースタイル hatena bookmark anond spam keywords ngwords filter mute hide hidden display none block userscript JavaScript js css style
async function f() {
console.log(1);
await new Promise(r => setTimeout(r, 1000));
console.log(2);
}
f();
console.log("done");
結果は、まず「1」「done」が出力され、1秒後に「2」が出力される。
流れを説明すると、f()が実行されると「console.log(1);」と「new Promise(r => setTimeout(r, 1000));」が実行される。
その時点でf()の戻り値として 先ほどnewされた ……じゃなかった、f()を非同期実行中でそのうち続きが実行されますよというPromiseオブジェクトが返ってくる。
このPromiseオブジェクトは「resolveされたときにawait以降が実行される」というPromiseオブジェクトだ。
そして通常の処理の流れとして、その次の行の「console.log("done");」が実行される。
んで、1秒後にsetTimeoutで終わったことでキューに「r(イコール、resolve関数)」が登録される。
最後に、resolveが実行されたので、await以降……つまり「console.log(2);」が実行される。
どこか分からないとこある?
実行キューに入るのは非同期処理が終了した後だ。
なんか矛盾してない?
async function f() {
console.log(1);
new Promise(r => setTimeout(r, 1000));
console.log(2);
}
f();
console.log("done");
こう書いたらコンソールに出る順は1,2,doneだよ。1,2を出してる関数は非同期でありそれ以外にはこのコードに非同期関数は存在しないんだけど。
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
https://anond.hatelabo.jp/20250908203539#
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaL6/iwAKCRBwMdsubs4+
SNVCAQDh/59YPp/11Ts/tp7JdxGIs6BqRv1PhkFmjUkBZH00owD/fN0PnyFGyJ8N
QGQlMNJvfsFGvNT5tbsEY1d/dhjCmQI=
=KsE1
-----END PGP SIGNATURE-----
ソースコードをいくら見てもそのソースコード作成者が想定してるエンジン側の実装なんて推測できんだろ。
ちなみにその「細かいこと」が気になった発端は初歩であるはずのコードの挙動の理解につまづいたからだけどな。
setTimeout(() => { console.log("A"); }, 1001); setTimeout(() => { console.log("B"); },1000); console.log("C"); 上のsetTimeout()の各第二引数の大小関係をどのように調整しても常に第二引数により小さい数が指定されたもののコールバック関数が先に実行されます。 これはどういうことですか?先にsetTimeoutと書かれたもののコールバック関数からキューに登録され、キューへの取り出し方はjsの場合fifoが採用されているので、上記のコードのような場合は、()=>console.log("A")の方が先にキューに登録されたものとして第二引数に無関係に()=>console.log("B")より先に実行されるのではないのですか?-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 https://anond.hatelabo.jp/20250908192456# -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaL6u+QAKCRBwMdsubs4+ SAtwAP49XRX8yOJwd/XLSKjjP1TASfuVR29t/NIhuLSNb0vr2AD8CtGJTYMzavjS i9TuxJTV/DSYuwhuBLnKkd0lsJOldQI= =wm4J -----END PGP SIGNATURE-----
async function collectAllUrls(startUrl) {
const urls = [];
let nextUrl = startUrl;
while (nextUrl) {
const res = await fetch(nextUrl);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, "text/html");
const links = doc.querySelectorAll("div.section > h3 > a:first-child");
urls.push(...[...links].map(link => link.href));
const nextLink = [...doc.querySelectorAll("a")].find(a => a.textContent.includes("次の25件>"));
nextUrl = nextLink ? nextLink.href : null;
console.log(nextUrl)
}
return urls;
}
(async () => {
const allUrls = await collectAllUrls(window.location.href);
console.log("総件数:", allUrls.length);
await Promise.all(allUrls.map(url =>{console.log(url);
fetch('https://b.hatena.ne.jp/dorawii_bukuma/add.edit.json', {
method: 'POST',
headers: {
},
body: new URLSearchParams({
'url': url,
'private': '0',
'comment': '[dorawii]わしが書いた',
'post_twitter': '0',
'with_status_op': '1',
'from': 'web-confirm'
})
});
} ));
console.log("全送信完了");
})();
https://b.hatena.ne.jp/site/anond.hatelabo.jp/?sort=eid
途中でブクマ数増えなくなったんだよね。待機処理つけるべきだったか。
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 https://anond.hatelabo.jp/20250823194237# -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaKmbHwAKCRBwMdsubs4+ SJC0AP0Q7RDjUSe8p2aNNKV0KLhlbhnTY+kD7uuWCS8yLJILDgEA2Sm4b1496jjy C0ue64hovLwS3C4dcF5r5TBMyfRifw8= =zIYi -----END PGP SIGNATURE-----
https://profile.hatena.ne.jp/dorawii_bukuma/
はてなのサイト側で読み込まれているはずのrksトークンを生成する関数を直接叩く方法がどうしても分からず結局request処理を自分で書く方法ではなく自動でUI側の保存ボタンをクリックするという無難な方向に落ち着いた。
最初から後者の方法をとっていればもっと全然早く作れたのにというは所詮言い訳か。
とにかくスクリプトを公開しておく。
@echo off
cd /d "C:\Users\user\Documents\jsscript"
:: Nodeサーバーを別ウィンドウで起動
start /min "" node run-batch-server.js
:: Pythonサーバーを別ウィンドウで起動(hatenaserver配下)
start cmd /k "" python hatenaserver\server.py
{
"username": "",
"password": ""
}from flask import Flask, request, jsonify
import json
import os
from hatena_client import HatenaClient
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, encoding='utf-8') as f:
config = json.load(f)
@app.route('/bookmark', methods=['POST'])
def handle_bookmark():
data = request.json
url = data.get("url")
if not url:
return jsonify({"error": "Missing URL"}), 400
client = HatenaClient(config["username"], config["password"])
client.start_browser()
if not client.login():
client.quit()
return jsonify({"error": "Login failed"}), 403
success = client.add_bookmark(url)
client.quit()
return jsonify({"status": "ok" if success else "fail"})
if __name__ == "__main__":
app.run(port=12347)
// ==UserScript==
// @name 自動セルクマ送信
// @namespace tampermonkey.net/
// @version 2025-08-07
// @description try to take over the world!
// @author You
// @match anond.hatelabo.jp/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const url = location.href;
if (!/^https:\/\/anond\.hatelabo\.jp\/\d+$/.test(url)) return;
const editLink = document.querySelector('a.edit');
if (!editLink) {
// 既に編集ページなので処理をスキップ
console.log('編集リンクが存在するため、スクリプトを終了します。');
return;
}
fetch('localhost:12347/bookmark', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: url })
}).then(r => console.log("通知成功")).catch(e => console.error("通知失敗", e));
})();
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 https://anond.hatelabo.jp/20250821192753# -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaKb0qwAKCRBwMdsubs4+ SHfiAQDcXmTHBaZ5Zzr1KI/OxZ0xl69oevOdy1FXJYwYvsmo5AD/ZPtZiO1JgTDj m+27iymlkdzIXOIGWfC82UTr1mJ7EwU= =YoV+ -----END PGP SIGNATURE-----
Chrome系ブラウザには増田を快適に閲覧するための コンパクトな増田 という古い拡張機能があったが、Chromeの更新に対応し切れておらず、既にChromeには新規インストールできなくなってしまっている。Edgeにはまだインストール可能だが、いずれ対応しなくなる可能性が高い。
そこで、「増田のトップページで、言及エントリ(返信・トラバ)を一覧から除外することで、新規エントリだけを一覧できる」という機能に絞ってコンパクトな増田を再現、ついでにいくつかのおまけ機能を付与したスタイルシート(CSS)を今年の4月に公開していたのだが、今回改めて英文スパム対策を追加したので公開する。
これを導入するには Stylus という拡張が必要で、少し気軽さには欠けるが、増田以外にも活用できるので、この機会にぜひ導入してみてほしい。拡張をインストールしたあとは、下記のコードをコピペして新規スタイルとして導入する方法もあるが、スタイルシートを公開できる userstyles.world の増田CSSページ(※毎朝9:00直後はアクセスできない) から [Install] ボタンでインストールするほうが、自動更新にも対応するので便利かもしれない。
/* トップページで言及エントリを除外 */ /* via: 最近ファーストブクマカが静か https://anond.hatelabo.jp/20250326171302 */ h1/*はてな匿名ダイアリー*/ + #intro/*名前を隠して楽しく日記。*/ + #body div.section:has(h3 > a/*■*/ + a:not(.keyword, .edit)/*anond:YYYYMMDDhhmmss*/){ display: none; } /* うっかりクリックしがちなキーワードリンクを無効に */ a.keyword{ pointer-events: none; } /* 執筆時のテキストエリアを広く */ textarea#text-body{ min-height: 50vh !important; } /* 執筆時に特殊記号のヒント(疑似要素なので選択してコピペできないのがもどかしいけど) */ p.post-submit > span.explain::after{ margin-left: 1em; padding-left: 1em; content: "特殊記号: &[&] <[<] >[>]"; background: url(/images/common/outsite-wh.gif) 0 3px no-repeat; } /* スパム対策部分は下記URLの [Install] ボタンで事前確認できます(随時更新中) */ /* https://userstyles.world/style/23028/ */
なお、このCSSを適用すると、NGワードを含むこの増田自体も、増田トップページからは消えてしまう(この増田単体の個別ページなら閲覧できる)。
念のため、PC・スマホにCSSを適用する方法の解説にもリンクしておく。
PC: 【Stylus】ウェブサイトにCSSを適用できる拡張機能。自由にカスタマイズ! | ナポリタン寿司のPC日記
https://www.naporitansushi.com/stylus/
※ StylusはFirefox版もある https://addons.mozilla.org/ja/firefox/addon/styl-us/
iPhone: MaKeoverアプリでiPhone SafariのCSSをカスタマイズ!万博パビリオン予約結果一覧を見やすくする使い方
https://gintachan.com/makeover-app-css-change-safari-how-to/
Android: スマートフォン Android版FirefoxのCSSカスタマイズ Stylus の使い方・初期設定方法
(7/21追記) また、スパムが特に多い時は、1ページまるごとスパムということもあるので、PCなら uAutoPagerize (Chrome) や weAutoPagerize (Firefox) などの拡張を使うと、自動でページが継ぎ足されて快適に読み進められる。ただし、継ぎ足ししまくるとメモリ不足などでブラウザが重くなることがあるので、そうなったら page: 20 などのページ番号をクリックしてから続きを読もう。
また、スパム対策の簡易NGワードは、下記のスクリプトを使って抽出した「直近の増田の頻出キーワードリンク上位20件」から、誤判定しそうな line と user を除いた18件を用いた。10件だと生き残る英文スパムがあったので20件にしたが、それでもわずかに洩れはある。しかし日本語による真っ当な(?)増田の直近の誤判定はなかった。はてなキーワードのリンクだけを対象にしているので、URLにはこれらのキーワードが入っていても大丈夫だ。ただし、スパムのトレンドが変われば話は変わってくるかもしれないし、過去や未来の増田の誤判定は当然あるだろう。気になる人は前掲のCSSを行単位で編集してほしい。
// AutoPagerizeでまとまった数のページを読み込ませた後に実行するとよい。 (function(){ const keywords = []; // はてなキーワードの集計 document.querySelectorAll('a.keyword').forEach(a => { // 4文字未満は誤検出の可能性が高まるので除外 if(a.textContent.length < 4) return; let index = keywords.findIndex(k => k.keyword === a.textContent); if(index >= 0) keywords[index].count += 1; else keywords.push({keyword: a.textContent, count: 1}); }); keywords.sort((a, b) => a.count < b.count); // ランキング配列の出力 console.log(keywords); // CSS埋め込み用に上位キーワードのみをURIエンコードして出力 console.log(keywords.slice(0, 20).map(k => encodeURIComponent(k.keyword)).join('\n')); })();
anond:20250326171302 ←元はこの増田がきっかけでした。
anond:20250701194328 ←キーワード判定に踏み切る後押しとなりました。
一度投稿したうえで別タブを開いてプログラム的(fetch)に送信してその別タブが閉じられる仕組み。
// ==UserScript==
// @name PGP未署名検出と別タブ自動編集
// @namespace http://tampermonkey.net/
// @version 1.0
// @description PGP署名がない投稿を自動編集ページへ誘導
// @match https://anond.hatelabo.jp/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM.openInTab
// ==/UserScript==
(function () {
'use strict';
const body = document.getElementById('entry-page');
if (!body) return;
const titleText = document.title;
if (!titleText.includes('dorawii')) return;
const pgpRegex = /BEGIN.*PGP(?: SIGNED MESSAGE| SIGNATURE)?/;
const preElements = document.querySelectorAll('div.body pre');
let hasPgpSignature = false;
for (const pre of preElements) {
if (pgpRegex.test(pre.textContent)) {
hasPgpSignature = true;
break;
}
}
if (hasPgpSignature) return;
const editLink = document.querySelector('a.edit');
const childTab = GM.openInTab(editLink.href, { active: false, insert: true, setParent: true });
})();
// ==UserScript==
// @name 編集ページ処理と自動送信・閉じ
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 編集ページで署名処理と送信、タブ自動閉じ
// @match https://anond.hatelabo.jp/dorawii_31/edit?id=*
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_notification
// @connect localhost
// ==/UserScript==
(async function () {
'use strict';
const shouldRun = await GM_getValue('open-tab-for-edit', '0');
const textareaId = 'text-body';
const textarea = document.getElementById(textareaId);
if (!textarea) return;
const content = textarea.value;
const pgpSignatureRegex = /-----BEGIN PGP SIGNED MESSAGE-----[\s\S]+?-----BEGIN PGP SIGNATURE-----[\s\S]+?-----END PGP SIGNATURE-----/;
if (pgpSignatureRegex.test(content)) {
console.log('[PGPスクリプト] 署名が検出されたためそのまま送信します');
return;
}
const httpRequest = (url, data) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: `value=${encodeURIComponent(data)}`,
onload: function (response) {
resolve(response.responseText);
},
onerror: function (error) {
reject(error);
}
});
});
};
// textarea の値を取得
// 1. 現在のページのURLからURLオブジェクトを作成
const currentUrl = new URL(window.location.href);
// 2. ベースとなる部分 (例: "https://anond.hatelabo.jp") を取得
const origin = currentUrl.origin;
// 3. 'id' パラメータの値 (例: "20250610184705") を取得
const idValue = currentUrl.searchParams.get('id');
// 4. ベース部分とIDを結合して、目的のURL文字列を生成
// idValueが取得できた場合のみ実行する
let newUrl = null;
if (idValue) {
newUrl = `${origin}/${idValue}`;
}
// 5. 生成されたURLを変数に代入し、コンソールに出力して確認
console.log(newUrl);
const valueToSend = newUrl;
try {
const signatureText = await httpRequest('http://localhost:12345/run-batch', valueToSend);
console.log('バッチ応答:', signatureText);
if (!signatureText.includes('BEGIN PGP SIGNED MESSAGE')) {
alert('PGP署名がクリップボードに見つかりませんでした。');
return;
}
const newText = content.replace(/\s*$/, '') + '\n' + signatureText + '\n';
textarea.value = newText;
console.log('[PGPスクリプト] 署名を貼り付けました。送信を再開します。');
const form = document.forms.edit;
const newForm = form.cloneNode(true);
form.replaceWith(newForm);
newForm.addEventListener('submit', async (e) => {
e.preventDefault(); // HTML標準のsubmitをキャンセル
const bodyText = textarea?.value || '';
// reCAPTCHA トークンの取得
const recaptchaToken = await new Promise((resolve) => {
grecaptcha.enterprise.ready(() => {
grecaptcha.enterprise.execute('hoge', { action: 'EDIT' })
.then(resolve);
});
});
// POSTするデータの構築
const formData = new FormData(newForm);
formData.set('body', bodyText);
formData.set('recaptcha_token', recaptchaToken);
formData.set('edit', '1');
try {
const response = await fetch(newForm.action, {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (response.ok) {
console.log('送信成功');
window.close();
} else {
console.error('送信失敗', response.status);
}
} catch (err) {
console.error('送信中にエラーが発生', err);
}
});
// プログラム的に送信トリガー
newForm.dispatchEvent(new Event('submit', { bubbles: true }));
} catch (e) {
console.error('バッチ呼び出し失敗:', e);
}
})();
const http = require('http'); const { exec } = require('child_process'); const querystring = require('querystring'); const server = http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/ping') { res.writeHead(200); res.end('pong'); } else if (req.method === 'POST' && req.url === '/run-batch') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { const parsed = querystring.parse(body); const value = parsed.value || 'default'; // 値を引数としてバッチに渡す exec(`C:\\Users\\hoge\\Desktop\\makesign.bat "${value}"`, { encoding: 'utf8' }, (err, stdout, stderr) => { if (err) { res.writeHead(500); res.end('Error executing batch: ' + stderr); } else { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end(stdout.trim()); } }); }); } else { res.writeHead(404); res.end('Not found'); } }); server.listen(12345, () => { console.log('Batch server running at http://localhost:12345/'); });
@echo off setlocal enabledelayedexpansion :: 署名するファイル名 set "infile=%~1" set outfile=%TEMP%\pgp_output.asc :: 以前の出力があれば削除 if exist "%outfile%" del "%outfile%" :signloop :: AutoHotkeyでパスフレーズ入力(gpgがパスワード要求するダイアログが出た場合に備える) start "" /b "C:\Users\hoge\Documents\AutoHotkey\autopass.ahk" :: PGPクリア署名を作成 echo %infile% | gpg --yes --clearsign --output "%outfile%" :: 署名が成功していればループを抜ける if exist "%outfile%" ( goto postprocess ) else ( timeout /t 1 > nul goto signloop ) :postprocess powershell -nologo -command ^ "$header = '>|'; $footer = '|<'; $body = Get-Content '%outfile%' -Raw; Write-Output ($header + \"`r`n\" + $body + $footer)" powershell -nologo -command ^ "$header = '>|'; $footer = '|<'; $body = Get-Content 'signed.asc' -Raw; Set-Clipboard -Value ($header + \"`r`n\" + $body + $footer)" endlocal exit /b
#Persistent #SingleInstance ignore SetTitleMatchMode, 2 WinWaitActive, pinentry SendInput password Sleep 100 SendInput {Enter} ExitApp
動けばいいという考えで作っているので余分なコードも含んでいるかもしれない。
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 https://anond.hatelabo.jp/20250613185036 -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaEv1FQAKCRBwMdsubs4+ SHHkAQDUOLgBcdji2T6MJ7h/vlMdFfGlWAzNdXijjE1gIuEPywEAiMNMZqhrMmtl c7UqRuggNJ/UTa5xTIcKp622+7jJQQg= =Lgkl -----END PGP SIGNATURE-----
ようやく(ほぼ)すべてが自動化された。
あとはローカルサーバーの起動をスタートアップに設定する(方法をAIに聞いて指示に従う)だけの消化試合。
署名時要求してくるパスワードを自動入力するahkファイルはドキュメントのAutoHotkey配下に置いた。
バッチファイル(make.sign.bat)はデスクトップに置いた。
#Persistent #SingleInstance ignore SetTitleMatchMode, 2 WinWaitActive, pinentry SendInput お前のパスワード Sleep 100 SendInput {Enter} ExitApp
// run-batch-server.js const http = require('http'); const { exec } = require('child_process'); const server = http.createServer((req, res) => { if (req.url === '/ping') { res.writeHead(200); res.end('pong'); } else if (req.url === '/run-batch') { exec('C:\\Users\\you\\Desktop\\makesign.bat', (err) => { res.writeHead(200); res.end(err ? 'Error' : 'OK'); }) ; } else { res.writeHead(404); res.end('Not found'); } }); server.listen(12345, () => { console.log('Batch server running at http://localhost:12345/'); });
@echo off setlocal enabledelayedexpansion :: ミリ秒単位のUTC時刻を取得 for /f %%a in ('powershell -nologo -command "[int64]::Parse((Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmssfff'))"') do set timestamp=%%a :: 署名するファイル名 set infile=%TEMP%\pgp_input.txt set outfile=%TEMP%\pgp_output.asc :: 以前の出力があれば削除 if exist "%outfile%" del "%outfile%" :: タイムスタンプを原文として保存 echo %timestamp% > "%infile%" :signloop :: AutoHotkeyでパスフレーズ入力(gpgがパスワード要求するダイアログが出た場合に備える) start "" /b "C:\Users\infini\Documents\AutoHotkey\autopass.ahk" :: PGPクリア署名を作成 gpg --yes --clearsign --output "%outfile%" "%infile%" :: 署名が成功していればループを抜ける if exist "%outfile%" ( echo [INFO] 署名成功 goto postprocess ) else ( echo [WARN] 署名失敗、再試行します… timeout /t 1 > nul goto signloop ) :postprocess :: PowerShellで余計な改行なしに |< をつけてクリップボードにコピー powershell -nologo -command ^ "$header = '>|'; $footer = '|<'; $body = Get-Content '%outfile%' -Raw; Set-Clipboard -Value ($header + \"`r`n\" + $body + $footer)" echo Done. signed.asc created and clipboard updated (no extra blank line). endlocal exit /b
// ==UserScript== // @name PGP署名自動付加スクリプト(GM_xmlhttpRequest版) // @namespace http://tampermonkey.net/ // @version 1.0 // @description 投稿前にPGP署名を付けてから送信(fetch未使用) // @match https://anond.hatelabo.jp/dorawii_31/edit* // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_notification // / @connect localhost // ==/UserScript== (function () { 'use strict'; const submitId = 'submit-button'; const textareaId = 'text-body'; const localServer = 'http://localhost:12345/run-batch'; const pgpSignatureRegex = /-----BEGIN PGP SIGNED MESSAGE-----[\s\S]+?-----BEGIN PGP SIGNATURE-----[\s\S]+?-----END PGP SIGNATURE-----/; const httpRequest = (url) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (response) { resolve(response.responseText); }, onerror: function (error) { reject(error); } }); }); }; const interceptClick = () => { const btn = document.getElementById(submitId); if (!btn || btn.dataset.pgpIntercepted === 'true') return; btn.dataset.pgpIntercepted = 'true'; btn.addEventListener('click', async function (e) { const textarea = document.getElementById(textareaId); if (!textarea) return; const content = textarea.value; if (pgpSignatureRegex.test(content)) { console.log('[PGPスクリプト] 署名が検出されたためそのまま送信します'); return; } e.preventDefault(); e.stopImmediatePropagation(); console.log('[PGPスクリプト] 署名が見つからないため処理を停止し、署名を取得します'); try { await httpRequest(localServer); // バッチ実行 const signatureText = await navigator.clipboard.readText(); if (!signatureText.includes('BEGIN PGP SIGNED MESSAGE')) { alert('PGP署名がクリップボードに見つかりませんでした。'); return; } const newText = content.replace(/\s*$/, '') + '\n' + signatureText + '\n'; textarea.value = newText; console.log('[PGPスクリプト] 署名を貼り付けました。送信を再開します。'); btn.click(); // イベント再発火 } catch (err) { alert('PGP署名の取得または貼り付けに失敗しました。\n' + err); } }, true); }; window.addEventListener('load', () => { setTimeout(interceptClick, 1000); }); })();
プロミスメソッドとか全然まだ理解してなくてそのなかに関数代入したその関数にオブジェクトのプロパティにresponseを?いやまあそのあたりのコードが示すデータの流れが全然理解できないような人間でもここまでできちゃった。
AIすごいなと思うよ。そして思うのは今後重要になってくるのは文法とか自体に詳しいことじゃなくて、そのプログラムの処理内容を指示できるシステムエンジニア的な言語化能力のほうじゃないかなと思った。
-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 20250609111559680 -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTEe8eLwpVRSViDKR5wMdsubs4+SAUCaEbCbwAKCRBwMdsubs4+ SLueAPwOv7PBk4voAe5qlcCEvs/PJhmKc5QAb/1R43JMQFuDZgD/UTPEKsL/PhK9 jFGv2HDXK1dVjLNwvosgX9uYJh5xxwY= =qiOE -----END PGP SIGNATURE-----
増田で 3 分以上投稿されない期間があるのか気になったから調べた
直近の 1 日だとこれだけあった
2025-03-22 00:14 -- 2025-03-22 00:18 2025-03-22 00:10 -- 2025-03-22 00:14 2025-03-21 07:56 -- 2025-03-21 08:00 2025-03-21 07:50 -- 2025-03-21 07:56 2025-03-21 07:44 -- 2025-03-21 07:48 2025-03-21 07:28 -- 2025-03-21 07:32 2025-03-21 06:58 -- 2025-03-21 07:03 2025-03-21 06:45 -- 2025-03-21 06:54 2025-03-21 06:32 -- 2025-03-21 06:37 2025-03-21 05:56 -- 2025-03-21 06:04 2025-03-21 05:51 -- 2025-03-21 05:56 2025-03-21 05:34 -- 2025-03-21 05:38 2025-03-21 05:30 -- 2025-03-21 05:34 2025-03-21 05:00 -- 2025-03-21 05:09 2025-03-21 04:56 -- 2025-03-21 05:00 2025-03-21 04:45 -- 2025-03-21 04:50 2025-03-21 04:09 -- 2025-03-21 04:13 2025-03-21 03:41 -- 2025-03-21 03:45 2025-03-21 03:29 -- 2025-03-21 03:39 2025-03-21 03:03 -- 2025-03-21 03:07 2025-03-21 02:56 -- 2025-03-21 03:02 2025-03-21 02:44 -- 2025-03-21 02:48 2025-03-21 02:33 -- 2025-03-21 02:37 2025-03-21 02:21 -- 2025-03-21 02:27 2025-03-21 02:14 -- 2025-03-21 02:19
秒はみてないから 00:01:01 - 00:03:59 はほぼ 3 分だけど 2 分扱いだし、 00:01:59 - 00:04:00 はほぼ 2 分だけど 3 分扱いになるくらいの誤差はある
日によって違うだろうし、曜日の影響も大きそうだから 1 ヶ月分くらい調査しようかと思ったけど、
増田の量が思いの外多すぎて 1 日分だけでも 100 ページ以上取得しないといけなかった
件数だと 2500 以上
一応取得に使ったコードも載せとく
import { setTimeout } from "node:timers/promises" import { Browser } from "happy-dom" const getTimestamps = async function* () { const browser = new Browser() const page = browser.newPage() try { for (let num = 1; ; num++) { await setTimeout(3000) await page.goto(`https://anond.hatelabo.jp/?page=${num}`) const days = page.mainFrame.document.querySelectorAll(".day") for (const day of days) { const date = day.querySelector("h2 .date").textContent.trim() for (const footer of day.querySelectorAll(".sectionfooter")) { const time = footer.textContent.match(/\d{2}:\d{2}/)[0] yield `${date} ${time}` } } } } finally { await page.close() await browser.close() } } const diff = (a, b) => { return new Date(b + ":00") - new Date(a + ":00") } let prev = null for await (const datetime of getTimestamps()) { if (prev &amp;&amp; diff(datetime, prev) > 1000 * 60 * 3) { console.log(datetime, prev) } prev = datetime }
基本は空いても 5 分程度であり、最大でも 10 分となっている
検索式を書きますので、それで見つかったファイルを全て選択して、削除などのアクションをしてください。
細かな調整は各自のお好みで変更してください。
◆1年以上前のファイルサイズの大きなメール(300kB以上)を検索(星を付けたメールは除く)。削除しましょう。
older_than:1y larger:300k -is:starred
◆プロモーションやソーシャルに分類された1か月以上前のメールを検索。削除しましょう
(category:promotions OR category:social) older_than:30d
◆受信トレイの180日以上前のメールを検索。アーカイブして、受信トレイのメール数を減らしましょう
label:inbox older_than:180d
◆プロモーションやソーシャルに入ったメールで2日経ったものを検索。既読にしましょう。
label:inbox (category:promotions OR category:social) older_than:2d
Google Apps Script(GAS)を使うとこれを毎日自動で行ってもらえます。
実行の左のアイコンで保存。関数をcleanUpGmailを選んで実行。初回は権限確認のメッセージが出るのでOKを押してください。
無事実行出来たら一番の難関はクリアです。これを毎日自動で実行してもらいましょう。一度に250通が処理されます。
また余裕があれば、左上の無題のプロジェクトになっているところの名前を「gmail自動処理」などに変更しても良いでしょう。
実行する関数:CleanUpGmail デプロイ:Head イベント:時間主導 時間べース:時間ベース 時間の間隔:6時間おき
これで1日に4回、合計1000通が自動処理されますので、たくさんメールが溜まっている方でも、1か月程度で全て処理されると思います。
function cleanUpGmail() { // メインの関数の開始ログ console.log("=== cleanUpGmail start ==="); // 1) 2年以上前 &amp; 300KB以上 &amp; from:gmail.comではない &amp; スター付きではない → 削除 console.log("古い大きなメールは削除"); processThreads("older_than:2y larger:300k -from:gmail.com -is:starred", "trash"); // 2) プロモーション or ソーシャル &amp; 30日以上前 → 削除 console.log("プロモーションとソーシャルは1か月で削除"); processThreads("(category:promotions OR category:social) older_than:30d", "trash"); // 3) 受信トレイ &amp; 180日以上前 → アーカイブ console.log("受信トレイの180日以上前 → アーカイブ"); processThreads("label:inbox older_than:180d", "archive"); // 4) 受信トレイ &amp; プロモーション or ソーシャル &amp; 2日以上前 → 既読 console.log("受信トレイでプロモーション or ソーシャル かつ 2日以上前 → 既読"); processThreads("label:inbox (category:promotions OR category:social) older_than:2d", "markRead"); console.log("=== cleanUpGmail end ==="); } function processThreads(query, action) { // 1回あたり250件だけ処理 var batchSize = 250; // 最初の 250 件のみ取得 var threads = GmailApp.search(query, 0, batchSize); var count = threads.length; Logger.log("検索クエリ: [" + query + "] | 取得スレッド数: " + count); // スレッドごとにアクションを実行 threads.forEach(function(thread) { switch (action) { case "trash": thread.moveToTrash(); break; case "archive": thread.moveToArchive(); break; case "markRead": thread.markRead(); break; default: Logger.log("不明なアクション: " + action); } }); Logger.log("処理したスレッド数: " + count); }
AIインフルエンサーたちはChatGPTが世に出る前は何を投稿していたのか?(1/1)の続き
このTwitterアカウントは、主にYouTube、SEO、競馬、アニメなどの話題について呟いており、自身の活動や興味関心について発信しています。
このアカウントは、日々の米国株や暗号通貨の市場動向、特にテクノロジー関連株やビットコインの価格変動についてツイートしています。
「ゼロコロナ政策再び。 $AAPL の生産に影響が懸念され大きく下落。更に経済混乱が想定され、株価はSP500全業種で
一方で年末商戦売上堅調な滑り出しで $AMZN
暗号資産レンディングBlockFiが経営破綻、暗号通貨も軒並み下落。
チャート上、なんとか踏み止まるか? 」
このアカウントは、AI、特にプロマネAIや量子AIに関する話題や、仕事効率化、プログラミング、そして最新の技術トレンドについて呟いています。
"プロマネAIの実証実験を開始しました!量子AI×NotionによるプロマネAIの実証実験を開始 https://prtimes.jp/main/html/rd/p/000000003.000082094.html… via @PRTIMES_JP"
このアカウントは、主にOpenAIの最新言語モデル「text-davinci-003」の進化と、そのモデルを用いた英語学習ツールやサービスについて呟いています。特に、英語学習におけるAI活用とその重要性を強調しています。
"朝起きたら世界がまた変わっていた(笑)。@OpenAI が最新モデル「text-davinci-003」を発表。主な特徴は、①より明確で、説得力のある文章が書ける②より複雑な指示にも対応③より長い形式のコンテンツが生成可 1月に"InstructGPT"が出た時も感動したけどそれを遥かに上回る進化、です。"
このアカウントは、主にAI、特に画像生成AIに関する話題を呟いています。特にStable Diffusionのバージョンアップや使い方について多くのツイートをしています。
ここにリストアップした人たち以外もみましたが、分類すると3種類のアカウントがいて
またIDを変更してログがヒットしない人は含まれていません(IDから特定できるけどそこまでやらなかった)
javascript:(function() {
const text = Array.from(document.querySelectorAll('[data-testid="tweetText"]')).map(s => s.textContent.trim()).join('\n');
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
try {
document.execCommand('copy');
console.log('結果がクリップボードにコピーされました!');
} catch (err) {
console.error('クリップボードへのコピーに失敗しました:', err);
}
document.body.removeChild(textarea);
})();
const axios = require('axios'); // HTTPリクエストを行うためのモジュール
const fs = require('fs'); // ファイル操作モジュール
const xml2js = require('xml2js'); // XMLをJSONに変換するためのモジュール
const chardet = require('chardet'); // 文字エンコーディングを検出するためのモジュール
const iconv = require('iconv-lite'); // 文字エンコーディングを変換するためのモジュール
const rssUrl = 'https://www.mlit.go.jp/important.rdf'; // 例としてRSSフィードのURLを指定
async function fetchAndSaveRSS() {
try {
// RSSを取得
const response = await axios.get(rssUrl, { responseType: 'arraybuffer' });
const rssData = response.data;
const detectedEncoding = chardet.detect(rssData);
console.log('検出された文字エンコーディング:', detectedEncoding);
// UTF-8に変換
const utf8Data = iconv.decode(rssData, detectedEncoding);
xml2js.parseString(utf8Data, (err, result) => {
if (err) {
console.error('XML解析エラー:', err);
return;
}
const jsonData = JSON.stringify(result, null, 2);
fs.writeFileSync('rss_feed.json', jsonData, 'utf8');
console.log('RSSフィードがファイルに保存されました。');
});
console.error('エラーが発生しました:', error);
}
}
// 実行
fetchAndSaveRSS();
anond:20240904032316についてたブクマカの初めて買ったCDのリリース年を調べた。
for (let a of document.body.getElementsByClassName('entry-comment-text js-bookmark-comment')) console.log(a.textContent)
ってやってテキスト集める。
Microsoft Copilotに「以下の文章に出てくる、音楽CDのタイトルとリリース年を一覧で表にしてください。」って指示。
途中で切れたので分割する。 続き→ anond:20240905115337
特殊文字は、実体参照(& や <)ではうまくいきません。数値参照を使ってください。
| 出したい文字 | 数値文字参照 |
|---|---|
| & | & |
| < | < |
| > | > |
const dinner = (chicken, curry) => {
if (chicken && curry) {
console.log('チキンかつカレー')
}
}
このように入力してください。
<pre>const dinner = (chicken, curry) => {<br> if (chicken && curry) {<br> console.log('チキンかつカレー')<br> }<br>}</pre>
JavaScript でさあ
変数 value が null でも undefined でもない事を確認するのに
if (value) { console.log('null でも undefined でもねーわ'); }
これほんとやめろって。
おかげで value に 0 とかが入ってる時に、このコンディションが false になるわけだ。
色んな会社さんのコード見てきたけど、このタイプのバグ本当に多い。
昨年は、世界的にも有名な会社さんのフレームワークがこれでバグってた。
でももう既にシステムの一部は本番稼働しててフレームワークはいじれない。
仕方ないので value には一旦文字列の '0' を渡しておいて if (value) {~} の中の重要なロジックを動かして
(めっちゃ幸運な事に、数値 0 のかわりに文字列 '0' でも正しく動くような、型について緩いロジックだったから)
その後で改めて value に数値 0 を入れなおすという、きったないハックで誤魔化した事もある。
<title>Document</title> の下に <script src="./script.js"></script> という行を追加する <body> と </body> の間にテスト文言を <h1>てすと</h1> とでも書いておく bodyの中に書いたテスト文言が左上に表示されているはず console.log("Hello, World!"); とタイプし、上書き保存する
Hello, World!
と出力されていれば成功。
これで JavaScript を実行する最小限の環境は整った。
好きなようにプログラムを書いてコンソールに出力したり画面に書き出したりしてみて。
「指示の通りにならない!」という時はどこでつまずいてるか書いて。対応策を助言できるかもしれない。
本当にあった話だけど、JavaScriptでstrっていう変数にテキストが入っていて
その変数に'apple'とか'banana'とかが入ってるかどうかを判別するっていうロジックを作るときに
const re = new RegExp("apple") if(re.test(str)) console.log("match")
const re_apple = new RegExp("apple") const re_banana = new RegExp("banana") if ( re_apple.test(str) || re_banana.test(str) ) console.log("match")
っていうコードを書く人がおるんよ
別にプログラミング初めて3ヶ月の初心者じゃ無くて20年以上やってるようなベテランだったり
なんならチーフプログラマーとして若手指導してるような人でもこういうレベルの人って割といるわけ
これに対して
「正規表現でORを書くべき」
「includeで十分」
綺麗にコードを書くっていうのは
「これってappleとかbanana以外に増えたりしないの?」
「fruitsかどうかを判別するならその変数を作った方が良くない?」
っていうのを考えて実装するのが綺麗にコードを書くっていうことで
ちなみに
「正規表現でORを書きましょう」
「動いてるからいいじゃない」
「綺麗に書いても性能上意味ない」
「言ってることが良く分からない。何が違うのか」
とか言ってくるし
https://b.hatena.ne.jp/site/anond.hatelabo.jp
で動くスクリプトでたとえば投稿後10分以内にブクマされページに乗ったら「1 user」が「1 user セルクマ 1とか5(何分後にブクマされたか)」になる。もしマイナスなら誤判定なので無視して。
時間を置いたセルクマには効かないし普通のファーストブクマカがどれぐらいの頻度で確認してるかしらないけど5分以内や1分以内もポロポロあるのでまあ目安に。
.forEach(div => {
('.entrylist-contents-title > a')
とかの
を
<>
に変えてね
他にも見落としあるかも
誤判定が減るから非公開ファーストブクマを判定できたらいいんだけどね。
// ==UserScript== // @name hatebu masuda selkmark // @namespace http://tampermonkey.net/ // @version 0.1 // @description 特定時間以内にブクマされた増田を強調する // @author You // @match https://b.hatena.ne.jp/site/anond.hatelabo.jp* // @grant none // ==/UserScript== (function() { 'use strict'; const threshold = 60 * 10 // 何秒以内か const domain = 'https://anond.hatelabo.jp/' const dateTemplate = '202301020304' // 時分まで urlの時刻表記 const dateTest = new RegExp('\\d{' + dateTemplate.length + '}') document.querySelectorAll('div.entrylist-contents').forEach(div => { const masuda = div.querySelector('.entrylist-contents-title > a') const dateStr = masuda.href.substring(domain.length + dateTemplate.length, domain.length) if (!dateTest.test(dateStr)) { // キーワードとか console.log('not diary', dateStr) return } // new Dateできるように変換 // https://amateur-engineer.com/javascript-date-yyyymmddhhmm/ const year = parseInt(dateStr.substring(0, 4)) const month = parseInt(dateStr.substring(4, 6)) const day = parseInt(dateStr.substring(6, 8)) const hour = parseInt(dateStr.substring(8, 10)) const min = parseInt(dateStr.substring(10, 12)) const date = new Date(year, month - 1, day, hour, min) const bukumaDate = new Date(div.querySelector('.entrylist-contents-date').textContent) // 2023/01/23 00:00 const diffSec = (bukumaDate - date) / 1000 // ms to sec if (diffSec > threshold) { return } // ブクマ数 user const user = div.querySelector('span.entrylist-contents-users a').lastChild user.textContent += ' セルクマ ' + (diffSec / 60) // 古い記事がマイナスになる でも2015年ぐらいの記事までかな?新着はセーフ臭い /* if(diffSec < 0) { user.textContent += ' 異常差分:' + diffSec } */ }) })();
console.logでデバッグしてたけどデバッガ使ったら便利でしたくらいのレベル感の話