11# noqa: PLC0415
2+ import datetime
23import os
34import re
45from collections import Counter
1213
1314from adscrawler .dbcon .connection import PostgresCon
1415from adscrawler .dbcon .queries import (
16+ delete_and_insert ,
1517 query_all_store_app_descriptions ,
18+ query_apps_to_process_keywords ,
1619 query_keywords_base ,
1720 upsert_df ,
18- query_keywords_base ,
19- delete_and_insert ,
20- query_apps_to_process_keywords ,
2121)
2222
23+ from adscrawler .config import get_logger
24+
25+ logger = get_logger (__name__ )
26+
2327# Custom stopwords to remove personal pronouns & other irrelevant words
2428CUSTOM_STOPWORDS = {
2529 "your" ,
@@ -75,6 +79,26 @@ def clean_text(text: str) -> str:
7579 return re .sub (r"[^a-zA-Z\s]" , ". " , text .lower ())
7680
7781
82+ def clean_df_text (df : pd .DataFrame , column : str ) -> pd .DataFrame :
83+ # Note these are same as clean_text function
84+ df [column ] = (
85+ df [column ]
86+ .str .replace ("\r " , ". " )
87+ .replace ("\n " , ". " )
88+ .replace ("\t " , ". " )
89+ .replace ("\xa0 " , ". " )
90+ .replace ("•" , ". " )
91+ .replace ("'" , "" )
92+ .replace ("’" , "" )
93+ .replace ("-" , " " )
94+ .replace (r"\bhttp\S*" , "" , regex = True )
95+ .replace (r"\bwww\S*" , "" , regex = True )
96+ .replace (r"[^a-zA-Z\s]" , ". " , regex = True )
97+ .str .lower ()
98+ )
99+ return df
100+
101+
78102def count_tokens (phrase : str ) -> int :
79103 """Count the number of tokens in a phrase."""
80104 return len (word_tokenize (phrase ))
@@ -177,8 +201,14 @@ def extract_unique_app_keywords_from_text(
177201 # Remove stopwords from filtered keywords
178202 filtered_keywords = [kw for kw in filtered_keywords if kw not in STOPWORDS ]
179203
180- matched_base_keywords = matched_base_keywords ["keyword_text" ].str .strip ().tolist ()
181- combined_keywords = list (sorted (set (filtered_keywords + matched_base_keywords )))
204+ # keywords_base = query_keywords_base(database_connection)
205+ # matched_base_keywords = keywords_base[
206+ # keywords_base["keyword_text"].apply(lambda x: x in description_text)
207+ # ]
208+
209+ # matched_base_keywords = matched_base_keywords["keyword_text"].str.strip().tolist()
210+ # combined_keywords = list(sorted(set(filtered_keywords + matched_base_keywords)))
211+ combined_keywords = list (sorted (set (filtered_keywords )))
182212
183213 return combined_keywords
184214
@@ -191,22 +221,7 @@ def get_global_keywords(database_connection: PostgresCon) -> list[str]:
191221 language_slug = "en" , database_connection = database_connection
192222 )
193223
194- # Note these are same as clean_text function
195- df ["description" ] = (
196- df ["description" ]
197- .str .replace ("\r " , ". " )
198- .replace ("\n " , ". " )
199- .replace ("\t " , ". " )
200- .replace ("\xa0 " , ". " )
201- .replace ("•" , ". " )
202- .replace ("'" , "" )
203- .replace ("’" , "" )
204- .replace ("-" , " " )
205- .replace (r"\bhttp\S*" , "" , regex = True )
206- .replace (r"\bwww\S*" , "" , regex = True )
207- .replace (r"[^a-zA-Z\s]" , ". " , regex = True )
208- .str .lower ()
209- )
224+ df = clean_df_text (df , "description" )
210225
211226 from sklearn .feature_extraction .text import TfidfVectorizer # noqa: PLC0415
212227
@@ -261,107 +276,65 @@ def insert_global_keywords(database_connection: PostgresCon) -> None:
261276 )
262277
263278
264- def process_app_keywords (database_connection : PostgresCon ) -> None :
265- """Process app keywords."""
279+ def process_app_keywords (database_connection : PostgresCon , limit : int ) -> None :
280+ """Process app keywords.
281+
282+ While Python might be less efficient than SQL it's more flexible for
283+ the query input limiting which apps and when to run.
284+ This way apps can be processed in batches and only when really needed.
285+ """
286+ logger .info (f"Extracting app keywords for { limit } apps" )
287+ extract_app_keywords_from_descriptions (database_connection , limit )
288+ logger .info ("Extracted app keywords finished" )
266289
267290
268- def process_keywords_for_app (
269- description_df : pd . DataFrame , database_connection : PostgresCon
291+ def extract_app_keywords_from_descriptions (
292+ database_connection : PostgresCon , limit : int
270293) -> None :
271- """Process keywords for an app."""
272- description_df = query_apps_to_process_keywords (database_connection )
294+ """Process keywords for app descriptions ."""
295+ description_df = query_apps_to_process_keywords (database_connection , limit = limit )
273296 keywords_base = query_keywords_base (database_connection )
297+ keywords_base ["keyword_text" ] = (
298+ " " + keywords_base ["keyword_text" ].str .lower () + " "
299+ )
274300 description_df ["description_text" ] = (
275- description_df ["description_short" ] + " " + description_df ["description" ]
301+ " "
302+ + description_df ["description_short" ]
303+ + " "
304+ + description_df ["description" ]
305+ + " "
276306 ).str .lower ()
307+ description_df = clean_df_text (description_df , "description_text" )
277308 all_keywords_dfs = []
309+ logger .info (f"Processing { len (description_df )} app descriptions" )
278310 for _i , row in description_df .iterrows ():
279- print (f"Processing description: { _i } /{ len (description_df )} " )
311+ logger . debug (f"Processing app description: { _i } /{ len (description_df )} " )
280312 description_id = row ["description_id" ]
313+ store_app = row ["store_app" ]
281314 description_text = row ["description_text" ]
282315 matched_base_keywords = keywords_base [
283- keywords_base ["keyword_text" ].apply (lambda x : x in description_text )
316+ keywords_base ["keyword_text" ].apply (
317+ lambda x , text = description_text : x in text
318+ )
284319 ]
285- keywords_df = pd .DataFrame (matched_base_keywords , columns = ["keyword_text" ])
320+ keywords_df = pd .DataFrame (
321+ matched_base_keywords , columns = ["keyword_text" , "keyword_id" ]
322+ )
286323 keywords_df ["description_id" ] = description_id
287- keywords_df = keywords_df [[ "keyword_text" , "description_id" ]]
324+ keywords_df [ "store_app" ] = store_app
288325 all_keywords_dfs .append (keywords_df )
289326 main_keywords_df = pd .concat (all_keywords_dfs )
290- main_keywords_df = main_keywords_df .merge (
291- keywords_base , on = "keyword_text" , how = "left"
292- )
293- main_keywords_df = main_keywords_df [["description_id" , "keyword_id" ]]
294- table_name = "description_keywords"
295- insert_columns = ["description_id" , "keyword_id" ]
296- key_columns = ["description_id" , "keyword_id" ]
327+ main_keywords_df = main_keywords_df [["store_app" , "description_id" , "keyword_id" ]]
328+ main_keywords_df ["extracted_at" ] = datetime .datetime .now (tz = datetime .UTC )
329+ table_name = "app_keywords_extracted"
330+ insert_columns = ["store_app" , "description_id" , "keyword_id" , "extracted_at" ]
331+ logger .info (f"Delete and insert { len (main_keywords_df )} app keywords" )
297332 delete_and_insert (
298333 df = main_keywords_df ,
299334 table_name = table_name ,
300335 schema = "public" ,
301336 database_connection = database_connection ,
302337 insert_columns = insert_columns ,
303- key_columns = key_columns ,
304- delete_by_keys = ["description_id" ],
338+ delete_by_keys = ["store_app" ],
305339 delete_keys_have_duplicates = True ,
306340 )
307-
308- all_matches = []
309- for _i , keyword in enumerate (keywords_base ["keyword_text" ].unique ()):
310- print (f"Processing keyword: { _i } /{ len (keywords_base ['keyword_text' ].unique ())} " )
311- # Find all descriptions containing this keyword
312- mask = description_df ["description_text" ].str .contains (
313- re .escape (keyword ), # Escape special regex characters
314- case = False ,
315- regex = True ,
316- na = False ,
317- )
318- matched_descriptions = description_df [mask ]["description_id" ]
319- # Create rows for each match
320- for desc_id in matched_descriptions :
321- keyword_rows = keywords_base [keywords_base ["keyword_text" ] == keyword ]
322- for _ , kw_row in keyword_rows .iterrows ():
323- all_matches .append ({"description_id" : desc_id , ** kw_row .to_dict ()})
324-
325- all_keywords_df = pd .DataFrame (all_matches )
326-
327- pattern = "|" .join (re .escape (kw ) for kw in keywords_base ["keyword_text" ].unique ())
328-
329- # Find matches
330- all_matches = []
331- for _i , desc_row in description_df .iterrows ():
332- print (f"Processing description: { _i } /{ len (description_df )} " )
333- matches = keywords_base [
334- keywords_base ["keyword_text" ].apply (
335- lambda x : bool (
336- re .search (
337- r"\b" + re .escape (x ) + r"\b" , desc_row ["description_text" ]
338- )
339- )
340- )
341- ]
342- if len (matches ) > 0 :
343- matches ["description_id" ] = desc_row ["description_id" ]
344- all_matches .append (matches )
345-
346- all_keywords_df = (
347- pd .concat (all_matches , ignore_index = True ) if all_matches else pd .DataFrame ()
348- )
349-
350- # Method 2: Cross join + filter (best for large datasets)
351- description_df ["key" ] = 1
352- keywords_base ["key" ] = 1
353-
354- # Create cartesian product
355- cross_join = (
356- description_df [["description_id" , "description_text" , "key" ]]
357- .merge (keywords_base , on = "key" )
358- .drop ("key" , axis = 1 )
359- )
360-
361- # Filter matches
362- cross_join ["match" ] = cross_join .apply (
363- lambda row : row ["keyword_text" ] in row ["description_text" ], axis = 1
364- )
365- all_keywords_df = cross_join [cross_join ["match" ]].drop (
366- ["match" , "description_text" ], axis = 1
367- )
0 commit comments