python 抓取百度百科词条内容

130 阅读25分钟

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

燎然,这段代码的核心问题不是“能不能跑”,而是职责、鲁棒性和可维护性。从大佬级别的角度,我会这样点评并给出一个“可直接复用”的改造版本。

你现在的主要问题

  1. 职责不清baike() 里既抓取又打印,最后还 print(baike(...)),但函数本身没有 return(默认返回 None)。规范做法是:函数负责“返回数据”,IO(打印/保存)放外面

  2. 选择器易碎lemmaSummary_URFUi 这类 class 看起来像哈希/构建时生成,随时可能变。建议用更稳健的选择器(如“starts-with”前缀匹配或语义更强的结构节点),并准备多套 fallback

  3. 未做 URL 编码keyword 含空格/中文没做编码,可能 400/跳转异常。

  4. 缺少请求头与容错:没有 UA、没有重试、没有 404/重定向处理,极易翻车。

  5. 数据结构欠清晰:只拼接一大段字符串,不利于后续存储/分析。建议返回结构化字典(标题、url、摘要段落列表、首图、参考文献数……)。

  6. 脚注与上标:百科摘要常带“上标引用”(sup)。你现在直接 .get_text(),会把它们揉进来或丢掉,建议保留为 [1]剔除,二选一可配置。

  7. 并发与限频(后续扩展):批量查词条时要加速率限制,否则容易被风控。

建议返回什么?

返回结构化数据,而不是打印。最少也要:

  • 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 随机),避免风控。

  • 缓存:同一关键词一天内不重复请求(sqlitediskcache)。

  • 解析器:对大页面换 lxml 解析器(更快更稳):BeautifulSoup(html, "lxml")

  • 单元测试:准备几条关键词样本断言“至少抓到 1 段摘要”,防回归。

  • 合法性:尊重站点使用条款与 robots;仅做学习研究用途。

你原代码里“while + nth-child”的问题

  • 逻辑能跑,但脆弱nth-child 强依赖 DOM 结构(有些摘要段落不一定就是 div:nth-child(2..n))。一旦中间插了广告/脚注节点就对不上。

  • .select(selector) 每次都从同一个 soup 选,看似 OK,但可读性差,不如直接一次性选中所有段落再遍历。

  • 函数未 returnprint(baike(keyword)) 只会打印 None

结论:换成“一次性选中多段,返回 list[str]”的写法,最稳、最清晰。

你先把这个版本跑起来;后面你要不要我再给你加批量关键词 Excel 输入→Excel 输出的版本(带限频与异常表)?我直接给你成品脚本。

您说:

fallback 是什么意思? 这类 class 看起来像哈希/构建时生成,我今天好几次去搜索,都没有变化呀,keyword 含空格/中文没做编码 我试过了中文也可以搜索没问题呀,加空格也不影响搜索结果呀;asdict(data) 这是什么

ChatGPT 说:

已思考 31s

燎然,直说结论 + 例子👇

  1. 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。

  1. “中文/空格不编码也能搜”——为啥我仍建议编码?

确实,百度百科对中文和空格很宽容(会重定向/自纠错),但通用健壮性要求你自己编码,理由:

  • 可移植:换站就可能 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})

你在百科上“看起来没问题”,只是该站容错好;写成通用组件、批量跑不同站点时,不编码迟早踩坑。

  1. 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&%26safe="" 表示不保留任何特殊字符(最稳妥)。

注意:不要把整条 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”的最小示例,里面把 quoteurlencode 的差别打印出来,一眼看懂?

您说:

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

燎然,核心就一句话:用不用 quoteurlencode,不看“关键词多少个”,只看它放在 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
  • 这是把关键词作为路径的一段(门牌号的一部分),必须用 quotesafe=""/ 也编码为 %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=valueurlencode({"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/***:如果关键词是精确条目名,百科常直接重定向到词条页,看起来像“没有搜索页”。

实战建议(稳健优先级)

  1. 首选:直打词条路径(知道条目名时)

    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. 其次:用“查询参数”走搜索入口(不知道精确条名时)
    百科在不同版本上有多入口,代码里轮询 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

  3. 兜底:用通用百度搜索 + 站内限定
    (最抗风控、最通用)

    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(拿最终页,简单稳)。

  • 做诊断/拿 Locationallow_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 实验:先 FalseLocation,再决定手动跟随。

  • 遇到“跳转死循环/太多跳”可限制: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 说:

很可能是这三种原因之一(按概率从高到低):

  1. 拼写错了
    你写的是 Option,正确的是 Optional(多了 al)。
    正确写法:def baike(keyword: str) -> Optional[BaikeSummary]:

  2. 没导入 Optional
    需要先从 typing 导入:

    from typing import Optional

  3. 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

燎然,超简单说法:

  • typingPython 标准库里的“类型标注工具箱”

  • 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)