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

资讯详情

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

用Python从爬虫到推荐系统:手把手搭建旅游景点智能推荐

用Python从爬虫到推荐系统:手把手搭建旅游景点智能推荐 选这个项目一半是因为我自己的需求。当时想规划一次跨省自驾游翻遍了各种旅游攻略和打卡榜单发现大多数推荐无非是商业排序或者编辑拍脑袋跟我真正想去的人少、景美、交通方便完全不沾边。我手里正好又积累了一套通过Python爬虫采集回来的景点和用户评论数据那就干脆自己动手从数据采集到智能推荐系统完整做一遍。这篇文章是整个项目从立项到上线的完整复盘包含目标站点怎么分析、爬虫架构怎么设计、反爬怎么应对、脏数据怎么清洗以及推荐算法怎么一步步从能算变成好用。全程面向想用Python把爬虫、数据清洗、推荐系统串成一个真实项目的读者有完整思路也有可以直接复用的代码片段。1. 为什么自己动手做旅游推荐系统项目目标与整体拆解1.1 这个项目解决的真实问题市面上的旅游App推荐逻辑对用户来说是个黑盒。你搜索过某个景区、收藏过某篇游记系统下次就会拼命推同类内容但具体是看你最近浏览多一点还是看你历史行为多一点普通人根本不知道。我更在意的其实是另一面如果自己要做一个面向特定人群的推荐服务手里只有爬虫抓来的公开数据到底能不能撑起一套个性化推荐答案是可以但前提是把数据链路走通。这个项目的目标用户是自助游偏好者推荐对象是城市内的景点和路线。和美团、携程这类平台不一样我不需要精确到毫秒级的实时推荐也不需要几千万用户的行为日志我的核心诉求是当用户表达出我喜欢自然风光、不太喜欢人造景点、单日预算300以内这类偏好时系统能给出合理的推荐列表。为了达成这个目标数据层面需要三样东西景点基础信息包括名称、所在城市、经纬度、简介、门票价格、开放时间、评分用户对景点的评论数据特别是带情感的文本用来做后续的相似度计算用户自身的偏好行为也就是用户浏览过哪些景点、收藏过哪些景点、给哪些景点打了高分前两类数据靠爬虫采集第三类数据在早期没有真实用户时需要自己构造或者用公开数据集模拟。整个项目的技术链路也从这三个需求自然延伸出来。1.2 技术栈选型与理由技术选型方面我没有刻意追新全部选了Python生态里最稳、资料最全的组件模块技术选型选型理由网络请求requests requests.Session不需要异步高并发请求量在几万级别Session能自动维持Cookie页面解析BeautifulSoup4 lxml解析容错性好适合网页结构相对稳定的目标站点数据存储SQLAlchemy 2.0 SQLite开发期用SQLite免安装后期可以平滑切到MySQL数据处理pandas清洗、去重、重组字段比纯Python循环快一个数量级中文分词jieba评论数据必须分词后才能做文本向量化推荐算法scikit-learn 自定义规则TF-IDF向量化、余弦相似度都有现成实现协同过滤自己写也不难API服务FastAPI轻量、性能够用、自带Swagger文档方便调试没有上Scrapy是因为这个项目的站点规模不大用Requests手动控制请求队列反而更直观出了问题也更好排查。如果目标站点有几十万级页面或者需要分布式采集Scrapy会更合适但这个量级下没必要。1.3 数据流全景从采集到推荐的完整链路整个系统的数据流可以用一个直观的流程来描述目标站点分析 - 请求页面 - 解析提取字段 - 清洗去重 - 结构化入库 - 构建用户行为数据 - 计算内容特征与物品相似度 - 混合召回与排序 - 输出推荐列表。这条链路上最容易出问题的是两个衔接点一是从原始网页到干净数据表的转换二是从静态数据到可推荐特征的转换。前者考验爬虫和清洗功底后者考验对推荐算法的理解。很多项目死在中间地带——数据抓下来了但是脏得没法用或者能用了却不知道怎么变成推荐依据。所以我在做架构规划的时候提前拆成了三个独立模块采集模块、数据服务模块、推荐模块。三个模块之间用数据库表作为接口互不依赖。这样后续任何一部分出了问题调试时只需要看对应的表而不需要把整条链路重新跑一遍。2. 旅游数据源评估与爬虫架构设计先想清楚再动手2.1 目标站点的层级结构分析动手写爬虫之前最忌讳的就是打开一个页面就开始写解析代码。第一步应该是把目标站点的层级结构摸清楚。我选择的数据源包含三类站点第一类是景区官方网站。这类站点的景点基础信息最权威简介、开放时间、联系方式都是从景区运营方直接来的很少出错。但缺点是覆盖的城市有限不少小众景点根本没有官网。第二类是综合旅游平台的景点页面。这类平台的页面结构高度模板化城市列表到景点列表再到评论列表URL路径很有规律非常适合批量采集。但平台方通常有反爬措施频率控制很严格而且字段可能包含大量推广内容需要在清洗阶段过滤。第三类是游记分享平台的公开内容。这里的评论数据最有价值因为用户会用自然语言描述真实体验风景不错但交通不便、周末人太多不建议去这种信息是官方简介里永远不会写的。层级结构上一般有三级城市列表页 - 景点列表页 - 景点详情页。详情页下方再接评论列表加了分页。搞清楚这个树形结构之后爬虫的调度逻辑就很清晰了先采集城市列表再遍历每个城市下的景点列表然后逐个进入详情页采集深度信息最后翻页采集评论。2.2 单机多线程爬虫的架构设计这个项目的数据量大概在几万页单线程跑大概三四个小时多线程能把时间压缩到40分钟以内但代价是代码复杂度明显上升。我的设计思路是生产者-消费者模式队列作为中间缓冲。import queue import threading import time import requests class TravelSpider: def __init__(self, seed_urls, worker_num4): self.task_queue queue.Queue() self.result_queue queue.Queue() self.session requests.Session() for url in seed_urls: self.task_queue.put((city, url)) self.worker_num worker_num self.stop_flag threading.Event() def worker(self): while not self.stop_flag.is_set(): try: task_type, url self.task_queue.get(timeout3) except queue.Empty: break try: html self.fetch(url) parsed self.parse(task_type, url, html) self.result_queue.put(parsed) except Exception as e: self.log_error(url, e) finally: self.task_queue.task_done() time.sleep(random.uniform(0.5, 1.5)) def fetch(self, url): resp self.session.get(url, headersbuild_headers(), timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def run(self): threads [threading.Thread(targetself.worker) for _ in range(self.worker_num)] for t in threads: t.start() self.task_queue.join() self.stop_flag.set()多个worker线程从队列取任务但是共享同一个Session、同一个请求头模板。这里有个容易踩的坑requests的Session字段不是天然的线程安全多线程共用Session时如果同时发起请求偶尔会出现Cookie串线的问题。稳妥的做法是每个线程各建一个Session。2.3 robots协议与采集边界做爬虫不等于薅数据聊爬虫绕不开合规问题。我自己的处理原则很明确只采集目标站点的公开可访问页面严格遵守robots.txt声明采集频率控制在人手浏览的正常范围内不做任何绕过权限限制的操作不对站点服务器造成压力。具体落到代码层面我在启动采集任务前会先做一次robots检查from urllib.robotparser import RobotFileParser def check_robots(domain): rp RobotFileParser() rp.set_url(fhttps://{domain}/robots.txt) rp.read() return rp.can_fetch(*, fhttps://{domain}/)如果某个路径声明了Disallow那就直接跳过换下一个数据源。这次项目里确实有几个站点禁止了部分路径的抓取我没有去绕而是换了个数据源。做爬虫这行长期主义比短期收获重要得多一旦因为恶意采集被目标站点拉黑损失的是整个数据源的稳定性。3. 反爬处理与采集稳定性爬虫能不能跑得久全看这里3.1 请求头不完整的经典报错与修正很多新手爬到一半发现返回了一堆访问异常或者403第一反应是加代理、上验证码识别其实80%的情况只是请求头不完整。浏览器的HTTP请求会带十几个Header字段如果一个请求看起来不像浏览器发起服务端的反爬系统甚至不需要看IP和频率单凭Header就能直接拒绝。我踩过的具体问题是这样的最初我只加了User-Agent和Accept结果目标站点直接返回了一个空页面。后来看了正常的浏览器请求才发现Accept-Language、Referer、Sec-Fetch-*这些字段也是服务端校验的对象。完整的请求头模板参考def build_headers(refererNone): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: same-origin, Referer: referer or https://example.com/ } return headers注意Accept-Encoding里如果带上br需要确认自己的requests库是否支持brotli解码否则可能会得到乱码。我后来直接把它去掉用gzip就够。还有一个细节点同一个IP配合完整的请求头比十个IP配合残缺请求头要稳定得多。因为目标站点先识别的是你是否像一个真实用户其次才判断你的访问频率是否合理。我的建议顺序是先做好请求头再考虑限速最后才考虑代理池。3.2 限速、重试与退避策略反爬最核心的原则是不要让目标站点感觉到你的存在。我给自己定的采集速度标准是不超过每秒0.5个请求也就是每次请求间隔最少2秒。听起来很慢但24小时不间断跑也有4万多个请求对这个项目规模来说足够了。重试策略不能简单写个for循环重试必须带上指数退避。我的实现是第一次失败等2秒第二次等4秒第三次等8秒最多重试3次。如果3次都失败说明不是瞬时网络问题大概率是被封了或者接口变了这时候应该停止整个任务而不是无限重试。def fetch_with_retry(url, max_retries3): wait_time 2 for attempt in range(max_retries): try: resp session.get(url, headersbuild_headers(), timeout10) if resp.status_code 200: return resp.text elif resp.status_code in (403, 429): time.sleep(wait_time * 2) except requests.RequestException as e: log.error(fAttempt {attempt1} failed: {e}) time.sleep(wait_time) wait_time * 2 raise RuntimeError(fFailed after {max_retries} retries: {url})这里还有个容易忽略的点timeout参数一定要设置。如果不设置requests默认会无限等待服务端响应一旦目标站点某个请求挂住了整个线程都会卡死后面的任务全部堵塞。3.3 动态加载页面的应对思路旅游平台这两年改动比较激进很多列表页改成了AJAX动态加载直接请求HTML拿不到列表数据。这时候有两个思路第一个思路是直接分析XHR接口。按F12打开开发者工具的Network面板刷新页面看评论列表到底是请求了哪个接口返回的是JSON还是HTML片段。很多时候接口返回的是干净的JSON数据字段结构还比HTML解析更规整。这就是逆向接口的正当用法只分析公开的接口出入参不做任何越权操作。第二个思路是使用浏览器自动化工具比如Selenium或者Playwright。这类工具的代价是资源占用大、速度慢而且容易被检测。我的经验是能用XHR分析就尽量不进浏览器自动化只有那些接口参数做了加密、确实分析不了的情况才退回到浏览器方案。在浏览器自动化方案下也要注意设置无头模式、禁用图片加载、限制CSS渲染把速度提升上来。不过这个项目的评论数据后来通过XHR接口拿到了大部分真正用到浏览器自动化的场景其实只有两三个异常页面。4. 数据清洗与结构化存储从原始HTML到可用数据表4.1 解析选型BeautifulSoup还是XPath页面解析我通常先试BeautifulSoup因为它容错性好遇到HTML标签闭合不严的情况不会直接报错。但项目规模上来之后XPath效率更高、定位更精确特别是处理列表页时一个XPath表达式一次就能取到目标节点集合。举个实际例子采集评论列表时每条评论的结构是固定的div classcomment-item div classuser小明/div div classscore4.5/div p classcontent风景非常好但是爬山很累建议穿运动鞋/p span classdate2024-10-03/span /div用XPath可以一次性拿到全部字段from lxml import etree tree etree.HTML(html) items tree.xpath(//div[contains(class, comment-item)]) for item in items: user item.xpath(.//div[contains(class, user)]/text()) score item.xpath(.//div[contains(class, score)]/text()) content item.xpath(.//p[contains(class, content)]/text()) date item.xpath(.//span[contains(class, date)]/text())BeautifulSoup的find_all当然也能实现但代码会啰嗦不少。建议是页面结构简单用Soup结构复杂且固定用XPath两者都掌握实际工作中看情况选择。4.2 常见的脏数据形态与清洗规则我采集完第一批数据后做过一次统计大约有15%的记录存在明显的脏数据问题。大概分这几类第一类是HTML转义符残留比如nbsp;应该转成空格amp;应该转成。Python里直接用html.unescape()就能处理。第二类是字段错位。比如评论日期里混入了人在旅途这种个性签名或者评分字段里出现了4.8分/5分这种带说明的文字。第三类是经纬度数据缺失或者明显错误。有些景点的经纬度直接是0或者空这些记录在后续做推荐时会拖累坐标距离计算。第四类是评论内容过于简短比如只有好、不错、打卡这几个词这种文本在向量化后没有区分度需要过滤掉一般以字数少于4个字符作为门槛。清洗规则我写成了一个独立的处理函数逐条校验字段def clean_attraction(row): row[name] html.unescape(row[name]).strip() row[intro] re.sub(r\s, , html.unescape(row[intro])).strip() if len(row[intro]) 10: row[intro] row[score] extract_score(row[score]) row[lng], row[lat] validate_location(row[lng], row[lat]) return row清洗这一层看起来不写算法、不涉及推荐效果但它直接决定了后续模型输入的质量。脏数据一旦混进训练集推荐结果会非常诡异而且极难排查——因为你不知道这是数据问题还是算法问题。4.3 去重策略与自增主键的坑数据采集如果跑多轮就必然遇到重复问题。最典型的场景是同一个景点的简介页今天采集了一次明天因为新增评论又采集了一次基础信息字段都没变但评论表里新增了几条记录。我的去重策略不是靠数据库主键而是设计了一个唯一性业务键unique_key hashlib.md5( f{city}|{attraction_name}.encode(utf-8) ).hexdigest()这个unique_key保存在景点表里。每次插入新数据前先按unique_key查询如果已存在就执行更新而不是插入。这样即使同一景点被不同渠道采集到也能合并成一条。这里有个容易踩的坑SQLite的自增主键在删除记录后不会重置如果去重逻辑依赖主键id会出现最新记录主键比总数大很多的情况不影响功能但会误导排查。更重要的问题是如果主键是自增的数据导入导出、多源合并时很容易产生主键冲突。所以我的建议是核心表别用自增主键直接用哈希唯一键或者复合唯一键。4.4 SQLAlchemy建模的最佳实践SQLAlchemy的ORM写法在模型定义明确之后非常高效。我的景点表和评论表大概长这样from sqlalchemy import create_engine, Column, String, Float, Integer, Text from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class Attraction(Base): __tablename__ attractions unique_key Column(String(32), primary_keyTrue) city Column(String(32), indexTrue) name Column(String(128), nullableFalse) intro Column(Text, default) score Column(Float, indexTrue) ticket_price Column(Float, default0.0) lng Column(Float, default0.0) lat Column(Float, default0.0) open_time Column(String(128), default) update_time Column(String(32)) class Review(Base): __tablename__ reviews id Column(Integer, primary_keyTrue, autoincrementTrue) attraction_key Column(String(32), indexTrue) user_name Column(String(64), default) score Column(Float) content Column(Text) review_date Column(String(32))有个细节值得说字符串字段长度不能拍脑袋也不要全省一气地全用Text类型。user_name这种短字段用String就够content这种长篇文本才用Text。Length设置过大会导致索引膨胀设置过小会导致数据截断实际写入时才发现。先看一批样本数据的最大长度再加个50%的余量这是最稳妥的做法。5. 推荐引擎的核心算法用户画像、相似度与混合召回5.1 用户画像是怎么构建出来的推荐系统要生效首先得有用户的兴趣信号。在真实产品里兴趣信号来自浏览、点击、收藏、支付等行为。我的项目早期没有真实用户于是按照公开数据集的格式模拟了一批用户行为每条行为记录包括用户ID、景点ID、行为类型浏览/收藏/评分、时间戳。用户画像的核心动作是把行为数据映射到兴趣标签。比如用户收藏过西湖和九寨沟这两类景点都有自然风光标签那么用户画像里自然风光的标签权重就要升高。标签怎么打我用的方法是景点简介文本通过jieba分词后提取高频词作为候选标签再由一个预设的标签词典把高频词归并成大类。import jieba tag_dict { 自然风光: [山, 湖, 海, 森林, 瀑布, 峡谷], 人文古迹: [寺, 古城, 博物馆, 遗址, 故居], 亲子游: [乐园, 动物园, 科技馆, 沙滩], 极限运动: [攀岩, 漂流, 跳伞, 滑雪], } def extract_tags(intro_text): words jieba.lcut(intro_text) tags set() for category, keywords in tag_dict.items(): for kw in keywords: if kw in words or kw in intro_text: tags.add(category) break return list(tags)用户画像最终是一个稀疏向量用户ID对应的标签权重列表。这个向量是后面所有推荐计算的输入底座。5.2 内容相似度向量化与余弦相似度推荐系统里计算两个景点是否相似最直接的方法是比较它们的文本描述。先把所有景点的简介和评论拼起来做TF-IDF向量化然后计算余弦相似度。用scikit-learn做这个非常简单from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity corpus [f{a.intro} { .join(comments_by_attr[a.unique_key])} for a in attractions] vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(corpus) similarity_matrix cosine_similarity(tfidf_matrix)这里有个优化点直接把所有景点两两算相似度景点数量是5000的时候相似度矩阵是5000x5000内存占用大约200MB还能接受。但到了20000个景点矩阵规模就膨胀到3.2GB基本跑不动。解决办法是只对每个景点的Top N个相似景点存储用稀疏矩阵保存而不是保存全量。另外一个隐藏的坑TF-IDF对中文文本要先分词如果直接拿原始文本分词的结果喂给TF-IDF效果会很差。一定要先把jieba分词的结果用空格拼接成新的语料再传给TfidfVectorizer。5.3 基于物品的协同过滤实现细节文本相似度解决的是景点本身像不像协同过滤解决的是喜欢A景点的人也喜欢B景点这个行为规律。后者对用户行为的依赖更强个性化程度也更高。基于物品的协同过滤实现分为两大步第一步是构造用户-景点行为矩阵第二步是根据用户的历史行为找出相似物品。算法公式不复杂核心是计算两个景点之间的共现相似度from collections import defaultdict import math def itemcf(user_items): item_sim defaultdict(dict) item_count defaultdict(int) for user, items in user_items.items(): for i in items: item_count[i] 1 for j in items: if i ! j: item_sim[i][j] item_sim[i].get(j, 0) 1 for i in item_sim: for j in item_sim[i]: item_sim[i][j] / math.sqrt(item_count[i] * item_count[j]) return item_sim注意惩罚系数1 / sqrt(item_count[i] * item_count[j])如果一个景点被所有人收藏比如城市地标类的打卡景点它的共现次数会被这个系数大幅惩罚避免热门景点吞掉所有推荐结果。这个细节对推荐效果影响很大不加这个惩罚推荐列表里全是热门景点个性化程度会很低。5.4 混合召回与排序把多路结果合并到这一步我有三路候选来源基于内容相似的推荐适合冷启动的新景点基于物品协同过滤的推荐适合有行为需求的用户基于规则的热门兜底适合完全没有行为信号的新用户混合策略我采用的是加权融合去重重排。每路召回给一个基础分再乘以权重最后按总分排序。权重不是拍脑袋定的我试过几次调参发现协同过滤的权重应该超过内容相似度因为行为信号比文本信号更能反映真实兴趣。我的经验权重是协同过滤0.5内容相似度0.3热门兜底0.2。def hybrid_recommend(user_id, top_n10): candidates {} for item in cf_recall(user_id): candidates[item] candidates.get(item, 0) 0.5 for item in content_recall(user_id): candidates[item] candidates.get(item, 0) 0.3 for item in hot_recall(): candidates.setdefault(item, 0.2) for item, score in candidates.items(): candidates[item] score * freshness_penalty(item) top_items sorted(candidates.items(), keylambda x: x[1], reverseTrue)[:top_n] return top_items还有个需要处理的细节是去重。同一景点可能会出现在多个候选列表里权重加总没问题但如果它同时出现在协同过滤和内容相似度里基础分会偏高。这个偏差在可接受范围内因为一个景点被两路算法同时选中本来就是个强兴趣信号。6. 项目落地中的性能优化与冷启动问题6.1 冷启动的三种处理思路系统刚上线时没有用户行为数据协同过滤算法等于空转。这是推荐系统经典冷启动问题我用了三种策略组合应对。第一种是热门兜底直接推荐当前城市评分最高、评论数最多的景点。这个策略老土但有效新用户收到一个大家都说好的列表至少不会立刻流失。第二种是偏好问卷。用户首次进入系统时让用户选择几个偏好的景点标签比如自然风光、亲子游、人文古迹然后根据标签召回对应景点。第三种是利用设备或地区信息做默认画像。比如检测到用户的IP归属地是某个城市就把该城市Top景点作为候选池再结合少量内容相似度做排序。冷启动期的推荐质量不需要追求完美关键是别让用户面对一个空列表。目标是把用户留住等用户产生了几个浏览行为后协同过滤就能接管推荐。6.2 缓存与预计算的实战经验推荐系统在线请求如果不做优化每个请求都要实时计算相似度、跑召回、排序延迟会高到用户无法接受。我的做法是把能离线算的都离线算好线上只做查表。预计算的内容包括三块景点相似度矩阵、用户画像的标签权重、热门景点列表。这些数据以Redis或内存字典的形式缓存每天定时更新一次。线上推荐时服务端根据用户ID直接查缓存把候选列表拿出来排序即可单个请求的响应时间从几百毫秒降到了几十毫秒。这里有个容易被忽视的性能隐患Python的字典查询虽然快但如果有几千个用户同时请求每个请求都要排序一个几百项的候选列表CPU会飙高。解决办法是限制候选集大小在召回阶段就只保留前50个候选而不是全量排序。6.3 效果怎么评估离线指标与人工验证推荐系统的效果评估不能只靠我感觉推荐得挺准。我做了一套简单但能说明问题的评估流程。离线层面把用户行为数据按时间切分前80%做训练集后20%做测试集。推荐系统在训练集上生成推荐结果看测试集的Top N命中率Recall10和NDCG。这两个指标能说明推荐列表是否真的覆盖了用户之后实际发生的行为。在线层面我做了个小范围的人工试用请了十几个朋友实际用了一周。重点观察两个数据推荐列表的点击率和收藏转化率。如果点击率高但收藏率低说明推荐的东西用户愿意看但不够精准到愿意收藏需要优化排序权重。还有一个非常有效的定性验证方法随机抽几个用户看推荐列表里有没有明显不该出现的搭配。比如用户只收藏过城市人文类景点结果推荐列表出现大量极限运动景点这种错误说明画像构建或者权重设置有漏洞需要回头排查。6.4 从采集到推荐的扩展思路项目做到这个程度主体功能已经完整但想再往上走有几个明确的扩展方向。第一个方向是加入实时数据管道。目前的数据采集是批处理每天定时跑一次。如果要响应节假日效应或者突发热点事件可以用消息队列把爬虫采集、清洗、入库、推荐更新串起来实时性会好很多。第二个方向是引入更丰富的信号源。除了文本评论还可以尝试采集图片数据和点评中的具体出行时间、出行方式、人均消费等结构化字段。这些额外信号能让推荐结果从按标签匹配变成按场景匹配。第三个方向是推荐解释的生成。我在实际使用中发现用户对推荐结果的信任度很大程度上取决于为什么推荐这个。如果能根据用户行为生成一句解释比如因为你收藏了西湖系统为你推荐了同属自然风光的千岛湖体验会完全不一样。生成解释不需要大模型基于规则拼接就能做出不错的效果。我最后想说的是这个项目最大的价值不在于推荐算法本身有多高级而在于把整个数据链路走通了。爬虫采集的数据经过清洗变成结构化存储再变成推荐系统的输入特征最后服务于具体的推荐业务。每一个环节单独拿出来都有很多文章能写但真正把它们串起来你才会理解数据质量、特征工程、算法选型之间的耦合关系。我强烈建议对爬虫和推荐系统感兴趣的朋友找一个小领域从数据采集一路做到最终推荐完整跑一遍这比看一百篇教程都管用。
返回列表