tm-b.hatenablog.com Open in urlscan Pro
35.75.255.9  Public Scan

URL: https://tm-b.hatenablog.com/
Submission: On May 29 via api from US — Scanned from JP

Form analysis 0 forms found in the DOM

Text Content

中年プログラマーの息抜き
読者になる


中年プログラマーの息抜き


ブログをはじめました。気の向くままにプログラム関連ネタをメモしていきます。

月額900円(税抜)から、高速・多機能・高安定レンタルサーバー『エックスサーバー』

この広告は、90日以上更新していないブログに表示しています。

2023-07-17


PYTHONでスクレイピング(SELENIUM,BEAUTIFULSOUP,OTP)

プログラム Python スクレイピング selenium beautifulsoup oathtool

ランキング参加中
Python
ランキング参加中
プログラミング




やりたいこと

pythonスクリプトを実行し、以下の順で自動処理するマクロを作る。

 1. ブラウザ(Chrome)をシークレットモードで開く
 2. amazonショッピングサイトに自分のアカウントでログイン認証
 3. あらかじめ選んでおいた商品をカートに入れる


実行環境

Pythonを使いますが、いくつかライブラリを追加しました。
・chromedriver-binary
・selenium
・beautifulsoup4
・oathtool



早速、環境を作りましょう。
といっても、簡単に進めたいので、PIPを使います。


PYTHONのバージョン

python --version
Python 3.9.7


PIPのバージョン

pip --version
pip 22.2.2

・・・うーん、では、最新化しておきましょうか。

python -m pip install --upgrade pip
pip --version
pip 23.2


CHROMEのバージョン

ブラウザを開いてバージョンを調べますね。・・115.0.5790.99を確認




ライブラリ「CHROMEDRIVER-BINARY」を追加

pip install chromedriver-binary==115.*

ERROR: Could not find a version that satisfies the requirement chromedriver-
・・・
112.0.5615.49.0, 113.0.5672.24.0, 113.0.5672.63.0, 114.0.5735.16.0,
114.0.5735.90.0)
ERROR: No matching distribution found for chromedriver-binary==115.*

エラーが出たので、一番近い「114.0.5735.90.0」を追加しましょう。

pip install chromedriver-binary==114.0.5735.90.0
pip list | grep chromedriver-binary
chromedriver-binary 114.0.5735.90.0


ライブラリ「SELENIUM」を追加

pip install selenium
pip list | grep selenium
selenium           4.10.0


ライブラリ「BEAUTIFULSOUP4」を追加

pip install beautifulsoup4
pip list | grep beautifulsoup4
beautifulsoup4     4.12.2


ライブラリ「OATHTOOL」を追加

pip install oathtool
pip list | grep oathtool
oathtool            2.3.1

pypi.org
ちょっと最終更新が古いですが、Amazonの2段階認証はパスできた。ネット調べるとミドル的なoath-toolkitの情報が多く見つかりますね。(今回のネタでは使いません)

Python+Seleniumで2段階認証(6桁のパスコード)を突破する全手順 | たぬハック


ライブラリ「そのほかすべて」を最新で更新しておこう

pip-reviewを導入します。

pip install pip-review
pip list | grep pip-review
pip-review          1.3.0

 

パスを通します。(設定していなければ追加します)

set path=C:\Users\・・・\AppData\Roaming\Python\Python39\Scripts;%path%

 

まとめて最新で更新してみます。

pip-review --auto

 

私の環境だと依存がらみでエラーになりました。

RROR: pip's dependency resolver does not currently take into account all the
packages that are installed. This behaviour is the source of the following
dependency conflicts.
fabric3 1.14.post1 requires paramiko<3.0,>=2.0, but you have paramiko 3.2.0
which is incompatible.
html5print 0.1.2 requires ply==3.4, but you have ply 3.11 which is incompatible.

1つずつ調整します。

まずは「ply」と「fabric3」

pip uninstall ply
pip install ply==3.4
pip install -U fabric3

 

続けて「paramiko」と「html5print」

pip uninstall paramiko
pip install paramiko==2.12.0
pip install -U html5print

 


2段階認証アプリ(OATHTOOL)を登録していきます。

アカウント > ログインとセキュリティ > 2段階認証 > 管理ボタンからバックアップ手段を追加(新しい電話または認証アプリを追加)とリンクをたどります。



バーコードをスキャンできませんか? をクリックするとシークレットが表示されるのでこれを控えておき、OTPで利用します。

python -m oathtool "!!! your secret !!!"

999999

↑をワンタイムパスワードに入力して「ワンタイムパスワードを確認して次に進む」と2段階認証アプリ(oathtool)がアカウントに紐づきます。


2段階認証アプリ(OATHTOOL)をソースに組み込む。

こんな感じで組み込めます。

totp = oathtool.generate_otp("!!! your secret !!!")


全体のソース(PYTHON)はこんな感じです。

This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

import time import oathtool from selenium import webdriver def request(url):
global driver try: driver.get(url) time.sleep(3) except: pass def click(xpath):
global driver try: el1 = driver.find_element(webdriver.common.by.By.XPATH,
xpath) el1.click() time.sleep(3) except: pass def input(xpath, text): global
driver try: el1 = driver.find_element(webdriver.common.by.By.XPATH, xpath)
el1.send_keys(text) except: pass options = webdriver.ChromeOptions()
options.add_argument('--incognito') options.add_argument('--headless') driver =
webdriver.Chrome(options=options) request('https://www.amazon.co.jp/')
click('//*[@id="nav-signin-tooltip"]/a') input('//*[@id="ap_email"]', '!!! your
email !!!') click('//*[@id="continue"]') input('//*[@id="ap_password"]', '!!!
your password !!!') click('//*[@id="signInSubmit"]') totp =
oathtool.generate_otp("!!! your totp secret !!!")
input('//*[@id="auth-mfa-otpcode"]', totp)
click('//*[@id="auth-signin-button"]')
request('https://www.amazon.co.jp/dp/・・・/')
click('//*[@id="add-to-cart-button"]') driver.quit()

view raw python-selenium-beautifulsoup-oathtool.py hosted with ❤ by GitHub

import time import oathtool from selenium import webdriver def request(url):
global driver try: driver.get(url) time.sleep(3) except: pass def click(xpath):
global driver try: el1 = driver.find_element(webdriver.common.by.By.XPATH,
xpath) el1.click() time.sleep(3) except: pass def input(xpath, text): global
driver try: el1 = driver.find_element(webdriver.common.by.By.XPATH, xpath)
el1.send_keys(text) except: pass options = webdriver.ChromeOptions()
options.add_argument('--incognito') options.add_argument('--headless') driver =
webdriver.Chrome(options=options) request('https://www.amazon.co.jp/')
click('//*[@id="nav-signin-tooltip"]/a') input('//*[@id="ap_email"]', '!!! your
email !!!') click('//*[@id="continue"]') input('//*[@id="ap_password"]', '!!!
your password !!!') click('//*[@id="signInSubmit"]') totp =
oathtool.generate_otp("!!! your totp secret !!!")
input('//*[@id="auth-mfa-otpcode"]', totp)
click('//*[@id="auth-signin-button"]')
request('https://www.amazon.co.jp/dp/・・・/')
click('//*[@id="add-to-cart-button"]') driver.quit()

casimalu (id:tm-b) 317日前 読者になる




広告を非表示にする

 * もっと読む

コメントを書く
2023-02-23


【C#】キーフック-左CAPSLOCKを無効にして左CTRLキーに偽装してみた

C# プログラム グローバルフック キー配置 キーマップ キーボード

ランキング参加中
Microsoft .NET
ランキング参加中
プログラミング



早速経緯から、

つい最近までHHKBキーボードを使っていましたが、パワポやワードなど資料作成時間が増えたことから矢印キー(↑→↓←)の利用頻度が増えます。

長時間作業すると 右手首が痛いんじゃ 状態

※HHKBでは、矢印キーは「Fn」割り当て

 

そんな理由から、手元でほこりをかぶっていたRealForce108UBKキーボードを使うようにしたのですが、これCapsLockと左Controlキーの割り当て変更が出来ません。

 

HHKBキーボードの左Ctrlキーに慣れきっているのでタイピング中の手の位置も中央付近が習慣付けされていて、こうなると左下にある左Ctrlキーを使うときの違和感が半端ないんですよね。さらに、間違えてCapsLockしてしまうことでイライラ

そして、長時間作業すると 左手首が痛いんじゃ 状態

 

キーボード買い換えも考えますが、そんな余裕もないしそもそもこんな理由で買い替えるなんていやだしなぁといった感じで我慢して何日か作業したんですが、どうにも慣れない機材を使うと作業時間に比例してイライラ

※少し使うと慣れてくるかと思いましたが、全然でした。

 

ということで、プログラム書いてCapsLockキー入力を無効にして使ってます。

※単に無効化するだけでは面白くなかったので「CtrlLockキー」にしてしまいました。

 

以下C#ソースコード

public static class KeyboardHook { [StructLayout(LayoutKind.Sequential)] private
struct KBDLLHOOKSTRUCT { public uint vkCode; public uint scanCode; public uint
flags; public uint time; public UIntPtr dwExtraInfo; }
[StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort
wVk; public ushort wScan; public uint dwFlags; public uint time; public UIntPtr
dwExtraInfo; public int dummy1; public int dummy2; };
[StructLayout(LayoutKind.Sequential)] private struct INPUT { public int type;
public KEYBDINPUT ki; }; [DllImport("user32.dll")] private static extern IntPtr
SetWindowsHookEx(int idHook, KeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static
extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll")] private
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr
lParam); [DllImport("kernel32.dll")] private static extern IntPtr
GetModuleHandle(string lpModuleName); [DllImport("user32.dll")] private static
extern uint SendInput(int nInputs, INPUT pInputs, int cbsize); private const int
WH_KEYBOARD_LL = 0x000D; private static readonly IntPtr TRUE = new IntPtr(1);
private static readonly IntPtr WM_KEYDOWN = new IntPtr(0x0100); private static
readonly IntPtr WM_KEYUP = new IntPtr(0x0101); private static readonly IntPtr
WM_SYSKEYDOWN = new IntPtr(0x0104); private static readonly IntPtr WM_SYSKEYUP =
new IntPtr(0x0105); private static readonly INPUT CTRL_D = { new INPUT { type =
1, ki = new KEYBDINPUT() { wVk = 162, wScan = 0, dwFlags = 0, time = 0,
dwExtraInfo = UIntPtr.Zero } } }; private static readonly INPUT[] CTRL_U = { new
INPUT { type = 1, ki = new KEYBDINPUT() { wVk = 162, wScan = 0, dwFlags = 2,
time = 0, dwExtraInfo = UIntPtr.Zero } } }; private static readonly int SZ_INPUT
= Marshal.SizeOf(typeof(INPUT)); private delegate IntPtr KeyboardProc(int nCode,
IntPtr wParam, IntPtr lParam); private static readonly KeyboardProc proc = Proc;
private static IntPtr sHookId = IntPtr.Zero; private static bool sHoldCtrl =
false; public static void Start() { if (sHookId != IntPtr.Zero) return; using
(var p = Process.GetCurrentProcess()) using (var m = p.MainModule) sHookId =
SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(m.ModuleName), 0); }
public static void Stop() { UnhookWindowsHookEx(sHookId); sHookId = IntPtr.Zero;
if (sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); } private static IntPtr Proc(int
nCode, IntPtr wParam, IntPtr lParam) { if (nCode < 0) return
CallNextHookEx(sHookId, nCode, wParam, lParam); if (wParam == WM_KEYDOWN ||
wParam == WM_SYSKEYDOWN) { var kb =
(KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); var
vkCode = kb.vkCode; if (vkCode == 20) { SendInput(1, CTRL_D, SZ_INPUT);
sHoldCtrl = true; return TRUE; } else if (vkCode == 240 || vkCode == 242) { if
(sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); return TRUE; } else if (vkCode ==
162) { if (sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); } } else if (wParam ==
WM_KEYUP || wParam == WM_SYSKEYUP) { var kb =
(KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); var
vkCode = kb.vkCode; if (vkCode == 20 || vkCode == 240 || vkCode == 242) { if
(sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); return TRUE; } else if (vkCode ==
162) { sHoldCtrl = false; } } return CallNextHookEx(sHookId, nCode, wParam,
lParam); } }



This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

public static class KeyboardHook { [StructLayout(LayoutKind.Sequential)] private
struct KBDLLHOOKSTRUCT { public uint vkCode; public uint scanCode; public uint
flags; public uint time; public UIntPtr dwExtraInfo; }
[StructLayout(LayoutKind.Sequential)] private struct KEYBDINPUT { public ushort
wVk; public ushort wScan; public uint dwFlags; public uint time; public UIntPtr
dwExtraInfo; public int dummy1; public int dummy2; };
[StructLayout(LayoutKind.Sequential)] private struct INPUT { public int type;
public KEYBDINPUT ki; }; [DllImport("user32.dll")] private static extern IntPtr
SetWindowsHookEx(int idHook, KeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static
extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll")] private
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr
lParam); [DllImport("kernel32.dll")] private static extern IntPtr
GetModuleHandle(string lpModuleName); [DllImport("user32.dll")] private static
extern uint SendInput(int nInputs, INPUT[] pInputs, int cbsize); private const
int WH_KEYBOARD_LL = 0x000D; private static readonly IntPtr TRUE = new
IntPtr(1); private static readonly IntPtr WM_KEYDOWN = new IntPtr(0x0100);
private static readonly IntPtr WM_KEYUP = new IntPtr(0x0101); private static
readonly IntPtr WM_SYSKEYDOWN = new IntPtr(0x0104); private static readonly
IntPtr WM_SYSKEYUP = new IntPtr(0x0105); private static readonly INPUT[] CTRL_D
= { new INPUT { type = 1, ki = new KEYBDINPUT() { wVk = 162, wScan = 0, dwFlags
= 0, time = 0, dwExtraInfo = UIntPtr.Zero } } }; private static readonly INPUT[]
CTRL_U = { new INPUT { type = 1, ki = new KEYBDINPUT() { wVk = 162, wScan = 0,
dwFlags = 2, time = 0, dwExtraInfo = UIntPtr.Zero } } }; private static readonly
int SZ_INPUT = Marshal.SizeOf(typeof(INPUT)); private delegate IntPtr
KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); private static readonly
KeyboardProc proc = Proc; private static IntPtr sHookId = IntPtr.Zero; private
static bool sHoldCtrl = false; public static void Start() { if (sHookId !=
IntPtr.Zero) return; using (var p = Process.GetCurrentProcess()) using (var m =
p.MainModule) sHookId = SetWindowsHookEx(WH_KEYBOARD_LL, proc,
GetModuleHandle(m.ModuleName), 0); } public static void Stop() {
UnhookWindowsHookEx(sHookId); sHookId = IntPtr.Zero; if (sHoldCtrl) SendInput(1,
CTRL_U, SZ_INPUT); } private static IntPtr Proc(int nCode, IntPtr wParam, IntPtr
lParam) { if (nCode < 0) return CallNextHookEx(sHookId, nCode, wParam, lParam);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { var kb =
(KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); var
vkCode = kb.vkCode; if (vkCode == 20) { SendInput(1, CTRL_D, SZ_INPUT);
sHoldCtrl = true; return TRUE; } else if (vkCode == 240 || vkCode == 242) { if
(sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); return TRUE; } else if (vkCode ==
162) { if (sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); } } else if (wParam ==
WM_KEYUP || wParam == WM_SYSKEYUP) { var kb =
(KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); var
vkCode = kb.vkCode; if (vkCode == 20 || vkCode == 240 || vkCode == 242) { if
(sHoldCtrl) SendInput(1, CTRL_U, SZ_INPUT); return TRUE; } else if (vkCode ==
162) { sHoldCtrl = false; } } return CallNextHookEx(sHookId, nCode, wParam,
lParam); } }

view raw GlovalHock-KeyboardHook hosted with ❤ by GitHub




KEYBOARDHOOKクラスリファレンス

CapsLockキー入力を無効化します。

CapsLockキー入力で状態を切り替えます。

 → Ctrlキーロック状態をエミュレート

 → Ctrlキーロック状態をリリース


 * KEYBOARDHOOK.START() 
   グローバルフック(キーフック)開始


 * KEYBOARDHOOK.STOP()
   グローバルフック(キーフック)停止

casimalu (id:tm-b) 1年前 読者になる




広告を非表示にする

 * もっと読む

コメントを書く
2022-10-19


WORDPRESS の記事を短く表示したい。語尾に三点リーダー(…)を付ける方法

php wordpress プログラム Shopify

ランキング参加中
PHP




はじめに

ブログなどでタイトルや一覧画面のレイアウトを考えるとき、本文が長すぎたり画像が表示されてしまったりと、想定している枠内に文字が収まりきらなくなる場合があります。

PHP(ワードプレス)の投稿一覧画面と投稿内容画面との関係を考えていて投稿一覧画面にも簡単な説明文を表示したいことはありませんか? この場合、例えばカスタムフィールドなどを利用して、一覧表示文と投稿本文とをそれぞれで保存しておくのが簡単です。といっても面白くないので、今回は、投稿本文の中から100文字の一覧表示文を自動作成して、100文字を超えたところで語尾に三点リーダー(…)を付ける方法を紹介します。こうしておくと一覧表示レイアウトから簡単な説明が読み取れますし『あ、まだ続きがあるんだ…。』ということも伝わりますね。


まずは完成イメージ

今手伝っているフェスの準備でWordpressを使ってみました。(期間限定の紹介です)※レイアウトはデザイナーさんがきれいに仕上げてくれます。

onestead.xsrv.jp


目指すこと(実現したいこと)

「PHP(ワードプレス)の投稿本文に設定した内容を一覧画面へ反映する

「テキストを含むタグ以外(img)などは除去する」

「文字数は上限(100文字)を設ける」

「文字数上限を超える場合、語尾に三点リーダーを付与する」

「XSERVER」環境(Wordpress)で動作する


PHP(ワードプレス)で語尾に三点リーダー

function.phpなどへ作成しておき表示用のPHPファイルから実行するだけです。

<?php echo(strimwidth_post_contents($post->post_content)); ?>



This file contains bidirectional Unicode text that may be interpreted or
compiled differently than what appears below. To review, open the file in an
editor that reveals hidden Unicode characters. Learn more about bidirectional
Unicode characters
Show hidden characters

function __strimwidth_post_contents($node, &$count) { $result = 0; $strwidth =
100; if ($node->hasChildNodes() === false) { if (property_exists($node,
"nodeValue") === true && $node->nodeValue !== null && $node->nodeType ===
XML_TEXT_NODE) { if ($count < $strwidth) { $value = preg_replace("/\s/", '',
$node->nodeValue); if ($value === null) { $result = 1; } else { $length =
mb_strlen($value, 'UTF-8'); if ($length === 0) { $result = 1; } else if (($count
+ $length) < $strwidth) { $count += $length; } else { $node->nodeValue =
mb_substr($node->nodeValue, 0, ($strwidth - $count), 'UTF-8').' ... '; $count =
$strwidth + 5; } } } else { $result = 1; } } else { $result = 1; } } else { for
($i = 0; $i < $node->childNodes->length; $i++) if
(__strimwidth_post_contents($node->childNodes[$i], $count) === 1)
$node->removeChild($node->childNodes[$i--]); if ($node->hasChildNodes() ===
false) $result = 1; } return $result; } function
strimwidth_post_contents($post_content) { if (isset($post_content) === false)
return ""; $count = 0; $doc = new DOMDocument('1.0', 'UTF-8');
$doc->loadHTML('<div
class="strimwidth_post_contents">'.mb_convert_encoding(str_replace('&','&amp;',$post_content).'</div>','HTML-ENTITIES','UTF-8'),LIBXML_HTML_NOIMPLIED|LIBXML_HTML_NODEFDTD|LIBXML_NOERROR);
for ($i = 0; $i < $doc->childNodes->length; $i++) if
(__strimwidth_post_contents($doc->childNodes[$i], $count) === 1)
$doc->removeChild($doc->childNodes[$i--]); return
mb_convert_encoding($doc->saveHTML(), 'UTF-8', 'HTML-ENTITIES'); }

view raw strimwidth_post_contents hosted with ❤ by GitHub




まとめ

ちょっとしたことですがヒューマンエラーを減らすことにもつながりますし、自動処理させるという視点は大切なのかなと感じます。REST化してJS経由から実行すればShopifyなどで活用するなど利用シーンはあるのかな。


DOMDOCUMENTって何者?

ここで詳しく説明されてます。とても参考になります、ありがとうございます。

PHPでDOMを使ってHTMLをロード...


能古島フェスの紹介

W@F 福岡ワーケーションフェス2022

日本でもっともチルな島、能古島

casimalu (id:tm-b) 1年前 読者になる




広告を非表示にする

 * もっと読む

コメントを書く
次のページ

月別アーカイブ
 * ▼ ▶
   2023 (2)
   * 2023 / 7 (1)
   * 2023 / 2 (1)
 * ▼ ▶
   2022 (1)
   * 2022 / 10 (1)
 * ▼ ▶
   2021 (5)
   * 2021 / 9 (1)
   * 2021 / 8 (3)
   * 2021 / 2 (1)
 * ▼ ▶
   2020 (9)
   * 2020 / 11 (1)
   * 2020 / 10 (1)
   * 2020 / 9 (6)
   * 2020 / 7 (1)
 * ▼ ▶
   2019 (12)
   * 2019 / 10 (2)
   * 2019 / 2 (3)
   * 2019 / 1 (7)
 * ▼ ▶
   2018 (1)
   * 2018 / 6 (1)
 * ▼ ▶
   2017 (25)
   * 2017 / 11 (1)
   * 2017 / 10 (2)
   * 2017 / 8 (11)
   * 2017 / 7 (8)
   * 2017 / 3 (1)
   * 2017 / 1 (2)
 * ▼ ▶
   2016 (32)
   * 2016 / 12 (2)
   * 2016 / 11 (4)
   * 2016 / 10 (1)
   * 2016 / 9 (1)
   * 2016 / 8 (9)
   * 2016 / 6 (2)
   * 2016 / 4 (5)
   * 2016 / 2 (7)
   * 2016 / 1 (1)
 * ▼ ▶
   2015 (5)
   * 2015 / 10 (4)
   * 2015 / 8 (1)

プロフィール
読者です 読者をやめる 読者になる 読者になる
20
このブログについて
中年プログラマーの息抜き

Powered by Hatena Blog | ブログを報告する



引用をストックしました

ストック一覧を見る 閉じる

引用するにはまずログインしてください

ログイン 閉じる

引用をストックできませんでした。再度お試しください

閉じる

限定公開記事のため引用できません。

読者です 読者をやめる 読者になる 読者になる
20