1. 软著公开数据到底能看出什么软件著作权登记信息里藏着企业技术路线的时间戳。软件全称、著作权人、开发完成日期、首次发表日期、版本号、登记号这些字段单看一条没什么感觉但把同一家企业三年内的登记记录拉成时间线技术重心的迁移就非常直观。一家做传统电控的公司2021 年登记的软著还集中在 PLC 配置工具、电机控制固件2023 年突然出现数字孪生仿真平台、边缘网关管理软件这个信号比财报早至少两个季度。适合读这篇的人有三类做产业研究的分析师需要把几百家目标企业的软著拉成结构化表格投资机构的尽调人员想快速验证标的公司宣称的技术方向是否有登记记录支撑企业战略岗定期盯竞争对手的软著增量。共同点是都需要批量、可重复、字段完整的采集流程而不是一条条手查。OpenClaw 在这里的角色是采集调度层。它把请求构造、分页循环、限流、重试、字段落库这几件事拆成配置和少量插件代码你不需要从零写一个爬虫框架。下面按“先跑通一次完整抓取再谈分析”的顺序展开所有配置都可以直接复制修改。2. 前置准备TaoToken 与 OpenClaw 环境2.1 为什么采集流程里会用到 TaoTokenOpenClaw 本身负责 HTTP 请求和解析但在两个环节会调用大模型能力一是软著“主要功能特点”这类自由文本字段的技术标签自动归类二是采集过程中遇到页面结构微调时用模型辅助判断字段映射是否偏移。这两步如果直接调官方接口密钥管理和多模型切换会比较碎。TaoToken 的定位是统一模型接入层兼容 OpenAI 风格的接口协议OpenClaw 的 pipeline 里可以直接把它当成一个 base_url 来配。你只需要在官网注册后拿到 API Key后续模型对话、coding-plan、console、api-keys、doc 这些入口都在同一套账号体系下。对于软著采集这种“采集 文本分类”混合任务把模型调用统一到一个 Key 上配置会干净很多。注意TaoToken 在这里承担的是模型调用通道不参与网页抓取本身。抓取频率、robots 遵守、字段合规仍然由你的 OpenClaw 配置决定。2.2 环境安装Python 3.9 以上建议用虚拟环境隔离依赖python3 -m venv openclaw-env source openclaw-env/bin/activate # Windows 下用 openclaw-env\Scripts\activate pip install openclaw beautifulsoup4 lxml requests如果 PyPI 上的版本落后可以从源码装git clone https://github.com/openclaw-project/openclaw.git cd openclaw pip install -e .初始化项目骨架openclaw startproject soft_crawler cd soft_crawler生成的结构里config.toml是主配置spiders/放采集逻辑pipelines/放数据处理items.py定义字段模型。下面所有配置都基于这个结构。3. 可复制的 config.toml 骨架3.1 全局请求参数OpenClaw 的配置文件用 TOML 格式比 YAML 更适合写嵌套的请求参数。先看全局段[settings] user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 download_delay 3.0 concurrent_requests 1 retry_times 2 retry_status [429, 500, 502, 503] timeout 15 log_level INFO [settings.limiter] enabled true base_delay 3.0 max_delay 30.0 backoff_factor 1.5download_delay设 3 秒是底线concurrent_requests保持 1不要开并发。limiter段是自适应限流当响应里出现 429 或访问频繁提示时延迟按backoff_factor递增最高到 30 秒。这个机制比固定延迟更稳遇到服务器压力大时自动退让。3.2 目标站点与查询参数软著查询走的是 POST 表单核心参数是查询类型、关键词、页码、每页条数[spider.ccopyright] name ccopyright enabled true base_url https://www.ccopyright.com.cn list_path /querySoftwareList detail_path_prefix /softwareDetail [spider.ccopyright.form] searchType softwareName pageSize 20 [spider.ccopyright.keywords] words [人工智能, 大数据, 工业软件, 物联网, 区块链] max_pages 100searchType可以换成authorName按著作权人查做企业维度分析时用这个。max_pages是保护性上限防止某个宽泛关键词翻出几百页把时间耗光。关键词列表按你的分析目标填做行业扫描就填技术词做竞对追踪就填企业全称。3.3 模型调用段TaoToken文本分类和字段校验走 TaoToken 的统一接口[llm] provider taotoken base_url https://taotoken.net/api api_key ${TAOTOKEN_API_KEY} model gpt-4o-mini timeout 30 max_retries 2 [llm.tasks] tech_tagging true field_validate trueapi_key用环境变量注入不要写死在文件里。tech_tagging打开后pipeline 会对“主要功能特点”字段做技术标签抽取field_validate打开后对解析出的日期、登记号做格式校验异常时调模型判断是字段错位还是数据本身缺失。3.4 存储与管道[pipelines] items [ pipelines.clean_pipeline.CleanPipeline, pipelines.tag_pipeline.TagPipeline, pipelines.storage_pipeline.PostgresPipeline ] [pipelines.postgres] dsn postgresql://user:passlocalhost:5432/softcopyright table software_copyrights unique_key register_no batch_size 50unique_key设成登记号去重在这一层完成。batch_size控制批量插入条数50 条一批对 PostgreSQL 比较友好。4. 一次完整抓取验证从请求到落库4.1 定义字段模型items.py里把软著的核心字段列全列表页和详情页的字段都放进来缺失的留空from openclaw import Item, Field class SoftwareCopyrightItem(Item): register_no Field() software_name Field() short_name Field() version Field() author Field() register_date Field() dev_complete_date Field() first_publish_date Field() rights_method Field() rights_range Field() language Field() hardware Field() software_env Field() functions Field() tech_tags Field() source_keyword Field()source_keyword记录这条数据是通过哪个关键词命中的后续做召回率分析时有用。4.2 列表页与详情页的请求构造采集逻辑分两步先按关键词翻列表页拿到登记号和详情页链接再逐个进详情页补全字段。核心代码如下import time from openclaw import Spider, Request, FormRequest from bs4 import BeautifulSoup from ..items import SoftwareCopyrightItem class CCopyrightSpider(Spider): name ccopyright def start_requests(self): for word in self.config[spider.ccopyright.keywords][words]: yield self.search_request(word, page1) def search_request(self, keyword, page1): formdata { searchType: self.config[spider.ccopyright.form][searchType], searchKey: keyword, pageNo: str(page), pageSize: self.config[spider.ccopyright.form][pageSize], } return FormRequest( urlself.config[spider.ccopyright][base_url] self.config[spider.ccopyright][list_path], formdataformdata, callbackself.parse_list, meta{keyword: keyword, page: page}, ) def parse_list(self, response): soup BeautifulSoup(response.text, lxml) rows soup.select(table.result-table tr)[1:] for row in rows: cols row.find_all(td) if len(cols) 5: continue item SoftwareCopyrightItem() item[register_no] cols[0].get_text(stripTrue) item[software_name] cols[1].get_text(stripTrue) item[short_name] cols[2].get_text(stripTrue) item[version] cols[3].get_text(stripTrue) item[author] cols[4].get_text(stripTrue) item[source_keyword] response.meta[keyword] detail_url self.build_detail_url(cols[0].a[href]) yield Request( urldetail_url, callbackself.parse_detail, meta{item: item}, ) keyword response.meta[keyword] page response.meta[page] max_pages self.config[spider.ccopyright.keywords][max_pages] if len(rows) int(self.config[spider.ccopyright.form][pageSize]) and page max_pages: time.sleep(2) yield self.search_request(keyword, page 1) def parse_detail(self, response): item response.meta[item] soup BeautifulSoup(response.text, lxml) table soup.select_one(table.detail-table) if table: rows table.find_all(tr) item[dev_complete_date] self.extract_cell(rows, 5) item[first_publish_date] self.extract_cell(rows, 6) item[rights_method] self.extract_cell(rows, 7) item[rights_range] self.extract_cell(rows, 8) item[language] self.extract_cell(rows, 9) item[hardware] self.extract_cell(rows, 10) item[software_env] self.extract_cell(rows, 11) item[functions] self.extract_cell(rows, 12) yield item def extract_cell(self, rows, index): try: return rows[index].find_all(td)[1].get_text(stripTrue) except Exception: return None def build_detail_url(self, relative_path): return self.config[spider.ccopyright][base_url] relative_pathparse_list里判断是否翻页的条件是“本页条数等于 pageSize”比判断“是否有下一页按钮”更稳因为部分页面结构里下一页按钮在最后一页仍然渲染。parse_detail里的行索引需要根据实际页面结构调整建议先用浏览器开发者工具确认详情表格的行顺序。4.3 清洗与标签管道clean_pipeline.py负责日期标准化和字段去噪import re from datetime import datetime from openclaw import Pipeline class CleanPipeline(Pipeline): def process_item(self, item, spider): for field in [register_date, dev_complete_date, first_publish_date]: item[field] self.normalize_date(item.get(field)) if item.get(software_name): item[software_name] re.sub(rnbsp;|\s, , item[software_name]).strip() return item def normalize_date(self, value): if not value: return None value value.strip() for fmt in (%Y-%m-%d, %Y年%m月%d日, %Y%m%d): try: return datetime.strptime(value, fmt).date().isoformat() except ValueError: continue return Nonetag_pipeline.py调用 TaoToken 做技术标签抽取import requests from openclaw import Pipeline class TagPipeline(Pipeline): def open_spider(self, spider): cfg spider.config[llm] self.api_url cfg[base_url].rstrip(/) /v1/chat/completions self.headers { Authorization: fBearer {cfg[api_key]}, Content-Type: application/json, } self.model cfg[model] def process_item(self, item, spider): text item.get(functions) or item.get(software_name) or if not text: item[tech_tags] [] return item prompt ( 从下面的软件功能描述中抽取技术领域标签 只返回 JSON 数组标签从这些里选人工智能、大数据、云计算、 物联网、信息安全、工业软件、区块链、其他。\n\n text[:800] ) try: resp requests.post( self.api_url, headersself.headers, json{ model: self.model, messages: [{role: user, content: prompt}], temperature: 0, }, timeout30, ) content resp.json()[choices][0][message][content] item[tech_tags] self.parse_tags(content) except Exception: item[tech_tags] [] return item def parse_tags(self, content): import json, re match re.search(r\[.*\], content, re.S) if not match: return [] try: return json.loads(match.group(0)) except Exception: return []temperature设 0 保证标签结果稳定同一段文本多次调用返回一致。text[:800]截断是防止超长功能描述把 token 打满软著的功能特点字段通常不会超过这个长度。4.4 运行与验证启动采集export TAOTOKEN_API_KEY你的key openclaw run ccopyright跑通后先看日志里的请求计数和落库条数。验证动作分三步第一查数据库里register_no是否有重复正常应该为零第二随机抽 5 条记录对照官网详情页核对dev_complete_date和language字段是否一致第三看tech_tags字段的分布如果某个关键词命中的记录里“其他”占比超过 40%说明标签体系需要补充该领域的词。SELECT source_keyword, COUNT(*) AS total, COUNT(*) FILTER (WHERE tech_tags {}) AS untagged FROM software_copyrights GROUP BY source_keyword ORDER BY total DESC;这条 SQL 能快速看出哪个关键词的标签覆盖率低优先补那个领域的标签词。5. 本篇常见错误与排障5.1 列表页解析为空最常见的原因是页面结构变了table.result-table这个选择器失效。排查方法把response.text存到本地文件用浏览器打开看实际 DOM 结构确认结果列表的容器标签和 class 名。如果页面改成前端渲染POST 返回的是 JSON 而不是 HTML那就需要抓包找到真正的数据接口直接解析 JSON比解析 HTML 更稳。5.2 翻页到一定页数后返回空部分查询接口对深分页有限制比如只返回前 100 页。遇到这种情况把关键词拆细用“人工智能医疗”“人工智能金融”这种组合词替代宽泛的“人工智能”每个组合词的结果集变小总页数控制在限制以内。max_pages配置就是为这个场景准备的。5.3 429 状态码频繁出现说明当前延迟不够。检查limiter段是否启用base_delay调到 5 秒max_delay调到 60 秒。如果仍然频繁 429说明该时间段服务器压力大把采集任务挪到非工作时段跑。不要试图通过换 IP 绕过合规采集的前提是尊重目标站点的承载能力。5.4 模型标签返回格式不稳定TaoToken 接口返回的 content 里可能带 markdown 代码块标记parse_tags里的正则\[.*\]能兜住大部分情况。如果仍然解析失败在 prompt 里加一句“不要用 markdown 代码块包裹”同时把max_retries设为 2失败时重试一次。标签抽取失败不应该阻塞整条数据落库所以except里返回空数组而不是抛异常。5.5 日期字段解析出 None软著登记信息里日期格式不统一是常态早期记录可能只有年份。normalize_date里只处理了三种格式遇到“2023年5月”这种缺日的会返回 None。这是预期行为不要强行补全在分析阶段把 None 单独归为一类避免引入错误日期。6. 从采集结果到技术布局分析数据落库后分析动作可以分三层。第一层是企业维度的标签分布按author分组统计tech_tags的频次能看出这家企业的技术重心。第二层是时间维度的迁移按register_date的年份分组看各标签占比的年度变化技术方向的转向通常在两到三年内完成。第三层是竞对对比选三到五家同行业企业把标签分布拉成对照表差距和互补关系一目了然。SELECT author, unnest(tech_tags) AS tag, COUNT(*) AS cnt FROM software_copyrights WHERE author IN (目标企业A, 目标企业B, 目标企业C) GROUP BY author, tag ORDER BY author, cnt DESC;这条查询直接产出竞对标签对照的原始数据。如果要做年度趋势把register_date的年份提取出来加进 GROUP BY 即可。采集环节的稳定性决定了分析结论的可信度。字段缺失率、标签覆盖率、去重后的记录数这三个指标在每次采集后都应该检查一遍。字段缺失率高的企业分析时标注数据置信度标签覆盖率低的领域优先补标签词而不是急着下结论。整套流程跑顺之后你可以把关键词列表和采集频率做成定时任务每周增量拉一次只采集新登记的记录。增量采集的判断依据是register_date大于上次采集的最大日期这样每次请求量小对目标站点也友好。
