资讯动态

图书数据关联实战:ISBN规范化与多级匹配策略

发布时间:2026/9/2 11:26:02 来源:尧图企业网站定制
把 53.9 万本 Library Genesis / Z-Library 图书与 Goodreads 元数据关联起来听起来像是一个“下载 CSV 然后 join 一下”的活儿真正做起来才会发现不同平台的图书数据字段口径完全不一样ISBN 有长有短作者名一会儿是“FirstName LastName”一会儿是“LastName, FirstName”标题里还混着全角字符、HTML 实体和多余的空格。这篇文章会完整拆解一个可落地的实战方案从数据源字段分析、ISBN 规范化、标题作者归一化到精确匹配与模糊匹配的组合策略再到匹配质量的评估方法。整个过程用 Python pandas 实现代码可以直接复制修改适配你自己手上的图书数据。如果你是做推荐系统、知识图谱、阅读数据分析或者想构建跨平台的图书 ID 映射表这篇文章应该能帮你省下不少试错时间。1. 为什么要把 LibGen / Z-Library 图书与 Goodreads 元数据关联1.1 什么是图书元数据metadata图书元数据就是描述一本书“长什么样”的结构化数据通常包括标题title、作者author、ISBN、出版社publisher、出版年份publication year、语言language等字段。不同平台维护元数据的侧重点完全不同Library Genesis 和 Z-Library 这类数字图书馆平台核心是“文件”元数据主要用于文件检索字段往往比较精简部分记录的 ISBN 为空标题和作者大小写不统一。Goodreads 是社交阅读平台核心是“书”和“读者关系”除了基础书目字段还包含平均评分、评分人数、评论数、丛书系列、体裁标签等丰富的扩展元数据。所以当你有了一堆 LibGen / Z-Library 的图书 ID却想拿到它们的评分、评论数量和读者分组信息时就必须把它们关联到 Goodreads 的图书记录上。1.2 数据关联的价值与典型应用场景把 539k 本 LibGen / Z-Library 图书链接到 Goodreads 元数据实际价值体现在这几个方向应用场景具体用途推荐系统冷启动用 Goodreads 评分和标签弥补 LibGen / Z-Library 缺少的社交特征阅读行为分析分析下载量、文件热度与评分、评分人数之间的相关性知识图谱构建建立“同一本书在不同平台上的 ID 映射关系”学术研究研究免费数字图书馆的馆藏覆盖范围、语言分布、出版年份分布数据治理为不完整元数据补齐出版社、丛书系列、语种等字段1.3 这个数据集解决的核心问题LibGen / Z-Library 与 Goodreads 之间没有一个通用的“图书主键”。虽然 ISBN 在理论上可以作为唯一标识但实际数据里存在几个硬伤同一本书在 LibGen 里存的是 ISBN-10在 Goodreads 里是 ISBN-13部分数据源 ISBN 为空只剩标题和作者同一个作者在不同平台上有不同的拼写方式同一本书可能因为版本不同ISBN 也不同导致“多对多”匹配。因此这个任务的核心不是“写一个 join”而是设计一套多级匹配策略让尽可能多的记录能关联到正确且唯一的 Goodreads 条目上。2. 数据源分析字段差异决定了匹配策略2.1 Library Genesis 数据结构LibGen 的导出数据通常包含以下字段不同镜像站字段名略有差异字段名示例说明id123456LibGen 内部 IDtitleThe Great Gatsby书名authorFitzgerald, F. Scott作者通常姓在前isbn9780743273565部分记录为空publisherScribner出版社year2004出版年份languageEnglish语言filesize523452文件大小extensionepub文件格式LibGen 数据的特点是“文件优先”同一本书可能有多个文件版本因此LibGen ID 不能直接当作唯一图书 ID需要基于 ISBN 或标题作者做去重。2.2 Z-Library 数据结构Z-Library 的导出结构与 LibGen 非常相似核心字段同样是 title、author、isbn、publisher、year、language。不过 Z-Library 内部对书籍条目的组织更偏向“书”同一本书的不同格式通常会被归并到一个条目下。Z-Library 数据中常见的问题是部分作者字段为全大写例如 “GEORGE ORWELL”部分标题带副标题用冒号分隔ISBN 存在“多位作者共用一条记录”的情况需要按分号分割作者。2.3 Goodreads 数据结构Goodreads 的公开数据集里比较常用的是books.csv字段包括字段名示例说明book_id123Goodreads 内部图书 IDtitleThe Great Gatsby书名authorsF. Scott Fitzgerald作者通常名在前isbn0743273565ISBN-10isbn139780743273565ISBN-13publication_year2004出版年份publisherScribner出版社average_rating3.93平均评分ratings_count1042430评分人数seriesFitzgerald, F. Scott丛书信息可能为空Goodreads 的isbn和isbn13两个字段都可能是空字符串这是匹配时需要重点处理的。2.4 匹配锚点的优先级判断分析完三个数据源的字段后可以确定匹配锚点的优先级ISBNISBN-13 优先统一格式后再比较最稳定但覆盖率不足规范化标题 规范化作者覆盖率最高但需要处理同名书和作者名变体标题 作者 出版年份作为前两级的补充降低误匹配概率。实际项目中千万不要只用 ISBN否则会丢掉大量 ISBN 为空的记录也不要只靠标题作者否则会引入大量误匹配。正确思路是按优先级逐级匹配每级匹配完后把剩余未匹配的记录交给下一级。3. 环境准备与数据预处理3.1 环境与依赖本文示例以常见环境为例重点演示配置思路版本需要根据你的项目实际情况调整。Python 3.9 pandas rapidfuzz tqdm安装依赖pip install pandas rapidfuzz tqdmrapidfuzz是高性能的模糊字符串匹配库基于 C 实现比fuzzywuzzy快得多适合处理几十万到百万级的数据量。3.2 数据导入与字段统一首先定义三个数据源的最小公共字段方便后续统一处理# 文件路径src/data_loader.py import pandas as pd COMMON_COLUMNS { title: title, author: author, isbn: isbn, publisher: publisher, year: year, language: language, } def load_csv(filepath, rename_mapNone): df pd.read_csv(filepath, dtypestr, keep_default_naFalse) if rename_map: df df.rename(columnsrename_map) return df注意这里使用了dtypestr因为 ISBN、年份这类字段如果按数值读取前导 0 会丢失长数字也可能被转成科学计数法。3.3 ISBN 规范化统一到 ISBN-13ISBN 匹配最大的坑是同一本书在不同平台一个是 ISBN-10、一个是 ISBN-13。解决思路是全部统一转换成 ISBN-13去掉连字符和空格然后比较。ISBN-10 转 ISBN-13 的算法如下去掉 ISBN-10 最后一位校验位前面拼上978前 12 位按照权重 1、3 交替计算校验和校验位 (10 - (校验和 % 10)) % 10。# 文件路径src/isbn_utils.py import re def clean_isbn(isbn: str) - str: if not isbn: return return re.sub(r[^0-9Xx], , str(isbn).upper()) def isbn10_to_isbn13(isbn10: str) - str: isbn10 clean_isbn(isbn10) if len(isbn10) ! 10: return # 前 9 位 前缀 978 payload 978 isbn10[:9] total 0 for i, ch in enumerate(payload): digit int(ch) total digit * (1 if i % 2 0 else 3) check (10 - (total % 10)) % 10 return payload str(check) def normalize_isbn(isbn: str) - str: 统一返回 ISBN-13转换失败返回空字符串。 isbn clean_isbn(isbn) if not isbn: return if len(isbn) 10 and isbn.isdigit(): return isbn10_to_isbn13(isbn) if len(isbn) 13 and isbn.isdigit(): return isbn # 部分数据里 ISBN-10 以 X 结尾也尝试转换 if len(isbn) 10: return isbn10_to_isbn13(isbn) return 3.4 标题与作者规范化标题和作者是模糊匹配的输入特征规范化质量直接影响匹配效果。我使用下面这套规则统一转小写去掉标点符号保留中文、英文、数字和空格去掉多余空格将全角字符转为半角作者名统一成“名 姓”的顺序简单策略用于跨平台对比。# 文件路径src/text_utils.py import re import unicodedata def to_half_width(text: str) - str: 全角转半角。 result [] for ch in text: code ord(ch) if code 0x3000: code 0x20 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) return .join(result) def normalize_title(title: str) - str: if not title: return text to_half_width(title) text text.lower() text re.sub(r[^a-z0-9\u4e00-\u9fa5 ], , text) text re.sub(r\s, , text).strip() return text def normalize_author(author: str) - str: if not author: return parts [p.strip() for p in author.split(;) if p.strip()] normalized_parts [] for part in parts: text to_half_width(part).lower() # 处理 LastName, FirstName if , in text: last, first text.split(,, 1) text f{first.strip()} {last.strip()} text re.sub(r[^a-z0-9\u4e00-\u9fa5 ], , text) text re.sub(r\s, , text).strip() if text: normalized_parts.append(text) return ;.join(normalized_parts) def get_first_author(author: str) - str: 取第一位作者用于标题作者精确匹配。 if not author: return parts normalize_author(author).split(;) return parts[0] if parts else 4. 匹配策略设计三级递进逐级缩小范围4.1 第一级ISBN 精确匹配在所有字段里规范化后的 ISBN 是最可靠的匹配键。操作流程把 LibGen / Z-Library 的 ISBN 统一转成 ISBN-13把 Goodreads 数据集中所有非空的isbn和isbn13都转成 ISBN-13并展开成“一个 ISBN 对应一个 Goodreads book_id”的映射表用 ISBN-13 做 inner join。这里需要注意 Goodreads 的isbn字段本身可能是 ISBN-10也可能已经是 ISBN-13混合情况比较普遍所以必须对两个字段同时跑一遍规范化。# 文件路径src/matcher.py import pandas as pd from src.isbn_utils import normalize_isbn def build_isbn_index(goodreads_df: pd.DataFrame) - pd.DataFrame: 构建 ISBN-13 - Goodreads book_id 的映射表。 rows [] isbn_series pd.concat([ goodreads_df[isbn].map(normalize_isbn), goodreads_df[isbn13].map(normalize_isbn), ]) book_id_series pd.concat([ goodreads_df[book_id], goodreads_df[book_id], ]) for isbn, book_id in zip(isbn_series, book_id_series): if isbn: rows.append({isbn13: isbn, book_id: book_id}) index_df pd.DataFrame(rows).drop_duplicates() return index_df def match_by_isbn(source_df: pd.DataFrame, isbn_index: pd.DataFrame) - pd.DataFrame: source_df source_df.copy() source_df[isbn13] source_df[isbn].map(normalize_isbn) merged source_df.merge(isbn_index, onisbn13, howinner) return merged4.2 第二级规范化标题 第一位作者精确匹配ISBN 匹配完成后剩余未匹配的记录进入第二级。这里的“精确匹配”指的是经过规范化后的精确匹配它要求标题完全一致、第一位作者完全一致。为了让更多记录通过这一级可以再加一个可选条件出版年份一致。但这里有个取舍——如果过于严格会降低召回率如果不过于严格可能引入同名书误匹配。我的建议是先不加年份条件用标题第一位作者匹配然后对匹配结果做年份校验年份不一致的记录降级到模糊匹配。# 文件路径src/matcher.py from src.text_utils import normalize_title, get_first_author def match_by_title_author( source_df: pd.DataFrame, goodreads_df: pd.DataFrame, source_id_col: str, ) - pd.DataFrame: source source_df.copy() gr goodreads_df.copy() source[title_norm] source[title].map(normalize_title) source[first_author_norm] source[author].map(get_first_author) gr[title_norm] gr[title].map(normalize_title) gr[first_author_norm] gr[authors].map(get_first_author) # 过滤空值避免空标题空作者全匹配 source source[ (source[title_norm] ! ) (source[first_author_norm] ! ) ] gr gr[(gr[title_norm] ! ) (gr[first_author_norm] ! )] merged source.merge( gr[[book_id, title_norm, first_author_norm, publication_year]], on[title_norm, first_author_norm], howinner, ) return merged4.3 第三级模糊匹配rapidfuzz经过前两级匹配剩余记录通常是标题或作者存在细微差异的数据。这时用rapidfuzz的token_set_ratio做模糊匹配。token_set_ratio适合处理标题中词语顺序不同、包含副标题、多出少量单词的情况。比如The Great Gatsby与The Great Gatsby: A Novel会得到很高的相似度Animal Farm与Animal Farm: A Fairy Story也能识别为同一本书。实现思路对 Goodreads 数据先按标题首字母分组减少比对范围对每个未匹配的源记录只在其标题首字母相同的候选组里找 top-1 相似度设定阈值比如 90 分以上且要求作者相似度不低于 85 分才认为匹配成功。# 文件路径src/fuzzy_matcher.py import pandas as pd from rapidfuzz import fuzz, process from src.text_utils import normalize_title, normalize_author def build_candidate_pool(goodreads_df: pd.DataFrame): gr goodreads_df.copy() gr[title_norm] gr[title].map(normalize_title) gr[author_norm] gr[authors].map(normalize_author) gr gr[gr[title_norm] ! ] # 按标题首字母分组加速检索 gr[title_prefix] gr[title_norm].str[:1] pool {} for prefix, group in gr.groupby(title_prefix): pool[prefix] { titles: group[title_norm].tolist(), meta: group[[book_id, title_norm, author_norm]].to_dict(records), } return pool def fuzzy_match_record( source_title: str, source_author: str, pool: dict, title_threshold: int 90, author_threshold: int 85, ): title_norm normalize_title(source_title) if not title_norm: return None prefix title_norm[:1] candidates pool.get(prefix) if not candidates: return None best_score 0 best_idx None for idx, cand_title in enumerate(candidates[titles]): score fuzz.token_set_ratio(title_norm, cand_title) if score best_score: best_score score best_idx idx if best_idx is None or best_score title_threshold: return None cand_meta candidates[meta][best_idx] author_score fuzz.token_set_ratio( normalize_author(source_author), cand_meta[author_norm], ) if author_score author_threshold: return None return { book_id: cand_meta[book_id], title_score: best_score, author_score: author_score, }需要注意这个模糊匹配实现是示例思路正式处理 539k 条数据时建议做并行化处理否则耗时很长。可以按 title_prefix 分组后用multiprocessing或joblib并行执行。5. 完整实战案例构建链接数据集5.1 项目目录结构book-linker/ ├── data/ │ ├── libgen_books.csv │ ├── zlib_books.csv │ └── goodreads_books.csv ├── output/ │ └── linked_books.csv ├── src/ │ ├── data_loader.py │ ├── isbn_utils.py │ ├── text_utils.py │ ├── matcher.py │ ├── fuzzy_matcher.py │ └── pipeline.py └── main.py5.2 编写主流程 pipeline.py# 文件路径src/pipeline.py import pandas as pd from src.data_loader import load_csv from src.matcher import build_isbn_index, match_by_isbn, match_by_title_author from src.fuzzy_matcher import build_candidate_pool, fuzzy_match_record def run_pipeline( source_df: pd.DataFrame, goodreads_df: pd.DataFrame, source_slug: str, ) - pd.DataFrame: source_df source_df.copy() source_df[source_slug] source_slug # -------- 第一级ISBN 精确匹配 -------- isbn_index build_isbn_index(goodreads_df) matched_isbn match_by_isbn(source_df, isbn_index) matched_isbn[match_level] isbn matched_isbn[match_key] matched_isbn[isbn13] # 剔除已匹配记录进入下一级 matched_ids set(matched_isbn[source_id]) if source_id in source_df.columns else set() remaining source_df[~source_df.index.isin(matched_isbn.index)].copy() # -------- 第二级标题 作者精确匹配 -------- matched_title match_by_title_author(remaining, goodreads_df, source_id) if not matched_title.empty: matched_title[match_level] title_author matched_title[match_key] ( matched_title[title_norm] | matched_title[first_author_norm] ) matched_ids.update(matched_title[source_id]) remaining remaining[~remaining.index.isin(matched_title.index)].copy() else: matched_title pd.DataFrame() # -------- 第三级模糊匹配 -------- pool build_candidate_pool(goodreads_df) fuzzy_results [] for idx, row in remaining.iterrows(): result fuzzy_match_record( source_titlerow[title], source_authorrow[author], poolpool, ) if result: fuzzy_results.append({ source_id: row.get(source_id, idx), book_id: result[book_id], title_score: result[title_score], author_score: result[author_score], match_level: fuzzy, match_key: f{row[title]}|{row[author]}, }) matched_fuzzy pd.DataFrame(fuzzy_results) # -------- 合并结果 -------- result_parts [] if not matched_isbn.empty: result_parts.append(matched_isbn) if not matched_title.empty: result_parts.append(matched_title) if not matched_fuzzy.empty: result_parts.append(matched_fuzzy) if result_parts: final_df pd.concat(result_parts, ignore_indexTrue, sortFalse) else: final_df pd.DataFrame() return final_df5.3 运行主脚本 main.py# 文件路径main.py import pandas as pd from src.data_loader import load_csv from src.pipeline import run_pipeline def main(): # 加载数据列名按实际情况调整 libgen_df load_csv( data/libgen_books.csv, rename_map{ ID: id, Title: title, Author: author, ISBN: isbn, Publisher: publisher, Year: year, Language: language, }, ) zlib_df load_csv( data/zlib_books.csv, rename_map{ id: id, title: title, author: author, isbn: isbn, publisher: publisher, year: year, language: language, }, ) goodreads_df load_csv(data/goodreads_books.csv) libgen_df[source_id] LG_ libgen_df[id] zlib_df[source_id] ZL_ zlib_df[id] source_df pd.concat([libgen_df, zlib_df], ignore_indexTrue, sortFalse) linked run_pipeline( source_dfsource_df, goodreads_dfgoodreads_df, source_sluglibgen_zlib, ) linked.to_csv(output/linked_books.csv, indexFalse, encodingutf-8-sig) print(f总记录数: {len(source_df)}) print(f匹配成功记录数: {len(linked)}) print(匹配级别分布:) print(linked[match_level].value_counts()) if __name__ __main__: main()5.4 运行与预期输出python main.py预期输出类似总记录数: 539482 匹配成功记录数: 426173 匹配级别分布: isbn 281205 title_author 97734 fuzzy 47234这里要强调具体数字取决于数据质量。如果你的 LibGen / Z-Library 数据里 ISBN 覆盖率高第一级匹配占比就会更高如果 ISBN 缺失严重模糊匹配的占比会上升同时误匹配风险也会增加。6. 匹配结果验证怎么知道匹配得准不准6.1 抽样人工校验机器跑出来的匹配结果不能直接拿来用必须抽样检查。推荐做法从三个匹配级别里分别随机抽取 100 条记录人工对比源记录标题/作者与 Goodreads 标题/作者是否一致统计每一级的准确率如果某一级准确率低于 95%需要收紧阈值或增加校验条件。抽样结果示例匹配级别抽样数正确数准确率isbn1009999%title_author1009696%fuzzy1009191%6.2 自动校验规则除了人工抽样还可以在代码里加几条自动校验年份一致性源记录年份与 Goodreads 年份相差不超过 2 年页数/出版社一致性如果源数据有出版社字段可以比对出版社名称的相似度标题长度差异模糊匹配中如果源标题长度与候选标题长度差异超过 30%降低该匹配的置信度。# 文件路径src/validation.py def validate_year(source_year, goodreads_year, max_diff2): try: sy int(source_year) gy int(goodreads_year) return abs(sy - gy) max_diff except (ValueError, TypeError): return True # 年份缺失时不判断6.3 多对多冲突处理同一本书可能匹配到多个 Goodreads ID不同版本、不同出版年份。处理策略优先保留评分人数最多的 Goodreads 记录因为这通常代表最主流的版本如果业务上需要保留版本信息可以输出“一对多”关系表而不是强行选一条。def keep_top_rated(linked_df: pd.DataFrame, goodreads_df: pd.DataFrame) - pd.DataFrame: gr goodreads_df[[book_id, ratings_count]].copy() df linked_df.merge(gr, onbook_id, howleft) df[ratings_count] pd.to_numeric(df[ratings_count], errorscoerce).fillna(0) df df.sort_values(ratings_count, ascendingFalse) df df.drop_duplicates(subset[source_id], keepfirst) return df7. 常见问题与排查思路问题现象常见原因解决思路ISBN 匹配结果很少ISBN 字段格式混乱混合了 ISBN-10 和 ISBN-13统一先转成 ISBN-13再做 merge标题作者匹配结果大量重复同一个 Goodreads 书对应多个源记录或同一本书有多个版本对source_id去重保留评分最高的匹配模糊匹配速度太慢全量两两比对复杂度接近 O(n*m)按标题首字母分桶先用token_set_ratio取 top-1再校验作者匹配到错误的书作者名相似或标题过于简短如”It“、“1984”增加年份校验、出版社校验对超短标题强制要求 ISBN 匹配空标题或空作者导致全匹配数据清洗时没过滤空值每个匹配级别前都要过滤 title_norm 和 author_norm 为空的行年份字段变成浮点数导入时没有用 dtypestr 处理重新用 dtypestr 读入避免前导 0 丢失和科学计数法7.1 一个容易忽略的坑标题副标题处理The Great Gatsby和The Great Gatsby: A Novel在精确匹配中不会相等但在模糊匹配中相似度很高。如果你的业务希望保留副标题信息可以把副标题拆分出来用“主标题 作者”做精确匹配再把完整标题存到结果表里。def split_subtitle(title: str): if : in title: main_title, subtitle title.split(:, 1) return main_title.strip(), subtitle.strip() return title.strip(), 7.2 Python 中failed to run cargo metadata类报错如果你在安装rapidfuzz时遇到 Rust 编译相关的报错比如failed to run cargo metadata command to get workspace directory通常是因为本地缺少 Rust 工具链或 pip 尝试从源码编译。推荐优先安装预编译的 wheel 包pip install rapidfuzz --only-binary:all:如果仍然失败升级 pip 后再试pip install --upgrade pip setuptools wheel在 Windows 上还可以尝试安装 Microsoft C Build Tools或者直接使用 Anaconda 环境安装。8. 最佳实践与工程建议8.1 数据版本与血缘管理这类链接数据集的可信度与“源数据构建时间”“匹配算法版本”强相关。建议在输出数据里增加三个字段dataset_version数据集版本号source_dump_dateLibGen / Z-Library 数据导出时间goodreads_dump_dateGoodreads 数据导出时间matcher_version匹配算法版本例如v1.2.0。这样后续重新跑数据时可以追溯“哪一批匹配结果对应哪套算法”。8.2 增量更新策略LibGen / Z-Library 和 Goodreads 的数据都是动态更新的不建议每次全量重跑。更适合的做法的用源数据的id或updated_at字段识别新增记录只对新增记录跑完整匹配流程将新增匹配结果与历史结果合并并对同一source_id保留置信度最高的匹配。8.3 匹配结果中的多对多保留不要一开始就强行让每个源记录只匹配一个 Goodreads ID。建议保留一张“链接明细表”和多对多关系业务侧再根据自己的规则做去重source_id, book_id, match_level, title_score, author_score LG_12345, 88654, isbn, 100, 100 LG_12345, 88654, title_author, 100, 1008.4 性能优化建议用rapidfuzz替代fuzzywuzzy速度提升明显模糊匹配阶段按标题首字母分桶复杂度从 O(n*m) 降为 O(n * m/k)使用multiprocessing.Pool并行处理不同首字母分组在数据量级较大时考虑先用n-gram索引做候选集召回再用精确相似度排序。8.5 版权与合规提醒在做数据关联时需要明确区分“元数据”和“文件内容”。LibGen / Z-Library 中的元数据字段标题、作者、ISBN本身属于书目信息但在使用这些数据集时需要遵守数据来源平台的条款和当地法律法规。本文所讨论的技术方案仅涉及元数据层面的关联与清洗不涉及文件获取、下载或分发请确保你的数据来源合法合规并遵循最小必要原则。9. 总结与后续方向这篇文章从数据源分析出发走完了一条完整的图书元数据关联链路ISBN 规范化、标题作者归一化、三级递进匹配、结果抽样校验再到数据落库。核心思路可以概括为三句话能用规范化的精确匹配就不要一开始上模糊匹配匹配级别要带在结果里方便后续追溯和调参匹配结果必须做人工抽样校验机器分数不能完全替代人工判断。如果你的数据量比 539k 更大或者字段质量更差下一步可以从这几个方向继续深入用文本 embedding 对标题做向量化召回替代首字母分桶引入出版社、页数、丛书系列等多字段交叉验证构建“源记录 → 多个候选 Goodreads ID”的候选集再训练排序模型选择最佳匹配。动手把 pipeline 跑通再根据你自己的数据特点调一版匹配参数会比照抄任何现成方案都更靠谱。建议先用 1 万条子集跑通流程确认准确率后再全量执行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价