分類
讀書

一塊小黑板

田園將蕪

——後鄉村時代記事

作者:江子

ISBN:978-7-224-10540-7-

江西人民出版社


我在這個城市所在的小區,旁邊是一排失地農民的安置房。與周圍到處是被稱為﹁花園﹂﹁街區﹂﹁新城﹂的住宅區不太協調的是,房子似乎有些年份了,而且樣式簡單,外表是裸露的、顯得陳舊的水泥。

沒有圍牆的五棟房子,外面看起來有些雜亂無章︵有一小塊地方甚至還種了幾行菜︶。每一天,我進出小區,都可以看到一些衣著簡陋的人在房子之間出沒。他們說著最為正宗的本地方言。有時候在樹下,他們還會湊在一起打牌,坐下是城裡很少見的那種舊竹椅。抓牌的時候,椅子會發出吱吱呀呀的響聲。

我不禁對他們產生了好奇。這些失地的農民,糊裡糊塗成了城裡人。他們的生活狀態發生了什麼樣的變化?他們有怎樣的悲歡?

在這排房子最前面的那棟房的牆上,砌了一塊小黑板。

最前面的房子是所有人必經的路口。從路口轉過去,就是通往市區的大路。我每天上下班都要經過那塊小黑板。有時候我會看到有人在寫字,旁邊圍著一群人在等著他繼續書寫。而大多數時候,書寫者已經離開,只留下一些字跡在上面,落款是﹁××社區﹂。而所書內容,無疑和這排房子的住戶有關。

我曾經是個老師,自然對黑板之類的東西有緣自職業的敏感。而書寫在上面的消息,又多少讓我對我家旁邊的那排房子裡的居民的生活有一些了解。

當然,黑板上大多是一些簡單的通知。有提請小區適齡兒童人學的:﹁如果你的孩子年滿 6 周歲,請帶好戶口簿於 9 月 3 日之前攜適齡兒童到學校報名。﹂有通知參加娛樂活動的:﹁我區將舉行好家庭體育競賽,歡迎本社區廣大家庭踴躍報名參加。有意者請到社區辦報名。聯繫人:×××。﹂這兩天,颱風﹁鳳凰﹂登陸我省,黑板上立即有了一項關於颱風登陸的通知:﹁據有關氣象部門消息,颱風﹃鳳凰﹄將於今日登陸我省。請大家及時做好應對準備,提防花盆、磚塊被颱風吹落砸傷行人,注意安全。請大家相互轉告。﹂

黑板上最讓小區居民關注的,大概是那些關於低保戶的公示了。隔一段時間,就會有一條公示出現在黑板上。

現將我用手機拍攝保存下的一條抄錄如下:

2007 年×月低保申請戶情況公示
本著充分發揚民主的宗旨,徵求廣大群眾意見,現將本月申請低保人員情況公布如下:
姓名 年齡 家庭人口 每月家庭收入
萬蒼生 48歲 三口 200元
萬孝全 40歲 三口 150元
李望桃︵女︶ 41歲 二口 119元
熊狗仔 52歲 三口 120元
如有不實,請撥打投訴電話:×××××××

我並不認識上面標示的四個人中的任何人。可是這樣一條公示讓我無由擔心:在這樣消費水平很高的城市裡,他們每月不到兩百塊錢的收入怎麼生活?

我死死地盯著第三個每月家庭收入裡的 119 這個數字。——為什麼是這個數字?它的諧音是﹁要要救﹂,這也是火警電話。是這戶人家實際的收入水平還是戶主出於對自身生活的怨氣故意虛報?

有段時間,本市要爭創全國衛生城市。小黑板上許多天都書寫了與爭創全國衛生城市相關的內容。有倡議書,注意事項,具體達標條款,等等。

有一天,小黑板上發布了一項通知:﹁現我市爭創全國衛生城市已到了攻堅階段。希望廣大市民密切配合。明日下午三點整,本小區內所有低保戶請自帶掃帚到××路某某飯店旁集中,由社區領導統一安排進行大掃除,任何人不得缺席,違者扣發三個月低保金。﹂

通知發布的第三天,小黑板上又換上了新的內容:﹁昨天下午,有低保戶萬國元公然視社區通知於不顧,不參加社區組織的大掃除活動,在整個全國衛生城市爭創活動中影響惡劣。現經社區研究決定,扣發萬國元低保金三個月。﹂

第四天我經過的時候,那條通知依然赫然在目。只是在通知空白的地方,有三個寫得大大的歪歪扭扭的字。字體風格完全迥異於通知的書寫,而且用的是不同於粉筆的類似板結的石灰塊之類的材料。三個字的後面是一個大得有些誇張的驚嘆號。

那三個字是:我有病!

分類
程序

在 CentOS 8 上使用 Xvfb 和 PyAutoGUI

本文介紹在 CentOS 8 上使用 PyAutoGUI 操作運行與 Xvfb 桌面環境中的 Firefox 所需的準備。由於所操作網頁需要登錄並有 OTP,所以還需要使用 VNC 來手動輸入密碼以做準備工作。

安裝 Xvfb

sudo yum install xorg-x11-server-Xvfb
#啓動 Xvfb
Xvfb :0 -screen 0 1366x768x24+32 -br +bs -ac &
#修改環境變量 DISPLAY
export DISPLAY=:0
#查看環境變量 DISPLAY
$DISPLAY
#查看 Xvfb 進程
ps -ef | grep Xvfb

如果只是爲了使用 webdriver 操作瀏覽器那麼這樣安裝運行 Xvfb 後就可以了,但是我們要使用 PyAutoGUI,所以還有依賴要裝。

安裝火狐瀏覽器

我個人比較偏好 ESR 版火狐,下載後解壓就可以使用了。

tar -xf firefox-115.5.0esr.tar.bz2
cd Firefox
./firefox
#如果要指定窗口大小可以
./firefox -width 1350 -height 764

安裝 x11vnc 服務

其實只要下載執行檔,運行即可,非常簡單。不過 x11vnc 12 年未有更新,不知道還能用多久。如果那天不能用了可以嘗試 Tiger VNC。

mv x11vnc-0.9.13_amd64-Linux x11vnc
chmod +x x11vnc
#運行 vnc server,密碼設置長一點,端口是 9999
./x11vnc -display :0 -ncache 0 -passwd piHrcHxmJauvIOftenUseA64digitPasswordpokiHJHQWdsgcGFTG -rfbport 9999
#上面命令在一次連接後會停止,如果要服務一直可以用,就加上 forever 參數
./x11vnc -display :0 -ncache 0 -passwd piHrcHxmJauvIOftenUseA64digitPasswordpokiHJHQWdsgcGFTG -rfbport 9999 -shared -forever
#注意服務器的網絡防火牆要打開 9999 端口
#用 nmap 查看端口是否打開
nmap -p 9999 1.1.1.1

Fedora 本地可以安裝 Remmina 作爲 VNC 客戶端。

sudo dnf install remmina -y
#如果想要使用 socks5 代理
proxychains4 /usr/bin/remmina

打開 Remmina 後在地址欄的協議選項裏選擇 VNC,然後地址填 1.1.1.1:9999 回車就可以連上服務器的桌面並看到剛剛打開的火狐瀏覽器進行設置了。點左上角的 + 號可以添加 profile,這樣下次雙擊就能連上 VNC 了。

安裝與配置 PyAutoGUI

我自己從源碼編譯的 Python 一直提示沒有 _tkinter 模塊。搜了半天沒有解決,於是使用系統自帶的 Python 3.9 得以解決。雖然我是用 Python 3.11 開發的,但是 Python 3.9 跑起來也沒問題。

import _tkinter # If this fails your Python may not be configured for Tk
ImportError: No module named _tkinter
#安裝 Python 3.9 以及依賴
sudo yum install libnsl ImageMagick xclip
sudo yum install python39 python39-tkinter

#xdotool 並不支持 Xvfb,所以不用裝
#sudo yum install xdotool

#創建虛擬環境
cd your_project
python3.9 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

示例代碼

import json
import random
import sys
import requests
import time
import logging
import os
import pyperclip
import subprocess
from urllib.parse import urlparse, parse_qs, urlunparse
from dotenv import load_dotenv
import psutil
import pyautogui
import pyscreeze

script_dir = os.path.dirname(os.path.realpath(__file__))

load_dotenv()
api_key = os.getenv("API_KEY")
proxy = os.getenv("PROXY")
de = os.getenv("DE")
batch = int(os.getenv("BATCH"))

if proxy is None:
    proxies = None
else:
    proxies = {"http": proxy, "https": proxy}

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger()
# Create a file handler to save logs to a file
log_file = os.path.join(script_dir, "logfile.log")
file_handler = logging.FileHandler(log_file)

# Set the logging level for the file handler (if different from the root logger)
file_handler.setLevel(logging.INFO)  # Adjust the log level if needed

# Create a formatter and add it to the file handler
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)

# Add the file handler to the logger
logger.addHandler(file_handler)


def activate_window(window_title):
    if de == "Xvfb":
        firefox_exists = False
        for proc in psutil.process_iter():
            try:
                if "firefox" in proc.name().lower():
                    firefox_exists = True
                    break
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                pass
        if firefox_exists is False:
            subprocess.Popen(
                [
                    "/home/centos/fred/programs/firefox/firefox",
                    "-width",
                    "1350",
                    "-height",
                    "764",
                ]
            )
            logger.info("firefox started")
            time.sleep(10)
    elif de == "Xfce":
        command = f"xdotool search --onlyvisible --name '{window_title}' windowactivate"
        subprocess.run(command, shell=True)


def wait_for_image(image_path, timeout=10):
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            location = pyautogui.locateOnScreen(
                image_path, region=(0, 0, 1360, 760), confidence=0.9, grayscale=True
            )
            if location:
                return location
        except pyautogui.ImageNotFoundException:
            pass
        time.sleep(1.5)
    return None


def wait_for_images(image_path, timeout=10):
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            location = pyautogui.locateAllOnScreen(
                image_path, region=(0, 0, 1366, 760), confidence=0.9
            )
            if location:
                return list(location)
        except pyautogui.ImageNotFoundException:
            pass
        except pyscreeze.ImageNotFoundException:
            pass
        time.sleep(1.5)
    return []

def right_click():
    pyautogui.mouseDown(button="right")  # Perform a right-click
    time.sleep(0.1)  # Adjust the delay if needed (time in seconds)
    pyautogui.mouseUp(button="right")  # Release the right-click


def triple_click(x, y):
    for _ in range(3):
        pyautogui.click(x, y)
        time.sleep(0.1)

def get_data(obj):
    start_time = time.time()
    time.sleep(0.5)
    activate_window("Mozilla Firefox")
    time.sleep(0.5)
    # load webpage
    url_profile = obj["contact_name_href"]
    if url_profile.find("?") != -1:
        url_profile = url_profile[: url_profile.find("?")]
    logger.info(url_profile)
    # open a new tab
    # pyautogui.hotkey('ctrl', 't')
    pyautogui.hotkey("ctrl", "l")
    time.sleep(0.5)
    pyautogui.typewrite(url_profile)
    time.sleep(0.5)
    pyautogui.press("enter")
    time.sleep(8)

    # check page loaded
    image_path = os.path.join(script_dir, "img/ContactDetails.png")
    loaded = wait_for_image(image_path, 40)

    # Box(left=535, top=541, width=74, height=19)
    if loaded:
        logger.info("Page loaded")

    # LinkedIn
    image_path = os.path.join(script_dir, "img/LinkedIn.png")
    l = wait_for_images(image_path, 3)
    l_index = 0
    retry = 0
    temp_l = None
    while l_index < len(l):
        logger.info(f"l_index:{l_index+1}/{len(l)}")
        if temp_l is not None:
            if temp_l[0] == l[l_index][0]:
                l_index = l_index + 1
                continue
        li = l[l_index]
        pyautogui.moveTo(li, duration=2, tween=pyautogui.easeInOutQuad)
        time.sleep(0.5)
        pyautogui.click(li)
        time.sleep(2)
        pyautogui.hotkey("ctrl", "l")
        time.sleep(0.5)
        pyautogui.hotkey("ctrl", "c")
        time.sleep(0.5)
        pyautogui.hotkey("ctrl", "w")
        time.sleep(0.5)
        copied_text = pyperclip.paste()
        url = copied_text.strip()
        url = get_link_from_redirect(url)

        if url is not None and url != "":
            # logger.info(len(url))
            if url.find("linkedin") == -1:
                logger.info("not found 'linkedin'")
                if retry < 3:
                    retry = retry + 1
                else:
                    l_index = l_index + 1
                continue

            # personal LindedIn
            if li[0] < loaded_x_left:
                obj["LinkedIn_Personal_URL"] = url
            else:
                obj["LinkedIn_URL"] = url
        temp_l = li
        l_index = l_index + 1

    # Supplemental_Email
    image_path = os.path.join(script_dir, "img/Supplemental.png")
    l = wait_for_image(image_path, 3)
    if l:
        pyautogui.moveTo(
            l[0] + l[2] + 50 + random.randint(-5, 5),
            l[1] + l[3] / 2,
            duration=1,
            tween=pyautogui.easeInOutQuad,
        )
        time.sleep(1)
        right_click()
        time.sleep(1)
        pyautogui.hotkey("l")
        time.sleep(1)
        pyautogui.hotkey("esc")
        # [email protected]
        copied_text = pyperclip.paste()
        obj["Supplemental_Email"] = copied_text.strip()

    # Local address
    image_path = os.path.join(script_dir, "img/Local.png")
    l = wait_for_image(image_path, 3)
    if l:
        x = l[0] + l[2] + 30 + random.randint(-5, 5)
        pyautogui.moveTo(x, l[1] + l[3] / 2, duration=1, tween=pyautogui.easeInOutQuad)
        time.sleep(1)
        triple_click(x, l[1] + l[3] / 2)
        time.sleep(1)
        pyautogui.hotkey("ctrl", "c")
        time.sleep(1)
        # [email protected]
        copied_text = pyperclip.paste()
        obj["Local_Location_Address"] = copied_text.strip()

def main(max_id):
    failed_list = []
    for i in range(batch):
        logger.info(f"batch:{i+1}/{batch}")
        obj = get_job()
        if obj is None:
            continue
        if obj["contact_name_href"] == "" or obj["contact_name_href"] is None:
            res = post_job(obj)
            logger.info(res)
            continue

        if obj["id"] > max_id:
            logger.info("max_id reached")
            continue
        obj = get_data(obj)
        if obj["message"] != "success":
            if obj["id"] in failed_list:
                res = post_job(obj)
                logger.info(res)
            else:
                failed_list.append(obj["id"])
            continue

        res = post_job(obj)
        logger.info(res)

if __name__ == "__main__":
    # time.sleep(10)
    # countdown
    for i in range(6):
        logger.info(f"countdown:{6-i}")
        time.sleep(1)

    # get max id from command
    if len(sys.argv) > 1:
        max_id = int(sys.argv[1])
        logger.info(f"max_id:{max_id}")
    else:
        max_id = 9999999

    main(max_id)
分類
程序

下載指定版本 Chromium 與其 Webdriver

以下載 Linux 平臺 119 版本 Chromium 與其 Webdriver 爲例:

首先在 Chromium 版本發佈頁 找到想要下載版本的 Branch Base Position 比如 1204232。然後在 https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ 中搜索 1204232。如果發現搜索結果爲空,不用怕,把最後一位去掉試試,這時就會搜到 1204234。也可以再刪掉一位,就會出來更多相近的結果。點進去後下載 chrome-linux.zip 和 chromedriver_linux64.zip 就可以了。

unzip chrome-linux.zip
cd chrome-linux
#查看 Chrome 版本
./chrome --version
#Chromium 119.0.6045.0

在 Selenium 中指定 Chrome、Webdriver 與 Profile 位置

import os
import 
import datetime
import undetected_chromedriver as uc

MAX_RUNTIME_SECONDS = 240
# Check for and kill any Chrome processes that have been running for too long
for proc in psutil.process_iter():
    try:
        if "chrome" in proc.name().lower():
            create_time = proc.create_time()
            elapsed_time = time.time() - create_time
            if elapsed_time > MAX_RUNTIME_SECONDS:
                proc.kill()
                print("killed" + proc.name())
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
        pass

script_dir = os.path.dirname(os.path.realpath(__file__))
profile_dir = os.path.join(script_dir, "chrome_profile")
path_to_chrome_binary = os.path.abspath(
    script_dir + "/../../" + "chrome-linux64/chrome"
)
driver_executable_path = os.path.abspath(
    script_dir + "/../../" + "chromedriver-linux64/chromedriver"
)
proxy = "socks5://127.0.0.1:8080"

options = uc.ChromeOptions()
options.add_argument("--disable-notifications")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-save-password-bubble")

# Limit CPU and memory usage
options.add_argument("--disable-software-rasterizer")
options.add_argument("--disable-extensions")
# options.add_argument("--disable-webgl")

options.add_argument(proxy)
options.binary_location = path_to_chrome_binary

# if os.getenv("DEBUG") != "True":
#     options.add_argument("headless=True")

prefs = {
    "credentials_enable_service": False,
    "profile.password_manager_enabled": False,
    "profile.privacy_sandbox_prompt_enabled": False,
    # "profile.managed_default_content_settings.images": 1,
}
options.add_experimental_option("prefs", prefs)

try:
    driver = uc.Chrome(
        executable_path=driver_executable_path,
        browser_executable_path=path_to_chrome_binary,
        options=options,
        version_main=119,
        user_data_dir=profile_dir,
        # use_subprocess=False,
    )
except Exception as e:
    return None


driver.set_window_size(1366, 768)
driver.set_page_load_timeout(30)

#fix timeout bug
try:
    driver.get(url)
except Exception as e:
    try:
        #send Esc key to stop loading
        driver.find_element(By.XPATH, '//body').send_keys(Keys.ESCAPE)
        time.sleep(1)
        # check page loaded
        WebDriverWait(driver, 10).until(
                EC.visibility_of_element_located(
                    (By.XPATH, "//dl[@class='dfn']")
                )
            )
    except Exception as e:
        #take a screen shot
        png_name = datetime.datetime.now().isoformat()[:19]
        driver.save_screenshot(png_name + "_load" + ".png")
        return None
分類
陰陽怪氣

20231011

每根可用的救命稻草都被抓住、彎曲和扭曲,以適合作者的目的。細枝末節的證據被推到了合理的限度之外;相互矛盾的數據被忽視或遺漏,文章中充斥着不嚴謹的陳述。
分類
音乐

中秋快樂

分享兩個音樂電臺,祝大家中秋快樂。

第一個是來自德國的電子樂電臺 Hirschmilch Electronic:https://hirschmilch.de/electronic/listen.plshttp://www.surfmusik.de/m3u/hirschmilch-electronic,17335.m3u

第二個是馬來西亞的泰米尔音乐電臺 JeiFM:https://usa3.fastcast4u.com/proxy/jeifm?mp=/1

分類
說說

20230926

404 is a pain for many people on the Internet. But 40.4°C is the best temperature to take a bath.

分類
說說

230829

我現在住到的地方可以買到本地產的青皮蕉,酸酸甜甜非常好吃又便宜。但是香蕉皮放在垃圾桶裏,不用半天就會引來小果蠅。雖然我家裝有沙窗,但是果蠅太小,可以穿網而過。小陽臺的角落裏放着我家的小小堆肥箱,底部是一些土,上面用來放咖啡渣和茶葉渣。最近從山上移栽了一棵艾草回來,不僅順利成活並且長勢喜人。

昨晚聽到壁虎叫的很大聲,今天早上一看,一個可愛的小壁虎從堆肥箱後面探出頭來吃果蠅。前段時間廚房也出現了蜘蛛了來吃果蠅,但是蜘蛛會在牆角留下一些一毫米左右的小黑屎,增加了 Emanon 的家務。