记录一个近期困扰了一些时间的问题。
我很喜欢在爬虫中遇到问题,因为这意味着在这个看似简单的事情里还是有很多值得去探索的新东西。其实本身爬虫也是随着前后端技术的不断更新在进步的。
本文假定你已经拥有基于ChromeDriver的Selenium脚本开发经验以及基础的前端知识。
长文本输入阻塞的问题是Selenium老大难问题,其原因是Selenium的send_keys()
函数在输入字符串时,会将字符串分解为每个字符进行处理,具体可见源码(keys_to_typing
函数for
循环里最后一个else
分支):
def send_keys(self, *keys_to_send):
"""
Sends keys to current focused element.
:Args:
- keys_to_send: The keys to send. Modifier keys constants can be found in the
'Keys' class.
"""
typing = keys_to_typing(keys_to_send)
if self._driver.w3c:
for key in typing:
self.key_down(key)
self.key_up(key)
else:
self._actions.append(lambda: self._driver.execute(
Command.SEND_KEYS_TO_ACTIVE_ELEMENT, {'value': typing}))
return self
def keys_to_typing(value):
"""Processes the values that will be typed in the element."""
typing = []
for val in value:
if isinstance(val, Keys):
typing.append(val)
elif isinstance(val, int):
val = str(val)
for i in range(len(val)):
typing.append(val[i])
else:
for i in range(len(val)):
typing.append(val[i])
return typing
这显然是挺笨重的操作,因为我们正常将长文本复制到文本框中并不需要花费很长时间,但是用
driver.find_element(By.xxx, xxx).send_keys(...)
逐字输入就很容易使得页面阻塞,这里有个隐藏的问题是输入换行符\n
有时会触发文本框内容的提交,这个问题相对容易解决,因为是闭合标签,输入的文本内容本质上是在
中,因此将
send_keys(...)
中的换行符\n
替换成
即可避免。
题外话,源码为什么要这样写一定会有它的道理。仔细想想也不难理解,keys_to_typing
函数的参数value
并不总是字符串,也可能是键盘的某个键位,比如常用的我们通过send_keys(Key.CONTROL, 'v')
实现向输入框中粘贴文本,因此需要分解成各个字符处理。
图1 智谱清言官网👇👇👇
本文以智谱清言(ChatGLM)官网的标签的文本框输入为例(你也可以在任何一个带有
文本框的网站进行测试),直接向里面粘贴长文本是非常容易的,但是通过浏览器驱动输入长文本则很容易造成阻塞。
下面的代码是基于Chrome浏览器驱动的测试脚本:
需要配置用户数据路径(initialize_chrome_driver
函数中的chrome_user_data_path
变量),以跳过智谱清言的登录。
目前访问智谱清言官网还有一个问题,就是会在页面加载上卡很久(有一层Loading…的mask层使得页面元素完全无法交互,应该是在验证登录),此时页面标签和元素已经加载出来,因此不能通过简单的element.is_display
来确定页面是否可用。这个状态下可以用Selenium向文本框输入文字,但是无法点击提交按钮,可能可以通过selenium.webdriver.support.expected_condition.element_to_be_clickable
函数判断元素是否可交互来确定页面是否完全加载。
这个问题不展开讨论,一般来说很少彻底卡死(报错信息是element not interactable
)。
# -*- coding: utf-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn
import os
import time
import logging
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.action_chains import ActionChains
def initialize_chrome_driver(headless, timeout):
chrome_user_data_path = r"C:\Users\caoyang\AppData\Local\Google\Chrome\User Data"
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument(f"user-data-dir={chrome_user_data_path}") # Import user data
if headless:
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(chrome_options=chrome_options)
driver.set_page_load_timeout(timeout)
if not headless:
driver.maximize_window()
return driver
def check_element_by_xpath(driver, xpath, timeout):
WebDriverWait(driver, timeout).until(lambda _driver: _driver.find_element_by_xpath(xpath).is_displayed())
# Regular-used XPaths
layout_xpaths = {"input-box" : "//textarea[@class=\"scroll-display-none\"]", # XPath of the input box for human
"send-button-1" : "//img[@class=\"enter_icon\"]", # XPath of the button to send text of input box
"send-button-2" : "//div[@class=\"enter\"]", # XPath of the button to send text of input box
"chat-area" : "//div[@id=\"session-container\"]", # XPath of the chat area which contains all the talks (consist of several chat boxes)
"human-box" : "//div[@class=\"pr\"]", # XPath of human chat box
"ai-box" : "//div[@class=\"answer-content-wrap\"]", # XPath of AI chat box
"ai-box-text" : "//div[@class=\"markdown-body\"]", # XPath of the text contained in AI chat box
"create-new-button" : "//div[@class=\"new-session-button\"]", # XPath of create new talk
"like-or-dislike-area" : "//div[@class=\"interact-operate\"]", # XPath of div tag contains like and dislike icons
"delete-session" : "//span[@class=\"btn delete\"]", # XPath of button to delete old talk
}
# Initialize
driver = initialize_chrome_driver(headless=False, timeout=60)
driver.get("https://chatglm.cn/main/detail")
check_element_by_xpath(driver, xpath=layout_xpaths["input-box"], timeout=60) # Check if input box is rendered
check_element_by_xpath(driver, xpath=layout_xpaths["send-button-1"], timeout=60) # Check if send button is rendered
check_element_by_xpath(driver, xpath=layout_xpaths["send-button-2"], timeout=60) # Check if send button is rendered
# Delete old talk
try:
driver.find_element_by_xpath(self.layout_xpaths["delete-session"]).click()
logging.info("Delete old talk ...")
except:
logging.info("No old talk found ...")
# Request
logging.info("Prompt ...")
prompt = """这是一段很长的文本!""" # e.g. prompt = 'x' * 8000
## Method 1: Use `element.send_keys`
driver.find_element_by_xpath(layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
在上述代码中,prompt
字段过长容易导致send_keys(prompt)
造成阻塞,以至于影响后续的代码逻辑。
ChromeDriver无法输入非BMP字符(据说Geckodriver是可行的),具体报错信息为:
selenium.common.exceptions.WebDriverException: Message: unknown error: ChromeDriver only supports characters in the BMP
这个问题就比上面的长文本阻塞要麻烦多了,前者只是需要等待一段时间就能输入完,但这个问题是真的绕不过去。
好在我们有万能的Javascript😋:
# Request
logging.info("Prompt ...")
prompt = '😋'
# ## Method 1: Use `element.send_keys`
# driver.find_element_by_xpath(layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
## Method 2: Use Javascript with one auguments (Fail)
js = """var txt = arguments[0]; document.getElementsByTagName("textarea")[0].value = txt;"""
driver.execute_script(js, prompt)
logging.info(" - Use Javascript to input ...")
注意,这里Selenium不能直接用JQuery的语法使用$("textarea")
来定位元素,因为页面上只有一个标签。
图2 使用Javascript输入Emoji👇👇👇(大功告成,我们成功地把😋输入到文本框中啦!)
问题结束了吗?
然而,问题才刚刚开始,在图2中,只要你点击提交按钮,就会发现页面提示发送内容不能为空!,甚至只要将鼠标移动到文本框中,😋就会消失。
这个问题确实很让人苦恼,笔者搜索很多关于Javascript修改textarea内容不生效的文章,众说纷纭,但是没有一个起作用的。事实上不止是修改标签的
value
,包括innerText
,innerHTML
,textContent
都是不起作用的:
图3 测试textarea的value, innerText, innerHTML属性的修改👇👇👇
控制台的输出信息会发现,单纯修改value
,确实可以看到修改的文本出现在界面上,但是实际HTML中并不会显示,点击提交按钮($(".enter_icon").click()
)也无法发送内容。
如果修改innerText
或innerHTML
,HTML中确实出现了值,修改的文本也出现在界面上,但是仍然无法提交,且点击提交按钮后,界面上不再显示文本,但HTML中依然可以看到Hello
某些回答指出,需要进行dispatchEvent
,从最后的正解来看,这种回答说对了一半,如下面的代码所示,通过加入elm.dispatchEvent(new Event('change'));
,似乎很有道理,但是实际上依然无法进行提交。
# Request
logging.info("Prompt ...")
prompt = 'Hello!'
# ## Method 1: Use `element.send_keys`
# driver.find_element_by_xpath(layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
# ## Method 2: Use Javascript with one auguments (Fail)
# js = """var txt = arguments[0]; document.getElementsByTagName("textarea")[0].value = txt;"""
# driver.execute_script(js, prompt)
# logging.info(" - Use Javascript to input ...")
# Method 3: Use Javascript with event dispatch (Fail)
js = """var elm = arguments[0], txt = arguments[1]; elm.value += txt; elm.dispatchEvent(new Event('change'));"""
element = driver.find_element_by_xpath(self.layout_xpaths["input-box"])
driver.execute_script(js, element, prompt)
logging.info(" - Use Javascript to input ...")
真是糟透了,难道真的没有人遇到跟我一样的问题吗?
在控制台测了一遍又一遍后,被这离奇消失的文本折磨得实在是无能为力,我决定还是用最笨得方法来解决:
pip install pyperclip
进行安装)send_keys
方法在
文本框中进行粘贴文本效果拔群!事实证明,能抓到猫的就是好老鼠,笨方法一下子就解决了这个该死的问题!
# Request
logging.info("Prompt ...")
prompt = 'Hello!'
# ## Method 1: Use `element.send_keys`
# driver.find_element_by_xpath(layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
# ## Method 2: Use Javascript with one auguments (Fail)
# js = """var txt = arguments[0]; document.getElementsByTagName("textarea")[0].value = txt;"""
# driver.execute_script(js, prompt)
# logging.info(" - Use Javascript to input ...")
# # Method 3: Use Javascript with event dispatch (Fail)
# js = """var elm = arguments[0], txt = arguments[1]; elm.value += txt; elm.dispatchEvent(new Event('change'));"""
# element = driver.find_element_by_xpath(self.layout_xpaths["input-box"])
# driver.execute_script(js, element, prompt)
# logging.info(" - Use Javascript to input ...")
# Method 4: Use keyboard operation (Success)
import pyperclip
pyperclip.copy(prompt)
time.sleep(1)
driver.find_element_by_xpath(self.layout_xpaths["input-box"]).send_keys(Keys.CONTROL, 'v')
logging.info(" - Use keyboard to input ...")
但是我们总归还是要解决这个问题的,不能因为走了歪门邪道就自鸣得意。其实很容易能想到,之所以无法提交Javascript修改的文本框内容,肯定是提交按钮没有绑定到修改的内容,仍然是文本框原本的默认值,这当然需要定义事件进行发送。这里我也搜索到一些人发现无法使用Selenium清空
文本框的内容,但是他们最后还是向用
send_keys(Keys.BACK_SPACE)
的键盘操作方法进行妥协。
最后是在StackFlow上找到了这个问题的正解:https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-change-or-input-event-in-react-js
For React 16 and React >=15.6
- Setter .value= is not working as we wanted because React library overrides input value setter but we can call the function directly on the input as context.
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; nativeInputValueSetter.call(input, 'react 16 value'); var ev2 = new Event('input', { bubbles: true}); input.dispatchEvent(ev2);
- 1
- 2
- 3
- 4
- For textarea element you should use prototype of HTMLTextAreaElement class.
这是涉及React框架的知识,可是我再也不会回去学习前端了。
最后的正解代码应该是:
# Request
logging.info("Prompt ...")
prompt = 'Hello!'
# ## Method 1: Use `element.send_keys`
# driver.find_element_by_xpath(layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
# ## Method 2: Use Javascript with one auguments (Fail)
# js = """var txt = arguments[0]; document.getElementsByTagName("textarea")[0].value = txt;"""
# driver.execute_script(js, prompt)
# logging.info(" - Use Javascript to input ...")
# # Method 3: Use Javascript with event dispatch (Fail)
# js = """var elm = arguments[0], txt = arguments[1]; elm.value += txt; elm.dispatchEvent(new Event('change'));"""
# element = driver.find_element_by_xpath(self.layout_xpaths["input-box"])
# driver.execute_script(js, element, prompt)
# logging.info(" - Use Javascript to input ...")
# # Method 4: Use keyboard operation (Success)
# import pyperclip
# pyperclip.copy(prompt)
# time.sleep(1)
# driver.find_element_by_xpath(self.layout_xpaths["input-box"]).send_keys(Keys.CONTROL, 'v')
# logging.info(" - Use keyboard to input ...")
# Method 5: Use Javascript with DispatchEvent (Success)
js = """var txt = arguments[0];
const textarea = $("textarea");
var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeTextAreaValueSetter.call(textarea, txt);
const event = new Event("input", {bubbles: true});
textarea.dispatchEvent(event);"""
driver.execute_script(js, prompt)
logging.info(" - Use Javascript to input ...")
感谢您阅读到这里,作为本文的结束,附上完整的代码:
# -*- coding: utf-8 -*-
# @author: caoyang
# @email: caoyang@163.sufe.edu.cn
import os
import re
import time
import logging
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
class BaseCrawler:
tag_regex = re.compile(r"<[^>]+>|\n|\t")
global_timeout = 60
global_interval = 300
chrome_user_data_path = r"C:\Users\caoyang\AppData\Local\Google\Chrome\User Data"
# Convert request headers copied from Firefox to dictionary
@classmethod
def headers_to_dict(cls, headers: str) -> dict:
lines = headers.splitlines()
headers_dict = {}
for line in lines:
key, value = line.strip().split(':', 1)
headers_dict[key.strip()] = value.strip()
return headers_dict
# Easy use of WebDriverWait
@classmethod
def check_element_by_xpath(cls, driver, xpath, timeout=30):
WebDriverWait(driver, timeout).until(lambda _driver: _driver.find_element_by_xpath(xpath).is_displayed())
# @param method: e.g. GET, POST
def easy_requests(self, method, url, **kwargs):
while True:
try:
response = requests.request(method, url, **kwargs)
break
except Exception as e:
logging.warning(f"Error {method} {url}, exception information: {e}")
logging.warning(f"Wait for {self.global_interval} seconds ...")
time.sleep(self.global_interval)
return response
# Initialize driver
def initialize_driver(self, browser="chrome", headless=True, timeout=60, **kwargs):
browser = browser.lower()
assert browser in ["chrome", "firefox"], f"Unknown browser name: {browser}"
return eval(f"self._initialize_{browser}_driver")(headless, timeout, **kwargs)
# Initialize Google Chrome driver
def _initialize_chrome_driver(self, headless, timeout, **kwargs):
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument(f"user-data-dir={self.chrome_user_data_path}") # Import user data
if headless:
chrome_options.add_argument("--headless")
driver = webdriver.Chrome(chrome_options=chrome_options)
driver.set_page_load_timeout(timeout)
if not headless:
driver.maximize_window()
return driver
# Initialize Mozilla Firefox driver
def _initialize_firefox_driver(self, headless, timeout, **kwargs):
options = webdriver.FirefoxOptions()
if headless:
options.add_argument("--headless")
driver = webdriver.Firefox(options=options)
driver.set_page_load_timeout(timeout)
if not headless:
driver.maximize_window()
return driver
# Get cookies by driver
def get_cookies(self, url, driver=None, browser="chrome"):
quit_flag = False
if driver is None:
# If there is no driver passed
quit_flag = True
driver = self.initialize_driver(browser=browser, headless=True, timeout=30)
driver.get(url)
cookies = driver.get_cookies()
def _cookie_to_string(_cookies):
_string = str()
for _cookie in _cookies:
_name = _cookie["name"]
_value = _cookie["value"].replace(' ', '%20') # %20 refers to space char in HTML
_string += f"{_name}={_value};"
return _string.strip()
if quit_flag:
driver.quit()
return _cookie_to_string(cookies)
class ChatGLMCrawler(BaseCrawler):
urls = {"home": "https://chatglm.cn/main/detail", # Home URL
}
layout_xpaths = {"input-box" : "//textarea[@class=\"scroll-display-none\"]", # XPath of the input box for human
# "input-box" : "//div[@class=\"input-box-inner\"]", # XPath of the input box for human (div tag cannot be interacted)
"send-button-1" : "//img[@class=\"enter_icon\"]", # XPath of the button to send text of input box
"send-button-2" : "//div[@class=\"enter\"]", # XPath of the button to send text of input box
"chat-area" : "//div[@id=\"session-container\"]", # XPath of the chat area which contains all the talks (consist of several chat boxes)
"human-box" : "//div[@class=\"pr\"]", # XPath of human chat box
"ai-box" : "//div[@class=\"answer-content-wrap\"]", # XPath of AI chat box
"ai-box-text" : "//div[@class=\"markdown-body\"]", # XPath of the text contained in AI chat box
"create-new-button" : "//div[@class=\"new-session-button\"]", # XPath of create new talk
"like-or-dislike-area" : "//div[@class=\"interact-operate\"]", # XPath of div tag contains like and dislike icons
"delete-session" : "//span[@class=\"btn delete\"]", # XPath of button to delete old talk
}
forbidden_strings = []
def __init__(self):
super(ChatGLMCrawler, self).__init__()
# @param driver : WebDriver object
# @param prompt : The question you would like to ask AI
# @param model_name : One of the key in `model_card_xpath`, e.g. "chatgpt3.5(16k)"
def request(self, driver, prompt, first_trial=True):
prompt = prompt.replace('\n', "\\n")
if first_trial:
driver.get(self.urls["home"])
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["input-box"], timeout=60) # Check if input box is rendered
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["send-button-1"], timeout=60) # Check if send button is rendered
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["send-button-2"], timeout=60) # Check if send button is rendered
# Delete old talk
try:
driver.find_element_by_xpath(self.layout_xpaths["delete-session"]).click()
logging.info("Delete old talk ...")
except:
logging.info("No old talk found ...")
# Request
logging.info("Prompt ...")
try:
## Method 1: Use `element.send_keys`
driver.find_element_by_xpath(self.layout_xpaths["input-box"]).send_keys(prompt) # Input the given prompt
logging.info(" - ok!")
except:
# ## Method 2: Use Javascript with one auguments (Fail)
# js = """var txt = arguments[0]; document.getElementsByTagName("textarea")[0].value = txt;"""
# driver.execute_script(js, prompt)
# logging.info(" - Use Javascript to input ...")
# # Method 3: Use Javascript with event dispatch (Fail)
# js = """var elm = arguments[0], txt = arguments[1]; elm.value += txt; elm.dispatchEvent(new Event('change'));"""
# element = driver.find_element_by_xpath(self.layout_xpaths["input-box"])
# driver.execute_script(js, element, prompt)
# logging.info(" - Use Javascript to input ...")
# # Method 4: Use keyboard operation (Success)
# import pyperclip
# pyperclip.copy(prompt)
# time.sleep(1)
# driver.find_element_by_xpath(self.layout_xpaths["input-box"]).send_keys(Keys.CONTROL, 'v')
# logging.info(" - Use keyboard to input ...")
# Method 5: Use Javascript with DispatchEvent (Success)
js = """var txt = arguments[0];
const textarea = $("textarea");
var nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
nativeTextAreaValueSetter.call(textarea, txt);
const event = new Event("input", {bubbles: true});
textarea.dispatchEvent(event);"""
driver.execute_script(js, prompt)
logging.info(" - Use Javascript to input ...")
while True:
# The button is dynamic and sometimes fail to click on
try:
driver.find_element_by_xpath(self.layout_xpaths["send-button-1"]).click() # Click on the button to send the prompt
logging.info("Use send button 1 ...")
break
except:
try:
driver.find_element_by_xpath(self.layout_xpaths["send-button-2"]).click() # Click on the button to send the prompt
logging.info("Use send button 2 ...")
break
except:
logging.info("Use send button error ...")
raise Exception("Use send button error ...")
# Wait for response
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["chat-area"], timeout=30) # Check if chat area is rendered
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["human-box"], timeout=30) # Check if human chat box is rendered
self.check_element_by_xpath(driver, xpath=self.layout_xpaths["ai-box"], timeout=30) # Check if AI chat box is rendered
finish_flag = True # Indicating if AI generation is finished
while finish_flag:
try:
# If like or dislike appear, then stop
driver.find_element_by_xpath(self.layout_xpaths["like-or-dislike-area"])
finish_flag = False
except:
ai_box_text = driver.find_element_by_xpath(self.layout_xpaths["ai-box-text"]) # Find AI response text element
# ai_box_text = driver.find_element_by_xpath(self.layout_xpaths["ai-box"]) # Find AI response text element
ai_box_text_inner_html = ai_box_text.get_attribute("innerHTML") # Get inner HTML of the element
response = self.tag_regex.sub(str(), ai_box_text_inner_html).strip("\n\t ").replace('\n', '\\n') # Process response text
forbidden_flags = [forbidden_string in response for forbidden_string in self.forbidden_strings]
if sum(forbidden_flags) > 0:
# It indicates that a forbidden string occurs
finish_flag = False
# Extract AI response text
ai_box_text = driver.find_element_by_xpath(self.layout_xpaths["ai-box-text"]) # Find AI response text element
ai_box_text_inner_html = ai_box_text.get_attribute("innerHTML") # Get inner HTML of the element
response = self.tag_regex.sub(str(), ai_box_text_inner_html).strip("\n\t ") # Process response text
return response
# @param data_path: EXCEL file of job descriptions
# @param save_path: file path for storing AI response
# @param model_name: defined in model_card_xpaths
def demo(self, model_name="chatgpt3.5(16k)"):
driver = self.initialize_driver(browser="chrome", headless=False, timeout=60)
driver.implicitly_wait(15) #
prompt = "给我讲述一下《基督山伯爵》的故事,500字左右。"
response = self.request(driver, prompt)
with open(f"d:/answer-chatglm.txt", 'w', encoding="utf8") as f:
f.write(response)
time.sleep(5)
driver.quit()
if __name__ == "__main__":
crawler = ChatGLMCrawler()
crawler.demo()
图4 脚本测试截图👇👇👇
昨天S消防演习,人在18楼,走到10楼就堵得下不去了,宝玺姐根本就没有下楼,一直呆在工位上,我回来后说如果S真的失火,应该会有直升机来营救,我们应该往上面天台跑而不是往楼下逃生。宝玺姐非常认真地跟我说,S如果失火,我们肯定都是死路一条,有一种高知女性特有的决绝感。我不知道SXY是否也是如此,其实我现在离TA很近,可是又离TA很远。
在这次高校百英里接力赛,摸到10km路跑40分钟的大门之后,我坚定了一个信念,人可以办成任何事情,人也不要办成每一件事情。十年前,甚至五年、三年前我都不敢想象有一天能把10km跑进40分钟,但现在确确实实做到了,即便是在这样奔波的生活中。未来我也一定有机会全马破三、完成铁三越野。
现在我觉得,人生是一个等待和追求的过程,迷茫的时候可以静静地等待,明确的时候应当无畏地追求,一切安顿时就该好好休息。我看到不同的人、不同的事、不同的生活、不同的态度,我觉得平等的交流是最难能可贵的事,让每个认知水平不同的人,即便是在一个相对小的群体里,都能够发声,也敢于发声,让每一种声音都有它的容身之处。
我们总是有一种莫名的主观的客观视角,总会觉得有一些自己认为理所当然的事情,别人都应当如此,然而并不其然。当我看到35岁的吉米和乔总吃饭时还在讨论《咒术回战》的五条悟、《进击的巨人》的地鸣,听到儿子都已经10多岁的慧悦姐称自己在工作中很闷骚时,觉得很不可思议。跟宝玺姐一起工作的三个月里,也看到她身上很多看似矛盾、但细想又很合理的事情。人类就是这样多态的实例化对象,也注定是多变的非常量实体。
每次写到这里,都会想起很多遗憾。岁数越长,越觉得人生是很奇特的历程,你永远不知道盒子里下一块巧克力是什么颜色,你甚至无法知晓盒子里到底是不是一块巧克力,或许是根棒棒糖也说不定,就像曾经错过的人和事,在将来的某个节点是否会再次相遇?
罢了。