數據清洗(二):崗位職責與要求的分離

    在現有的全部互聯網招聘網站上,崗位信息裏的全部條目都是在同一級標籤下。所以,崗位信息做爲一個總體,就須要額外的操做把要求與職責分離開。鑑於崗位信息裏數據格式的不統一,所以博主放棄了使用正則表達式的方法,而是選擇了模糊匹配+結構化匹配,將字符串比較的問題轉化成了機率問題。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上跑個模型,親自操刀一下機器學習了,哈哈哈。

相關文章
相關標籤/搜索