其实也不爬虫只是ctrl+s了一个html然后调库解析而已想写async还失败了
莫提威甚 学习Spring Boot 中,打开了其 Api Doc. , 看到大片的英文懒得思考(讲道理智力正常的人类是不是也不会直接看 Api Doc. ) 点击 chrome 自带的页面翻译依旧宕机,打开F12看到页面规整得要命(至少我想爬的东西还挺规则),一看又是纯html,于是启动py准备自己写脚本机翻。 > 初见端倪
么梭多洛基 解析页面 前世记忆之xpath 首先反正只有一个页面于是连 requests 都懒, Ctrl+S 存下来直接开造。 启动F12后观察到这些表格元素的规律:
<div class ="summary-table two-column-summary" > <div class ="table-header col-first" > Package</div > <div class ="table-header col-last" > Description</div > <div class ="col-first even-row-color all-packages-table all-packages-table-tab1" > <a href ="org/springframework/boot/package-summary.html" > org.springframework.boot</a > </div > <div class ="col-last even-row-color all-packages-table all-packages-table-tab1" > <div class ="block" > Core Spring Boot classes.</div > </div > ... </div >
哦哦哦class写得都很有规律不赖,但是其实我完全忘了py怎么解析html,紧急询问了鲸爷deepseek 。 > 警告一次 好的于是uv add beautifulsoup4 lxml。
继续观察到鲸爷说:
鲸爷: ✅ 1. BeautifulSoup(最主流)
适合:大多数爬虫、页面解析、数据提取
✅ 优点:易学、容错强 ❌ 缺点:速度一般(但够用)
✅ 2. lxml + XPath(速度快、语法强)
适合:复杂结构、高性能解析
✅ 优点:快、表达能力强 ❌ 缺点:XPath 学习成本稍高
突然前世记忆占领大脑,想起来其实xpath更好用点,毕竟最轮椅的是在F12里右键html可以直接copy xpath还挺方便,虽然一般直接copy的是根目录开始的xpath也就纯对单,不过至少不需要大脑。 但是这次要扒同一个页面的这么多东西肯定不用copy xpath,那么找规律启动! > 警告两次
笑点+2:class 标签 + div 套娃 不难观察到其实F12里有一大片col-first ...table-tab1和col-last ...table-tab1,于是有:
tree = etree.HTML(html) col0_class = "col-first even-row-color all-packages-table all-packages-table-tab1" col1_class = "col-last even-row-color all-packages-table all-packages-table-tab1" text_col0 = tree.xpath(f'//div[@class="{col0_class} "]/a/text()' ) text_col1 = tree.xpath(f'//div[@class="{col1_class} "]/div/text()' ) df = pd.DataFrame( { "Package" : text_col0, "Description" : text_col1, } ) df.to_csv("./test01.csv" )
不出意外也是df哈气报ValueError: All arrays must be of the same length了,嗐,html嘛,数据脏点很正常。 看看len:
2026-05-28 09:43:04 [INFO] => Done text_col0, len: 251 2026-05-28 09:43:04 [INFO] => Done text_col1, len: 274
> 这时候甚至还没意识到数字太小 > 哦对了关于logging配置这一块:
logging.basicConfig( level=logging.INFO, format ="%(asctime)s [%(levelname)s] %(message)s" , datefmt="%Y-%m-%d %H:%M:%S" , )
对不上那就先可视化printf 看看从哪里对不上:
max_len = max (len (text_col0), len (text_col1)) max_str_len_col_0 = max (map (len , text_col0)) max_str_len_col_1 = max (map (len , text_col1)) logger.info(f"=> max_str_len_col_0: {max_str_len_col_0} " ) logger.info(f"=> max_str_len_col_1: {max_str_len_col_1} " ) with open ("./test01.txt" , "w" , encoding="utf8" ) as fp: for i in range (max_len): col0_text = text_col0[i] if i < len (text_col0) else "<none>" col1_text = text_col1[i] if i < len (text_col1) else "<none>" fp.write(f"{col0_text:<{max_str_len_col_0 + 2 } }:={col1_text} \n" )
观察test01.txt中,发现一些 Description 的内容有点破碎; 回头查看页面发现 Description 里可能嵌套一些行内代码格式导致div下还有div,于是 xpath 的text()取的稀里糊涂。
但是查看页面时发现不对,为什么test01.txt感觉缺了好多行,再查看 F12 :
<div class ="col-first even-row-color all-packages-table all-packages-table-tab1" > ...</div > <div class ="col-last even-row-color all-packages-table all-packages-table-tab1" > ...</div > <div class ="col-first odd-row-color all-packages-table all-packages-table-tab1" > ...</div > <div class ="col-last odd-row-color all-packages-table all-packages-table-tab1" > ...</div >
。。。
<div class ="... even-row-color ..." > ...</div > <div class ="... odd-row-color ..." > ...</div >
> 猪!!!!! 啊哈时刻
🐋: contains class 好吧那还是得看看 class 里有什么的,显然好像选择 col-first 和 col-last 比较好,于是:
text_col0 = tree.xpath(f'//div[@class="col-first"]/a/text()' ) text_col1 = tree.xpath(f'//div[@class="col-last"]/div/text()' )
。。。
2026-05-28 09:53:59 [INFO] => Done text_col0, len: 0 2026-05-28 09:53:59 [INFO] => Done text_col1, len: 0
鲸爷救我——
text_col0 = tree.xpath(f'//div[contains(@class, "col-first")]/a/text()' ) text_col1 = tree.xpath(f'//div[contains(@class, "col-last")]/div/text()' )
也是终于把偶数行也爬出来了,但是那个问题还没解决:
死亡回放: 回头查看页面发现Description里可能嵌套一些行内代码格式导致div下还有div,于是xpath的text()取的稀里糊涂。
🐋: string(.) 依旧鲸爷神力:
text_col1 = tree.xpath(f'//div[contains(@class, "{col1_class} ")]' ) text_col1 = [x.xpath("string(.)" ).replace("\n" , " " ) for x in text_col1[1 :]]
鲸爷: 📌 string(.) 的含义:
.:当前节点
string():递归拼接该节点下 所有文本节点
半场香槟 ok啊,现在输出也是非常地干净整洁心旷神怡:
org.springframework.boot := Core Spring Boot classes. org.springframework.boot.activemq.autoconfigure := Auto-configuration for ActiveMQ. ...
速度也挺快的其实完全1s内。
2026-05-28 10:14:21 [INFO] => Done read 2026-05-28 10:14:21 [INFO] => Done etree 2026-05-28 10:14:21 [INFO] => Done text_col0, len: 502 2026-05-28 10:14:21 [INFO] => Done text_col1, len: 502 2026-05-28 10:14:21 [INFO] => max_str_len_col_0: 83 2026-05-28 10:14:21 [INFO] => max_str_len_col_1: 122 2026-05-28 10:14:21 [INFO] => Done save
那么解析页面就结束了
翻译 🐋: translators 翻译感觉暂时还不是我本地小破机能跑的东西,直接怼鲸爷api里做翻译也挺诡异的,于是问鲸爷。 > ? 好的于是问到了:
鲸爷:
1. translators ⭐推荐 封装 Google、Bing、百度、有道、DeepL、Yandex 等 30+ 引擎,统一接口。
pip install translators --upgrade
import translators as tsresult = ts.translate_text("Hello World" , to_language='zh' , translator='bing' ) print (result)
✅ 零配置、支持切引擎对比
⚠️ 国内访问 Google 需代理,建议用 translator='bing' 或 'baidu'
不赖,uv add translators,顺手装个tqdm看看速度。
2%|██ | 10/502 [00:15<12:55, 1.58s/it]
!?这么慢?!
(SPOILED WARNING) 烂尾的asynico 于是鬼脑发动,都 web api 了不难想到 asynico ,那么我是否要抓住此生仅有 (> ¿) 的机会学习一下神秘的异步。 其实是发现了:
def translate_text (*args, **kwargs ): if kwargs.get('if_use_async' , False ): return translate_text_with_async(*args, **kwargs) return translate_text_with_sync(*args, **kwargs)
它都做 async 了我岂能不用?
首先咨询鲸爷:
translators 本身是同步库 ,不支持真正的 async/await,但可以通过 多线程 / 多进程 / 协程包装 来实现“异步并发翻译多个文本”。下面按推荐程度 给你几种实战可用的写法。 。。。不信
但是鲸爷给的第一个方案还没怎么听说过,虽然今天完全没有用到但是总之先拉一坨在这里:
✅ 推荐方案一:ThreadPoolExecutor(最简单 & 实用) 适合 I/O 密集(网络请求),对 ts.translate_text 最友好。
import translators as tsfrom concurrent.futures import ThreadPoolExecutor, as_completedtexts = [ "Hello world" , "Machine learning is amazing" , "How are you today?" ] def translate_one (text ): return ts.translate_text( text, to_language='zh' , translator='bing' ) with ThreadPoolExecutor(max_workers=5 ) as executor: futures = [executor.submit(translate_one, t) for t in texts] results = [f.result() for f in as_completed(futures)] for r in results: print (r)
✅ 优点
不阻塞事件循环
代码简单
对 translators 最稳定
⚠️ 不建议 max_workers > 10,否则容易被目标站点限流。
俺寻思异步 总之先俺寻思写了一点 async :
async def get_one_tgt_line (line ): ts_res = ( line[0 ], line[1 ], await ts.translate_text( line[1 ], to_language='zh' , translator='bing' , if_use_async=True ) ) return ts_res async def get_all_tgt_line (): tgt_line_task = [get_one_tgt_line(line) for line in src_line] tgt_line = await asyncio.gather(*tgt_line_task) return tgt_line tgt_line = asyncio.run(get_all_tgt_line()) with open ("./test02.txt" , "w" , encoding="utf8" ) as fp: for line in tgt_line: fp.write(f"{line[0 ]:<85 } :={line[1 ]} / {line[2 ]} " )
再求救鲸爷加了限制同时请求数量:
sem = asyncio.Semaphore(5 ) async def get_one_tgt_line (line ): async with sem: ts_res = ... return ts_res
再再求救鲸爷加了进度播报,以及自己补了个统计单个请求耗时的计时:
class Counter : def __init__ (self ): self._value = 0 self._lock = asyncio.Lock() async def inc (self ): async with self._lock: self._value += 1 return self._value @property def value (self ): return self._value counter = Counter() async def get_one_tgt_line (line ): tic = time() async with sem: ts_res = ... tac = time() done = await counter.inc() print (f"=> {done:>3 } /{len (src_line)} , spent {tac - tic:.2 f} s" ) return ts_res
完 全 战 败 启动!
Unclosed client session client_session: <aiohttp.client.ClientSession object at 0x000002032AB4E900> Unclosed client session client_session: <aiohttp.client.ClientSession object at 0x000002032A861BD0> Unclosed client session client_session: <aiohttp.client.ClientSession object at 0x000002032AB3F390> Unclosed client session client_session: <aiohttp.client.ClientSession object at 0x000002032A860F50> bing aiohttp session closed. => 1/502, spent 2.41 s Traceback (most recent call last): File "...\test02.py", line 57, in <module> tgt_line = asyncio.run(get_all_tgt_line()) ... File "...\test02.py", line 54, in get_all_tgt_line tgt_line = await asyncio.gather(*tgt_line_task) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "...\test02.py", line 42, in get_one_tgt_line await ts.translate_text( ...<2 lines>... ) File "...\.venv\Lib\site-packages\translators\server_async.py", line 6328, in translate_text result = await self.translators_dict[translator](query_text=query_text, from_language=from_language, to_language=to_language, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "...\.venv\Lib\site-packages\translators\server_async.py", line 122, in _wrapper return await func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "...\.venv\Lib\site-packages\translators\server_async.py", line 294, in _wrapper return await func(*args, **{**kwargs, **{'query_text': query_text}}) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "...\.venv\Lib\site-packages\translators\server_async.py", line 1971, in bing_api response = await self.session.post(api_url, headers=self.host_headers, data=payload, timeout=timeout) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ... aiohttp.client_exceptions.ServerDisconnectedError: Server disconnected
。。。好的那我们还是用同步方法吧不就十分钟吗
100%|████████████████████████████████████████████████████████████████████████████████████████████████████| 502/502 [11:10<00:00, 1.34s/it]
战败结算 好的也是跑完了哈看看结果 怎么只有一行?
fp.write(f"{line[0 ]:<85 } :={line[1 ]} / {line[2 ]} " )
。。。
于是急头白脸又写了个脚本把每一行拆开来。 好现在好看多了。
肯克录甚 > 完全猪!!!!!
也不能这么说吧应该还是复习了一下xpath啥的也难得捣鼓一下asynico虽然完全没成功。
但是谁还记得我一开始是来学Spring Boot的