首页 / 资讯中心 / 文章详情

数据获取与处理实战:从数据源选择到数据清洗的完整指南

数据获取与处理实战:从数据源选择到数据清洗的完整指南 ★ FEATURED ARTICLE
这几年做数据相关的工作被问得最多的一个问题是“我该从哪儿拿数据拿到手之后又该怎么处理”说实话大多数人在第一步就卡住了要么不知道怎么稳定地拿到数据要么千辛万苦拿到一份Excel结果里面全是乱码、重复行、格式不一致光清洗就耗掉半天。我把这些年“踩坑攒经验”的过程整理了一下这篇东西不聊虚的全是数据处理与获取过程中真正能落地的技巧怎么选数据源、怎么写一个不容易崩的请求函数、怎么把脏数据洗得干干净净、怎么存才能方便后续使用以及我在实际项目里遇见的各种奇葩问题和排错思路。适合准备入行的数据分析师、刚接手数据采集任务的开发以及所有被“脏数据”折磨过的人。1. 数据获取的先决条件先想清楚来源再动手写代码很多新手拿到需求的第一反应是“打开网页、右键查看源代码、开爬”。但这样做十次有八次要返工因为数据获取不是“写个请求”这么简单你选的数据源直接决定了后续所有环节的工作量。所以真正开始前我建议先花时间做一个判断数据到底要以什么方式获取是最优先的。1.1 三类常见数据源怎么选最省力我平时把数据源分成三类第一类开放API接口。这是最理想的情况。平台方已经封装好了数据输出格式你只需要看文档、申请权限、按照参数调用即可。数据结构稳定更新及时也不容易触发访问限制。缺点是接口往往有频率限制、需要Token、有些数据要付费或者申请审核。第二类静态HTML页面。很多行业网站、公开统计页面、政府公示页数据都在HTML里不涉及动态加载。这种页面直接用HTTP库请求然后解析HTML拿数据过程比较简单粗暴。风险点在于页面改版会导致解析代码失效所以一定要把解析逻辑封装好方便维护。第三类动态渲染页面。比如用React、Vue做的网站数据不是直接写在HTML源代码里的而是页面加载后通过JavaScript发请求再渲染。这时候千万别一上来就搞无头浏览器先按下F12打开开发者工具里的“网络”面板刷新页面找XHR或Fetch请求。数据绝大多数情况都在某个JSON接口里直接请求那个接口速度比无头浏览器快得多成功率也高。举一个真实遇到过的例子。有一次我要抓某个行业指数网站的历史数据直接用HTTP请求首页发现返回的HTML里全是空的div。打开网络面板一看发现页面加载后会请求一个带日期参数的JSON文件返回就是完整的时间序列。我直接把请求地址里的日期参数改掉绕开了所有的DOM解析一分钟就拿到了十年的数据。这就是为什么我总说动手之前先打开控制台看看流量走了哪些请求远比自己瞎猜要靠谱。1.2 请求工程的基础操作超时、重试、请求头都要认真对待很多人写请求代码翻车不是不会requests而是没把“工程化”这件事做好。所谓的“工程化”体现在几个不起眼的小地方请求头要构造像真实用户。最基本的User-Agent一定要设置。有些网站还会检查Referer意思就是看你这个请求是从哪个页面过去的。模板可以这样import requests from fake_useragent import UserAgent HEADERS { User-Agent: UserAgent().random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, }需要传Referer时继续往里面加。注意fake_useragent这个库偶尔会命中反爬策略里的“非浏览器特征”所以更稳妥的做法是准备一个固定的、常用的浏览器UA字符串而不是每次都随机。超时和重试必须具备。网络请求不设置超时程序会一直挂在那里尤其在批量抓取时一个卡死的请求能把整个流程堵住。我自己习惯用元组形式同时设置连接超时和读取超时resp requests.get(url, headersHEADERS, timeout(3, 10))如果请求失败比如连接被重置、超时、返回5xx不能直接就放弃建议加一个带退避的重试逻辑import time import requests from requests.adapters import HTTPAdapter s requests.Session() s.mount(https://, HTTPAdapter(max_retries3)) def fetch(url, retries3): for i in range(retries): try: resp s.get(url, headersHEADERS, timeout(3, 10)) if resp.status_code in (200, 404): return resp if resp.status_code in (429, 500, 502, 503): time.sleep(2 * (i 1)) continue except requests.RequestException as e: time.sleep(2 * (i 1)) return None这里面的逻辑是404说明资源本身不存在没必要重复请求429/5xx属于临时性问题退避重试一下大概率能过。控制请求频率。不要一口气发几百个请求特别是对没有你账号体系的公开网站。合规的获取方式应当遵守数据源方的使用条款与限速要求这是前提。如果确实需要大量数据先把请求间隔控制了比如每请求一次sleep 0.5到1秒。慢是慢一点但胜在稳定。提示数据获取必须遵守目标网站的服务条款与robots规则仅获取公开信息并且在用于商业用途前确认数据授权方式。很多纠纷都是因为过度请求或滥用数据导致的这一条值得给所有做数据的人提个醒。2. 从网页和接口中精准抽取目标数据数据源一旦确定接下来就是“解析”。这块最忌讳的就是把解析逻辑写成一次性脚本只跑一次就完事。只要你的数据需要定期更新就应该把解析代码写得足够灵活这样哪怕页面结构微微变化你也只需要改一小段配置而不是重写。2.1 HTML解析的工具选型与组合策略解析HTML有哪些选项正则表达式、XPath、CSS选择器、BeautifulSoup、lxml等等。我的实践经验是这样正则表达式适合处理非常规整、短小且没有嵌套的文本。比如从一段JavaScript代码里提取json字符串、从页面中提取邮箱或电话号码。但它不适合解析整个HTML文档因为HTML的标签嵌套太深正则根本处理不了层级关系硬写只会把自己绕晕。BeautifulSoup lxml 是新手最顺手的组合。尤其是find、find_all配合CSS选择器写起来很直观。但因为BeautifulSoup对节点操作非常灵活大型页面上性能不算最好。如果抓的是列表页还好一旦遇上那种几万行的大表格解析速度会明显变慢。lxml直接配合XPath是我目前的主力方案。XPath能精准地表达“我要第几个表格里的第几行”这种复杂逻辑而且底层是C语言实现速度很快。一个典型的写法from lxml import html tree html.fromstring(page_source) # 提取所有tr节点 for tr in tree.xpath(//table[iddata_list]//tr): cells tr.xpath(./td/text()) if not cells: continue record [cell.strip() for cell in cells] print(record)为什么用lxml而不是正则因为数据抽取最重要的是“定位”而不是“匹配文本”。XPath让人能直接依赖标签层级和属性进行定位比如//tr[contains(class, item)]这种写法正则写起来会非常痛苦。2.2 JSON接口返回数据怎么处理现在越来越多的数据源直接返回JSON这比解析HTML要省力得多。但JSON也有自己的问题嵌套特别深、字段名不统一、数组结构模拟两可。处理这块我常用的套路是先用json.loads把字符串变成字典。再用jsonpath-ng或者标准的递归遍历去抽取目标字段。遇到缺失字段时用.get方法指定默认值不要直接抛异常。如果要把嵌套JSON展平成表格直接用pandas.json_normalize它能把层级嵌套的列表、字典自动展开成二维表格。举个例子import json from pandas import json_normalize raw json.loads(resp.text) records raw[data][list] df json_normalize(records, sep_)这里有个经验接口返回的字段名通常比较“开发视角”比如user_id、created_at但在后续分析时我们可能希望它叫用户ID、创建时间。规范的流程是取到原始数据后立刻做一次字段重命名形成一个标准化的schema这样下游不管谁用看到的名字都是稳定的。2.3 表格抽取的隐蔽坑位网页里最常见的结构化数据载体是HTML表格但想从表格里提取出干净的数据并不像看起来那么简单。我经常遇到这么几种情况表头不固定。有些页面第一行是标题第二行才是数据表头有些页面有合计行混在中间。写解析时不要用死位置至少要加一个判断这行的第一个单元格是否为“序号”“名称”之类的表头标识。合并单元格。在展示层里表格经常会把相同内容的相邻行合并成一个单元格但HTML源码里可能只有一个td这就导致下面的行缺列。处理方式是根据当前行的列数动态补齐或者用前一行合并单元格里的值来填充当前行缺失的字段。翻页接口位置不对。大部分翻页除了“下一页”按钮还有一个隐藏的页码参数。建议不要用正则去抓“下一页”按钮的href因为很多站点的翻页是JavaScript动态生成的事件并没有真正的href属性。最好的办法仍然是打开网络面板观察点击下一页时候产生了哪个新请求。3. 数据清洗把“能看”的数据变成“能用”的数据我一直觉得获取数据只是第一公里的活儿数据清洗才真正决定你后续分析是否靠谱。很多情况下拿到的数据格式千疮百孔根本原因是数据生产方自己的录入格式就不统一。清洗的本质不是写一长串代码而是定义“什么叫干净的数据”然后逐个把脏数据转化成你定义的标准形态。3.1 脏数据的常见面谱与整体清洗流程我在实际项目里见过的脏数据大概有这几类文本格式不统一。同一列里有人填“1000元”有人填“1,000”有人填“一千”还有人是全角数字。这种情况需要统一转半角去掉货币符号和千分位分隔符再识别人工中文数字。日期格式混乱。同一个日期列可能出现“2023-01-01”“2023/1/1”“20230101”“2023年1月1日”等多种写法。可以用pandas的to_datetime配合format参数去解析但纯靠函数解析不保证100%准确最好先把日期统一转换成标准字符串格式。空值处理。拿到数据后先看每列的空值比例。空值不是非得填充要看业务含义。如果订单金额空可能表示未支付或数据缺失处理策略完全不同。重复记录。特别是整合多个来源数据的时候同一实体出现多次。重复的定义也要先想清楚是主键完全相同才算重复还是某些关键字段相同就算重复。异常值。比如年龄出现200销售额出现负数。异常值不能直接删要留一份原始存档并记录删除原因这样后续复盘的时候能说清楚数据为什么少了一截。我的清洗流程基本固定为六步备份原始数据 - 定义字段类型与格式标准 - 去重 - 缺失值处理 - 异常值识别 - 输出清洗报告。3.2 用Pandas高效处理常见清洗场景Pandas是做数据清洗最顺手的工具但很多人只会用str.strip()和fillna()。我分享几个实际使用频率很高的组合动作文本清理实战模板df[金额] ( df[金额] .astype(str) .str.replace(,, , regexFalse) .str.extract(r(-?\d\.?\d*))[0] .pipe(pd.to_numeric, errorscoerce) )这个思路是先去掉千分位逗号再用正则把数字部分提取出来最后统一转成数值类型。注意extract返回的是DataFrame所以取[0]才变成Series。单位归一化有些数据里金额单位写成“万”有些写成“亿”分析时如果不统一就是灾难。我习惯这样处理def parse_money(text): if isinstance(text, str): if text.endswith(亿): return float(text[:-1]) * 100000000 if text.endswith(万): return float(text[:-1]) * 10000 return float(text) return text这个简单逻辑能解决80%的单位问题。注意有些字符串里带空格先strip()再判断。日期解析df[日期] pd.to_datetime(df[原始日期], format%Y-%m-%d, errorscoerce)这里必须提一句errorscoerce的意思是解析失败时填NaT缺失时间。加上这个参数不会让程序崩溃但你还得回过头来看一下有多少行变成了NaT如果比例太高说明原始格式判断错了。重复值处理df df.drop_duplicates(subset[主键字段], keepfirst)keepfirst表示保留第一次出现的记录。如果你的业务是每天跑增量那么更合理的做法是保留最新一条设置keeplast。3.3 编码错乱与“看不见的空格”怎么解我一度以为乱码问题只存在于老旧的Windows系统直到后来发现哪怕数据源是网页也可能因为服务端返回的编码声明与实际内容不一致导致乱码。解决办法很简单resp.encoding resp.apparent_encoding这行代码让requests用响应内容反推编码虽然偶尔不准但比默认的iso-8859-1要好太多。下载到本地文件后如果读取乱码可以先用chardet检测import chardet with open(data.csv, rb) as f: raw f.read() encoding chardet.detect(raw)[encoding]然后重新按检测到的编码读取。另一种坑是“看不见的空格”。字符串里可能包含\u3000全角空格、\xa0不间断空格、零宽字符等。普通strip()处理不了需要这样清df[文本] ( df[文本] .str.replace(\u3000, , regexFalse) .str.replace(\xa0, , regexFalse) .str.strip() )4. 数据存储与输出别让Excel成为唯一归宿数据清洗完不代表事情结束怎么存、怎么交付同样影响后续的使用体验。很多人习惯把结果直接扔给Excel但有些场景下Excel并不是最优解。4.1 不同存储场景的选型逻辑我个人的选择标准是这样如果要交付给不懂技术的人看优先Excel。但注意两点一是写入Excel之前把字段用英文或规范的拼音命名避免一些老版本软件打开时报表乱码二是用openpyxl或pandas.to_excel导出时可以顺便设置列宽、冻结首行让业务方一眼能看懂。如果数据量几百万到几千万行但并不需要多人同时写用SQLite足够。SQLite是单文件数据库不需要单独部署服务端随取随用尤其适合做本地数据处理过程中的中间层。如果数据需要支撑一个在线分析系统或者Web应用那就要用MySQL、PostgreSQL这类成熟的关系型数据库。写入时不要一条条insert用批量写入比如executemany或to_sql一次性插入几千行。贴一个Pandas直接写数据库的例子from sqlalchemy import create_engine engine create_engine(sqlite:///output.db) df.to_sql(table_name, engine, if_existsreplace, indexFalse)这里的if_existsreplace是全表覆盖模式。做增量更新的时候更常用if_existsappend或者先查已有主键只插入新增行。如果只是给自己做后续分析和机器学习直接存成Parquet。Parquet格式有压缩、列式存储读取速度明显优于CSV。可以用三行代码搞定df.to_parquet(data.parquet, indexFalse) new_df pd.read_parquet(data.parquet)4.2 文件输出避坑编码、大文件与增量更新写CSV文件的时候最经典的问题就是用Excel打开中文乱码。解决办法只有一个输出的编码必须是utf-8-sig这样才能带BOM头。如果你用UTF-8不带BOMExcel默认用本地字符集解析就会乱。所以df.to_csv(output.csv, indexFalse, encodingutf-8-sig)如果文件特别大比如几个GB不要用Pandas一次性读入内存再转存。正确做法是分块读取、分块处理、分块写出。比如从数据库里按主键分页拉取每次取5万行清洗完追加到目标文件。这样内存占用始终可控程序也不会因为内存爆炸而挂掉。还有一个容易被忽略的问题重复跑同一个任务的时候怎么保证没有重复数据我的建议是“先查后插”。在写入目标表之前先按主键字段查询目标表已有的ID集合然后只插入不存在的记录。如果主键不是单一字段那就拼接一个业务主键字符串来做对比。这个方法很简单但能保证数据的幂等性比单纯依赖drop_duplicates稳妥得多。5. 高频问题与排查技巧实录数据处理与获取过程中真正让人头秃的往往不是哪种算法高级而是那些“莫名其妙”的问题。我挑几个频率最高的把排查思路和对应的处理方式都列出来。5.1 请求类问题的判断顺序遇到403先确认是不是没有带User-Agent或Referer然后检查是否需要登录后的Cookie。在自己账号权限允许的前提下可以登录后把Cookie放进请求头但还是那句话一切以数据源方的条款和授权为准。另外有些服务器的403是安全策略拦截了不常见的行为特征比如请求头顺序、HTTP版本、指纹一致性问题。先把请求头里的多余字段删掉只保留最基础的几个往往就能解决。遇到429说明请求太频繁被限流了。我试过一边sleep 1秒一边请求还是被限后来发现是并发线程太多。解决办法是把请求间隔拉大并且把任务分批每批跑完休息一分钟。没有万能参数核心是在不影响自己和别人正常使用的前提下做一个足够有礼貌的调用者。页面内容为空先确认返回的text里有没有你要的关键词。如果HTML里没有大概率数据是动态加载的。这时回到第1部分讲的去网络面板里找XHR/JSON接口。如果接口也找不到看看是不是内容在script标签里以window.__DATA__这样的全局变量形式存在可以用正则提取。5.2 数据清洗类问题的经典场景全表日期解析失败多半是日期格式里混杂了null、空字符串或者非标准写法。可以先用正则把原始字符串里的日期片段提取出来再交给to_datetime。比如先把所有分隔符统一成-把中文年月日替换掉再解析成功率会高很多。数值列突然出现大量NaN除了原始数据确实为空更常见的是类型转换失败。比如一个“金额”列里既有数字又有“待定”这样的字符pd.to_numeric就会把整列转不过来。我的做法是保留原始列新建一个标准化列并且统计转换失败的行数避免查到一半才发现大量数据被置空。处理完数据行数对不上要么是去重过于激进要么是解析时漏掉了某页。建议清洗前后都打印一个shape并且把主键的次数分布打出来看看哪行数据被重复计数或丢失。5.3 程序稳定性问题程序跑一半断了这种事情不可避免。我的解决方案是“断点续跑”把已经成功获取并解析的数据实时落盘成CSV/JSON文件每次启动任务时先判断本地是否已经存在结果文件如果存在就跳过那些已经处理过的记录。这个思路不仅防止重复请求也能让你在程序意外挂掉时不会一夜回到解放前。内存占用过高首先看是因为数据量太大还是代码里放了大量的中间变量。排查方法是把循环体里创建的临时变量清掉或者干脆用生成器逐行迭代而不是把所有结果都append到一个大列表里再转DataFrame。实在要用列表暂存那就每处理一万行就把这个列表转存成文件并清空列表。字段顺序乱了多文件合并时两个文件可能字段顺序不一样。用pandas合并时别依赖位置要用列名对齐比如pd.concat([df1, df2], joinouter)然后手动调整最终列顺序。我整理了一张速查表方便大家遇到问题的时候快速对照现象可能原因解决思路请求失败状态码403反爬策略拦截检查请求头、频率、账号权限返回内容与浏览器不同动态加载数据寻找XHR/JSON接口中文乱码编码识别错误设置resp.encoding或用chardet检测日期解析失败格式混用统一分隔符与年月日表达后解析清洗后行数变少去重或过滤过严检查主键重复度与过滤条件程序中途内存溢出过度堆积中间数据分块处理及时释放变量重复写入数据库无幂等控制先查主键再插入输出Excel字段错位合并时使用了位置对应用列名对齐统一schema6. 把整套流程变得可维护日志、调度与自检写脚本跑一次跟写一套可持续运行的流程差别很大。前者随时能跑通一次就够了后者却要求你有“长期主义”的思维哪怕数据源结构变了、服务器抽风了、某个字段没了你也能第一时间发现并定位。6.1 日志与运行状态记录别靠肉眼盯控制台现在的数据处理脚本多少会有一点定时执行的需求人不可能天天守在电脑前看它跑完。所以我习惯在每个阶段都输出结构化的日志至少包含时间、任务名、状态、影响行数。不要用一堆print用logging模块就够了再把日志写到文件里import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(etl.log, encodingutf-8), logging.StreamHandler() ] ) logging.info(开始拉取第1页数据当前记录数%d, start_count)等到脚本跑完再看一眼“成功数”和“失败数”。如果失败率突然变高通常不是网络波动而是页面结构变了。6.2 定时执行与增量更新用最简单的方式养成习惯我见过很多团队硬上Airflow等重量级调度框架来处理日常几万行的数据任务其实有点杀鸡用牛刀。对绝大多数个人或小团队场景直接用系统自带的cron按时跑脚本就够了# 每天凌晨2点执行一次 0 2 * * * cd /data/project python main.py logs/cron.log 21如果要用在执行机上跑并且需要任务编排还是要保证脚本是“可重复执行”的。具体做法是每次执行前先记录开始时间执行期间处理逻辑只处理某个时间窗口之后的新数据执行完后把时间窗口往前推一格存成状态文件。这样就算某天任务重跑也不会重复处理历史数据。增量更新的核心思想从来不是写多复杂的框架而是“主键时间戳”。只要目标表里存在可对比的主键就用“存在则更新不存在则插入”的方式。用SQL表达就是先查一遍已有主键把剩下的新增数据批量插入。6.3 数据正确性自检跑完不代表跑对长期运行的数据任务最怕“每次都能跑成功但数据结果悄悄变了”。所以我每次跑完之后都会加一个自检环节一般包含三个动作数量级核对。比如今天的记录数跟昨天比浮动超过30%就要告警。就算业务真的有波动也要人工确认一遍。关键字段抽样。随机抽取十行数据打印原始页面里的样本与清洗后表格里的样本人工或脚本比对几个核心字段是否一致。schema校验。检查输出DataFrame的列名是否跟预期的标准schema完全一致多一列少一列都要输出来。这一步可以用简单的断言实现assert set(required_cols).issubset(df.columns), f缺列: {required_cols - set(df.columns)}这一套下来数据正确性有底后面别人问起来也能拿日志和自检记录说明结果可信。在这几年的实操经历里我自己最大的体会是做数据处理工作千万不要在一开始追求“完美代码”而是要把“可复现、可排查、可恢复”放在第一位。数据源是变化的业务需求也是变化的一套流程里最值钱的并不是那几行爬虫代码而是你在运行过程中沉淀下来的日志、状态文件、清洗规则和排错经验。每次处理一份新数据我都会先把原始数据原封不动存一份再另存一版清洗后的数据。两个版本都留着后面哪怕发现清洗规则写错了也能从原始数据重新生成一次而不会因为源头数据被覆盖而无从下手。最后再分享一个小技巧所有清洗规则尽量写成配置文件或者独立的函数不要散落在各种脚本里。把“金额单位统一” “日期格式标准”这类逻辑抽出来下次遇到同类型数据还能复用。数据获取和处理这件事做得多了你会发现稳定比速度重要规则清晰比代码炫技重要。希望对正在跟数据打交道的你有帮助。
阅读完成 · 觉得有帮助?
咨询建站