import requests from bs4 import BeautifulSoup # 基本配置 BASE="baike.baidu.com/item/" s=requests.Session() s.trust_env=False def baike(keyword): # 这个函数应该return 什么内容呢,是return content 这个字符串内容还是什么内容? url=BASE+keyword html=s.get(url,timeout=(4,10)).text soup=BeautifulSoup(html,"html.parser") i=2 selector = f'#J-lemma-main-wrapper div.contentWrapper_hFIBc div.lemmaSummary_URFUi.J-summary div:nth-child(2)' while soup.select(selector): content = soup.select(selector) content="".join(_.get_text() for _ in content) print(content) i+=1 selector = f'#J-lemma-main-wrapper div.contentWrapper_hFIBc div.lemmaSummary_URFUi.J-summary div:nth-child({i})' if __name__=="__main__": keyword=input("请输入关键词\n") print(baike(keyword)) 从大佬级别的专业程序员角度点评
ChatGPT 说:
已思考 10s
燎然,这段代码的核心问题不是“能不能跑”,而是职责、鲁棒性和可维护性。从大佬级别的角度,我会这样点评并给出一个“可直接复用”的改造版本。
你现在的主要问题
-
职责不清:
baike()里既抓取又打印,最后还print(baike(...)),但函数本身没有return(默认返回None)。规范做法是:函数负责“返回数据”,IO(打印/保存)放外面。 -
选择器易碎:
lemmaSummary_URFUi这类 class 看起来像哈希/构建时生成,随时可能变。建议用更稳健的选择器(如“starts-with”前缀匹配或语义更强的结构节点),并准备多套 fallback。 -
未做 URL 编码:
keyword含空格/中文没做编码,可能 400/跳转异常。 -
缺少请求头与容错:没有 UA、没有重试、没有 404/重定向处理,极易翻车。
-
数据结构欠清晰:只拼接一大段字符串,不利于后续存储/分析。建议返回结构化字典(标题、url、摘要段落列表、首图、参考文献数……)。
-
脚注与上标:百科摘要常带“上标引用”(
sup)。你现在直接.get_text(),会把它们揉进来或丢掉,建议保留为 [1] 或剔除,二选一可配置。 -
并发与限频(后续扩展):批量查词条时要加速率限制,否则容易被风控。
建议返回什么?
返回结构化数据,而不是打印。最少也要:
-
title: 词条标题 -
url: 最终跳转后的规范 URL -
summary_paras: 摘要段落(list[str]) -
raw_summary: 合并后的摘要(str)
再进阶可加 first_image, infobox, ref_count 等。
一个更专业、可直接用的实现
(保留你思路,但改成“稳 + 可维护”的写法)
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote
from dataclasses import dataclass, asdict
# ---- 基本配置 ----
BASE = "https://baike.baidu.com/item/"
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
s = requests.Session()
s.trust_env = False
s.headers.update(HEADERS)
TIMEOUT = (5, 12) # 连接/读取
@dataclass
class BaikeSummary:
title: str
url: str
summary_paras: list
raw_summary: str
def _extract_summary_paras(soup: BeautifulSoup) -> list[str]:
"""
尝试多套选择器,谁先命中用谁,保证对结构变更有韧性。
"""
# ① 新版:摘要一般在 [class^=lemmaSummary_] 下的 .para
nodes = soup.select("#J-lemma-main-wrapper [class^=lemmaSummary_] .para")
if not nodes:
# ② 旧版/其他皮肤:常见的 .lemma-summary .para
nodes = soup.select("#J-lemma-main-wrapper .lemma-summary .para")
if not nodes:
# ③ 兜底:正文开头几段(可能会混入非摘要段,尽量谨慎)
nodes = soup.select("#J-lemma-main-wrapper .para")[:2]
# 可选:处理上标脚注 <sup>,把 ①删除 或 ②替换为 [n]
for node in nodes:
for sup in node.find_all("sup"):
# 保留为 [n]:例如 <sup>[3]</sup> → [3]
txt = sup.get_text(strip=True)
sup.replace_with(f"{txt}")
# 若想完全删掉,改成 sup.decompose()
paras = [n.get_text(" ", strip=True) for n in nodes if n.get_text(strip=True)]
return paras
def baike(keyword: str) -> BaikeSummary | None:
"""
抓取并返回结构化摘要;失败返回 None。
"""
if not keyword:
return None
url = BASE + quote(keyword, safe="")
try:
resp = s.get(url, timeout=TIMEOUT, allow_redirects=True)
resp.raise_for_status()
except Exception as e:
# 你可在这里用 logging.warning 记录错误
return None
final_url = str(resp.url) # 可能跳转到 /item/xxx/123456
soup = BeautifulSoup(resp.text, "html.parser")
# 标题(多方案)
title = ""
# ① 页面主标题
t1 = soup.select_one("#J-lemma-main-wrapper h1")
if t1 and t1.get_text(strip=True):
title = t1.get_text(strip=True)
# ② 兜底:<title>
if not title and soup.title and soup.title.string:
title = soup.title.string.strip().replace("_百度百科", "")
# 摘要段落
summary_paras = _extract_summary_paras(soup)
raw_summary = "\n".join(summary_paras)
return BaikeSummary(
title=title or keyword,
url=final_url,
summary_paras=summary_paras,
raw_summary=raw_summary,
)
if __name__ == "__main__":
kw = input("请输入关键词:\n").strip()
data = baike(kw)
if not data:
print("抓取失败或未找到。")
else:
print(f"标题:{data.title}")
print(f"URL:{data.url}")
print("摘要:")
print(data.raw_summary)
为什么这样改?
-
数据与表现分离:
baike()只做抓取 + 解析 + 返回数据。打印放到__main__。后续你要保存到 Excel/DB/接口,都很顺手(asdict(data)一把梭)。 -
稳健选择器:先试
#J-lemma-main-wrapper [class^=lemmaSummary_] .para,再回退.lemma-summary .para,再兜底正文前两段。多 fallback 抗变更。 -
正确编码:
quote(keyword)解决中文、空格、特殊字符。 -
处理脚注:可选保留为
[n]或删除,避免把“上标”粘成奇怪字符。 -
更易扩展:要抓 infobox、首图、参考文献数?在同一个
soup上再加几个选择器即可。
进阶建议(等你批量化、工程化时可加)
-
重试+指数退避:对
requests加简单重试(for attempt in range(3)+time.sleep(1<<attempt))。 -
速率限制:批量关键词时
time.sleep(0.8~1.2 随机),避免风控。 -
缓存:同一关键词一天内不重复请求(
sqlite或diskcache)。 -
解析器:对大页面换
lxml解析器(更快更稳):BeautifulSoup(html, "lxml")。 -
单元测试:准备几条关键词样本断言“至少抓到 1 段摘要”,防回归。
-
合法性:尊重站点使用条款与 robots;仅做学习研究用途。
你原代码里“while + nth-child”的问题
-
逻辑能跑,但脆弱:
nth-child强依赖 DOM 结构(有些摘要段落不一定就是div:nth-child(2..n))。一旦中间插了广告/脚注节点就对不上。 -
.select(selector)每次都从同一个soup选,看似 OK,但可读性差,不如直接一次性选中所有段落再遍历。 -
函数未
return,print(baike(keyword))只会打印None。
结论:换成“一次性选中多段,返回 list[str]”的写法,最稳、最清晰。
你先把这个版本跑起来;后面你要不要我再给你加批量关键词 Excel 输入→Excel 输出的版本(带限频与异常表)?我直接给你成品脚本。
您说:
fallback 是什么意思? 这类 class 看起来像哈希/构建时生成,我今天好几次去搜索,都没有变化呀,keyword 含空格/中文没做编码 我试过了中文也可以搜索没问题呀,加空格也不影响搜索结果呀;asdict(data) 这是什么
ChatGPT 说:
已思考 31s
燎然,直说结论 + 例子👇
- fallback 是啥?
fallback = 兜底方案。先用最理想的办法抓;如果失效(结构变了/选不到),就自动换第二、第三方案,保证不至于“抓不到就崩”。
def extract_summary(soup):
# 首选(新皮肤)
nodes = soup.select("#J-lemma-main-wrapper [class^=lemmaSummary_] .para")
if not nodes: # fallback1
nodes = soup.select("#J-lemma-main-wrapper .lemma-summary .para")
if not nodes: # fallback2
nodes = soup.select("#J-lemma-main-wrapper .para")[:2]
return [n.get_text(" ", strip=True) for n in nodes]
2) “看起来像哈希/构建时生成的 class” 为什么我今天没变?
你今天没变,是同一批部署。但这类 class 往往来自 CSS Modules / 构建管线(如 Webpack、Vite) 的编译期随机/短哈希,版本一发就可能改名;还可能有 A/B 实验、按设备差异、灰度发布。
做爬虫要“面向未来破坏”设计——别把解析挂在易变的 class 上,改用更稳的结构/层级/前缀选择器(如 [class^=lemmaSummary_])+ 多套 fallback。
- “中文/空格不编码也能搜”——为啥我仍建议编码?
确实,百度百科对中文和空格很宽容(会重定向/自纠错),但通用健壮性要求你自己编码,理由:
-
可移植:换站就可能 400/404。
-
避免歧义:关键字里若有
? # / & % +等保留字符,不编码就会截断路径或改变语义(A/B会被当成两级路径)。 -
日志与缓存一致性:编码后的 URL 可复现、可比对。
-
代理/网关:有些反代会严格校验。
正确做法:
-
关键字作为路径的一段 →
quote() -
关键字作为查询参数 →
urlencode()from urllib.parse import quote, urlencode
kw = "齿轮 泵/A&B"
路径用 quote(不保留任何字符)
url1 = "site.com/item/" + quote(kw, safe="")
查询用 urlencode(会把空格变成 +,& ? 等也会被转义)
url2 = "site.com/search?" + urlencode({"q": kw})
你在百科上“看起来没问题”,只是该站容错好;写成通用组件、批量跑不同站点时,不编码迟早踩坑。
- asdict(data) 是什么?
dataclasses.asdict():把 dataclass 实例 → 普通 dict,方便打印、存库、转 JSON。
from dataclasses import dataclass, asdict
@dataclass
class BaikeSummary:
title: str
url: str
summary_paras: list[str]
raw_summary: str
data = BaikeSummary("齿轮泵", "https://baike...", ["段1","段2"], "段1\n段2")
print(asdict(data))
# {'title': '齿轮泵', 'url': 'https://baike...', 'summary_paras': ['段1','段2'], 'raw_summary': '段1\n段2'}
给你的实战建议(短平快)
-
选择器:先结构/前缀,再具体 class,最后兜底正文;别把希望全押在类似
lemmaSummary_URFUi上。 -
URL:路径
quote(),查询urlencode(),只编码一次,别双重编码。 -
返回值:函数返回结构化数据(dict/dataclass),打印留给
__main__。 -
监控:打点记录“命中的是哪套选择器”、失败关键词,方便回归和热修。
你把这几条落地,你的爬虫就不再“今天能跑、明天全挂”。需要我把你现有函数改成返回 dataclass + 三层 fallback + 编码的最小可用版,我直接给你可拷贝代码。
您说:
nodes 是什么东西;from urllib.parse import quote, urlencode 是什么
ChatGPT 说:
已思考 12s
燎然,直说要点:
1)nodes 是什么?
在这段里:
nodes = soup.select("#J-lemma-main-wrapper [class^=lemmaSummary_] .para")
-
soup.select(...)返回列表,列表里每一项是一个 BeautifulSoup 的Tag对象(也有人口头叫“DOM 节点/元素”)。 -
所以
nodes是 “若干段落元素”的集合,例如<div class="para">……</div>这些。
你常用的操作:
for node in nodes:
node.name # 标签名,比如 'div'、'p'、'span'
node.attrs # 属性字典,例如 {'class': ['para', 'xx']}
node.get('class', []) # 取属性,class 会是 list
node.get_text(" ", strip=True) # 纯文本
node.select('a') # 在该节点里再按 CSS 选子节点
node.find('sup') # 找第一个子节点 <sup>
相关方法区别:
-
select(css)→ 列表(可能0个、多个) -
select_one(css)→ 单个Tag或 None -
get_text()→ 当前节点及其子孙的纯文本
2)from urllib.parse import quote, urlencode 是什么?
这是从 Python 标准库 urllib.parse 里导入 URL 编码工具函数:
quote() —— 编码“路径片段”
-
作用:把路径中的特殊字符按 URL 规则转成百分号编码(percent-encoding)。
-
典型用法:把 “关键词/文件名/ID” 拼到 URL 路径里。
-
例子:
from urllib.parse import quote kw = "齿轮 泵/A&B" path = quote(kw, safe="") # '齿轮%20泵%2FA%26B' url = "https://site.com/item/" + path解释:空格→
%20,/→%2F,&→%26。safe=""表示不保留任何特殊字符(最稳妥)。
注意:不要把整条 URL 丢进
quote,否则https://里的:、/也会被转义,URL 会坏掉。quote只用在某一段组件上(如“路径的一段”)。
urlencode() —— 编码“查询参数”
-
作用:把字典编码成
key=value&key2=value2形式,并处理转义。 -
典型用法:构造
?q=关键词&page=2这种查询串。 -
例子:
from urllib.parse import urlencode params = {"q": "齿轮 泵/A&B", "page": 2} qs = urlencode(params) # 'q=%E9%BD%BF%E8%BD%AE+%E6%B3%B5%2FA%26B&page=2' url = "https://site.com/search?" + qs解释:
urlencode内部用的是quote_plus,会把空格转成+(在查询字符串中是等价且更常见的写法)。 -
多值参数(同一个 key 多个值):
params = {"tag": ["auto", "parts"], "q": "pump"} qs = urlencode(params, doseq=True) # 'tag=auto&tag=parts&q=pump'
什么时候用哪个?
-
关键字放路径:
BASE + quote(keyword, safe="") -
关键字放查询参数:
BASE + "?" + urlencode({"q": keyword})
一个对比小抄
需求
用哪个
示例
路径里安全拼接中文/空格/斜杠等
quote
/item/ + quote("齿轮 泵/A&B", safe="")
构造 ?key=value 查询串
urlencode
urlencode({"q":"齿轮 泵/A&B","page":1})
查询里空格变成 +
urlencode/quote_plus
q=a+b%2Fc%3F
避免双重编码
——
只编码一次,不要对已含 %xx 的字符串再 quote/urlencode
总结
-
nodes= 一组 Tag 节点(元素)对象;select给你列表,select_one给你单个。 -
quote编路径片段;urlencode编查询参数。百科现在“中文/空格也能搜”只是它容错好,但写通用/可迁移爬虫时,该编码必须编码,这能帮你少踩未来的坑。
你要不要我给你一段“同一关键词既能构造路径 URL,又能构造搜索 URL”的最小示例,里面把 quote 和 urlencode 的差别打印出来,一眼看懂?
您说:
safe="", 什么时候用哪个? 关键字放路径:BASE + quote(keyword, safe="") 关键字放查询参数:BASE + "?" + urlencode({"q": keyword}) 这里什么意思,通俗点解释?
ChatGPT 说:
已思考 17s
燎然,通俗版来了👇
路径 vs 查询(先分清两种“拼网址”的地方)
-
路径(Path):像门牌号
例:https://baike.baidu.com/item/【这里是路径的一段】 -
查询(Query):像问路时补充的条件
例:https://baike.baidu.com/search?word=【这里是查询参数】&from=pc
两者的编码方式不同,所以用的函数也不同:
-
路径用
quote():把“路径的一段”安全地变成 URL 片段 -
查询用
urlencode():把字典{key: value}变成key=value&key2=value2
safe="" 是啥?
quote() 有个 safe 参数=“允许不编码的字符白名单”。
-
Python 默认:
safe="/"(也就是不编码斜杠/) -
我们常用:
safe=""(连斜杠也编码,最稳)
为什么设成空串?
关键词里如果出现 /,默认会被当成目录分隔符,导致路径断裂。
设成 safe="" 后,/ 会被转成 %2F,这样仍是同一个关键词,不会被当成两级目录。
小例子
from urllib.parse import quote, urlencode
kw = "齿轮 泵/A&B?"
# —— PATH(路径片段)——
# 正确:把 kw 当成“同一段路径”
path_part = quote(kw, safe="")
# 结果:'齿轮%20泵%2FA%26B%3F'
url_path = "https://baike.baidu.com/item/" + path_part
# 完整:'https://baike.baidu.com/item/齿轮%20泵%2FA%26B%3F'
# —— QUERY(查询参数)——
qs = urlencode({"word": kw, "from": "pc"})
# 结果:'word=%E9%BD%BF%E8%BD%AE+%E6%B3%B5%2FA%26B%3F&from=pc'
url_query = "https://baike.baidu.com/search?" + qs
# 完整:'https://baike.baidu.com/search?word=...&from=pc'
你会看到:路径里空格编码为
%20,查询里空格多半会变成+(这就是urlencode的行为,标准且正常)。
什么时候用哪个?(一眼懂的小抄)
场景
用法
说明
把关键词放到 路径的一段
BASE + quote(keyword, safe="")
把整个关键词当成同一个路径片段;safe="" 以免 / 被误当成目录
把关键词放到 查询参数
BASE + "?" + urlencode({"q": keyword})
交给 urlencode 统一处理 &, =, 空格等细节
多个查询参数
urlencode({"q": kw, "page": 2})
自动拼成 q=...&page=2
同一个 key 多个值
urlencode({"tag": ["auto","parts"]}, doseq=True)
生成 tag=auto&tag=parts
千万别做
quote(整条URL)
会把 https:// 也编码坏掉;只对片段编码
避免双重编码
只编码一次
已有 %2F 的字符串不要再 quote 一遍
回到你的两句话(翻译成“白话”)
关键字放路径:
BASE + quote(keyword, safe="")
= “把关键词当作门牌号的一部分,所有危险字符(包括/)都先包起来再贴上去,谁也别把它误认为新楼层。”
关键字放查询参数:
BASE + "?" + urlencode({"q": keyword})
= “把关键词写在问路条件里,让专门的‘秘书’(urlencode)把中文、空格、等号、和号都处理好,形成标准问句。”
为什么你“今天中文/空格不编码也能搜”还建议编码?
因为 百科容错好,它帮你兜底了。但一旦换站、上代理、或关键词里含 ? & / # + 这些保留字符,就会出坑;做工程就要主动编码,可移植、可复现、少埋雷。
需要的话,我可以把你现有 baike() 改成**路径 quote(safe="") + 查询版 urlencode**的双入口示例,顺便把“命中哪个选择器/是否触发 fallback”打日志打印出来,便于你调试和扩展。你一句话我就给成品。
您说:
我还是不懂什么时候用quote 和 urlencode, 像url 里面放单个关键词用 quote,多个关键词就用urlencode 么
ChatGPT 说:
已思考 9s
燎然,核心就一句话:用不用 quote 或 urlencode,不看“关键词多少个”,只看它放在 URL 的哪里。
速记口诀
-
路径片段(Path)→ 用
quote()
把某一段路径安全包起来,防止/ ? & #这类保留字符把路径“切断”。
通常写:quote(value, safe="") -
查询参数(Query)→ 用
urlencode()
把一堆{"key": value}变成key=value&k2=v2,空格会变成+,其他保留字符会被正确转义。
⚠️ 与“单个/多个关键词”无关。单个关键词也可能在路径,也可能在查询;选哪个函数取决于“放哪儿”。
例子一:关键词是路径的一段(用 quote)
from urllib.parse import quote
BASE = "https://baike.baidu.com/item/"
kw = "齿轮 泵/A&B"
url = BASE + quote(kw, safe="") # safe="":连斜杠也编码,避免被当成子目录
# => https://baike.baidu.com/item/齿轮%20泵%2FA%26B
例子二:关键词做查询参数(用 urlencode)
from urllib.parse import urlencode
BASE = "https://baike.baidu.com/search"
kw = "齿轮 泵/A&B"
qs = urlencode({"word": kw, "from": "pc"}) # 自动把空格→+,/ → %2F,& → %26
url = BASE + "?" + qs
# => https://baike.baidu.com/search?word=%E9%BD%BF%E8%BD%AE+%E6%B3%B5%2FA%26B&from=pc
例子三:路径多段 + 查询参数(两者一起用)
from urllib.parse import quote, urlencode
host = "https://api.example.com"
category = "汽车/配件" # 路径第1段
keyword = "氧传感器 A&B" # 路径第2段
path = "/".join([quote(category, safe=""), quote(keyword, safe="")])
qs = urlencode({"page": 2, "lang": "zh"})
url = f"{host}/item/{path}?{qs}"
# 路径两段都用 quote,查询用 urlencode
为什么不用“单个→quote,多个→urlencode”的规则?
-
你一个关键词也可以放在查询参数里(就该用
urlencode)。 -
你多个词也可以合成一段路径(就该用
quote)。 -
决策点是“放哪儿”,不是“有几个”。
细节小抄
-
quote()只编码某个片段;不要拿来编码整条 URL。 -
safe=""更稳(把/也编码,避免被当成目录分隔符)。 -
urlencode()适合一个或多个查询参数;它内部使用quote_plus,空格→+(标准做法)。 -
你也可以手写:
"?q=" + quote(kw, safe=""),但一旦有多个参数就容易出错,统一用urlencode更省心。 -
只编码一次,避免把已含
%xx的字符串再次quote(双重编码会变乱)。
要不要我把你现在的 baike(keyword) 做成两个入口(路径版/搜索版)各写一个构造 URL 的函数,并打印“路径构造 vs 查询构造”的效果对照?这样你一跑就一眼明白。
您说:
哦我明白了是不是查询参数中间是有=号的? kw = "齿轮 泵/A&B" qs = urlencode({"word": kw, "from": "pc"}) # 自动把空格→+,/ → %2F,& → %26 这里是吧齿轮 泵 里面的空格变成+ 了吗? 那样不是不好么,空格一般是打错了呀,不能变成加号呀?from urllib.parse import quote BASE = "baike.baidu.com/item/" kw = "齿轮 泵/A&B" url = BASE + quote(kw, safe="") # safe="":连斜杠也编码,避免被当成子目录 # => baike.baidu.com/item/齿轮%20泵… 这里能用查询做吗
ChatGPT 说:
已思考 19s
燎然,你抓住了关键点👌 我把疑惑一次讲透:
1)查询参数“中间有 = 号”对吗?
对。**查询串(Query)**的标准长这样:
?key1=value1&key2=value2
也就是key=val用 & 连接多个。例子:
https://baike.baidu.com/search?word=齿轮泵&from=pc
2)为什么 urlencode 把空格变成 +?
-
在查询串里,空格有两种等价写法:
+或%20。 -
urlencode按 Web 表单规范(application/x-www-form-urlencoded)把空格编码成+,这是标准且正确的,不是把空格“变成加号”,而是URL 编码形式。 -
服务器收到后会把
+还原成空格,所以搜索词不会变味。
注意:只有 Query 部分(
?后面)的+会被当空格;在**路径(Path)**里,+就是字面加号。
想在 Query 里用 %20 而不是 +?
可以:
from urllib.parse import urlencode, quote
qs = urlencode({"word": "齿轮 泵"}, quote_via=quote) # 空格→%20
3)你的这两段,分别说明啥?
A. 路径版(item 路径里放关键词)→ 用 quote
from urllib.parse import quote
BASE = "https://baike.baidu.com/item/"
kw = "齿轮 泵/A&B"
url = BASE + quote(kw, safe="")
# => https://baike.baidu.com/item/齿轮%20泵%2FA%26B
- 这是把关键词作为路径的一段(门牌号的一部分),必须用
quote;safe=""连/也编码为%2F,避免被误当“子目录”。
B. “这里能用查询做吗?”
能,但不是 /item/ 这个路径。百科的搜索入口是:
https://baike.baidu.com/search?word=关键词
也就是查询串版本:
from urllib.parse import urlencode
BASE = "https://baike.baidu.com/search"
kw = "齿轮 泵/A&B"
qs = urlencode({"word": kw, "from": "pc"})
url = BASE + "?" + qs
# => https://baike.baidu.com/search?word=%E9%BD%BF%E8%BD%AE+%E6%B3%B5%2FA%26B&from=pc
-
这会到搜索结果页,你再点进第一条或跟随重定向,才能到具体
/item/...页。 -
直接对
/item/加?word=...一般没用,因为/item/关键词的语义是直接指向某个条目,不是搜索。
4)什么时候用 quote,什么时候用 urlencode(终极口诀)
-
放在路径(Path)的一段 →
quote(value, safe="") -
放在查询(Query)的 key=value →
urlencode({"key": value})
跟“关键词有几个”无关,只看你放哪儿。
5)再补两条工程建议
-
用
requests时,构造查询串尽量让库代劳:import requests r = requests.get("https://baike.baidu.com/search", params={"word": kw, "from": "pc"}) # requests 会自动 urlencode,稳且不出错 -
不要
quote(整条 URL),只编码片段;避免双重编码(已有%2F的再编码会错)。
如果你愿意,我可以把你的 baike() 做成两条路线(直接 item 路径 / search 查询),都返回同样的数据结构,并打印“使用了路径还是查询 + 是否命中 fallback 选择器”的调试信息,便于你对比学习。
您说:
baike.baidu.com/search?word… 这个url 不存在的
ChatGPT 说:
已思考 46s
确实,你在浏览器直接点 https://baike.baidu.com/search?word=python 有时会 404、跳首页、或被重定向,这是百度百科多套搜索入口 + 反爬/AB 实验/CDN 分发导致的正常现象,并不代表“没有搜索接口”。直说结论与做法:
为什么你会觉得“这个 URL 不存在”
-
AB/灰度:百科有多种搜索路径形态(
/search、/search/word、/search/none…),不同地区/UA/实验组表现不同。 -
反爬策略:裸请求、缺 UA/Referer、频率过高,会被 302/403/跳首页。
-
精确命中时会直跳
/item/***:如果关键词是精确条目名,百科常直接重定向到词条页,看起来像“没有搜索页”。
实战建议(稳健优先级)
-
首选:直打词条路径(知道条目名时)
from urllib.parse import quote import requests
s = requests.Session() s.trust_env = False s.headers.update({ "User-Agent": "Mozilla/5.0 ... Chrome/121 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.9", "Referer": "baike.baidu.com/" })
def get_item_url(keyword): url = "baike.baidu.com/item/" + quote(keyword, safe="") # 路径用 quote r = s.get(url, timeout=(5, 12), allow_redirects=True) return r.url, r.status_code, r.text # 精确命中会直接 200
-
其次:用“查询参数”走搜索入口(不知道精确条名时)
百科在不同版本上有多入口,代码里轮询 2~3 个即可:from urllib.parse import urlencode
SEARCH_ENDPOINTS = [ "baike.baidu.com/search", "baike.baidu.com/search/word", "baike.baidu.com/search/none", ]
def search_baike(keyword): params = {"word": keyword, "pn": 0, "rn": 10, "enc": "utf8"} for base in SEARCH_ENDPOINTS: # 查询参数用 urlencode(或让 requests 代劳:params=params) url = base + "?" + urlencode(params) r = s.get(url, timeout=(5, 12), allow_redirects=True) if r.status_code == 200 and ("search" in r.url or "/item/" in r.url): return r.url, r.text # 可能停在搜索结果,也可能被跳到 item return None, None
-
兜底:用通用百度搜索 + 站内限定
(最抗风控、最通用)def search_site(keyword): r = s.get("www.baidu.com/s", params={"wd": f"site:baike.baidu.com {keyword}"}, timeout=(5, 12)) return r.url, r.text
关键点回顾(帮你区分什么时候用什么)
-
路径里放关键词 →
quote(keyword, safe="")(避免/被当目录)
例:https://baike.baidu.com/item/+quote("齿轮 泵/A&B", safe="") -
查询里放关键词 →
urlencode({"word": keyword, ...})
例:https://baike.baidu.com/search?+urlencode(...)
urlencode把查询里的空格编码为+(标准表单编码),服务器会还原为空格,不是把词变成“加号”。
为什么我建议“同时支持多入口 + 合理头信息”
-
百科的前端 class、接口入口、重定向会变;你今天看到的没变,不代表明天不变。
-
你写的是长期可用的爬虫,就要用“多入口 + fallback”设计,且加上
User-Agent / Accept-Language / Referer,成功率会高很多。
给你的落地 checklist
-
Session 级别设置
UA / Accept-Language / Referer -
先尝试
/item/,失败再尝试/search//search/word//search/none -
请求用
allow_redirects=True,读取resp.url看最终跳到哪 -
路径用
quote、查询用urlencode,只编码一次 -
限频(0.8~1.2s 随机 sleep),避免触发风控
你要的话,我可以把你之前的 baike() 改成自动选择路径/搜索、多入口轮询、带选择器 fallback、返回结构化数据的一体化函数,直接可用。
您说:
def baike(keyword:str) -> Optional[BaikeSummary]: 这又是什么用法,Optional 是什么意思
ChatGPT 说:
已思考 12s
燎然,这一行是类型注解(type hints):
def baike(keyword: str) -> Optional[BaikeSummary]:
-
keyword: str:参数keyword期望是 字符串。 -
-> Optional[BaikeSummary]:函数返回值类型是 “BaikeSummary或None”。
Optional 是什么?
Optional[T] 等价于 Union[T, None],也就是 “要么是 T,要么是 None”。
在 Python 3.10+,更推荐写成新语法:T | None。
示例(两种写法等价):
# 兼容 3.8/3.9 的写法
from typing import Optional
def baike(keyword: str) -> Optional[BaikeSummary]:
if not keyword:
return None
return BaikeSummary(title="...", url="...", summary_paras=["..."], raw_summary="...")
# 3.10+ 推荐的写法
def baike(keyword: str) -> BaikeSummary | None:
...
类型注解有什么用?
-
不改变运行时行为(不会强制检查类型),但能给 IDE 自动补全、静态检查(mypy/pyright)更准。
-
帮你设计更清晰的接口:这函数可能失败时就返回 None,调用方据此做分支。
调用方怎么用:
data = baike("氧传感器")
if data is None:
print("抓取失败")
else:
print(data.title)
小贴士(容易踩坑的点)
-
如果
BaikeSummary的定义写在函数后面,在 Python 3.8/3.9 里需要前向引用:from typing import Optional def baike(keyword: str) -> 'Optional[BaikeSummary]': # 用引号包起来 ...或者在文件顶部加:
from __future__ import annotations这样就可以直接写不加引号的类型名了。
-
何时用
None、何时抛异常?-
预期内的“没结果/未命中” → 返回
None(更轻量)。 -
异常情况(网络错误、解析崩溃) →
raise或捕获后统一返回None,看你的接口约定。
-
一句话总结:
Optional[BaikeSummary] = “可能给你一个 BaikeSummary,也可能给你 None”。它帮助编辑器和类型检查器提前发现问题,让你的爬虫代码更专业、更可维护。
您说:
resp = s.get(url, timeout=TIMEOUT, allow_redirects=True) allow_redirects 是什么意思
ChatGPT 说:
已思考 13s
很好问。allow_redirects 决定 requests 是否自动跟随 3xx 重定向(301/302/303/307/308)。
一句话
-
allow_redirects=True(默认):自动跳转到最终地址,resp.url就是最终URL,resp.history保存了跳转轨迹。 -
allow_redirects=False:不跟随跳转,你能拿到 3xx 响应本身(resp.status_code=301/302…)和Location头,自己决定要不要跳。
为什么要开?
像你抓百度百科 /item/关键词 时,经常会被 301/302 跳到规范地址 /item/xxx/123456。开着它更省事:
r = s.get(url, timeout=(5,12), allow_redirects=True)
print(r.url) # 最终规范 URL
print([h.status_code for h in r.history]) # 例如 [302]
为什么有时要关?
当你需要检测是否被跳、拿原始 Location 或做反爬调试时,要关掉:
r = s.get(url, timeout=(5,12), allow_redirects=False)
if r.is_redirect or r.is_permanent_redirect:
loc = r.headers.get("Location")
print("被重定向到:", loc)
# 你可以自己再决定是否跟随:
r2 = s.get(loc, timeout=(5,12))
方法细节(有用的属性)
-
resp.url:最终 URL(如果跟随了跳转) -
resp.history:list[Response],每一步跳转的响应 -
resp.is_redirect/resp.is_permanent_redirect:是否(永久)重定向 -
resp.headers["Location"]:下一跳地址(在 3xx 响应里)
不同 3xx 的语义(requests 的行为)
-
301/302:大多数站会把 POST 变成 GET;requests 也会按标准处理,通常转成 GET 再请求目标。
-
303 See Other:明确要求用 GET 访问下一跳。
-
307/308:保持原 HTTP 方法(POST 还是 POST),requests 也会保持。
你一般用 GET 抓百科,开着
allow_redirects=True就行;如果你在做登录/提交表单,遇到 307/308 要注意方法是否被保留。
额外两个实用点
-
HEAD 请求默认 不自动跳(
requests.head的默认allow_redirects=False),需要的话手动设 True。 -
可限制最大跳转次数:
s.max_redirects = 5(默认 30),避免死循环。
小结(给你记)
-
抓内容:
allow_redirects=True(拿最终页,简单稳)。 -
做诊断/拿 Location:
allow_redirects=False(看清 3xx、自己跟随)。 -
看跳转链:
resp.history;看最终归一化链接:resp.url。
继续撸百科就用 True;一旦你觉得“怎么每次都回首页/被带跑了”,关掉看看 Location,就知道对方在把你重定向到哪儿了。
您说:
要跳还是不要跳
ChatGPT 说:
已思考 14s
短答:默认“跳”(allow_redirects=True)。只有当你需要看清它到底要跳去哪、或控制跳转时,才不跳。
怎么选(超快决策表)
-
✅ 要跳(True) —— 抓正文页面、想拿最终规范 URL、普通 GET 抓取:
resp = s.get(url, allow_redirects=True)
用resp.url拿最终地址,用resp.history看跳转链。 -
❌ 不要跳(False) —— 需要诊断/反爬排查、想读
Location头、限制跨域/异常跳、或者提交后不想被自动改成 GET:
resp = s.get(url, allow_redirects=False)
然后自己读:resp.headers["Location"]再决定跟不跟。
备忘:
requests.get默认就会跳;requests.head默认不跳。
什么时候特别要“不跳”
-
你怀疑被重定向到首页/验证码/登录,想确认
Location。 -
想禁止跨域跳转(例如只允许同域)。
-
POST/表单后要看返回是否 302/303/307/308,并决定是否保留方法再跳。
-
想统计、限速或记录跳转目标。
最小示例
import requests
from urllib.parse import urljoin
s = requests.Session()
# 跟随跳转:拿最终页面
r = s.get("https://baike.baidu.com/item/氧传感器", allow_redirects=True)
print("最终URL:", r.url)
print("跳转链:", [h.status_code for h in r.history])
# 不跟随:只看跳转到哪
r = s.get("https://baike.baidu.com/item/氧传感器", allow_redirects=False)
if r.is_redirect or r.is_permanent_redirect:
loc = r.headers.get("Location")
next_url = urljoin(r.url, loc) # 处理相对地址
print("将要跳到:", next_url)
给你的实战建议
-
抓百科内容:直接 True,省心稳。
-
排查风控/AB 实验:先 False 看
Location,再决定手动跟随。 -
遇到“跳转死循环/太多跳”可限制:
s.max_redirects = 5。
一句话记:日常抓取就“跳”,诊断问题就“不跳”。
您说:
try: html=s.get(url,timeout=(4,10)).text html.raise_for_status() 这样写对么
ChatGPT 说:
不对。你把 raise_for_status() 调在了 字符串 上。
requests.get() 返回的是 Response 对象,raise_for_status() 是它的方法;你先把 .text 取出来就变成了 str,自然没有这个方法。
用法应该是:
try:
resp = s.get(url, timeout=(4, 10), allow_redirects=True) # 先拿 Response
resp.raise_for_status() # 检查状态码,4xx/5xx 会抛 HTTPError
html = resp.text # 再取文本
except requests.exceptions.Timeout:
print("超时了")
except requests.exceptions.HTTPError as e:
print("HTTP 错误:", e.response.status_code, e.response.url)
except requests.exceptions.RequestException as e:
print("请求失败:", e)
几点补充(实话实说版):
-
timeout=(4,10)是(连接超时, 读取超时)。写成一个数也行,但分开更精细。 -
raise_for_status()只在 状态码 ≥ 400 时抛错;2xx/3xx 不会抛。 -
需要最终跳转后的地址,可用
resp.url;跳转链看resp.history。 -
如果遇到页面编码不对,可以:
resp.encoding = resp.apparent_encoding再读resp.text(不过多数情况下让 BS 自己探测也行)。
想要更简洁,可以封一个小函数:
def fetch_html(url, timeout=(5,12), allow_redirects=True):
r = s.get(url, timeout=timeout, allow_redirects=allow_redirects)
r.raise_for_status()
return r.text, r.url
然后:
html, final_url = fetch_html(url)
您说:
def baike(keyword:str) -> Optional[BaikeSummary]: Option 下面有红色下划线是什么意思
ChatGPT 说:
很可能是这三种原因之一(按概率从高到低):
-
拼写错了
你写的是Option,正确的是Optional(多了 al)。
正确写法:def baike(keyword: str) -> Optional[BaikeSummary]: -
没导入
Optional
需要先从typing导入:from typing import Optional
-
BaikeSummary名字此处不可见(未定义/定义在后面)
解决任一即可:
-
把
BaikeSummary的定义放到函数上面;或 -
在文件最上方加:
from __future__ import annotations;或 -
把注解写成前向引用:
def baike(keyword: str) -> 'Optional[BaikeSummary]':
给你一份“不会再红”的模板
from __future__ import annotations # 让类型名可延后解析(可选但推荐)
from typing import Optional
from dataclasses import dataclass
@dataclass
class BaikeSummary:
title: str
url: str
summary_paras: list[str]
raw_summary: str
def baike(keyword: str) -> Optional[BaikeSummary]:
...
用 Python 3.10+ 的更简洁写法(不必导入 Optional)
from __future__ import annotations
# 不用导入 Optional
def baike(keyword: str) -> BaikeSummary | None:
...
在 PyCharm 里,红色下划线通常表示未解析的名称/未导入/拼写错误。把鼠标悬停会看到具体报错信息;按
Alt+Enter往往能自动补导入(导入Optional)。
您说:
from typing import Optional 这又是什么东西
ChatGPT 说:
已思考 9s
燎然,超简单说法:
-
typing是 Python 标准库里的“类型标注工具箱”。 -
from typing import Optional是把工具箱里的Optional拿出来用。 -
Optional[T]的意思是:这个值要么是T,要么是None。等价于Union[T, None]。
比如:函数可能找不到结果,就返回 None:
from typing import Optional
def find_price(oe: str) -> Optional[float]:
if oe in db:
return db[oe]
return None
调用方就知道需要判空:
p = find_price("A001")
if p is None:
print("没找到")
else:
print("价格:", p)