Puppeteer はてなブログで誰を購読中かわけわからなくなったら…購読中のブログ一覧と読者一覧

こんにちは。レモンティーです。

今回ははてなブログの読者リストと購読中のブログリストのお話です。

はてなブログはSNSではないので購読中の一覧を楽に見る機能はありません。そのためそこそこ数が増えてくるともうわけがわからなくなってせっかく購読していても読みたいものが埋もれていたりしますが手動で管理するのは非常に面倒です。そこで今回は例によってPuppetterで購読中のブログと読者のhatenaIdの一覧をつくります。サムネ一括変更↓の時も言いましたがエラーがでたユーザーはスキップしていくので歯抜けはログを見ながら自分で埋める必要があります。
www.sawalemontea.com

準備

PuppeteerだけでOKです。

npm i puppeteer

概要

読者一覧をsubscribers.txtに、購読中のブログ一覧をsubscribings.txtにそれぞれ保存します。終わったら一方的な購読を両方向で探してone-way-subscribes.txtに保存します。

メイン部分

index.js

const puppeteer = require('puppeteer');
const fs = require('fs');

(async () => {
    const browser = await puppeteer.launch();
    await login(browser,id,pw);
    
    const data = await getSubscribersList(browser,id,domain);
    fs.writeFileSync('./data/subscribers.txt',data.join('\n'));
    const data = await getSubscribingList(browser);
    fs.writeFileSync('./data/subscribings.txt',data.join('\n'));
    const data  = await getOnewaySubscribeList('./data/subscribers.txt','./data/subscribings.txt');
    fs.writeFileSync('./data/one-way-subscribes.txt',data);

    await browser.close();
})();

ログインしてから概要の通りにしています。
ログインは前回のこれです

async function login(browser){
    const page =  await browser.newPage();
    await page.goto('https://www.hatena.ne.jp/login',{waitUntil:'domcontentloaded'});
    await page.type("#login-name",id);
    await page.type("input.password",password);
    await page.click("input.submit-button");
    await page.waitForNavigation({timeout:60000,waitUntil:'domcontentloaded'});
    console.log("login");
    await page.close();
}

読者一覧

読者一覧をつくります。

index.js

async function getSubscribersList(browser,hatenaId,blogDdomain){
    const url = `https://blog.hatena.ne.jp/${hatenaId}/${blogDdomain}/subscribers`;
    const page = await browser.newPage();
    await page.goto(url,{waitUntil:'domcontentloaded', timeout: 20000});
    let subscribersList = [];
    while(true){
        //ユーザーを追加
        const subscribers = await page.$$('span.username');
        for(const user of subscribers){
            const item = await (await user.getProperty('textContent')).jsonValue();
            let hatenaId = item.split('id:')[1];
            if(hatenaId.slice(-1) == ')')hatenaId = hatenaId.slice(0,-1);
            subscribersList.push(hatenaId);
        }
        //次のページへ
        const nextButton = await page.$('#subscriber-next-page');
        if(!nextButton)break;
        await Promise.all([
            page.waitForNavigation({timeout: 20000}),
            nextButton.click()
        ]);
    }
    await page.close();
    return subscribersList;
}

購読中一覧

購読中のブログの持ち主のhatenaIdを取得していきます。
相手のブログのプロフィールページがカスタマイズされている場合、要素を取得できすにエラーになることがありますがそこまで多くないので今回は無視してスキップ。ほかにもブログが消されている、非公開に変更されているなども当然エラーですし意外と多いですがどうしようもないのでスキップ。全部合わせると一割くらいエラーがあったり…。登録したのにずっと更新ないなーってときは削除or非公開にされてたってわけですね…。

index.js

async function getSubscribingList(browser){
    const page = await browser.newPage();
    let subscribingList = [];
    let pageNum = 0;
    while(true){
        //対象ページへ
        pageNum += 1;
        console.log('page : ',pageNum);
        const url = 'https://blog.hatena.ne.jp/-/antenna?page='+pageNum;
        await page.goto(url,{waitUntil: 'domcontentloaded', timeout: 20000});
        //ユーザーを追加
        const users = await page.$$('.admin-subscribe-wrapper-left .entry-unit-user-name');
        for(const user of users){
            const href = await (await user.getProperty('href')).jsonValue();
            const hatenaId = await getHatenaId(browser,href);
            if(hatenaId)subscribingList.push(hatenaId);
        }
        //ブログが10個未満なら最後のページということに…
        if(users.length < 10)break;
    }
    await page.close();
    return subscribingList;
}

async function getHatenaId(browser,blogTopUrl){
    try{
        const page = await browser.newPage();
        await page.goto(blogTopUrl+'about',{waitUntil: 'domcontentloaded', timeout: 20000});
        const elem = await page.$('.entry-content dl  dd  a  span');
        const value = await (await elem.getProperty('textContent')).jsonValue();
        await page.close();
        let hatenaId = value.split('id:')[1];
        if(hatenaId.slice(-1) == ')')hatenaId = hatenaId.slice(0,-1);
        console.log(hatenaId);
        return hatenaId;
    }catch(err){
        console.log('getHatenaId error at : ',blogTopUrl);
        console.log('  error : ',err);
        return undefined;
    }
}

一方向を探す

相手だけ・自分だけの登録を発見します。このブログでは両方100個くらいありました。消えてるのもあるので正確ではありませんが…。

index.js

async function getOnewaySubscribeList(path1,path2){
    const data1 = fs.readFileSync(path1,{encoding:'utf8'}).split('\n');
    const data2 = fs.readFileSync(path2,{encoding: 'utf8'}).split('\n');

    let txt = 'list A\n';
    for(const d of data1){
        if(!existIn(d,data2)){
            txt += d+'\n';
        }
    }
    txt += '\n\nlist B\n';
    for(const d of data2){
        if(!existIn(d,data1)){
            txt += d+'\n';
        }
    }
    return txt;
}

function existIn(item,list){
    for(const x of list){
        if(item == x)return true;
    }
    return false;
}

おしまい

今回はこれでおしまいです。
www.sawalemontea.com