数据清洗(二):岗位职责与要求的分离

    在现有的全部互联网招聘网站上,岗位信息里的全部条目都是在同一级标签下。所以,岗位信息做为一个总体,就须要额外的操做把要求与职责分离开。鉴于岗位信息里数据格式的不统一,所以博主放弃了使用正则表达式的方法,而是选择了模糊匹配+结构化匹配,将字符串比较的问题转化成了几率问题。html

 

1、数据存储结构

    在以前写的爬虫里,岗位信息一栏使用Xpath的String()方法抓取,做为一个大的字符串,全部信息都位于一个单元格中。如今计划在爬虫运行时,获得岗位信息后就将其分离,再写入硬盘中。因此,爬取数据时的格式会极大的影响分离的方法,字符串适合使用正则表达式,可是在格式混乱的岗位信息中,这显然不是完美的解法,如'岗位职责',与之相似的还有'工做内容','职位描述'等等,这些词的各类排列组合会极大的增长正则表达式的长度。正则表达式

    因此我决定将每一行信息都转化为数组的一个元素,再经过上下文信息与其自身的词汇信息判断其归属。在我爬取的51job移动端中,岗位信息的条目都在标签<article>下,所以使用//text()方法,将<article>标签下每一行的信息都转化为一个数组元素。编程

  1. info = selector.xpath('//*[@id="pageContent"]/div[3]/div[2]/article//text()')  

 

 

图表 1 数据在源码中的位置数组

 

2、数据的上下文关系

    上图所示的数据格式是最完美的,只须要正则表达式就能匹配成功。每一条数据都含有信息,'岗位职责'与'岗位要求'预示下文的数据与这个主题相关,其他信息则属于某一个主题。因此对这类结构化很是明显的信息,只须要匹配出'职责'与'要求'便可完成数据的分离。app

    另外一种状况以下所示,职位描述里包含了一眼就能看出来的岗位职责与要求,可是职责头信息缺失,经过上下文没法得出该信息的归属。所以,对于这类上下文无关的信息,就须要单独进行处理。机器学习

图表 2 缺乏主题的jd 函数

    经过上述分析,就得出了这样一个处理流程:若是如今处理的信息属于头部信息(职责、要求等),则进入结构化处理流程,不然单独处理。学习

图表 3 流程图测试

 

3、模糊匹配

    因为对相赞成思的不一样表述,以及输入过程当中可能会出现的错误,所以使用模糊匹配来近似地查找与字符串匹配的字串。网站

    字符串模糊匹配( fuzzy string matching)是一种近似地(而不是精确地)查找与模式匹配的字符串的技术。换句话说,字符串模糊匹配是一种搜索,即便用户拼错单词或只输入部分单词进行搜索,也可以找到匹配项。所以,它也被称为字符串近似匹配。

    先导入第三方库fuzzywuzzy:

  1. from fuzzywuzzy import fuzz  

    fuzz有五个经常使用的函数,先作一个简单的测试来看看区别。

    函数功能就像它们的名称同样,通俗易懂。再换一个长一点儿的:

    因此我选择partial_token_sort_ratio()的值做为判断的依据。

经过分析ratio函数的源码,能够发现ratio()函数的求值公式:

M是匹配的元素个数,T是字符串长度。因此针对partial的函数,咱们能够用2/(len(thisStr))*100来判断字符串是否知足模糊匹配。

另外有一个小问题,

    后来发现,str2 = 'word1word2word3'时,word1出如今str1中,则匹配失败。因此在str2前加入一个字,取'工做'后一字,则解决问题。

 

4、模糊匹配自定义数据集

    正如正则表达式须要本身定义匹配的字符同样,模糊匹配也须要本身定义一个相似的字符集。咱们总共须要四个字符集,分别是岗位要求与职责头的字符集,以及具体要求的字符集。

  1. str_responsibility = '做职责描述介绍内容'  
  2. str_requirement = '能力要求需求资格条件标准'  
  3. str_line_res = '负责基于构建根据制定规范需求'  
  4. str_line_req = '经验熟悉熟练掌握精通优先学历专业以上基础知识学习交流年龄编程了解'  

    

5、代码实现

    函数parse()做为信息处理的入口,接收一个含有岗位信息的数组,返回一个数组,数组元素分别是岗位职责与要求。

    因为爬取的信息含有大量制表符与空字符,因此须要排除无效的信息,并用一个新的数组'ls_jd'存储岗位信息。

  1. def parse(ls):  
  2.     if len(ls) == 0:  
  3.         return ['null','null']  
  4.     ls_jd = []  
  5.     result_res = []  
  6.     result_req = []  
  7.     str_responsibility = '做职责描述介绍内容'  
  8.     str_requirement = '能力要求需求资格条件标准'  
  9.     for i in range(len(ls)):  
  10.         str_line = str(ls[i]).strip()  
  11.         if len(str_line.strip()) < 2:  
  12.             continue  
  13.         else:  
  14.             ls_jd.append(str_line)  

    再声明一个变量Index,用来记录如今读取到数组元素的下标。

    使用一个循环,从第一个元素开始,依次读取数组元素,并求得其与'岗位职责'字串、'岗位要求'字串的类似度,再分别进行匹配。

    另外,因为须要对循环元素进行操做,因此不能使用for循环,所以此处使用了while()。

  15. index = 0  
  16. while int(index) < len(ls_jd):  
  17.     str_line = ls_jd[index]  
  18.     if len(str_line) < 10:  
  19.         fuzz_res = fuzz.partial_token_sort_ratio(str_line, str_responsibility)  
  20.         fuzz_req = fuzz.partial_token_sort_ratio(str_line, str_requirement)  
  21.     else:  
  22.         parse_line(str_line,result_res,result_req)  
  23.         index += 1  
  24.         continue  
  25.     if fuzz_res > fuzz_req and fuzz_res >= (2/len(str_responsibility)*100):  
  26.         index = parse_res(index,ls_jd,str_requirement,result_res)  
  27.     elif fuzz_req > fuzz_res and fuzz_req >= (2/len(str_requirement)*100):  
  28.         index = parse_req(index,ls_jd,str_responsibility,result_req)  
  29.     else:  
  30.         print('warn: '+ str_line)  
  31.     index += 1  

    基于结构的信息提取会修改当前读取数组的下标,因此须要将当前函数内读取到的下标返回。参数里传列表,实际上传的是地址,因此结果不须要额外操做。

  32. def parse_res(index,ls,str_break,result_res):  
  33.     # print('岗位职责()Start')  
  34.     while index < len(ls)-1:  
  35.         index += 1  
  36.         fuzz_break = fuzz.partial_token_sort_ratio(ls[index], str_break)  
  37.         if fuzz_break < 49:  
  38.             result_res.append(ls[index])  
  39.         else:  
  40.             return index-1  
  41.     return index  
  42. def parse_req(index,ls,str_break,result_req):  
  43.     # print('岗位要求()Start')  
  44.     while index < len(ls)-1:  
  45.         index += 1  
  46.         fuzz_break = fuzz.partial_token_sort_ratio(ls[index], str_break)  
  47.         if fuzz_break < 49:  
  48.             result_req.append(ls[index])  
  49.         else:  
  50.             return index-1  
  51.     return index  
  52. def parse_line(line,result_res,result_req):  
  53.     str_res = '负责基于构建根据制定规范需求'  
  54.     str_req = '经验熟悉熟练掌握精通优先学历专业以上基础知识学习交流年龄编程了解'  
  55.     fuzz_res = fuzz.partial_token_sort_ratio(line, str_res)  
  56.     fuzz_req = fuzz.partial_token_sort_ratio(line, str_req)  
  57.     if fuzz_res-1 >= (2/len(str_res)*100):  
  58.         result_res.append(line)  
  59.     elif fuzz_req-1 >= (2/len(str_req)*100):  
  60.         result_req.append(line)  

 

6、结果

    能够看出仍是有一些小问题的。

7、改进与设想

    基于几率匹配的信息分解受制于自定义匹配数据的完整性,尽管目前给出的几个关键词囊括了大部分的状况,不过仍然有至关多的漏网之鱼。

    另外,fuzz库的模糊匹配并不能完美适配这次字符匹配,除了第三大点后给出的问题外,对长句中不一样词语应该有不一样的权重,以免长居中词太多致使匹配失败。

    不过最可喜的应该是,此次有了相对统一的数据,回头能够用这些数据去Spark上跑个模型,亲自操刀一下机器学习了,哈哈哈。

相关文章
相关标签/搜索