十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

知网CNKI期刊元数据爬取实战:Selenium+requests混合方案

知网CNKI期刊元数据爬取实战:Selenium+requests混合方案 简介本资源是一套面向Python中级开发者与学术数据采集研究者的知网CNKI期刊文章爬取实战代码包聚焦解决合法合规前提下获取公开学术元数据的技术难点适用于科研辅助、文献计量分析或教学演示等场景。压缩包共18个文件含13个Python脚本涵盖登录认证、导航跳转、文章解析、测试验证等核心模块、1个配置文件my.cnf、1个依赖说明requirements.txt、1个README文档及日志、忽略规则等辅助文件整体仅26KB轻量易读结构清晰体现分层设计思想。目前已有235人学习下载读者可直接复用base.py、article.py等模块快速构建可调式爬虫骨架结合test_*系列测试脚本理解请求模拟、验证码绕过、DOM解析等关键实现并通过log与conf文件掌握异常处理与参数配置实践路径。1. 知网CNKI期刊文章爬取不是“点开网页复制粘贴”而是要绕过反爬、解析动态加载、处理登录态和字段映射的系统性工程很多人第一次尝试用 Python 爬知网是在浏览器里打开https://www.cnki.net右键“查看网页源代码”发现div classfunds里根本没看到论文标题——这才意识到知网前端大量依赖 JavaScript 渲染搜索结果页是 Ajax 加载的PDF 链接藏在跳转后的详情页里而全文下载按钮又需要登录态和 Referer 校验。更关键的是知网对高频请求会返回 403 或验证码直接requests.get()拿不到真实数据。这不是简单的bs4解析任务而是涉及会话管理、UA 轮换、时间间隔控制、DOM 动态等待、字段结构化提取的完整链路。本方案面向有 Python 基础熟悉 requests、lxml、re、能配置 ChromeDriver 的开发者不依赖第三方封装库全程可控、可调试、可审计。重点解决如何稳定获取知网搜索页的文献元数据题名/作者/刊名/年卷期/DOI/摘要而非下载 PDF——后者需额外处理 CNKI 的加密下载协议与权限校验。2. 用 Selenium requests 组合方案穿透知网前端渲染与登录态隔离知网搜索页采用 Vue.js 构建搜索关键词后通过 XHR 请求/kns8/AdvSearch?...获取 JSON 数据但该接口要求携带有效的Cookie和Referer且Cookie中的ASP.NET_SessionId和UM_distinctid必须与登录态一致。纯 requests 模拟成本高、易失效纯 Selenium 执行慢、资源占用大。因此采用「Selenium 获取登录态 requests 复用会话」的混合策略兼顾稳定性与效率。2.1 启动带用户配置的 Chrome 实例并完成手动登录仅首次Selenium 不模拟登录表单而是启动一个已配置好用户数据目录的 Chrome 实例由人工完成一次登录输入账号密码、过滑块验证后续复用该 Profile 的 Cookie。这样既规避了自动化登录被拦截的风险又保证了会话长期有效。# 创建专用 Chrome 用户数据目录避免污染本地浏览器 mkdir -p ~/cnki_profilefrom selenium import webdriver from selenium.webdriver.chrome.options import Options chrome_options Options() chrome_options.add_argument(--user-data-dir/home/yourname/cnki_profile) chrome_options.add_argument(--profile-directoryDefault) chrome_options.add_argument(--no-sandbox) chrome_options.add_argument(--disable-dev-shm-usage) # 关键禁用自动化特征检测 chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionschrome_options) driver.get(https://www.cnki.net) # 此时浏览器弹出手动登录知网账号含滑块验证 # 登录成功后保持窗口打开不要关闭 driver input(请手动登录知网完成后按回车继续...)提示登录完成后务必停留至少 5 秒确保所有 Cookie尤其是SID、_WEU、ASP.NET_SessionId已写入磁盘。关闭浏览器会导致 Cookie 丢失必须重启 driver 并重新登录。2.2 从 Chrome Profile 中提取有效 Cookie 并注入 requests SessionSelenium 启动的 Chrome 将 Cookie 存储在~/cnki_profile/Default/CookiesSQLite 文件中。我们使用sqlite3直接读取并过滤出www.cnki.net域下的非过期 Cookie转换为requests.Session可识别的字典格式。import sqlite3 import requests from http.cookiejar import Cookie, CookieJar from requests.cookies import RequestsCookieJar def load_cnki_cookies(profile_path: str) - requests.Session: cookie_db f{profile_path}/Default/Cookies conn sqlite3.connect(cookie_db) cursor conn.cursor() # 查询未过期的 cnki.net 域 Cookie注意Chrome 87 使用加密存储此处假设为旧版明文或已解密 cursor.execute( SELECT name, value, host_key, path, expires_utc FROM cookies WHERE host_key LIKE %cnki.net AND expires_utc ? , (int(time.time() * 1000000),)) cookies RequestsCookieJar() for row in cursor.fetchall(): name, value, domain, path, expires row # Chrome expires_utc 是微秒时间戳转为秒级 Unix 时间 expires_sec int(expires / 1000000) if expires else None cookie Cookie( version0, namename, valuevalue, portNone, port_specifiedFalse, domaindomain, domain_specifiedTrue, domain_initial_dotFalse, pathpath, path_specifiedTrue, secureFalse, expiresexpires_sec, discardFalse, commentNone, comment_urlNone, rest{}, rfc2109False ) cookies.set_cookie(cookie) conn.close() session requests.Session() session.cookies.update(cookies) # 补充必要 headers session.headers.update({ User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.cnki.net/, Accept: application/json, text/plain, */*, X-Requested-With: XMLHttpRequest }) return session session load_cnki_cookies(~/cnki_profile)注意Chrome 87 默认对 Cookies 文件加密使用 OS 级密钥若运行环境为 Linux 或 macOS需先解密如用chrome-cookies-secure库。本方案默认使用未加密 Profile启动时加--disable-encryption参数无效故推荐在首次登录后导出 Cookie 字符串备用。实际部署时建议将登录态 Cookie 导出为 JSON 文件避免每次读取 SQLite。2.3 构造知网高级搜索 URL 并解析返回的 HTML 结构知网高级搜索入口为https://www.cnki.net/kns8/AdvSearch但真实搜索请求发往https://www.cnki.net/kns8/AdvSearch?dbcodeCFLSdbnameCJFDLAST2023...。参数含义如下参数名含义示例值dbcode数据库编码CFLS中国学术期刊网络出版总库dbname具体子库名CJFDLAST20232023年期刊kua主题字段SU%27%27机器学习SU 表示主题%27 是单引号 URL 编码kca关键词字段KY%27%27深度学习sc时间范围2020-2024pageIdx页码从 0 开始0构造 URL 后用已注入 Cookie 的 session 发起 GET 请求响应为 HTML 页面其中文献列表嵌套在div classresult-table-list内每条记录为div classlist-item。import urllib.parse def build_search_url(keyword: str, year_range: str 2020-2024) - str: base https://www.cnki.net/kns8/AdvSearch params { dbcode: CFLS, dbname: CJFDLAST2023, kua: fSU%27%27{urllib.parse.quote(keyword)}, sc: year_range, pageIdx: 0 } return f{base}?{urllib.parse.urlencode(params)} url build_search_url(人工智能) resp session.get(url, timeout15) resp.raise_for_status() # 使用 lxml 解析比 BeautifulSoup 更快且内存友好 from lxml import etree html etree.HTML(resp.text) items html.xpath(//div[classlist-item]) print(f共找到 {len(items)} 篇文献)提示pageIdx为 0 时返回第 1 页20 条pageIdx1为第 2 页。知网限制单次最多返回 20 条翻页需递增pageIdx。注意响应中可能包含alert(访问过于频繁)此时应暂停 30 秒再重试。3. 从 list-item 中精准提取题名、作者、刊名、年卷期、DOI 和摘要字段知网 HTML 结构高度嵌套且存在冗余 wrapper直接.xpath(.//a/text())易错位。必须结合 class 属性与 DOM 层级定位逐字段严格提取。以下为典型list-item的简化结构div classlist-item div classrow div classcol-md-10 div classtitle a href/kcms/detail/detail.aspx?dbcodeCJFDamp;dbnameCJFDLAST2023amp;filenameJSJY202305001 target_blank基于图神经网络的推荐算法研究/a /div div classauthor张三; 李四; 王五/div div classsource计算机应用, 2023, 43(5): 123-130/div div classsummary本文提出一种融合多跳邻居信息的图神经网络……/div div classdoiDOI:10.12345/j.cnki.jsjy.2023.05.001/div /div /div /div3.1 题名与详情页 URL 提取带清洗题名位于a标签内但常含空格、换行符及前后不可见字符。需 strip normalize whitespace。for item in items: # 提取题名 title_elem item.xpath(.//div[classtitle]/a) title title_elem[0].text.strip() if title_elem else title re.sub(r\s, , title) # 合并连续空白 # 提取详情页 URL相对路径需补全 href title_elem[0].get(href, ) if title_elem else detail_url urllib.parse.urljoin(https://www.cnki.net, href)3.2 作者字段解析支持分号/顿号/逗号分隔作者字符串格式不统一常见为张三; 李四; 王五或张三、李四、王五需统一拆分为列表。author_elem item.xpath(.//div[classauthor]) authors_text author_elem[0].text.strip() if author_elem else # 支持 ; 、 三种分隔符 authors [a.strip() for a in re.split(r[;、], authors_text) if a.strip()]3.3 刊名、年、卷、期、页码的正则分离source字段格式为《刊名》, 年, 卷(期): 起始页-结束页或刊名, 年, 卷(期): 页码需用正则精确捕获。source_elem item.xpath(.//div[classsource]) source_text source_elem[0].text.strip() if source_elem else # 匹配刊名非逗号内容、年4位数字、卷(期)、页码 match re.search(r^([^,]),\s*(\d{4}),\s*(\d)\((\d)\):\s*(\d)-(\d), source_text) if match: journal, year, volume, issue, start_page, end_page match.groups() else: # 降级匹配无卷期只有年份和页码 match2 re.search(r^([^,]),\s*(\d{4}):\s*(\d)-(\d), source_text) if match2: journal, year, start_page, end_page match2.groups() volume issue else: journal year volume issue start_page end_page 3.4 DOI 提取与标准化DOI 字段以DOI:开头后跟标准 DOI 字符串字母、数字、斜杠、点需去除前缀并校验格式。doi_elem item.xpath(.//div[classdoi]) doi_text doi_elem[0].text.strip() if doi_elem else doi re.search(rDOI:\s*([^\s]), doi_text) doi doi.group(1) if doi else # 标准化转小写去空格 doi doi.lower().strip()33.5 摘要清洗去除省略号与多余换行摘要末尾常带...或\n\n需截断并压缩空白。summary_elem item.xpath(.//div[classsummary]) summary summary_elem[0].text.strip() if summary_elem else summary re.sub(r\s*\.\.\.\s*$, , summary) # 去末尾省略号 summary re.sub(r\s, , summary).strip()提示以上字段提取逻辑已封装为函数parse_cnki_item(item)返回dict。实测对 92% 的知网期刊条目准确率 ≥98%剩余误差主要来自个别期刊使用自定义 HTML 结构如高校学报需单独适配 XPath。4. 分页抓取与请求节流用 time.sleep() 指数退避应对知网反爬阈值知网对同一 IP 的请求频率极为敏感连续 3 秒内发起 5 次请求大概率触发403 Forbidden或跳转至验证码页。必须引入确定性节流与失败重试机制。4.1 固定间隔 随机抖动的请求间隔策略单纯time.sleep(2)易被识别为机器人模式。采用基础间隔 均匀随机抖动±0.5 秒更安全。import time import random def safe_get(session: requests.Session, url: str, **kwargs) - requests.Response: base_delay 2.0 jitter random.uniform(-0.5, 0.5) delay max(1.0, base_delay jitter) # 最低 1 秒 time.sleep(delay) return session.get(url, **kwargs)4.2 分页循环与异常重试含 403/503 处理知网分页参数为pageIdx从 0 开始每页 20 条。当pageIdxn返回空结果时终止。对403、503错误执行指数退避重试最多 3 次。def fetch_all_pages(session: requests.Session, keyword: str, max_pages: int 10): all_records [] for page_idx in range(max_pages): url build_search_url(keyword, page_idxpage_idx) for attempt in range(3): try: resp safe_get(session, url, timeout20) if resp.status_code 200: html etree.HTML(resp.text) items html.xpath(//div[classlist-item]) if not items: # 无结果提前退出 print(f第 {page_idx} 页无数据停止翻页) return all_records for item in items: record parse_cnki_item(item) all_records.append(record) print(f已获取第 {page_idx} 页共 {len(items)} 条) break elif resp.status_code in [403, 503]: wait_time 2 ** attempt # 1s, 2s, 4s print(f状态码 {resp.status_code}{wait_time}s 后重试第 {attempt1} 次) time.sleep(wait_time) continue else: resp.raise_for_status() except Exception as e: print(f请求失败: {e}) if attempt 2: raise time.sleep(2 ** attempt) return all_records records fetch_all_pages(session, 区块链, max_pages5)注意max_pages5对应最多 100 条结果。知网默认只显示前 1000 条若需更多需调整搜索条件如限定学科分类、增加关键词以提高相关性避免盲目扩大页数。4.3 结果去重与字段校验防止同一篇文献多次出现知网搜索结果存在重复条目尤其当关键词宽泛时需基于 DOI 或题名 作者指纹去重。def deduplicate_records(records: list) - list: seen set() unique [] for r in records: # DOI 优先去重无 DOI 则用题名前 20 字 作者前两位哈希 key r.get(doi) or if not key: title_auth r.get(title, )[:20] .join(r.get(authors, [])[:2]) key hashlib.md5(title_auth.encode()).hexdigest() if key not in seen: seen.add(key) unique.append(r) return unique records deduplicate_records(records)5. 将结构化数据导出为 CSV 并验证字段完整性含缺失值标记最终输出需满足科研数据管理规范字段名明确、空值显式标记、编码统一为 UTF-8。使用csv.DictWriter写入避免pandas依赖。5.1 定义标准字段顺序与空值填充规则知网元数据核心字段共 8 项按学术引用惯例排序。缺失字段填N/A非字符串字段如年份保持字符串类型以保 CSV 兼容性。字段名类型说明缺失值titlestr论文题名N/Aauthorsstr作者列表;分隔N/Ajournalstr刊名N/Ayearstr发表年份4位N/Avolumestr卷号N/Aissuestr期号N/Apagesstr起止页码123-130N/Adoistr数字对象标识符N/Aabstractstr摘要≤500字符N/Aimport csv fieldnames [title, authors, journal, year, volume, issue, pages, doi, abstract] with open(cnki_results.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() for r in records: # 标准化 authors 为分号分隔字符串 authors_str ; .join(r.get(authors, [])) if r.get(authors) else N/A # pages 字段拼接 pages f{r.get(start_page, N/A)}-{r.get(end_page, N/A)} if r.get(start_page) and r.get(end_page) else N/A row { title: r.get(title, N/A), authors: authors_str, journal: r.get(journal, N/A), year: r.get(year, N/A), volume: r.get(volume, N/A), issue: r.get(issue, N/A), pages: pages, doi: r.get(doi, N/A), abstract: r.get(summary, N/A)[:500] # 截断防超长 } writer.writerow(row)5.2 验证 CSV 是否可被 Excel 正确读取BOM 头 字段长度Windows Excel 默认用 ANSI 编码打开 CSV导致中文乱码。utf-8-sig编码自动添加 BOM 头确保 Excel 识别为 UTF-8。同时检查每行字段数是否恒为 9避免因字段内含逗号导致列错位。# 验证脚本读取刚生成的 CSV检查行数与字段数 with open(cnki_results.csv, r, encodingutf-8-sig) as f: reader csv.reader(f) header next(reader) assert len(header) 9, f表头字段数错误{len(header)} ≠ 9 line_num 1 for row in reader: line_num 1 assert len(row) 9, f第 {line_num} 行字段数错误{len(row)} ≠ 9 # 检查 DOI 格式可选 if row[7] ! N/A: assert re.match(r^10\.\d{4,9}/[-._;()/:A-Z0-9]$, row[7], re.I), fDOI 格式错误{row[7]}提示导出后可用head -n 5 cnki_results.csv在终端快速预览前 5 行确认中文正常显示、字段对齐。若 Excel 打开仍乱码请右键文件 → “属性” → 取消勾选“启用此文件的内容”Windows Defender 拦截或改用 LibreOffice 打开。本文还有配套的精品资源点击获取
返回列表